如何让Base Web嵌套导航菜单在移动端正常工作?
问题描述
我正在使用Base Web开发带子菜单的导航菜单,该菜单基于官方子菜单示例实现。桌面端运行正常:打开主菜单后,悬停菜单项即可显示子菜单;但在iPhone移动端无法访问子菜单,点击主菜单选项会直接关闭主菜单。
我推测需要将子菜单的触发事件从hover改为click,请问这个思路是否正确?如果正确,如何在Base Web菜单中实现该修改?我查阅了文档但未找到相关配置项。
在线示例:需在真实移动设备访问,不要用响应式视图
相关代码
HamburgerMenu.js
import React from 'react' import { themedUseStyletron as useStyletron } from 'Shared/Theme' import { StatefulMenu, NestedMenus } from 'baseui/menu' import { StatefulPopover, PLACEMENT } from 'baseui/popover' import { HamburgerMenu as MenuIcon } from 'Components/Icons' import InvisibleButtonWrapper from 'Components/Shared/InvisibleButtonWrapper/InvisibleButtonWrapper' type MenuItemT = { label: string onClick?: () => void } type HamburgerMenuProps = { items:MenuItemT[] placement: 'bottom' | 'auto' | 'topLeft' | 'top' | 'topRight' | 'rightTop' | 'right' | 'rightBottom' | 'bottomRight' | 'bottomLeft' | 'leftBottom' | 'left' | 'leftTop' | undefined ariaLabel: string id: string subItems?: {[key: string]: MenuItemT[]} fill: string } const HamburgerMenu = ({ items, placement = PLACEMENT.bottom, ariaLabel, id, subItems, fill }: HamburgerMenuProps) => { const [, theme] = useStyletron() return ( <StatefulPopover autoFocus={false} dismissOnClickOutside={false} content={({ close }) => ( <NestedMenus> <StatefulMenu items={items} onItemSelect={({ item }) => { typeof item?.onClick === 'function' && item.onClick() close() }} overrides={{ List: { style: ({ $theme }) => ({ borderTopLeftRadius: '6px', borderTopRightRadius: '6px', borderBottomLeftRadius: '6px', borderBottomRightRadius: '6px', border: `1px solid ${$theme.colors.backgroundQuaternary}` }) }, Option: { props: { getChildMenu: (item: MenuItemT) => { if (!subItems?.[item.label]) return null return ( <StatefulMenu items={subItems[item.label] as MenuItemT[]} overrides={{ List: { style: ({ $theme }) => ({ borderTopLeftRadius: '6px', borderTopRightRadius: '6px', borderBottomLeftRadius: '6px', borderBottomRightRadius: '6px', border: `1px solid ${$theme.colors.backgroundQuaternary}`, whiteSpace: 'nowrap' }) }, Option: { style: ({ $theme }) => ({ fontFamily: 'Roboto', color: $theme.colors.primary, fontWeight: 'bold' }) } }} onItemSelect={({ item }) => { typeof item?.onClick === 'function' && item.onClick() close() }} /> ) } }, style: ({ $theme }) => ({ fontFamily: 'Roboto', color: $theme.colors.primary, fontWeight: 'bold' }) } }} /> </NestedMenus> )} accessibilityType={'tooltip'} placement={placement} > <InvisibleButtonWrapper placement={'left'} > <MenuIcon size={24} aria-label={ariaLabel} fill={fill}/> </InvisibleButtonWrapper> </StatefulPopover> ) } export default HamburgerMenu
Header.js
import { themedUseStyletron as useStyletron } from 'Shared/Theme' import { useDispatch, useSelector } from 'react-redux' import { toggleDisplayCreateBoard } from 'Redux/Reducers/ModalDisplaySlice' import { switchTheme } from 'Redux/Reducers/ThemeSlice' import { THEME } from 'Shared/Constants' import Link from 'next/link' import { signOut } from 'next-auth/react' import { useSession } from 'next-auth/react' import HamburgerMenu from './HamburgerMenu/HamburgerMenu' export default function Header (props) { const [css, theme] = useStyletron() const dispatch = useDispatch() const boards = useSelector(state => state?.board) const { data: session } = useSession() const items = [ { label: 'Boards' }, { label: 'Create' }, { label: 'Theme' } ] items.push( { label: 'Sign out', onClick: async () => { await signOut({ callbackUrl: 'http://localhost:8080/signin' }) } } ) const subItems = { Boards: boards.map(board => ({ label: <Link href={{pathname: '/board/[boardId]'}} as={`/board/${board.id}`}><span className={css({ color: theme.colors.primary, width: '100%', display: 'block' })}>{board.name}</span></Link> })) , Create: [{ label: 'Board', onClick: () => { dispatch(toggleDisplayCreateBoard()) } } ], Theme: [ { label: 'Light', onClick: () => { dispatch(switchTheme({ theme: THEME.light })) } }, { label: 'Dark', onClick: () => { dispatch(switchTheme({ theme: THEME.dark })) } } ] } return ( <div className={css({ position: 'relative', display: 'flex', alignItems: 'center', width: '100%', height: '50px', padding: '0', fontFamily: 'bebas neue', boxShadow: 'rgba(0, 0, 0, 0.2) 0px 12px 28px 0px, rgba(0, 0, 0, 0.1) 0px 2px 4px 0px, rgba(255, 255, 255, 0.05) 0px 0px 0px 1px inset' })}> <Link href="/" style={{textDecoration: 'none'}}> <h1 className={css({ cursor: 'pointer', fontSize: '32px', lineHeight: '32px', margin: '0 10px 0 20px', padding: 0, color: theme.colors.accent, borderBottom: '2px solid theme.colors.black', ':only-child': { margin: '0 0 0 20px' } })}>Get It</h1> </Link> {session && <HamburgerMenu tabindex={0} ariaLabel={'main-menu'} items={items} subItems={subItems} fill={theme.colors.primary} /> } </div> ) }
解决方案
你的思路完全正确——移动端没有原生hover事件,必须把带下级菜单的主菜单项触发逻辑改成点击展开/收起,而不是直接触发菜单关闭。
核心修改点
- 区分菜单项类型:在
onItemSelect中判断当前点击的项是否有子菜单,带子菜单的项只切换展开状态,不关闭主菜单;普通项正常执行点击逻辑并关闭。 - 手动控制子菜单显示:新增状态变量跟踪当前展开的子菜单,配合点击事件切换状态,控制子菜单的渲染。
修改后的HamburgerMenu.js代码
import React, { useState } from 'react' import { themedUseStyletron as useStyletron } from 'Shared/Theme' import { StatefulMenu, NestedMenus } from 'baseui/menu' import { StatefulPopover, PLACEMENT } from 'baseui/popover' import { HamburgerMenu as MenuIcon } from 'Components/Icons' import InvisibleButtonWrapper from 'Components/Shared/InvisibleButtonWrapper/InvisibleButtonWrapper' type MenuItemT = { label: string onClick?: () => void } type HamburgerMenuProps = { items:MenuItemT[] placement: 'bottom' | 'auto' | 'topLeft' | 'top' | 'topRight' | 'rightTop' | 'right' | 'rightBottom' | 'bottomRight' | 'bottomLeft' | 'leftBottom' | 'left' | 'leftTop' | undefined ariaLabel: string id: string subItems?: {[key: string]: MenuItemT[]} fill: string } const HamburgerMenu = ({ items, placement = PLACEMENT.bottom, ariaLabel, id, subItems, fill }: HamburgerMenuProps) => { const [, theme] = useStyletron() // 记录当前展开的子菜单项label const [expandedSubMenu, setExpandedSubMenu] = useState<string | null>(null) return ( <StatefulPopover autoFocus={false} dismissOnClickOutside={false} content={({ close }) => ( <NestedMenus> <StatefulMenu items={items} onItemSelect={({ item }) => { const hasSubMenu = subItems?.[item.label] if (hasSubMenu) { // 切换子菜单展开状态:已展开则收起,未展开则展开 setExpandedSubMenu(expandedSubMenu === item.label ? null : item.label) return // 不关闭主菜单 } // 普通菜单项:执行点击逻辑并关闭菜单 typeof item?.onClick === 'function' && item.onClick() close() }} overrides={{ List: { style: ({ $theme }) => ({ borderTopLeftRadius: '6px', borderTopRightRadius: '6px', borderBottomLeftRadius: '6px', borderBottomRightRadius: '6px', border: `1px solid ${$theme.colors.backgroundQuaternary}` }) }, Option: { props: { getChildMenu: (item: MenuItemT) => { // 仅渲染当前展开的子菜单 if (!subItems?.[item.label] || expandedSubMenu !== item.label) return null return ( <StatefulMenu items={subItems[item.label] as MenuItemT[]} overrides={{ List: { style: ({ $theme }) => ({ borderTopLeftRadius: '6px', borderTopRightRadius: '6px', borderBottomLeftRadius: '6px', borderBottomRightRadius: '6px', border: `1px solid ${$theme.colors.backgroundQuaternary}`, whiteSpace: 'nowrap' }) }, Option: { style: ({ $theme }) => ({ fontFamily: 'Roboto', color: $theme.colors.primary, fontWeight: 'bold' }) } }} onItemSelect={({ item }) => { typeof item?.onClick === 'function' && item.onClick() close() }} /> ) } }, style: ({ $theme }) => ({ fontFamily: 'Roboto', color: $theme.colors.primary, fontWeight: 'bold' }) } }} /> </NestedMenus> )} accessibilityType={'tooltip'} placement={placement} > <InvisibleButtonWrapper placement={'left'} > <MenuIcon size={24} aria-label={ariaLabel} fill={fill}/> </InvisibleButtonWrapper> </StatefulPopover> ) } export default HamburgerMenu
效果说明
- 移动端点击"Boards"/"Create"/"Theme"时,会展开对应的子菜单,再次点击则收起;
- 点击子菜单选项或"Sign out"时,会执行对应逻辑并关闭整个菜单;
- 桌面端仍可通过hover展开子菜单,同时也支持点击切换,兼顾两种交互场景。
内容的提问来源于stack exchange,提问作者Neil Girardi
相关产品推荐
相关产品推荐

