mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at samuel/exp-cli 283 lines 7.7 kB view raw
1import React from 'react' 2import { 3 Pressable, 4 StyleSheet, 5 Text, 6 type View, 7 type ViewStyle, 8} from 'react-native' 9import {type IconProp} from '@fortawesome/fontawesome-svg-core' 10import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 11import {DropdownMenu} from 'radix-ui' 12import {type MenuItemCommonProps} from 'zeego/lib/typescript/menu' 13 14import {HITSLOP_10} from '#/lib/constants' 15import {usePalette} from '#/lib/hooks/usePalette' 16import {useTheme} from '#/lib/ThemeContext' 17 18// Custom Dropdown Menu Components 19// == 20export const DropdownMenuRoot = DropdownMenu.Root 21export const DropdownMenuContent = DropdownMenu.Content 22 23type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']> 24export const DropdownMenuItem = (props: ItemProps & {testID?: string}) => { 25 const theme = useTheme() 26 const [focused, setFocused] = React.useState(false) 27 const backgroundColor = theme.colorScheme === 'dark' ? '#fff1' : '#0001' 28 29 return ( 30 <DropdownMenu.Item 31 className="nativeDropdown-item" 32 {...props} 33 style={StyleSheet.flatten([ 34 styles.item, 35 focused && {backgroundColor: backgroundColor}, 36 ])} 37 onFocus={() => { 38 setFocused(true) 39 }} 40 onBlur={() => { 41 setFocused(false) 42 }} 43 /> 44 ) 45} 46 47// Types for Dropdown Menu and Items 48export type DropdownItem = { 49 label: string | 'separator' 50 onPress?: () => void 51 testID?: string 52 icon?: { 53 ios: MenuItemCommonProps['ios'] 54 android: string 55 web: IconProp 56 } 57} 58type Props = { 59 items: DropdownItem[] 60 testID?: string 61 accessibilityLabel?: string 62 accessibilityHint?: string 63 triggerStyle?: ViewStyle 64} 65 66export function NativeDropdown({ 67 items, 68 children, 69 testID, 70 accessibilityLabel, 71 accessibilityHint, 72 triggerStyle, 73}: React.PropsWithChildren<Props>) { 74 const [open, setOpen] = React.useState(false) 75 const buttonRef = React.useRef<HTMLButtonElement>(null) 76 const menuRef = React.useRef<HTMLDivElement>(null) 77 78 React.useEffect(() => { 79 if (!open) { 80 return 81 } 82 83 function clickHandler(e: MouseEvent) { 84 const t = e.target 85 86 if (!open) return 87 if (!t) return 88 if (!buttonRef.current || !menuRef.current) return 89 90 if ( 91 t !== buttonRef.current && 92 !buttonRef.current.contains(t as Node) && 93 t !== menuRef.current && 94 !menuRef.current.contains(t as Node) 95 ) { 96 // prevent clicking through to links beneath dropdown 97 // only applies to mobile web 98 e.preventDefault() 99 e.stopPropagation() 100 101 // close menu 102 setOpen(false) 103 } 104 } 105 106 function keydownHandler(e: KeyboardEvent) { 107 if (e.key === 'Escape' && open) { 108 setOpen(false) 109 } 110 } 111 112 document.addEventListener('click', clickHandler, true) 113 window.addEventListener('keydown', keydownHandler, true) 114 return () => { 115 document.removeEventListener('click', clickHandler, true) 116 window.removeEventListener('keydown', keydownHandler, true) 117 } 118 }, [open, setOpen]) 119 120 return ( 121 <DropdownMenuRoot open={open} onOpenChange={o => setOpen(o)}> 122 <DropdownMenu.Trigger asChild> 123 <Pressable 124 ref={buttonRef as unknown as React.Ref<View>} 125 testID={testID} 126 accessibilityRole="button" 127 accessibilityLabel={accessibilityLabel} 128 accessibilityHint={accessibilityHint} 129 onPointerDown={e => { 130 // Prevent false positive that interpret mobile scroll as a tap. 131 // This requires the custom onPress handler below to compensate. 132 // https://github.com/radix-ui/primitives/issues/1912 133 e.preventDefault() 134 }} 135 onPress={() => { 136 if (window.event instanceof KeyboardEvent) { 137 // The onPointerDown hack above is not relevant to this press, so don't do anything. 138 return 139 } 140 // Compensate for the disabled onPointerDown above by triggering it manually. 141 setOpen(o => !o) 142 }} 143 hitSlop={HITSLOP_10} 144 style={triggerStyle}> 145 {children} 146 </Pressable> 147 </DropdownMenu.Trigger> 148 149 <DropdownMenu.Portal> 150 <DropdownContent items={items} menuRef={menuRef} /> 151 </DropdownMenu.Portal> 152 </DropdownMenuRoot> 153 ) 154} 155 156function DropdownContent({ 157 items, 158 menuRef, 159}: { 160 items: DropdownItem[] 161 menuRef: React.RefObject<HTMLDivElement> 162}) { 163 const pal = usePalette('default') 164 const theme = useTheme() 165 const dropDownBackgroundColor = 166 theme.colorScheme === 'dark' ? pal.btn : pal.view 167 const {borderColor: separatorColor} = 168 theme.colorScheme === 'dark' ? pal.borderDark : pal.border 169 170 return ( 171 <DropdownMenu.Content 172 ref={menuRef} 173 style={ 174 StyleSheet.flatten([ 175 styles.content, 176 dropDownBackgroundColor, 177 ]) as React.CSSProperties 178 } 179 loop> 180 {items.map((item, index) => { 181 if (item.label === 'separator') { 182 return ( 183 <DropdownMenu.Separator 184 key={getKey(item.label, index, item.testID)} 185 style={ 186 StyleSheet.flatten([ 187 styles.separator, 188 {backgroundColor: separatorColor}, 189 ]) as React.CSSProperties 190 } 191 /> 192 ) 193 } 194 if (index > 1 && items[index - 1].label === 'separator') { 195 return ( 196 <DropdownMenu.Group key={getKey(item.label, index, item.testID)}> 197 <DropdownMenuItem 198 key={getKey(item.label, index, item.testID)} 199 onSelect={item.onPress}> 200 <Text selectable={false} style={[pal.text, styles.itemTitle]}> 201 {item.label} 202 </Text> 203 {item.icon && ( 204 <FontAwesomeIcon 205 icon={item.icon.web} 206 size={20} 207 color={pal.colors.textLight} 208 /> 209 )} 210 </DropdownMenuItem> 211 </DropdownMenu.Group> 212 ) 213 } 214 return ( 215 <DropdownMenuItem 216 key={getKey(item.label, index, item.testID)} 217 onSelect={item.onPress}> 218 <Text selectable={false} style={[pal.text, styles.itemTitle]}> 219 {item.label} 220 </Text> 221 {item.icon && ( 222 <FontAwesomeIcon 223 icon={item.icon.web} 224 size={20} 225 color={pal.colors.textLight} 226 /> 227 )} 228 </DropdownMenuItem> 229 ) 230 })} 231 </DropdownMenu.Content> 232 ) 233} 234 235const getKey = (label: string, index: number, id?: string) => { 236 if (id) { 237 return id 238 } 239 return `${label}_${index}` 240} 241 242const styles = StyleSheet.create({ 243 separator: { 244 height: 1, 245 marginTop: 4, 246 marginBottom: 4, 247 }, 248 content: { 249 backgroundColor: '#f0f0f0', 250 borderRadius: 8, 251 paddingTop: 4, 252 paddingBottom: 4, 253 paddingLeft: 4, 254 paddingRight: 4, 255 marginTop: 6, 256 257 // @ts-ignore web only -prf 258 boxShadow: 'rgba(0, 0, 0, 0.3) 0px 5px 20px', 259 }, 260 item: { 261 display: 'flex', 262 flexDirection: 'row', 263 justifyContent: 'space-between', 264 alignItems: 'center', 265 columnGap: 20, 266 cursor: 'pointer', 267 paddingTop: 8, 268 paddingBottom: 8, 269 paddingLeft: 12, 270 paddingRight: 12, 271 borderRadius: 8, 272 fontFamily: 273 '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif', 274 // @ts-expect-error web only 275 outline: 0, 276 border: 0, 277 }, 278 itemTitle: { 279 fontSize: 16, 280 fontWeight: '600', 281 paddingRight: 10, 282 }, 283})