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

Configure Feed

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

at 7ba2997af55363b5d381761963710d31f21bb2c9 407 lines 9.8 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 isNative: 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 {minHeight: 32, paddingHorizontal: 10}, 266 web({outline: 0}), 267 (hovered || focused) && 268 !rest.disabled && [ 269 web({outline: '0 !important'}), 270 t.name === 'light' 271 ? t.atoms.bg_contrast_25 272 : t.atoms.bg_contrast_50, 273 ], 274 style, 275 ])} 276 {...web({ 277 onMouseEnter, 278 onMouseLeave, 279 })}> 280 <ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}> 281 {children} 282 </ItemContext.Provider> 283 </Pressable> 284 </DropdownMenu.Item> 285 ) 286} 287 288export function ItemText({children, style}: ItemTextProps) { 289 const t = useTheme() 290 const {disabled} = useMenuItemContext() 291 return ( 292 <Text 293 style={[ 294 a.flex_1, 295 a.font_semi_bold, 296 t.atoms.text_contrast_high, 297 style, 298 disabled && t.atoms.text_contrast_low, 299 ]}> 300 {children} 301 </Text> 302 ) 303} 304 305export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) { 306 const t = useTheme() 307 const {disabled} = useMenuItemContext() 308 return ( 309 <View 310 style={[ 311 position === 'left' && { 312 marginLeft: -2, 313 }, 314 position === 'right' && { 315 marginRight: -2, 316 marginLeft: 12, 317 }, 318 ]}> 319 <Comp 320 size="md" 321 fill={ 322 disabled 323 ? t.atoms.text_contrast_low.color 324 : t.atoms.text_contrast_medium.color 325 } 326 /> 327 </View> 328 ) 329} 330 331export function ItemRadio({selected}: {selected: boolean}) { 332 const t = useTheme() 333 return ( 334 <View 335 style={[ 336 a.justify_center, 337 a.align_center, 338 a.rounded_full, 339 t.atoms.border_contrast_high, 340 { 341 borderWidth: 1, 342 height: 20, 343 width: 20, 344 }, 345 ]}> 346 {selected ? ( 347 <View 348 style={[ 349 a.absolute, 350 a.rounded_full, 351 {height: 14, width: 14}, 352 selected 353 ? { 354 backgroundColor: t.palette.primary_500, 355 } 356 : {}, 357 ]} 358 /> 359 ) : null} 360 </View> 361 ) 362} 363 364export function LabelText({ 365 children, 366 style, 367}: { 368 children: React.ReactNode 369 style?: StyleProp<TextStyle> 370}) { 371 const t = useTheme() 372 return ( 373 <Text 374 style={[ 375 a.font_semi_bold, 376 a.p_sm, 377 t.atoms.text_contrast_low, 378 a.leading_snug, 379 {paddingHorizontal: 10}, 380 style, 381 ]}> 382 {children} 383 </Text> 384 ) 385} 386 387export function Group({children}: GroupProps) { 388 return children 389} 390 391export function Divider() { 392 const t = useTheme() 393 return ( 394 <DropdownMenu.Separator 395 style={flatten([ 396 a.my_xs, 397 t.atoms.bg_contrast_100, 398 a.flex_shrink_0, 399 {height: 1}, 400 ])} 401 /> 402 ) 403} 404 405export function ContainerItem() { 406 return null 407}