diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/screens/Messages/components/MessageInput.web.tsx | 28 | ||||
-rw-r--r-- | src/screens/Messages/components/MessagesList.tsx | 2 | ||||
-rw-r--r-- | src/state/shell/composer/index.tsx | 3 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 9 | ||||
-rw-r--r-- | src/view/com/composer/text-input/web/EmojiPicker.web.tsx | 87 | ||||
-rw-r--r-- | src/view/shell/Composer.web.tsx | 20 |
6 files changed, 96 insertions, 53 deletions
diff --git a/src/screens/Messages/components/MessageInput.web.tsx b/src/screens/Messages/components/MessageInput.web.tsx index 72e0382a9..bac163685 100644 --- a/src/screens/Messages/components/MessageInput.web.tsx +++ b/src/screens/Messages/components/MessageInput.web.tsx @@ -3,6 +3,7 @@ import {Pressable, StyleSheet, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import Graphemer from 'graphemer' +import {flushSync} from 'react-dom' import TextareaAutosize from 'react-textarea-autosize' import {isSafari, isTouchDevice} from '#/lib/browser' @@ -106,11 +107,19 @@ export function MessageInput({ const onEmojiInserted = React.useCallback( (emoji: Emoji) => { - const position = textAreaRef.current?.selectionStart ?? 0 - setMessage( - message => - message.slice(0, position) + emoji.native + message.slice(position), - ) + if (!textAreaRef.current) { + return + } + const position = textAreaRef.current.selectionStart ?? 0 + textAreaRef.current.focus() + flushSync(() => { + setMessage( + message => + message.slice(0, position) + emoji.native + message.slice(position), + ) + }) + textAreaRef.current.selectionStart = position + emoji.native.length + textAreaRef.current.selectionEnd = position + emoji.native.length }, [setMessage], ) @@ -148,7 +157,14 @@ export function MessageInput({ <Button onPress={e => { e.currentTarget.measure((_fx, _fy, _width, _height, px, py) => { - openEmojiPicker?.({top: py, left: px, right: px, bottom: py}) + openEmojiPicker?.({ + top: py, + left: px, + right: px, + bottom: py, + nextFocusRef: + textAreaRef as unknown as React.MutableRefObject<HTMLElement>, + }) }) }} style={[ diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index ce189459e..071ce1cd7 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -101,7 +101,7 @@ export function MessagesList({ const [emojiPickerState, setEmojiPickerState] = React.useState<EmojiPickerState>({ isOpen: false, - pos: {top: 0, left: 0, right: 0, bottom: 0}, + pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null}, }) // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx index 7138945f4..f1ea41c64 100644 --- a/src/state/shell/composer/index.tsx +++ b/src/state/shell/composer/index.tsx @@ -13,6 +13,7 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {postUriToRelativePath, toBskyAppUrl} from '#/lib/strings/url-helpers' import {purgeTemporaryImageFiles} from '#/state/gallery' import {precacheResolveLinkQuery} from '#/state/queries/resolve-link' +import type {EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker.web' import * as Toast from '#/view/com/util/Toast' export interface ComposerOptsPostRef { @@ -29,7 +30,7 @@ export interface ComposerOpts { onPost?: (postUri: string | undefined) => void quote?: AppBskyFeedDefs.PostView mention?: string // handle of user to mention - openEmojiPicker?: (pos: DOMRect | undefined) => void + openEmojiPicker?: (pos: EmojiPickerPosition | undefined) => void text?: string imageUris?: {uri: string; width: number; height: number; altText?: string}[] videoUri?: {uri: string; width: number; height: number} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index e4b09cf0f..c9e40530e 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -530,7 +530,14 @@ export const ComposePost = ({ } const onEmojiButtonPress = useCallback(() => { - openEmojiPicker?.(textInput.current?.getCursorPosition()) + const rect = textInput.current?.getCursorPosition() + if (rect) { + openEmojiPicker?.({ + ...rect, + nextFocusRef: + textInput as unknown as React.MutableRefObject<HTMLElement>, + }) + } }, [openEmojiPicker]) const scrollViewRef = useAnimatedRef<Animated.ScrollView>() diff --git a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx index c72172902..f5e6a987c 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -1,15 +1,13 @@ import React from 'react' -import { - GestureResponderEvent, - TouchableWithoutFeedback, - useWindowDimensions, - View, -} from 'react-native' +import {Pressable, useWindowDimensions, View} from 'react-native' import Picker from '@emoji-mart/react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {DismissableLayer} from '@radix-ui/react-dismissable-layer' +import {FocusScope} from '@radix-ui/react-focus-scope' import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' -import {atoms as a} from '#/alf' +import {atoms as a, flatten} from '#/alf' import {Portal} from '#/components/Portal' const HEIGHT_OFFSET = 40 @@ -33,6 +31,7 @@ export interface EmojiPickerPosition { left: number right: number bottom: number + nextFocusRef: React.MutableRefObject<HTMLElement> | null } export interface EmojiPickerState { @@ -51,6 +50,7 @@ interface IProps { } export function EmojiPicker({state, close, pinToTop}: IProps) { + const {_} = useLingui() const {height, width} = useWindowDimensions() const isShiftDown = React.useRef(false) @@ -119,48 +119,63 @@ export function EmojiPicker({state, close, pinToTop}: IProps) { if (!state.isOpen) return null - const onPressBackdrop = (e: GestureResponderEvent) => { - // @ts-ignore web only - if (e.nativeEvent?.pointerId === -1) return - close() - } - return ( <Portal> - <TouchableWithoutFeedback - accessibilityRole="button" - onPress={onPressBackdrop} - accessibilityViewIsModal> + <FocusScope + loop + trapped + onUnmountAutoFocus={e => { + const nextFocusRef = state.pos.nextFocusRef + const node = nextFocusRef?.current + if (node) { + e.preventDefault() + node.focus() + } + }}> + <Pressable + accessible + accessibilityLabel={_(msg`Close emoji picker`)} + accessibilityHint={_(msg`Tap to close the emoji picker`)} + onPress={close} + style={[a.fixed, a.inset_0]} + /> + <View - style={[ + style={flatten([ a.fixed, a.w_full, a.h_full, a.align_center, + a.z_10, { top: 0, left: 0, right: 0, }, - ]}> - {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} - <TouchableWithoutFeedback onPress={e => e.stopPropagation()}> - <View style={[{position: 'absolute'}, position]}> - <DismissableLayer - onFocusOutside={evt => evt.preventDefault()} - onDismiss={close}> - <Picker - data={async () => { - return (await import('./EmojiPickerData.json')).default - }} - onEmojiSelect={onInsert} - autoFocus={true} - /> - </DismissableLayer> - </View> - </TouchableWithoutFeedback> + ])}> + <View style={[{position: 'absolute'}, position]}> + <DismissableLayer + onFocusOutside={evt => evt.preventDefault()} + onDismiss={close}> + <Picker + data={async () => { + return (await import('./EmojiPickerData.json')).default + }} + onEmojiSelect={onInsert} + autoFocus={true} + /> + </DismissableLayer> + </View> </View> - </TouchableWithoutFeedback> + + <Pressable + accessible + accessibilityLabel={_(msg`Close emoji picker`)} + accessibilityHint={_(msg`Tap to close the emoji picker`)} + onPress={close} + style={[a.fixed, a.inset_0]} + /> + </FocusScope> </Portal> ) } diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index 47a86ed24..cfd9f6280 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -9,6 +9,7 @@ import {useModals} from '#/state/modals' import {ComposerOpts, useComposerState} from '#/state/shell/composer' import { EmojiPicker, + EmojiPickerPosition, EmojiPickerState, } from '#/view/com/composer/text-input/web/EmojiPicker.web' import {useBreakpoints, useTheme} from '#/alf' @@ -42,16 +43,19 @@ function Inner({state}: {state: ComposerOpts}) { const {gtMobile} = useBreakpoints() const [pickerState, setPickerState] = React.useState<EmojiPickerState>({ isOpen: false, - pos: {top: 0, left: 0, right: 0, bottom: 0}, + pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null}, }) - const onOpenPicker = React.useCallback((pos: DOMRect | undefined) => { - if (!pos) return - setPickerState({ - isOpen: true, - pos, - }) - }, []) + const onOpenPicker = React.useCallback( + (pos: EmojiPickerPosition | undefined) => { + if (!pos) return + setPickerState({ + isOpen: true, + pos, + }) + }, + [], + ) const onClosePicker = React.useCallback(() => { setPickerState(prev => ({ |