I'm using Base Web. I have a menu with a child menu. It's based on this example. On desktop it works fine. I open the menu, I hover on an option, and the child menu appears. However, when I try it on my iPhone I can't access the child menu. Tapping the target opens the primary menu, but tapping the menu item closes the primary menu.
I imagine that to fix this I would have to change the target event for displaying the child menu from hover to click. Is that correct? And if so, does anyone know how I can do so with a Base Web menu? I looked at the docs but didn't see an option for that.
Here is a code pen: https://codesandbox.io/s/base-web-menu-focus-issue-forked-czz9kp. (NOTE: to see the issue, access that URL on your mobile. Do not use the codepen responsive view).
Here is the source code file for the menu:
//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
It is used in the Header.js file:
// 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>
)
}
Thanks in advance!
Okay, I finally it out. The problem was that I was calling the
close()
utility function in myonItemSelect
handler. The idea was roughly this:onClick
handler invoke itThis makes sense on non-touchscreen devices because you would use hover to open the nested child menu, not click. Here is a screen snap of the diff:
Here is the helper function for detecting touchscreen devices:
Now that I've made this change, I can click the primary menu item and it opens the nested menu.