How can I make my nested navigation menu work on mobile?

265 Views Asked by At

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!

1

There are 1 best solutions below

0
On

Okay, I finally it out. The problem was that I was calling the close() utility function in my onItemSelect handler. The idea was roughly this:

  1. Click on an outer menu item
  2. If said item has an onClick handler invoke it
  3. Now close the outer menu.

This 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:

enter image description here

Here is the helper function for detecting touchscreen devices:

export const isTouchScreenDevice = () => {
  try{
    document.createEvent('TouchEvent');
    return true
  }catch(e){
    return false
  }
}

Now that I've made this change, I can click the primary menu item and it opens the nested menu.