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>
This commit is contained in:
Samuel Newman
2025-08-08 01:01:01 +03:00
committed by GitHub
parent f708e88410
commit 93c4719a21
18 changed files with 593 additions and 246 deletions
+75 -9
View File
@@ -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>
)
}