重写Base Web Menu的Option组件致键盘绑定失效求助
自定义Base Web Kebab菜单的键盘绑定问题与解决方案
更新记录
8月20日更新
和@mark_reeder在Base Web Slack交流后得知,label属性支持传入组件而非仅字符串。现在无需重写Option组件,直接给label传入带图标的自定义标签组件就能实现需求,操作简单很多!再次感谢@mark_reeder。
8月16日更新
采纳@mark_reeder的修复方案后,焦点状态问题解决了,但按下Enter键选中聚焦元素的功能仍不正常。我临时给body添加了事件监听器实现了基础逻辑,希望有更优的解决办法。
问题描述
作为Base Web新手,我开发了一个自定义「更多选项」菜单(又称Kebab菜单),希望给菜单项添加图标,于是重写了Menu组件中的Option。菜单外观显示正常,但键盘绑定功能完全失效——注释掉重写代码后,键盘操作恢复正常,但图标也随之消失。
另外,Base Web文档提到「每个菜单项默认可选择添加图标,也可移除」,但没有提供具体示例。我尝试直接给items添加icon属性,并未成功渲染,想了解官方是否有惯用的实现方式?
初始代码
MoreOptionsMenu.js
//MoreOptionsMenu.js import React from 'react' import { StatefulPopover, PLACEMENT } from 'baseui/popover' import Overflow from 'baseui/icon/overflow' import { StatefulMenu } from 'baseui/menu' import IconOption from 'Components/Shared/IconOption/IconOption' import InvisibleButtonWrapper from 'Components/Shared/InvisibleButtonWrapper/InvisibleButtonWrapper' const MoreOptionsMenu = ({ items, placement = PLACEMENT.bottom, ariaLabel, id }) => { return ( <StatefulPopover content={({ close }) => ( <StatefulMenu items={items} overrides={{ Option: { component: IconOption, props: { close, ariaLabel, id } }, List: { style: ({ $theme }) => ({ borderTopLeftRadius: '6px', borderTopRightRadius: '6px', borderBottomLeftRadius: '6px', borderBottomRightRadius: '6px', border: `1px solid ${$theme.colors.lightGray}`, }) } }} /> )} accessibilityType={'tooltip'} placement={placement} > <InvisibleButtonWrapper> <Overflow size={24} aria-label={ariaLabel} style={{ marginLeft: 'auto', cursor: 'pointer' }}/> </InvisibleButtonWrapper> </StatefulPopover> ) } export default MoreOptionsMenu
IconOption.js
//IconOptions import React, { forwardRef } from 'react' import { useStyletron } from 'baseui' import { stringToKebabCase } from 'Shared/Utilities' import InvisibleButtonWrapper from 'Components/Shared/InvisibleButtonWrapper/InvisibleButtonWrapper' const IconOption = forwardRef(( { close, item, ariaLabel, id }, ref) => { const [css, theme] = useStyletron() return ( <li role='option' aria-disabled='false' aria-selected='false' id={stringToKebabCase(`${id}-${item.label}`)} className={css({ display: 'flex', alignItems: 'center', padding: '10px', cursor: 'pointer', ':hover': { outline: `${theme.colors.accent} solid 3px` }, ':focus': { outline: `${theme.colors.accent} solid 3px` }, ':active': { outline: `${theme.colors.accent} solid 3px` }, ':nth-child(even)': { backgroundColor: theme.colors.lighterGray } })} aria-labelledby={ariaLabel} ref={ref} onClick={() => { typeof item.callback === 'function' && item.callback() close() }}> <InvisibleButtonWrapper> {item.icon} <span style={{ marginLeft: '10px' }}>{item.label}</span> </InvisibleButtonWrapper> </li> ) }) export default IconOption
InvisibleButtonWrapper.js
// InvisibleButtonWrapper.js import { withStyle } from 'baseui' import { StyledBaseButton } from 'baseui/button' const InvisibleButtonWrapper = withStyle(StyledBaseButton, ({$theme}) => ({ paddingTop: 0, paddingBottom: 0, paddingLeft: 0, paddingRight: 0, color: `${$theme.colors.primaryA}`, backgroundColor: 'inherit', ':hover': { color: `${$theme.colors.primaryA}`, backgroundColor: 'inherit', } })) export default InvisibleButtonWrapper
补充说明
- 使用forwardRef是因为之前函数组件无法接收refs,但无论是否使用该方法,键盘绑定都失效。
- InvisibleButtonWrapper用于提升组件可访问性且不破坏样式,移除它不影响键盘绑定问题。
临时修复代码(结合mark_reeder方案)
import React, { forwardRef, useEffect } from 'react' import { useStyletron } from 'baseui' import { stringToKebabCase } from 'Shared/Utilities' import InvisibleButtonWrapper from 'Components/Shared/InvisibleButtonWrapper/InvisibleButtonWrapper' const IconOption = forwardRef((props, ref) => { const [css, theme] = useStyletron() const { close, item, ariaLabel, id, $isHighlighted, $isFocused } = props const handleKeyDown = ({ code }) => { if (code === 'Enter') { document.querySelector("[data-iconoption='true'][aria-selected='true']").click() } } useEffect(() => { document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('keydown', handleKeyDown) } }, []) return ( <li data-iconoption={'true'} tabIndex={0} role='option' aria-selected={props['aria-selected'] ? 'true' : 'false'} id={stringToKebabCase(`${id}-${item.label}`)} className={css({ backgroundColor: $isHighlighted ? theme.colors.lighterGray : theme.colors.white, display: 'flex', alignItems: 'center', padding: '10px', cursor: 'pointer', ':hover': { backgroundColor: theme.colors.lighterGray }, ':focus': { backgroundColor: theme.colors.lighterGray }, })} aria-labelledby={ariaLabel} ref={ref} onClick={() => { typeof item.callback === 'function' && item.callback() close() }}> <InvisibleButtonWrapper> {item.icon} <span style={{ marginLeft: '10px' }}>{item.label}</span> </InvisibleButtonWrapper> </li> ) }) export default IconOption
内容的提问来源于stack exchange,提问作者Neil Girardi
相关产品推荐
相关产品推荐

