From 3b8b5622688807f6d04c52cbd4d6977b203b75b3 Mon Sep 17 00:00:00 2001 From: Ansh Date: Fri, 28 Jul 2023 14:00:37 -0700 Subject: [APP-737] Accessible native dropdown menu (#988) * fix comments * add zeego package * get basic native dropdown working * add separator and icon components * refined native dropdown component * add android build properties to app.json * move `PostDropdownBtn` to its own component * fix selectors issue * move `PostDropdownBtn` to its own component * fix hitslop * fix post dropdown hitslop * fix android dropdown icons * move `UserAvatar.tsx` to native dropdown * use native dropdown in `ProfileHeader.tsx` * use native dropdown in `PostThreadItem.tsx` * use native dropdown in `UserBanner.tsx` * use native dropdown in `CustomFeed.tsx` * replace `testId` with `testID` (which is what is used everywhere) * move `Settings.tsx` to use native dropdown * create jest mocks for zeego * create jest mock for `zeego/dropdown-menu` * web styles for native dropdown * remove example native dropdown * adjust web styles * fix propagation * fix pressable in `Settings.tsx` * animate dropdown on web * add keyboard nav and hover styles * add hitslop to constants * add comments to NativeDropdown component * temporarily removed android icons * add testID to PostDropdownBtn * add testID back to all NativeDropdown button implementations * add postDropdownBtn testID * add testID to dropdown items * remove testID from dropdown menu item * refactor home-screen tests for native dropdown * refactor profile-screen tests for native dropdown * refactor thread-muting tests for native dropdown * refactor thread-screen tests for native dropdown * fix dropdown color for post dropdown button * remove icons from android dropdown menu * fix `create-account.test.ts` * fix `invite-codes.test.ts` --- src/view/com/util/forms/NativeDropdown.tsx | 250 +++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 src/view/com/util/forms/NativeDropdown.tsx (limited to 'src/view/com/util/forms/NativeDropdown.tsx') diff --git a/src/view/com/util/forms/NativeDropdown.tsx b/src/view/com/util/forms/NativeDropdown.tsx new file mode 100644 index 000000000..d8f16ce19 --- /dev/null +++ b/src/view/com/util/forms/NativeDropdown.tsx @@ -0,0 +1,250 @@ +import React from 'react' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import * as DropdownMenu from 'zeego/dropdown-menu' +import { + Pressable, + StyleSheet, + Platform, + StyleProp, + ViewStyle, +} from 'react-native' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' +import {usePalette} from 'lib/hooks/usePalette' +import {isWeb} from 'platform/detection' +import {useTheme} from 'lib/ThemeContext' +import {HITSLOP_10} from 'lib/constants' + +// Custom Dropdown Menu Components +// == +export const DropdownMenuRoot = DropdownMenu.Root +export const DropdownMenuTrigger = DropdownMenu.Trigger +export const DropdownMenuContent = DropdownMenu.Content +type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']> +export const DropdownMenuItem = DropdownMenu.create( + (props: ItemProps & {testID?: string}) => { + const pal = usePalette('default') + const theme = useTheme() + const [focused, setFocused] = React.useState(false) + const {borderColor: backgroundColor} = + theme.colorScheme === 'dark' ? pal.borderDark : pal.border + + return ( + { + setFocused(true) + props.onFocus && props.onFocus() + }} + onBlur={() => { + setFocused(false) + props.onBlur && props.onBlur() + }} + /> + ) + }, + 'Item', +) +type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']> +export const DropdownMenuItemTitle = DropdownMenu.create( + (props: TitleProps) => { + const pal = usePalette('default') + return ( + + ) + }, + 'ItemTitle', +) +type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']> +export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => { + return +}, 'ItemIcon') +type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']> +export const DropdownMenuSeparator = DropdownMenu.create( + (props: SeparatorProps) => { + const pal = usePalette('default') + const theme = useTheme() + const {borderColor: separatorColor} = + theme.colorScheme === 'dark' ? pal.borderDark : pal.border + return ( + + ) + }, + 'Separator', +) + +// Types for Dropdown Menu and Items +export type DropdownItem = { + label: string | 'separator' + onPress?: () => void + testID?: string + icon?: { + ios: MenuItemCommonProps['ios'] + android: string + web: IconProp + } +} +type Props = { + items: DropdownItem[] + children?: React.ReactNode + testID?: string +} + +/* The `NativeDropdown` function uses native iOS and Android dropdown menus. + * It also creates a animated custom dropdown for web that uses + * Radix UI primitives under the hood + * @prop {DropdownItem[]} items - An array of dropdown items + * @prop {React.ReactNode} children - A custom dropdown trigger + */ +export function NativeDropdown({items, children, testID}: Props) { + const pal = usePalette('default') + const theme = useTheme() + const dropDownBackgroundColor = + theme.colorScheme === 'dark' ? pal.btn : pal.viewLight + const defaultCtrlColor = React.useMemo( + () => ({ + color: theme.palette.default.postCtrl, + }), + [theme], + ) as StyleProp + + return ( + + + [{opacity: pressed ? 0.5 : 1}]} + hitSlop={HITSLOP_10}> + {children ? ( + children + ) : ( + + )} + + + + {items.map((item, index) => { + if (item.label === 'separator') { + return ( + + ) + } + if (index > 1 && items[index - 1].label === 'separator') { + return ( + + + {item.label} + {item.icon && ( + + + + )} + + + ) + } + return ( + + {item.label} + {item.icon && ( + + + + )} + + ) + })} + + + ) +} + +const getKey = (label: string, index: number, id?: string) => { + if (id) { + return id + } + return `${label}_${index}` +} + +const styles = StyleSheet.create({ + separator: { + height: 1, + marginVertical: 4, + }, + ellipsis: { + padding: isWeb ? 0 : 10, + }, + content: { + backgroundColor: '#f0f0f0', + borderRadius: 8, + paddingVertical: 4, + paddingHorizontal: 4, + marginTop: 6, + ...Platform.select({ + web: { + animationDuration: '400ms', + animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', + willChange: 'transform, opacity', + animationKeyframes: { + '0%': {opacity: 0, transform: [{scale: 0.5}]}, + '100%': {opacity: 1, transform: [{scale: 1}]}, + }, + boxShadow: + '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)', + transformOrigin: 'var(--radix-dropdown-menu-content-transform-origin)', + }, + }), + }, + item: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + columnGap: 20, + // @ts-ignore -web + cursor: 'pointer', + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 8, + }, + itemTitle: { + fontSize: 18, + }, +}) -- cgit 1.4.1