Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

at cope-settings-sync 150 lines 4.0 kB view raw
1import {createContext, useContext, useEffect, useMemo, useRef} from 'react' 2import EmojiPicker from '@emoji-mart/react' 3import {DropdownMenu} from 'radix-ui' 4 5import {useA11y} from '#/state/a11y' 6import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' 7import {atoms as a, flatten} from '#/alf' 8import * as Menu from '../Menu' 9import {useWebPreloadEmoji} from './preload' 10import { 11 type Emoji, 12 type PickerProps, 13 type RootProps, 14 type TriggerProps, 15} from './types' 16 17export * from './types' 18 19const EmojiPickerContext = createContext<{ 20 onEmojiSelect: (emoji: Emoji) => void 21 nextFocusRef: RootProps['nextFocusRef'] 22} | null>(null) 23 24/** 25 * Provides emoji picker context and wraps children in a {@link Menu.Root}. 26 * 27 * On emoji select, fires a `textInputWebEmitter` event (for web text inputs 28 * that listen for emoji insertions) and forwards to the optional 29 * `onEmojiSelect` callback. 30 * 31 * @platform web 32 */ 33export function Root({ 34 children, 35 control, 36 onEmojiSelect, 37 preloadOnMount = true, 38 nextFocusRef, 39}: RootProps) { 40 useWebPreloadEmoji({immediate: preloadOnMount}) 41 42 const value = useMemo( 43 () => ({ 44 onEmojiSelect: (emoji: Emoji) => { 45 textInputWebEmitter.emit('emoji-inserted', emoji) 46 47 if (onEmojiSelect) onEmojiSelect(emoji) 48 }, 49 nextFocusRef, 50 }), 51 [onEmojiSelect, nextFocusRef], 52 ) 53 54 return ( 55 <EmojiPickerContext value={value}> 56 <Menu.Root control={control}>{children}</Menu.Root> 57 </EmojiPickerContext> 58 ) 59} 60 61/** 62 * Passthrough to {@link Menu.Trigger}. Accepts the same render-prop children 63 * pattern. 64 * 65 * @platform web 66 */ 67export function Trigger(props: TriggerProps) { 68 return <Menu.Trigger {...props} /> 69} 70 71/** 72 * Renders the emoji picker inside a Radix `DropdownMenu.Portal`. 73 * 74 * Holding Shift while selecting an emoji keeps the picker open for 75 * multi-select. Otherwise the menu closes after each selection. 76 * 77 * Must be rendered inside a {@link Root}. 78 * 79 * @platform web 80 */ 81export function Picker({keepOpenWhenShiftHeld = true}: PickerProps) { 82 const {onEmojiSelect, nextFocusRef} = useEmojiPickerContext() 83 const {control} = Menu.useMenuContext() 84 const {reduceMotionEnabled} = useA11y() 85 const isShiftDown = useRef(false) 86 87 useEffect(() => { 88 const onKeyDown = (e: KeyboardEvent) => { 89 if (e.key === 'Shift') { 90 isShiftDown.current = true 91 } 92 } 93 const onKeyUp = (e: KeyboardEvent) => { 94 if (e.key === 'Shift') { 95 isShiftDown.current = false 96 } 97 } 98 window.addEventListener('keydown', onKeyDown, true) 99 window.addEventListener('keyup', onKeyUp, true) 100 101 return () => { 102 window.removeEventListener('keydown', onKeyDown, true) 103 window.removeEventListener('keyup', onKeyUp, true) 104 } 105 }, []) 106 107 return ( 108 <DropdownMenu.Portal> 109 <DropdownMenu.Content 110 sideOffset={5} 111 collisionPadding={{left: 5, right: 5, bottom: 5}} 112 className="dropdown-menu-transform-origin dropdown-menu-constrain-size" 113 onCloseAutoFocus={evt => { 114 if (!nextFocusRef) return 115 let element = 116 nextFocusRef instanceof Function 117 ? nextFocusRef() 118 : nextFocusRef.current 119 if (element) { 120 evt.preventDefault() 121 element.focus() 122 } 123 }}> 124 <div 125 onWheel={evt => evt.stopPropagation()} 126 style={flatten([!reduceMotionEnabled && a.zoom_fade_in])}> 127 <EmojiPicker 128 autoFocus 129 onEmojiSelect={(emoji: Emoji) => { 130 onEmojiSelect(emoji) 131 132 if (!keepOpenWhenShiftHeld || !isShiftDown.current) { 133 control.close() 134 } 135 }} 136 /> 137 </div> 138 </DropdownMenu.Content> 139 </DropdownMenu.Portal> 140 ) 141} 142 143function useEmojiPickerContext() { 144 const ctx = useContext(EmojiPickerContext) 145 if (!ctx) 146 throw new Error( 147 'EmojiPicker.Picker must be used within an EmojiPicker.Root component', 148 ) 149 return ctx 150}