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