From 8a4398608acac5f44e8d3c0ea8f6976b8ae1119a Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Fri, 29 Aug 2025 03:03:12 +0300
Subject: 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
---
 src/view/com/composer/text-input/TextInput.web.tsx | 104 ++++++++++-----------
 1 file changed, 49 insertions(+), 55 deletions(-)

(limited to 'src/view/com/composer/text-input/TextInput.web.tsx')

diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index cb7ed194a..9f6cc6ae2 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -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,
-- 
cgit 1.4.1

