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