mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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})