forked from
jollywhoppers.com/witchsky.app
fork
Configure Feed
Select the types of activity you want to include in your feed.
Bluesky app fork with some witchin' additions 馃挮
fork
Configure Feed
Select the types of activity you want to include in your feed.
1import {forwardRef, useCallback, useId, useMemo, useState} from 'react'
2import {
3 Pressable,
4 type StyleProp,
5 type TextStyle,
6 View,
7 type ViewStyle,
8} from 'react-native'
9import {msg} from '@lingui/macro'
10import {useLingui} from '@lingui/react'
11import {DropdownMenu} from 'radix-ui'
12
13import {useA11y} from '#/state/a11y'
14import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
15import {atoms as a, flatten, useTheme, web} from '#/alf'
16import type * as Dialog from '#/components/Dialog'
17import {useInteractionState} from '#/components/hooks/useInteractionState'
18import {
19 Context,
20 ItemContext,
21 useMenuContext,
22 useMenuItemContext,
23} from '#/components/Menu/context'
24import {
25 type ContextType,
26 type GroupProps,
27 type ItemIconProps,
28 type ItemProps,
29 type ItemTextProps,
30 type RadixPassThroughTriggerProps,
31 type TriggerProps,
32} from '#/components/Menu/types'
33import {Portal} from '#/components/Portal'
34import {Text} from '#/components/Typography'
35
36export {useMenuContext}
37
38export function useMenuControl(): Dialog.DialogControlProps {
39 const id = useId()
40 const [isOpen, setIsOpen] = useState(false)
41
42 return useMemo(
43 () => ({
44 id,
45 ref: {current: null},
46 isOpen,
47 open() {
48 setIsOpen(true)
49 },
50 close() {
51 setIsOpen(false)
52 },
53 }),
54 [id, isOpen, setIsOpen],
55 )
56}
57
58export function Root({
59 children,
60 control,
61}: React.PropsWithChildren<{
62 control?: Dialog.DialogControlProps
63}>) {
64 const {_} = useLingui()
65 const defaultControl = useMenuControl()
66 const context = useMemo<ContextType>(
67 () => ({
68 control: control || defaultControl,
69 }),
70 [control, defaultControl],
71 )
72 const onOpenChange = useCallback(
73 (open: boolean) => {
74 if (context.control.isOpen && !open) {
75 context.control.close()
76 } else if (!context.control.isOpen && open) {
77 context.control.open()
78 }
79 },
80 [context.control],
81 )
82
83 return (
84 <Context.Provider value={context}>
85 {context.control.isOpen && (
86 <Portal>
87 <Pressable
88 style={[a.fixed, a.inset_0, a.z_50]}
89 onPress={() => context.control.close()}
90 accessibilityHint=""
91 accessibilityLabel={_(
92 msg`Context menu backdrop, click to close the menu.`,
93 )}
94 />
95 </Portal>
96 )}
97 <DropdownMenu.Root
98 open={context.control.isOpen}
99 onOpenChange={onOpenChange}>
100 {children}
101 </DropdownMenu.Root>
102 </Context.Provider>
103 )
104}
105
106const RadixTriggerPassThrough = forwardRef(
107 (
108 props: {
109 children: (
110 props: RadixPassThroughTriggerProps & {
111 ref: React.Ref<any>
112 },
113 ) => React.ReactNode
114 },
115 ref,
116 ) => {
117 // @ts-expect-error Radix provides no types of this stuff
118 return props.children({...props, ref})
119 },
120)
121RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
122
123export function Trigger({
124 children,
125 label,
126 role = 'button',
127 hint,
128}: TriggerProps) {
129 const {control} = useMenuContext()
130 const {
131 state: hovered,
132 onIn: onMouseEnter,
133 onOut: onMouseLeave,
134 } = useInteractionState()
135 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
136
137 return (
138 <DropdownMenu.Trigger asChild>
139 <RadixTriggerPassThrough>
140 {props =>
141 children({
142 IS_NATIVE: false,
143 control,
144 state: {
145 hovered,
146 focused,
147 pressed: false,
148 },
149 props: {
150 ...props,
151 // No-op override to prevent false positive that interprets mobile scroll as a tap.
152 // This requires the custom onPress handler below to compensate.
153 // https://github.com/radix-ui/primitives/issues/1912
154 onPointerDown: undefined,
155 onPress: () => {
156 if (window.event instanceof KeyboardEvent) {
157 // The onPointerDown hack above is not relevant to this press, so don't do anything.
158 return
159 }
160 // Compensate for the disabled onPointerDown above by triggering it manually.
161 if (control.isOpen) {
162 control.close()
163 } else {
164 control.open()
165 }
166 },
167 onFocus: onFocus,
168 onBlur: onBlur,
169 onMouseEnter,
170 onMouseLeave,
171 accessibilityHint: hint,
172 accessibilityLabel: label,
173 accessibilityRole: role,
174 },
175 })
176 }
177 </RadixTriggerPassThrough>
178 </DropdownMenu.Trigger>
179 )
180}
181
182export function Outer({
183 children,
184 style,
185}: React.PropsWithChildren<{
186 showCancel?: boolean
187 style?: StyleProp<ViewStyle>
188}>) {
189 const t = useTheme()
190 const {reduceMotionEnabled} = useA11y()
191
192 return (
193 <DropdownMenu.Portal>
194 <DropdownMenu.Content
195 sideOffset={5}
196 collisionPadding={{left: 5, right: 5, bottom: 5}}
197 loop
198 aria-label="Test"
199 className="dropdown-menu-transform-origin dropdown-menu-constrain-size">
200 <View
201 style={[
202 a.rounded_sm,
203 a.p_xs,
204 a.border,
205 t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
206 t.atoms.shadow_md,
207 t.atoms.border_contrast_low,
208 a.overflow_auto,
209 !reduceMotionEnabled && a.zoom_fade_in,
210 style,
211 ]}>
212 {children}
213 </View>
214
215 {/* Disabled until we can fix positioning
216 <DropdownMenu.Arrow
217 className="DropdownMenuArrow"
218 fill={
219 (t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25)
220 .backgroundColor
221 }
222 />
223 */}
224 </DropdownMenu.Content>
225 </DropdownMenu.Portal>
226 )
227}
228
229export function Item({children, label, onPress, style, ...rest}: ItemProps) {
230 const t = useTheme()
231 const {control} = useMenuContext()
232 const {
233 state: hovered,
234 onIn: onMouseEnter,
235 onOut: onMouseLeave,
236 } = useInteractionState()
237 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
238
239 return (
240 <DropdownMenu.Item asChild>
241 <Pressable
242 {...rest}
243 className="radix-dropdown-item"
244 accessibilityHint=""
245 accessibilityLabel={label}
246 onPress={e => {
247 onPress(e)
248
249 /**
250 * Ported forward from Radix
251 * @see https://www.radix-ui.com/primitives/docs/components/dropdown-menu#item
252 */
253 if (!e.defaultPrevented) {
254 control.close()
255 }
256 }}
257 onFocus={onFocus}
258 onBlur={onBlur}
259 // need `flatten` here for Radix compat
260 style={flatten([
261 a.flex_row,
262 a.align_center,
263 a.gap_lg,
264 a.py_sm,
265 a.rounded_xs,
266 a.overflow_hidden,
267 {minHeight: 32, paddingHorizontal: 10},
268 web({outline: 0}),
269 (hovered || focused) &&
270 !rest.disabled && [
271 web({outline: '0 !important'}),
272 t.name === 'light'
273 ? t.atoms.bg_contrast_25
274 : t.atoms.bg_contrast_50,
275 ],
276 style,
277 ])}
278 {...web({
279 onMouseEnter,
280 onMouseLeave,
281 })}>
282 <ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}>
283 {children}
284 </ItemContext.Provider>
285 </Pressable>
286 </DropdownMenu.Item>
287 )
288}
289
290export function ItemText({children, style}: ItemTextProps) {
291 const t = useTheme()
292 const {disabled} = useMenuItemContext()
293 return (
294 <Text
295 style={[
296 a.flex_1,
297 a.font_semi_bold,
298 t.atoms.text_contrast_high,
299 style,
300 disabled && t.atoms.text_contrast_low,
301 ]}>
302 {children}
303 </Text>
304 )
305}
306
307export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) {
308 const t = useTheme()
309 const {disabled} = useMenuItemContext()
310 return (
311 <View
312 style={[
313 position === 'left' && {
314 marginLeft: -2,
315 },
316 position === 'right' && {
317 marginRight: -2,
318 marginLeft: 12,
319 },
320 ]}>
321 <Comp
322 size="md"
323 fill={
324 fill
325 ? fill({disabled})
326 : disabled
327 ? t.atoms.text_contrast_low.color
328 : t.atoms.text_contrast_medium.color
329 }
330 />
331 </View>
332 )
333}
334
335export function ItemRadio({selected}: {selected: boolean}) {
336 const t = useTheme()
337 const enableSquareButtons = useEnableSquareButtons()
338 return (
339 <View
340 style={[
341 a.justify_center,
342 a.align_center,
343 enableSquareButtons ? a.rounded_sm : a.rounded_full,
344 t.atoms.border_contrast_high,
345 {
346 borderWidth: 1,
347 height: 20,
348 width: 20,
349 },
350 ]}>
351 {selected ? (
352 <View
353 style={[
354 a.absolute,
355 enableSquareButtons ? a.rounded_sm : a.rounded_full,
356 {height: 14, width: 14},
357 selected
358 ? {
359 backgroundColor: t.palette.primary_500,
360 }
361 : {},
362 ]}
363 />
364 ) : null}
365 </View>
366 )
367}
368
369export function LabelText({
370 children,
371 style,
372}: {
373 children: React.ReactNode
374 style?: StyleProp<TextStyle>
375}) {
376 const t = useTheme()
377 return (
378 <Text
379 style={[
380 a.font_semi_bold,
381 a.p_sm,
382 t.atoms.text_contrast_low,
383 a.leading_snug,
384 {paddingHorizontal: 10},
385 style,
386 ]}>
387 {children}
388 </Text>
389 )
390}
391
392export function Group({children}: GroupProps) {
393 return children
394}
395
396export function Divider() {
397 const t = useTheme()
398 return (
399 <DropdownMenu.Separator
400 style={flatten([
401 a.my_xs,
402 t.atoms.bg_contrast_100,
403 a.flex_shrink_0,
404 {height: 1},
405 ])}
406 />
407 )
408}
409
410export function ContainerItem() {
411 return null
412}