Fix keyboard support on the dropdown (#1073)
* Fix: dropdown now supports accessibility labels and keyboard controls * Fix event propagation around the post dropdownzio/stable
parent
45da8a86c9
commit
1195f28992
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This utility function captures events and stops
|
||||||
|
* them from propagating upwards.
|
||||||
|
*/
|
||||||
|
export function EventStopper({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const stop = (e: any) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
onStartShouldSetResponder={_ => true}
|
||||||
|
onTouchEnd={stop}
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
onClick={stop}
|
||||||
|
onKeyDown={stop}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,13 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||||
import {
|
import {Pressable, StyleSheet, Platform, View} from 'react-native'
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
|
||||||
Platform,
|
|
||||||
StyleProp,
|
|
||||||
ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||||
import {MenuItemCommonProps} from 'zeego/lib/typescript/menu'
|
import {MenuItemCommonProps} from 'zeego/lib/typescript/menu'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
@ -18,16 +12,71 @@ import {HITSLOP_10} from 'lib/constants'
|
||||||
// Custom Dropdown Menu Components
|
// Custom Dropdown Menu Components
|
||||||
// ==
|
// ==
|
||||||
export const DropdownMenuRoot = DropdownMenu.Root
|
export const DropdownMenuRoot = DropdownMenu.Root
|
||||||
export const DropdownMenuTrigger = DropdownMenu.Trigger
|
// export const DropdownMenuTrigger = DropdownMenu.Trigger
|
||||||
export const DropdownMenuContent = DropdownMenu.Content
|
export const DropdownMenuContent = DropdownMenu.Content
|
||||||
|
|
||||||
|
type TriggerProps = Omit<
|
||||||
|
React.ComponentProps<(typeof DropdownMenu)['Trigger']>,
|
||||||
|
'children'
|
||||||
|
> &
|
||||||
|
React.PropsWithChildren<{
|
||||||
|
testID?: string
|
||||||
|
accessibilityLabel?: string
|
||||||
|
accessibilityHint?: string
|
||||||
|
}>
|
||||||
|
export const DropdownMenuTrigger = DropdownMenu.create(
|
||||||
|
(props: TriggerProps) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||||
|
const ref = React.useRef<View>(null)
|
||||||
|
|
||||||
|
// HACK
|
||||||
|
// fire a click event on the keyboard press to trigger the dropdown
|
||||||
|
// -prf
|
||||||
|
const onPress = isWeb
|
||||||
|
? (evt: any) => {
|
||||||
|
if (evt instanceof KeyboardEvent) {
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
ref.current?.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
testID={props.testID}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={props.accessibilityLabel}
|
||||||
|
accessibilityHint={props.accessibilityHint}
|
||||||
|
style={({pressed}) => [{opacity: pressed ? 0.5 : 1}]}
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
onPress={onPress}>
|
||||||
|
<DropdownMenu.Trigger action="press">
|
||||||
|
<View ref={ref}>
|
||||||
|
{props.children ? (
|
||||||
|
props.children
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="ellipsis"
|
||||||
|
size={20}
|
||||||
|
color={defaultCtrlColor}
|
||||||
|
style={styles.ellipsis}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'Trigger',
|
||||||
|
)
|
||||||
|
|
||||||
type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']>
|
type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']>
|
||||||
export const DropdownMenuItem = DropdownMenu.create(
|
export const DropdownMenuItem = DropdownMenu.create(
|
||||||
(props: ItemProps & {testID?: string}) => {
|
(props: ItemProps & {testID?: string}) => {
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const [focused, setFocused] = React.useState(false)
|
const [focused, setFocused] = React.useState(false)
|
||||||
const {borderColor: backgroundColor} =
|
const backgroundColor = theme.colorScheme === 'dark' ? '#fff1' : '#0001'
|
||||||
theme.colorScheme === 'dark' ? pal.borderDark : pal.border
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
@ -46,6 +95,7 @@ export const DropdownMenuItem = DropdownMenu.create(
|
||||||
},
|
},
|
||||||
'Item',
|
'Item',
|
||||||
)
|
)
|
||||||
|
|
||||||
type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']>
|
type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']>
|
||||||
export const DropdownMenuItemTitle = DropdownMenu.create(
|
export const DropdownMenuItemTitle = DropdownMenu.create(
|
||||||
(props: TitleProps) => {
|
(props: TitleProps) => {
|
||||||
|
@ -59,10 +109,12 @@ export const DropdownMenuItemTitle = DropdownMenu.create(
|
||||||
},
|
},
|
||||||
'ItemTitle',
|
'ItemTitle',
|
||||||
)
|
)
|
||||||
|
|
||||||
type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']>
|
type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']>
|
||||||
export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => {
|
export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => {
|
||||||
return <DropdownMenu.ItemIcon {...props} />
|
return <DropdownMenu.ItemIcon {...props} />
|
||||||
}, 'ItemIcon')
|
}, 'ItemIcon')
|
||||||
|
|
||||||
type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']>
|
type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']>
|
||||||
export const DropdownMenuSeparator = DropdownMenu.create(
|
export const DropdownMenuSeparator = DropdownMenu.create(
|
||||||
(props: SeparatorProps) => {
|
(props: SeparatorProps) => {
|
||||||
|
@ -97,8 +149,9 @@ export type DropdownItem = {
|
||||||
}
|
}
|
||||||
type Props = {
|
type Props = {
|
||||||
items: DropdownItem[]
|
items: DropdownItem[]
|
||||||
children?: React.ReactNode
|
|
||||||
testID?: string
|
testID?: string
|
||||||
|
accessibilityLabel?: string
|
||||||
|
accessibilityHint?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The `NativeDropdown` function uses native iOS and Android dropdown menus.
|
/* The `NativeDropdown` function uses native iOS and Android dropdown menus.
|
||||||
|
@ -107,36 +160,26 @@ type Props = {
|
||||||
* @prop {DropdownItem[]} items - An array of dropdown items
|
* @prop {DropdownItem[]} items - An array of dropdown items
|
||||||
* @prop {React.ReactNode} children - A custom dropdown trigger
|
* @prop {React.ReactNode} children - A custom dropdown trigger
|
||||||
*/
|
*/
|
||||||
export function NativeDropdown({items, children, testID}: Props) {
|
export function NativeDropdown({
|
||||||
|
items,
|
||||||
|
children,
|
||||||
|
testID,
|
||||||
|
accessibilityLabel,
|
||||||
|
accessibilityHint,
|
||||||
|
}: React.PropsWithChildren<Props>) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const dropDownBackgroundColor =
|
const dropDownBackgroundColor =
|
||||||
theme.colorScheme === 'dark' ? pal.btn : pal.viewLight
|
theme.colorScheme === 'dark' ? pal.btn : pal.viewLight
|
||||||
const defaultCtrlColor = React.useMemo(
|
|
||||||
() => ({
|
|
||||||
color: theme.palette.default.postCtrl,
|
|
||||||
}),
|
|
||||||
[theme],
|
|
||||||
) as StyleProp<ViewStyle>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuRoot>
|
<DropdownMenuRoot>
|
||||||
<DropdownMenuTrigger action="press">
|
<DropdownMenuTrigger
|
||||||
<Pressable
|
action="press"
|
||||||
testID={testID}
|
testID={testID}
|
||||||
accessibilityRole="button"
|
accessibilityLabel={accessibilityLabel}
|
||||||
style={({pressed}) => [{opacity: pressed ? 0.5 : 1}]}
|
accessibilityHint={accessibilityHint}>
|
||||||
hitSlop={HITSLOP_10}>
|
{children}
|
||||||
{children ? (
|
|
||||||
children
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="ellipsis"
|
|
||||||
size={20}
|
|
||||||
style={[defaultCtrlColor, styles.ellipsis]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
style={[styles.content, dropDownBackgroundColor]}
|
style={[styles.content, dropDownBackgroundColor]}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
NativeDropdown,
|
NativeDropdown,
|
||||||
DropdownItem as NativeDropdownItem,
|
DropdownItem as NativeDropdownItem,
|
||||||
} from './NativeDropdown'
|
} from './NativeDropdown'
|
||||||
import {Pressable} from 'react-native'
|
import {EventStopper} from '../EventStopper'
|
||||||
|
|
||||||
export function PostDropdownBtn({
|
export function PostDropdownBtn({
|
||||||
testID,
|
testID,
|
||||||
|
@ -141,8 +141,13 @@ export function PostDropdownBtn({
|
||||||
].filter(Boolean) as NativeDropdownItem[]
|
].filter(Boolean) as NativeDropdownItem[]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable testID={testID} accessibilityRole="button">
|
<EventStopper>
|
||||||
<NativeDropdown items={dropdownItems} />
|
<NativeDropdown
|
||||||
</Pressable>
|
testID={testID}
|
||||||
|
items={dropdownItems}
|
||||||
|
accessibilityLabel="More post options"
|
||||||
|
accessibilityHint=""
|
||||||
|
/>
|
||||||
|
</EventStopper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue