From 93c4719a2140070b33f69dd0f12b4de2619a25a6 Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Fri, 8 Aug 2025 01:01:01 +0300
Subject: Check handle as you type (#8601)

* check handle as you type

* metrics

* add metric types

* fix overflow

* only check reserved handles for bsky.social, fix test

* change validation check name

* tweak input

* move ghosttext component to textfield

* tweak styles to try and match latest

* add suggestions

* improvements, metrics

* share logic between typeahead and next button

* Apply suggestions from code review

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* update checks, disable button if unavailable

* convert to lowercase

* fix bug with checkHandleAvailability

* add gate

* move files around to make clearer

* fix bad import

* Fix flashing next button

* Enable for TF

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Eric Bailey <git@esb.lol>
---
 src/components/forms/TextField.tsx | 84 ++++++++++++++++++++++++++++++++++----
 1 file changed, 75 insertions(+), 9 deletions(-)

(limited to 'src/components/forms/TextField.tsx')

diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx
index 9b7ada319..3913c3283 100644
--- a/src/components/forms/TextField.tsx
+++ b/src/components/forms/TextField.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import {createContext, useContext, useMemo, useRef} from 'react'
 import {
   type AccessibilityProps,
   StyleSheet,
@@ -16,7 +16,9 @@ import {
   applyFonts,
   atoms as a,
   ios,
+  platform,
   type TextStyleProp,
+  tokens,
   useAlf,
   useTheme,
   web,
@@ -25,7 +27,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {type Props as SVGIconProps} from '#/components/icons/common'
 import {Text} from '#/components/Typography'
 
-const Context = React.createContext<{
+const Context = createContext<{
   inputRef: React.RefObject<TextInput> | null
   isInvalid: boolean
   hovered: boolean
@@ -48,7 +50,7 @@ const Context = React.createContext<{
 export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}>
 
 export function Root({children, isInvalid = false}: RootProps) {
-  const inputRef = React.useRef<TextInput>(null)
+  const inputRef = useRef<TextInput>(null)
   const {
     state: hovered,
     onIn: onHoverIn,
@@ -56,7 +58,7 @@ export function Root({children, isInvalid = false}: RootProps) {
   } = useInteractionState()
   const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
 
-  const context = React.useMemo(
+  const context = useMemo(
     () => ({
       inputRef,
       hovered,
@@ -96,7 +98,7 @@ export function Root({children, isInvalid = false}: RootProps) {
 
 export function useSharedInputStyles() {
   const t = useTheme()
-  return React.useMemo(() => {
+  return useMemo(() => {
     const hover: ViewStyle[] = [
       {
         borderColor: t.palette.contrast_100,
@@ -158,7 +160,7 @@ export function createInput(Component: typeof TextInput) {
   }: InputProps) {
     const t = useTheme()
     const {fonts} = useAlf()
-    const ctx = React.useContext(Context)
+    const ctx = useContext(Context)
     const withinRoot = Boolean(ctx.inputRef)
 
     const {chromeHover, chromeFocus, chromeError, chromeErrorHover} =
@@ -283,8 +285,8 @@ export function LabelText({
 
 export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
   const t = useTheme()
-  const ctx = React.useContext(Context)
-  const {hover, focus, errorHover, errorFocus} = React.useMemo(() => {
+  const ctx = useContext(Context)
+  const {hover, focus, errorHover, errorFocus} = useMemo(() => {
     const hover: TextStyle[] = [
       {
         color: t.palette.contrast_800,
@@ -342,7 +344,7 @@ export function SuffixText({
   }
 >) {
   const t = useTheme()
-  const ctx = React.useContext(Context)
+  const ctx = useContext(Context)
   return (
     <Text
       accessibilityLabel={label}
@@ -362,3 +364,67 @@ export function SuffixText({
     </Text>
   )
 }
+
+export function GhostText({
+  children,
+  value,
+}: {
+  children: string
+  value: string
+}) {
+  const t = useTheme()
+  // eslint-disable-next-line bsky-internal/avoid-unwrapped-text
+  return (
+    <View
+      style={[
+        a.pointer_events_none,
+        a.absolute,
+        a.z_10,
+        {
+          paddingLeft: platform({
+            native:
+              // input padding
+              tokens.space.md +
+              // icon
+              tokens.space.xl +
+              // icon padding
+              tokens.space.xs +
+              // text input padding
+              tokens.space.xs,
+            web:
+              // icon
+              tokens.space.xl +
+              // icon padding
+              tokens.space.xs +
+              // text input padding
+              tokens.space.xs,
+          }),
+        },
+        web(a.pr_md),
+        a.overflow_hidden,
+        a.max_w_full,
+      ]}
+      aria-hidden={true}
+      accessibilityElementsHidden
+      importantForAccessibility="no-hide-descendants">
+      <Text
+        style={[
+          {color: 'transparent'},
+          a.text_md,
+          {lineHeight: a.text_md.fontSize * 1.1875},
+          a.w_full,
+        ]}
+        numberOfLines={1}>
+        {children}
+        <Text
+          style={[
+            t.atoms.text_contrast_low,
+            a.text_md,
+            {lineHeight: a.text_md.fontSize * 1.1875},
+          ]}>
+          {value}
+        </Text>
+      </Text>
+    </View>
+  )
+}
-- 
cgit 1.4.1

