Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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