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