Any chance you could critique my menu component for accessiblity? Not really familiar with good accessiblity, so I was wondering how to optimize this menu component with props, etc:
import React, {
ReactNode,
createContext,
useContext,
useMemo,
useState,
ComponentProps,
useRef,
useImperativeHandle,
} from 'react'
import { View, Text, SxProp, Pressable as Press, Theme as DripsyTheme } from 'dripsy'
import { Popover } from 'react-native-popper'
import { Sizes } from '../types'
type MenuProps = {
children: ReactNode
/*
* Default: `240`
*/
width?: number
height?: number
menu?: React.Ref<MenuRef>
sx?: SxProp
} & Omit<ComponentProps<typeof Popover>, 'isOpen' | 'children' | 'onOpenChange'>
const MenuVisibleContext = createContext({
visible: false,
onClose() {
//
},
onShow() {
//
},
onChange(next: boolean) {
//
},
})
const useMenuVisibleContext = () => useContext(MenuVisibleContext)
function MenuProvider({ children }: { children: ReactNode }) {
const [visible, setVisible] = useState(false)
return (
<MenuVisibleContext.Provider
value={useMemo(
() => ({
visible,
onShow: () => setVisible(true),
onClose: () => setVisible(false),
onChange: setVisible,
}),
[visible]
)}
>
{children}
</MenuVisibleContext.Provider>
)
}
const Menu = function Menu(props: MenuProps) {
return (
<MenuProvider>
<MenuWithContext {...props} />
</MenuProvider>
)
}
function MenuDivider() {
return <View sx={{ height: 1, width: '100%', bg: 'mutedText', my: 2 }} />
}
function MenuWithContext({
children,
width = 240,
height,
menu,
...props
}: MenuProps) {
const { visible, onChange } = useMenuVisibleContext()
useImperativeHandle(menu, () => ({
show: () => onChange(true),
close: () => onChange(false),
}))
return (
<Popover offset={2} {...props} isOpen={visible} onOpenChange={onChange}>
<Popover.Backdrop />
<Popover.Content>
<View
sx={{
width,
height,
borderWidth: 1,
bg: 'background',
borderRadius: 3,
borderColor: 'mutedText',
py: 2,
}}
>
{children}
</View>
</Popover.Content>
</Popover>
)
}
type MenuItemProps = {
children: string | ReactNode
onPress?: () => void
prefix?: ReactNode
suffix?: ReactNode
disabled?: boolean
color?: keyof DripsyTheme['colors']
/*
* default: `true`
*/
shouldCloseOnPress?: boolean
}
function MenuItem({
children,
onPress,
prefix,
suffix,
disabled = false,
color,
shouldCloseOnPress = true,
}: MenuItemProps) {
const { onClose } = useMenuVisibleContext()
const composePress = () => {
onPress?.()
if (shouldCloseOnPress) {
onClose()
}
}
return (
<Press disabled={disabled} onPress={composePress}>
{({ hovered, pressed }) => {
return (
<View
sx={{
px: 3,
py: 2,
bg: hovered || pressed ? 'muted2' : undefined,
flexDirection: 'row',
alignItems: 'center',
opacity: disabled ? 0.7 : 1,
cursor: disabled ? 'not-allowed' : 'pointer',
}}
>
{!!prefix && <View sx={{ mr: 2 }}>{prefix}</View>}
<View sx={{ flex: 1 }}>
<Text
sx={{
color: color ?? (hovered || pressed ? 'text' : 'muted7'),
}}
>
{children}
</Text>
</View>
{!!suffix && <View sx={{ ml: 2 }}>{suffix}</View>}
</View>
)
}}
</Press>
)
}
type MenuSectionProps = {
children: ReactNode
title: string
}
function MenuSection({ children, title }: MenuSectionProps) {
return (
<View>
<Text sx={{ py: 2, px: 3, color: 'mutedText' }}>{title}</Text>
{children}
</View>
)
}
type MenuTriggerProps = {
children: ReactNode
onPress?: never
} & (
| {
unstyled?: true
}
| ({
unstyled?: false
} & ComponentProps<typeof Button>)
)
const TriggerContext = React.createContext(false)
const MenuTrigger = React.forwardRef(function MenuTrigger(
{ unstyled = true, ...props }: MenuTriggerProps,
ref
) {
let node = <Button ref={ref} {...props} />
if (unstyled) {
node = <Press ref={ref} {...(props as any)} />
}
return <TriggerContext.Provider value={true}>{node}</TriggerContext.Provider>
})
type MenuIconButtonProps = {
size?: Sizes
shape?: 'square' | 'circle' | 'none'
icon: IconProps['icon']
onPress?: never // internal usage only
}
Menu.Trigger = MenuTrigger
Menu.Section = MenuSection
Menu.Item = MenuItem
Menu.Divider = MenuDivider
export { Menu }