mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at samuel/exp-cli 310 lines 9.2 kB view raw
1import React from 'react' 2import {Platform, Pressable, StyleSheet, View, ViewStyle} from 'react-native' 3import {IconProp} from '@fortawesome/fontawesome-svg-core' 4import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5import * as DropdownMenu from 'zeego/dropdown-menu' 6import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' 7 8import {usePalette} from '#/lib/hooks/usePalette' 9import {useTheme} from '#/lib/ThemeContext' 10import {isIOS} from '#/platform/detection' 11import {Portal} from '#/components/Portal' 12 13// Custom Dropdown Menu Components 14// == 15export const DropdownMenuRoot = DropdownMenu.Root 16// export const DropdownMenuTrigger = DropdownMenu.Trigger 17export const DropdownMenuContent = DropdownMenu.Content 18 19type TriggerProps = Omit< 20 React.ComponentProps<(typeof DropdownMenu)['Trigger']>, 21 'children' 22> & 23 React.PropsWithChildren<{ 24 testID?: string 25 accessibilityLabel?: string 26 accessibilityHint?: string 27 }> 28export const DropdownMenuTrigger = DropdownMenu.create( 29 (props: TriggerProps) => { 30 const theme = useTheme() 31 const defaultCtrlColor = theme.palette.default.postCtrl 32 33 return ( 34 // This Pressable doesn't actually do anything other than 35 // provide the "pressed state" visual feedback. 36 <Pressable 37 testID={props.testID} 38 accessibilityRole="button" 39 accessibilityLabel={props.accessibilityLabel} 40 accessibilityHint={props.accessibilityHint} 41 style={({pressed}) => [{opacity: pressed ? 0.8 : 1}]}> 42 <DropdownMenu.Trigger action="press"> 43 <View> 44 {props.children ? ( 45 props.children 46 ) : ( 47 <FontAwesomeIcon 48 icon="ellipsis" 49 size={20} 50 color={defaultCtrlColor} 51 /> 52 )} 53 </View> 54 </DropdownMenu.Trigger> 55 </Pressable> 56 ) 57 }, 58 'Trigger', 59) 60 61type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']> 62export const DropdownMenuItem = DropdownMenu.create( 63 (props: ItemProps & {testID?: string}) => { 64 const theme = useTheme() 65 const [focused, setFocused] = React.useState(false) 66 const backgroundColor = theme.colorScheme === 'dark' ? '#fff1' : '#0001' 67 68 return ( 69 <DropdownMenu.Item 70 {...props} 71 style={[styles.item, focused && {backgroundColor: backgroundColor}]} 72 onFocus={() => { 73 setFocused(true) 74 props.onFocus && props.onFocus() 75 }} 76 onBlur={() => { 77 setFocused(false) 78 props.onBlur && props.onBlur() 79 }} 80 /> 81 ) 82 }, 83 'Item', 84) 85 86type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']> 87export const DropdownMenuItemTitle = DropdownMenu.create( 88 (props: TitleProps) => { 89 const pal = usePalette('default') 90 return ( 91 <DropdownMenu.ItemTitle 92 {...props} 93 style={[props.style, pal.text, styles.itemTitle]} 94 /> 95 ) 96 }, 97 'ItemTitle', 98) 99 100type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']> 101export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => { 102 return <DropdownMenu.ItemIcon {...props} /> 103}, 'ItemIcon') 104 105type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']> 106export const DropdownMenuSeparator = DropdownMenu.create( 107 (props: SeparatorProps) => { 108 const pal = usePalette('default') 109 const theme = useTheme() 110 const {borderColor: separatorColor} = 111 theme.colorScheme === 'dark' ? pal.borderDark : pal.border 112 return ( 113 <DropdownMenu.Separator 114 {...props} 115 style={[ 116 props.style, 117 styles.separator, 118 {backgroundColor: separatorColor}, 119 ]} 120 /> 121 ) 122 }, 123 'Separator', 124) 125 126// Types for Dropdown Menu and Items 127export type DropdownItem = { 128 label: string | 'separator' 129 onPress?: () => void 130 testID?: string 131 icon?: { 132 ios: MenuItemCommonProps['ios'] 133 android: string 134 web: IconProp 135 } 136} 137type Props = { 138 items: DropdownItem[] 139 testID?: string 140 accessibilityLabel?: string 141 accessibilityHint?: string 142 triggerStyle?: ViewStyle 143} 144 145/* The `NativeDropdown` function uses native iOS and Android dropdown menus. 146 * It also creates a animated custom dropdown for web that uses 147 * Radix UI primitives under the hood 148 * @prop {DropdownItem[]} items - An array of dropdown items 149 * @prop {React.ReactNode} children - A custom dropdown trigger 150 */ 151export function NativeDropdown({ 152 items, 153 children, 154 testID, 155 accessibilityLabel, 156 accessibilityHint, 157}: React.PropsWithChildren<Props>) { 158 const pal = usePalette('default') 159 const theme = useTheme() 160 const [isOpen, setIsOpen] = React.useState(false) 161 const dropDownBackgroundColor = 162 theme.colorScheme === 'dark' ? pal.btn : pal.viewLight 163 164 return ( 165 <> 166 {isIOS && isOpen && ( 167 <Portal> 168 <Backdrop /> 169 </Portal> 170 )} 171 <DropdownMenuRoot onOpenWillChange={setIsOpen}> 172 <DropdownMenuTrigger 173 action="press" 174 testID={testID} 175 accessibilityLabel={accessibilityLabel} 176 accessibilityHint={accessibilityHint}> 177 {children} 178 </DropdownMenuTrigger> 179 {/* @ts-ignore inheriting props from Radix, which is only for web */} 180 <DropdownMenuContent 181 style={[styles.content, dropDownBackgroundColor]} 182 loop> 183 {items.map((item, index) => { 184 if (item.label === 'separator') { 185 return ( 186 <DropdownMenuSeparator 187 key={getKey(item.label, index, item.testID)} 188 /> 189 ) 190 } 191 if (index > 1 && items[index - 1].label === 'separator') { 192 return ( 193 <DropdownMenu.Group 194 key={getKey(item.label, index, item.testID)}> 195 <DropdownMenuItem 196 key={getKey(item.label, index, item.testID)} 197 onSelect={item.onPress}> 198 <DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle> 199 {item.icon && ( 200 <DropdownMenuItemIcon 201 ios={item.icon.ios} 202 // androidIconName={item.icon.android} TODO: Add custom android icon support, because these ones are based on https://developer.android.com/reference/android/R.drawable.html and they are ugly 203 > 204 <FontAwesomeIcon 205 icon={item.icon.web} 206 size={20} 207 style={[pal.text]} 208 /> 209 </DropdownMenuItemIcon> 210 )} 211 </DropdownMenuItem> 212 </DropdownMenu.Group> 213 ) 214 } 215 return ( 216 <DropdownMenuItem 217 key={getKey(item.label, index, item.testID)} 218 onSelect={item.onPress}> 219 <DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle> 220 {item.icon && ( 221 <DropdownMenuItemIcon 222 ios={item.icon.ios} 223 // androidIconName={item.icon.android} 224 > 225 <FontAwesomeIcon 226 icon={item.icon.web} 227 size={20} 228 style={[pal.text]} 229 /> 230 </DropdownMenuItemIcon> 231 )} 232 </DropdownMenuItem> 233 ) 234 })} 235 </DropdownMenuContent> 236 </DropdownMenuRoot> 237 </> 238 ) 239} 240 241function Backdrop() { 242 // Not visible but it eats the click outside. 243 // Only necessary for iOS. 244 return ( 245 <Pressable 246 accessibilityRole="button" 247 accessibilityLabel="Dialog backdrop" 248 accessibilityHint="Press the backdrop to close the dialog" 249 style={{ 250 top: 0, 251 left: 0, 252 right: 0, 253 bottom: 0, 254 position: 'absolute', 255 }} 256 onPress={() => { 257 /* noop */ 258 }} 259 /> 260 ) 261} 262 263const getKey = (label: string, index: number, id?: string) => { 264 if (id) { 265 return id 266 } 267 return `${label}_${index}` 268} 269 270const styles = StyleSheet.create({ 271 separator: { 272 height: 1, 273 marginVertical: 4, 274 }, 275 content: { 276 backgroundColor: '#f0f0f0', 277 borderRadius: 8, 278 paddingVertical: 4, 279 paddingHorizontal: 4, 280 marginTop: 6, 281 ...Platform.select({ 282 web: { 283 animationDuration: '400ms', 284 animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', 285 willChange: 'transform, opacity', 286 animationKeyframes: { 287 '0%': {opacity: 0, transform: [{scale: 0.5}]}, 288 '100%': {opacity: 1, transform: [{scale: 1}]}, 289 }, 290 boxShadow: 291 '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)', 292 transformOrigin: 'var(--radix-dropdown-menu-content-transform-origin)', 293 }, 294 }), 295 }, 296 item: { 297 flexDirection: 'row', 298 justifyContent: 'space-between', 299 alignItems: 'center', 300 columnGap: 20, 301 // @ts-ignore -web 302 cursor: 'pointer', 303 paddingVertical: 8, 304 paddingHorizontal: 12, 305 borderRadius: 8, 306 }, 307 itemTitle: { 308 fontSize: 18, 309 }, 310})