mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1/* eslint-disable react/prop-types */
2
3import React from 'react'
4import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
5import {msg} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
7import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
8
9import {atoms as a, flatten, useTheme, web} from '#/alf'
10import * as Dialog from '#/components/Dialog'
11import {useInteractionState} from '#/components/hooks/useInteractionState'
12import {Context} from '#/components/Menu/context'
13import {
14 ContextType,
15 GroupProps,
16 ItemIconProps,
17 ItemProps,
18 ItemTextProps,
19 RadixPassThroughTriggerProps,
20 TriggerProps,
21} from '#/components/Menu/types'
22import {Portal} from '#/components/Portal'
23import {Text} from '#/components/Typography'
24
25export function useMenuControl(): Dialog.DialogControlProps {
26 const id = React.useId()
27 const [isOpen, setIsOpen] = React.useState(false)
28
29 return React.useMemo(
30 () => ({
31 id,
32 ref: {current: null},
33 isOpen,
34 open() {
35 setIsOpen(true)
36 },
37 close() {
38 setIsOpen(false)
39 },
40 }),
41 [id, isOpen, setIsOpen],
42 )
43}
44
45export function useMemoControlContext() {
46 return React.useContext(Context)
47}
48
49export function Root({
50 children,
51 control,
52}: React.PropsWithChildren<{
53 control?: Dialog.DialogOuterProps['control']
54}>) {
55 const {_} = useLingui()
56 const defaultControl = useMenuControl()
57 const context = React.useMemo<ContextType>(
58 () => ({
59 control: control || defaultControl,
60 }),
61 [control, defaultControl],
62 )
63 const onOpenChange = React.useCallback(
64 (open: boolean) => {
65 if (context.control.isOpen && !open) {
66 context.control.close()
67 } else if (!context.control.isOpen && open) {
68 context.control.open()
69 }
70 },
71 [context.control],
72 )
73
74 return (
75 <Context.Provider value={context}>
76 {context.control.isOpen && (
77 <Portal>
78 <Pressable
79 style={[a.fixed, a.inset_0, a.z_50]}
80 onPress={() => context.control.close()}
81 accessibilityHint=""
82 accessibilityLabel={_(
83 msg`Context menu backdrop, click to close the menu.`,
84 )}
85 />
86 </Portal>
87 )}
88 <DropdownMenu.Root
89 open={context.control.isOpen}
90 onOpenChange={onOpenChange}>
91 {children}
92 </DropdownMenu.Root>
93 </Context.Provider>
94 )
95}
96
97const RadixTriggerPassThrough = React.forwardRef(
98 (
99 props: {
100 children: (
101 props: RadixPassThroughTriggerProps & {
102 ref: React.Ref<any>
103 },
104 ) => React.ReactNode
105 },
106 ref,
107 ) => {
108 // @ts-expect-error Radix provides no types of this stuff
109 return props.children({...props, ref})
110 },
111)
112RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
113
114export function Trigger({children, label}: TriggerProps) {
115 const {control} = React.useContext(Context)
116 const {
117 state: hovered,
118 onIn: onMouseEnter,
119 onOut: onMouseLeave,
120 } = useInteractionState()
121 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
122
123 return (
124 <DropdownMenu.Trigger asChild>
125 <RadixTriggerPassThrough>
126 {props =>
127 children({
128 isNative: false,
129 control,
130 state: {
131 hovered,
132 focused,
133 pressed: false,
134 },
135 props: {
136 ...props,
137 // No-op override to prevent false positive that interprets mobile scroll as a tap.
138 // This requires the custom onPress handler below to compensate.
139 // https://github.com/radix-ui/primitives/issues/1912
140 onPointerDown: undefined,
141 onPress: () => {
142 if (window.event instanceof KeyboardEvent) {
143 // The onPointerDown hack above is not relevant to this press, so don't do anything.
144 return
145 }
146 // Compensate for the disabled onPointerDown above by triggering it manually.
147 if (control.isOpen) {
148 control.close()
149 } else {
150 control.open()
151 }
152 },
153 onFocus: onFocus,
154 onBlur: onBlur,
155 onMouseEnter,
156 onMouseLeave,
157 accessibilityLabel: label,
158 },
159 })
160 }
161 </RadixTriggerPassThrough>
162 </DropdownMenu.Trigger>
163 )
164}
165
166export function Outer({
167 children,
168 style,
169}: React.PropsWithChildren<{
170 showCancel?: boolean
171 style?: StyleProp<ViewStyle>
172}>) {
173 const t = useTheme()
174
175 return (
176 <DropdownMenu.Portal>
177 <DropdownMenu.Content sideOffset={5} loop aria-label="Test">
178 <View
179 style={[
180 a.rounded_sm,
181 a.p_xs,
182 t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
183 t.atoms.shadow_md,
184 style,
185 ]}>
186 {children}
187 </View>
188
189 {/* Disabled until we can fix positioning
190 <DropdownMenu.Arrow
191 className="DropdownMenuArrow"
192 fill={
193 (t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25)
194 .backgroundColor
195 }
196 />
197 */}
198 </DropdownMenu.Content>
199 </DropdownMenu.Portal>
200 )
201}
202
203export function Item({children, label, onPress, ...rest}: ItemProps) {
204 const t = useTheme()
205 const {control} = React.useContext(Context)
206 const {
207 state: hovered,
208 onIn: onMouseEnter,
209 onOut: onMouseLeave,
210 } = useInteractionState()
211 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
212
213 return (
214 <DropdownMenu.Item asChild>
215 <Pressable
216 {...rest}
217 className="radix-dropdown-item"
218 accessibilityHint=""
219 accessibilityLabel={label}
220 onPress={e => {
221 onPress(e)
222
223 /**
224 * Ported forward from Radix
225 * @see https://www.radix-ui.com/primitives/docs/components/dropdown-menu#item
226 */
227 if (!e.defaultPrevented) {
228 control.close()
229 }
230 }}
231 onFocus={onFocus}
232 onBlur={onBlur}
233 // need `flatten` here for Radix compat
234 style={flatten([
235 a.flex_row,
236 a.align_center,
237 a.gap_lg,
238 a.py_sm,
239 a.rounded_xs,
240 {minHeight: 32, paddingHorizontal: 10},
241 web({outline: 0}),
242 (hovered || focused) && [
243 web({outline: '0 !important'}),
244 t.name === 'light'
245 ? t.atoms.bg_contrast_25
246 : t.atoms.bg_contrast_50,
247 ],
248 ])}
249 {...web({
250 onMouseEnter,
251 onMouseLeave,
252 })}>
253 {children}
254 </Pressable>
255 </DropdownMenu.Item>
256 )
257}
258
259export function ItemText({children, style}: ItemTextProps) {
260 const t = useTheme()
261 return (
262 <Text style={[a.flex_1, a.font_bold, t.atoms.text_contrast_high, style]}>
263 {children}
264 </Text>
265 )
266}
267
268export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
269 const t = useTheme()
270 return (
271 <Comp
272 size="md"
273 fill={t.atoms.text_contrast_medium.color}
274 style={[
275 position === 'left' && {
276 marginLeft: -2,
277 },
278 position === 'right' && {
279 marginRight: -2,
280 marginLeft: 12,
281 },
282 ]}
283 />
284 )
285}
286
287export function Group({children}: GroupProps) {
288 return children
289}
290
291export function Divider() {
292 const t = useTheme()
293 return (
294 <DropdownMenu.Separator
295 style={flatten([
296 a.my_xs,
297 t.atoms.bg_contrast_100,
298 {
299 height: 1,
300 },
301 ])}
302 />
303 )
304}