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