Bluesky app fork with some witchin' additions 馃挮
at post-text-option 180 lines 4.7 kB view raw
1import React from 'react' 2import {Pressable, useWindowDimensions, View} from 'react-native' 3import Picker from '@emoji-mart/react' 4import {msg} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6import {DismissableLayer, FocusScope} from 'radix-ui/internal' 7 8import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' 9import {atoms as a, flatten} from '#/alf' 10import {Portal} from '#/components/Portal' 11 12const HEIGHT_OFFSET = 40 13const WIDTH_OFFSET = 100 14const PICKER_HEIGHT = 435 + HEIGHT_OFFSET 15const PICKER_WIDTH = 350 + WIDTH_OFFSET 16 17export type Emoji = { 18 aliases?: string[] 19 emoticons: string[] 20 id: string 21 keywords: string[] 22 name: string 23 native: string 24 shortcodes?: string 25 unified: string 26} 27 28export interface EmojiPickerPosition { 29 top: number 30 left: number 31 right: number 32 bottom: number 33 nextFocusRef: React.MutableRefObject<HTMLElement> | null 34} 35 36export interface EmojiPickerState { 37 isOpen: boolean 38 pos: EmojiPickerPosition 39} 40 41interface IProps { 42 state: EmojiPickerState 43 close: () => void 44 /** 45 * If `true`, overrides position and ensures picker is pinned to the top of 46 * the target element. 47 */ 48 pinToTop?: boolean 49} 50 51export function EmojiPicker({state, close, pinToTop}: IProps) { 52 const {_} = useLingui() 53 const {height, width} = useWindowDimensions() 54 55 const isShiftDown = React.useRef(false) 56 57 const position = React.useMemo(() => { 58 if (pinToTop) { 59 return { 60 top: state.pos.top - PICKER_HEIGHT + HEIGHT_OFFSET - 10, 61 left: state.pos.left, 62 } 63 } 64 65 const fitsBelow = state.pos.top + PICKER_HEIGHT < height 66 const fitsAbove = PICKER_HEIGHT < state.pos.top 67 const placeOnLeft = PICKER_WIDTH < state.pos.left 68 const screenYMiddle = height / 2 - PICKER_HEIGHT / 2 69 70 if (fitsBelow) { 71 return { 72 top: state.pos.top + HEIGHT_OFFSET, 73 } 74 } else if (fitsAbove) { 75 return { 76 bottom: height - state.pos.bottom + HEIGHT_OFFSET, 77 } 78 } else { 79 return { 80 top: screenYMiddle, 81 left: placeOnLeft ? state.pos.left - PICKER_WIDTH : undefined, 82 right: !placeOnLeft 83 ? width - state.pos.right - PICKER_WIDTH 84 : undefined, 85 } 86 } 87 }, [state.pos, height, width, pinToTop]) 88 89 React.useEffect(() => { 90 if (!state.isOpen) return 91 92 const onKeyDown = (e: KeyboardEvent) => { 93 if (e.key === 'Shift') { 94 isShiftDown.current = true 95 } 96 } 97 const onKeyUp = (e: KeyboardEvent) => { 98 if (e.key === 'Shift') { 99 isShiftDown.current = false 100 } 101 } 102 window.addEventListener('keydown', onKeyDown, true) 103 window.addEventListener('keyup', onKeyUp, true) 104 105 return () => { 106 window.removeEventListener('keydown', onKeyDown, true) 107 window.removeEventListener('keyup', onKeyUp, true) 108 } 109 }, [state.isOpen]) 110 111 const onInsert = (emoji: Emoji) => { 112 textInputWebEmitter.emit('emoji-inserted', emoji) 113 114 if (!isShiftDown.current) { 115 close() 116 } 117 } 118 119 if (!state.isOpen) return null 120 121 return ( 122 <Portal> 123 <FocusScope.FocusScope 124 loop 125 trapped 126 onUnmountAutoFocus={e => { 127 const nextFocusRef = state.pos.nextFocusRef 128 const node = nextFocusRef?.current 129 if (node) { 130 e.preventDefault() 131 node.focus() 132 } 133 }}> 134 <Pressable 135 accessible 136 accessibilityLabel={_(msg`Close emoji picker`)} 137 accessibilityHint={_(msg`Closes the emoji picker`)} 138 onPress={close} 139 style={[a.fixed, a.inset_0]} 140 /> 141 142 <View 143 style={flatten([ 144 a.fixed, 145 a.w_full, 146 a.h_full, 147 a.align_center, 148 a.z_10, 149 { 150 top: 0, 151 left: 0, 152 right: 0, 153 }, 154 ])}> 155 <View style={[{position: 'absolute'}, position]}> 156 <DismissableLayer.DismissableLayer 157 onFocusOutside={evt => evt.preventDefault()} 158 onDismiss={close}> 159 <Picker 160 data={async () => { 161 return (await import('./EmojiPickerData.json')).default 162 }} 163 onEmojiSelect={onInsert} 164 autoFocus={true} 165 /> 166 </DismissableLayer.DismissableLayer> 167 </View> 168 </View> 169 170 <Pressable 171 accessible 172 accessibilityLabel={_(msg`Close emoji picker`)} 173 accessibilityHint={_(msg`Closes the emoji picker`)} 174 onPress={close} 175 style={[a.fixed, a.inset_0]} 176 /> 177 </FocusScope.FocusScope> 178 </Portal> 179 ) 180}