Close web mention suggestions popup on Escape (#8605)

* alf web typeahead

* fix type error

* fix escape behaviour

* change selection on hover

* rm React.

* undo random change
This commit is contained in:
Samuel Newman
2025-08-29 03:03:12 +03:00
committed by GitHub
parent 27c1058568
commit 8a4398608a
5 changed files with 251 additions and 271 deletions
@@ -1,4 +1,11 @@
import React, {useRef} from 'react'
import {
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import {StyleSheet, View} from 'react-native'
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
@@ -16,7 +23,6 @@ import {EditorContent, type JSONContent, useEditor} from '@tiptap/react'
import Graphemer from 'graphemer'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {usePalette} from '#/lib/hooks/usePalette'
import {blobToDataUri, isUriImage} from '#/lib/media/util'
import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
import {
@@ -27,57 +33,34 @@ import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEm
import {atoms as a, useAlf} from '#/alf'
import {normalizeTextStyles} from '#/alf/typography'
import {Portal} from '#/components/Portal'
import {Text} from '../../util/text/Text'
import {createSuggestion} from './web/Autocomplete'
import {Text} from '#/components/Typography'
import {type TextInputProps} from './TextInput.types'
import {type AutocompleteRef, createSuggestion} from './web/Autocomplete'
import {type Emoji} from './web/EmojiPicker'
import {LinkDecorator} from './web/LinkDecorator'
import {TagDecorator} from './web/TagDecorator'
export interface TextInputRef {
focus: () => void
blur: () => void
getCursorPosition: () => DOMRect | undefined
}
interface TextInputProps {
richtext: RichText
placeholder: string
suggestedLinks: Set<string>
webForceMinHeight: boolean
hasRightPadding: boolean
isActive: boolean
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => void
onNewLink: (uri: string) => void
onError: (err: string) => void
onFocus: () => void
}
export const TextInput = React.forwardRef(function TextInputImpl(
{
richtext,
placeholder,
webForceMinHeight,
hasRightPadding,
isActive,
setRichText,
onPhotoPasted,
onPressPublish,
onNewLink,
onFocus,
}: // onError, TODO
TextInputProps,
export function TextInput({
ref,
) {
richtext,
placeholder,
webForceMinHeight,
hasRightPadding,
isActive,
setRichText,
onPhotoPasted,
onPressPublish,
onNewLink,
onFocus,
}: TextInputProps) {
const {theme: t, fonts} = useAlf()
const autocomplete = useActorAutocompleteFn()
const pal = usePalette('default')
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
const [isDropping, setIsDropping] = React.useState(false)
const [isDropping, setIsDropping] = useState(false)
const autocompleteRef = useRef<AutocompleteRef>(null)
const extensions = React.useMemo(
const extensions = useMemo(
() => [
Document,
LinkDecorator,
@@ -86,7 +69,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
HTMLAttributes: {
class: 'mention',
},
suggestion: createSuggestion({autocomplete}),
suggestion: createSuggestion({autocomplete, autocompleteRef}),
}),
Paragraph,
Placeholder.configure({
@@ -99,7 +82,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
[autocomplete, placeholder],
)
React.useEffect(() => {
useEffect(() => {
if (!isActive) {
return
}
@@ -109,7 +92,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
}
}, [onPressPublish, isActive])
React.useEffect(() => {
useEffect(() => {
if (!isActive) {
return
}
@@ -119,7 +102,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
}
}, [isActive, onPhotoPasted])
React.useEffect(() => {
useEffect(() => {
if (!isActive) {
return
}
@@ -296,13 +279,13 @@ export const TextInput = React.forwardRef(function TextInputImpl(
[modeClass],
)
const onEmojiInserted = React.useCallback(
const onEmojiInserted = useCallback(
(emoji: Emoji) => {
editor?.chain().focus().insertContent(emoji.native).run()
},
[editor],
)
React.useEffect(() => {
useEffect(() => {
if (!isActive) {
return
}
@@ -312,7 +295,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
}
}, [onEmojiInserted, isActive])
React.useImperativeHandle(ref, () => ({
useImperativeHandle(ref, () => ({
focus: () => {
editor?.chain().focus()
},
@@ -323,9 +306,10 @@ export const TextInput = React.forwardRef(function TextInputImpl(
const pos = editor?.state.selection.$anchor.pos
return pos ? editor?.view.coordsAtPos(pos) : undefined
},
maybeClosePopup: () => autocompleteRef.current?.maybeClose() ?? false,
}))
const inputStyle = React.useMemo(() => {
const inputStyle = useMemo(() => {
const style = normalizeTextStyles(
[a.text_lg, a.leading_snug, t.atoms.text],
{
@@ -360,10 +344,20 @@ export const TextInput = React.forwardRef(function TextInputImpl(
style={styles.dropContainer}
entering={FadeIn.duration(80)}
exiting={FadeOut.duration(80)}>
<View style={[pal.view, pal.border, styles.dropModal]}>
<View
style={[
t.atoms.bg,
t.atoms.border_contrast_low,
styles.dropModal,
]}>
<Text
type="lg"
style={[pal.text, pal.borderDark, styles.dropText]}>
style={[
a.text_lg,
a.font_bold,
t.atoms.text_contrast_medium,
t.atoms.border_contrast_high,
styles.dropText,
]}>
<Trans>Drop to add images</Trans>
</Text>
</View>
@@ -372,7 +366,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
)}
</>
)
})
}
function editorJsonToText(
json: JSONContent,