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

如何让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事件,必须把带下级菜单的主菜单项触发逻辑改成点击展开/收起,而不是直接触发菜单关闭。

核心修改点

  1. 区分菜单项类型:在onItemSelect中判断当前点击的项是否有子菜单,带子菜单的项只切换展开状态,不关闭主菜单;普通项正常执行点击逻辑并关闭。
  2. 手动控制子菜单显示:新增状态变量跟踪当前展开的子菜单,配合点击事件切换状态,控制子菜单的渲染。

修改后的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 16:10:24