You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

重写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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 09:15:30