From 20fa7feefed8ed4e168233f319997ceab4151c78 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 20 Dec 2024 10:09:37 -0600 Subject: [PATCH 1/7] Only portal the emoji picker where needed --- .../Messages/components/MessagesList.tsx | 15 ++-- .../text-input/web/EmojiPicker.web.tsx | 71 +++++++++---------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index ce189459e9..4864fd099a 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -38,6 +38,7 @@ import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill' import {MessageItem} from '#/components/dms/MessageItem' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {Loader} from '#/components/Loader' +import {Portal} from '#/components/Portal' import {Text} from '#/components/Typography' import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed' @@ -448,11 +449,15 @@ export function MessagesList({ {isWeb && ( - setEmojiPickerState(prev => ({...prev, isOpen: false}))} - /> + + + setEmojiPickerState(prev => ({...prev, isOpen: false})) + } + /> + )} {newMessagesPill.show && } 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 c721729020..1d5dad4861 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -10,7 +10,6 @@ import {DismissableLayer} from '@radix-ui/react-dismissable-layer' import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' import {atoms as a} from '#/alf' -import {Portal} from '#/components/Portal' const HEIGHT_OFFSET = 40 const WIDTH_OFFSET = 100 @@ -126,41 +125,39 @@ export function EmojiPicker({state, close, pinToTop}: IProps) { } return ( - - - - {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} - e.stopPropagation()}> - - evt.preventDefault()} - onDismiss={close}> - { - return (await import('./EmojiPickerData.json')).default - }} - onEmojiSelect={onInsert} - autoFocus={true} - /> - - - - - - + + + {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} + e.stopPropagation()}> + + evt.preventDefault()} + onDismiss={close}> + { + return (await import('./EmojiPickerData.json')).default + }} + onEmojiSelect={onInsert} + autoFocus={true} + /> + + + + + ) } From 03eb1d2368050b067d5578c9f5c8d3cf1e4c46f9 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 20 Dec 2024 10:11:52 -0600 Subject: [PATCH 2/7] Add optional portal prop to emoji picker --- src/screens/Messages/components/MessagesList.tsx | 16 ++++++---------- .../composer/text-input/web/EmojiPicker.web.tsx | 11 +++++++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 4864fd099a..6299e96d59 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -38,7 +38,6 @@ import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill' import {MessageItem} from '#/components/dms/MessageItem' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {Loader} from '#/components/Loader' -import {Portal} from '#/components/Portal' import {Text} from '#/components/Typography' import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed' @@ -449,15 +448,12 @@ export function MessagesList({ {isWeb && ( - - - setEmojiPickerState(prev => ({...prev, isOpen: false})) - } - /> - + setEmojiPickerState(prev => ({...prev, isOpen: false}))} + /> )} {newMessagesPill.show && } 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 1d5dad4861..9c036757ed 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -10,6 +10,7 @@ import {DismissableLayer} from '@radix-ui/react-dismissable-layer' import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' import {atoms as a} from '#/alf' +import {Portal} from '#/components/Portal' const HEIGHT_OFFSET = 40 const WIDTH_OFFSET = 100 @@ -47,9 +48,14 @@ interface IProps { * the target element. */ pinToTop?: boolean + /** + * If `true`, renders the picker in a portal at the document root. Useful if + * the positioning is getting wacky due to other wrapping elements. + */ + portal?: boolean } -export function EmojiPicker({state, close, pinToTop}: IProps) { +export function EmojiPicker({state, close, pinToTop, portal}: IProps) { const {height, width} = useWindowDimensions() const isShiftDown = React.useRef(false) @@ -124,7 +130,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) { close() } - return ( + const picker = ( ) + return portal ? {picker} : picker } From 07e2fbf45862f906be12b0aa9d82d6eb9a3a70fb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 20 Dec 2024 10:32:32 -0600 Subject: [PATCH 3/7] Use FocusScope to our advantage --- .../Messages/components/MessagesList.tsx | 1 - .../text-input/web/EmojiPicker.web.tsx | 79 +++++++++---------- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 6299e96d59..ce189459e9 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -449,7 +449,6 @@ export function MessagesList({ {isWeb && ( setEmojiPickerState(prev => ({...prev, isOpen: false}))} 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 9c036757ed..954a2d1e45 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -7,6 +7,7 @@ import { } from 'react-native' import Picker from '@emoji-mart/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' @@ -48,14 +49,9 @@ interface IProps { * the target element. */ pinToTop?: boolean - /** - * If `true`, renders the picker in a portal at the document root. Useful if - * the positioning is getting wacky due to other wrapping elements. - */ - portal?: boolean } -export function EmojiPicker({state, close, pinToTop, portal}: IProps) { +export function EmojiPicker({state, close, pinToTop}: IProps) { const {height, width} = useWindowDimensions() const isShiftDown = React.useRef(false) @@ -130,41 +126,44 @@ export function EmojiPicker({state, close, pinToTop, portal}: IProps) { close() } - const picker = ( - - - {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} - e.stopPropagation()}> - - evt.preventDefault()} - onDismiss={close}> - { - return (await import('./EmojiPickerData.json')).default - }} - onEmojiSelect={onInsert} - autoFocus={true} - /> - + return ( + + + + + {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} + e.stopPropagation()}> + + evt.preventDefault()} + onDismiss={close}> + { + return (await import('./EmojiPickerData.json')).default + }} + onEmojiSelect={onInsert} + autoFocus={true} + /> + + + - - + + ) - return portal ? {picker} : picker } From 8da2234e17690e11be782e0638d4326c70ed6864 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 20 Dec 2024 11:12:17 -0600 Subject: [PATCH 4/7] Pare back, add guards, fix focus trap --- .../text-input/web/EmojiPicker.web.tsx | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) 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 954a2d1e45..4ea74f6bbc 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -1,16 +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 @@ -52,6 +49,7 @@ interface IProps { } export function EmojiPicker({state, close, pinToTop}: IProps) { + const {_} = useLingui() const {height, width} = useWindowDimensions() const isShiftDown = React.useRef(false) @@ -120,49 +118,52 @@ 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 ( - - - - {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} - e.stopPropagation()}> - - evt.preventDefault()} - onDismiss={close}> - { - return (await import('./EmojiPickerData.json')).default - }} - onEmojiSelect={onInsert} - autoFocus={true} - /> - - - + + + + + + evt.preventDefault()} + onDismiss={close}> + { + return (await import('./EmojiPickerData.json')).default + }} + onEmojiSelect={onInsert} + autoFocus={true} + /> + - + + + ) From 3fe55d8f6f6e95f5280116efc080da0e0d83f533 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 20 Dec 2024 11:33:38 -0600 Subject: [PATCH 5/7] Don't return focus to emoji button --- src/view/com/composer/text-input/web/EmojiPicker.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4ea74f6bbc..93301a68c0 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -120,7 +120,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) { return ( - + e.preventDefault()}> Date: Fri, 20 Dec 2024 18:42:43 +0000 Subject: [PATCH 6/7] Set DM input position on emoji insert --- .../Messages/components/MessageInput.web.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/screens/Messages/components/MessageInput.web.tsx b/src/screens/Messages/components/MessageInput.web.tsx index 72e0382a93..5ac738ad51 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], ) From e6888847ef49f32e1db798ce9c8d28cf063c2b02 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 20 Dec 2024 19:40:53 +0000 Subject: [PATCH 7/7] Let the caller determine next focus node --- .../Messages/components/MessageInput.web.tsx | 9 ++++++++- .../Messages/components/MessagesList.tsx | 2 +- src/state/shell/composer/index.tsx | 3 ++- src/view/com/composer/Composer.tsx | 9 ++++++++- .../text-input/web/EmojiPicker.web.tsx | 13 +++++++++++- src/view/shell/Composer.web.tsx | 20 +++++++++++-------- 6 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/screens/Messages/components/MessageInput.web.tsx b/src/screens/Messages/components/MessageInput.web.tsx index 5ac738ad51..bac163685a 100644 --- a/src/screens/Messages/components/MessageInput.web.tsx +++ b/src/screens/Messages/components/MessageInput.web.tsx @@ -157,7 +157,14 @@ export function MessageInput({