import {createContext, useContext, useMemo, useRef} from 'react' import { type AccessibilityProps, StyleSheet, TextInput, type TextInputProps, type TextStyle, View, type ViewStyle, } from 'react-native' import {HITSLOP_20} from '#/lib/constants' import {mergeRefs} from '#/lib/merge-refs' import { android, applyFonts, atoms as a, ios, platform, type TextStyleProp, tokens, useAlf, useTheme, web, } from '#/alf' import {useInteractionState} from '#/components/hooks/useInteractionState' import {type Props as SVGIconProps} from '#/components/icons/common' import {Text} from '#/components/Typography' const Context = createContext<{ inputRef: React.RefObject | null isInvalid: boolean hovered: boolean onHoverIn: () => void onHoverOut: () => void focused: boolean onFocus: () => void onBlur: () => void }>({ inputRef: null, isInvalid: false, hovered: false, onHoverIn: () => {}, onHoverOut: () => {}, focused: false, onFocus: () => {}, onBlur: () => {}, }) Context.displayName = 'TextFieldContext' export type RootProps = React.PropsWithChildren< {isInvalid?: boolean} & TextStyleProp > export function Root({children, isInvalid = false, style}: RootProps) { const inputRef = useRef(null) const { state: hovered, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const context = useMemo( () => ({ inputRef, hovered, onHoverIn, onHoverOut, focused, onFocus, onBlur, isInvalid, }), [ inputRef, hovered, onHoverIn, onHoverOut, focused, onFocus, onBlur, isInvalid, ], ) return ( inputRef.current?.focus(), onMouseOver: onHoverIn, onMouseOut: onHoverOut, })}> {children} ) } export function useSharedInputStyles() { const t = useTheme() return useMemo(() => { const hover: ViewStyle[] = [ { borderColor: t.palette.contrast_100, }, ] const focus: ViewStyle[] = [ { backgroundColor: t.palette.contrast_50, borderColor: t.palette.primary_500, }, ] const error: ViewStyle[] = [ { backgroundColor: t.palette.negative_25, borderColor: t.palette.negative_300, }, ] const errorHover: ViewStyle[] = [ { backgroundColor: t.palette.negative_25, borderColor: t.palette.negative_500, }, ] return { chromeHover: StyleSheet.flatten(hover), chromeFocus: StyleSheet.flatten(focus), chromeError: StyleSheet.flatten(error), chromeErrorHover: StyleSheet.flatten(errorHover), } }, [t]) } export type InputProps = Omit & { label: string /** * @deprecated Controlled inputs are *strongly* discouraged. Use `defaultValue` instead where possible. * * See https://github.com/facebook/react-native-website/pull/4247 */ value?: string onChangeText?: (value: string) => void isInvalid?: boolean inputRef?: React.RefObject | React.ForwardedRef } export function createInput(Component: typeof TextInput) { return function Input({ label, placeholder, value, onChangeText, onFocus, onBlur, isInvalid, inputRef, style, ...rest }: InputProps) { const t = useTheme() const {fonts} = useAlf() const ctx = useContext(Context) const withinRoot = Boolean(ctx.inputRef) const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = useSharedInputStyles() if (!withinRoot) { return ( ) } const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) const flattened = StyleSheet.flatten([ a.relative, a.z_20, a.flex_1, a.text_md, t.atoms.text, a.px_xs, { // paddingVertical doesn't work w/multiline - esb lineHeight: a.text_md.fontSize * 1.1875, textAlignVertical: rest.multiline ? 'top' : undefined, minHeight: rest.multiline ? 80 : undefined, minWidth: 0, }, ios({paddingTop: 12, paddingBottom: 13}), // Needs to be sm on Paper, md on Fabric for some godforsaken reason -sfn android(a.py_sm), // fix for autofill styles covering border web({ paddingTop: 10, paddingBottom: 11, marginTop: 2, marginBottom: 2, }), style, ]) applyFonts(flattened, fonts.family) // should always be defined on `typography` // @ts-ignore if (flattened.fontSize) { // @ts-ignore flattened.fontSize = Math.round( // @ts-ignore flattened.fontSize * fonts.scaleMultiplier, ) } return ( <> { ctx.onFocus() onFocus?.(e) }} onBlur={e => { ctx.onBlur() onBlur?.(e) }} placeholder={placeholder || label} placeholderTextColor={t.palette.contrast_500} keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} style={flattened} /> ) } } export const Input = createInput(TextInput) export function LabelText({ nativeID, children, }: React.PropsWithChildren<{nativeID?: string}>) { const t = useTheme() return ( {children} ) } export function Icon({icon: Comp}: {icon: React.ComponentType}) { const t = useTheme() const ctx = useContext(Context) const {hover, focus, errorHover, errorFocus} = useMemo(() => { const hover: TextStyle[] = [ { color: t.palette.contrast_800, }, ] const focus: TextStyle[] = [ { color: t.palette.primary_500, }, ] const errorHover: TextStyle[] = [ { color: t.palette.negative_500, }, ] const errorFocus: TextStyle[] = [ { color: t.palette.negative_500, }, ] return { hover, focus, errorHover, errorFocus, } }, [t]) return ( ) } export function SuffixText({ children, label, accessibilityHint, style, }: React.PropsWithChildren< TextStyleProp & { label: string accessibilityHint?: AccessibilityProps['accessibilityHint'] } >) { const t = useTheme() const ctx = useContext(Context) return ( {children} ) } export function GhostText({ children, value, }: { children: string value: string }) { const t = useTheme() // eslint-disable-next-line bsky-internal/avoid-unwrapped-text return ( {children} {value} ) }