From 1e5f213f12837ddecab69424c84891237ea7be8c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 11:06:54 -0600 Subject: [PATCH] Refactor input --- src/components/forms/InputText.tsx | 52 ++-- src/components/forms/TextField.tsx | 316 +++++++++++++++++++++++++ src/components/forms/types.ts | 21 +- src/components/icons/ArrowTopRight.tsx | 8 +- src/components/icons/Globe.tsx | 7 +- src/components/icons/TEMPLATE.tsx | 4 +- src/view/screens/Storybook/Forms.tsx | 100 +++----- 7 files changed, 394 insertions(+), 114 deletions(-) create mode 100644 src/components/forms/TextField.tsx diff --git a/src/components/forms/InputText.tsx b/src/components/forms/InputText.tsx index 8933b1d085..67b50176f8 100644 --- a/src/components/forms/InputText.tsx +++ b/src/components/forms/InputText.tsx @@ -7,7 +7,7 @@ import { LayoutChangeEvent, } from 'react-native' -import {useTheme, atoms, web, tokens} from '#/alf' +import {useTheme, atoms as a, web, tokens} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' @@ -15,7 +15,7 @@ import {BaseProps} from '#/components/forms/types' type Props = BaseProps & Omit & { - placeholder: Required['placeholder'] + placeholder?: Required['placeholder'] icon?: React.FunctionComponent suffix?: React.FunctionComponent } @@ -25,8 +25,6 @@ export function createTextInput(Input: typeof TextInput) { value: initialValue, onChange, testID, - accessibilityLabel, - accessibilityHint, label, hasError, icon: Icon, @@ -146,21 +144,22 @@ export function createTextInput(Input: typeof TextInput) { ) return ( - + {label && ( {label} )} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx new file mode 100644 index 0000000000..0568caaf61 --- /dev/null +++ b/src/components/forms/TextField.tsx @@ -0,0 +1,316 @@ +import React from 'react' +import { + View, + TextInput, + TextInputProps, + TextStyle, + ViewStyle, + Pressable, + StyleSheet, + AccessibilityProps, +} from 'react-native' + +import {isWeb} from '#/platform/detection' +import {useTheme, atoms as a, web, tokens} from '#/alf' +import {Text} from '#/components/Typography' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {Props as SVGIconProps} from '#/components/icons/common' + +const Context = React.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: () => {}, +}) + +export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}> + +function Root({children, isInvalid = false}: RootProps) { + const inputRef = React.useRef(null) + const rootRef = React.useRef(null) + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const context = React.useMemo( + () => ({ + inputRef, + hovered, + onHoverIn, + onHoverOut, + focused, + onFocus, + onBlur, + isInvalid, + }), + [ + inputRef, + hovered, + onHoverIn, + onHoverOut, + focused, + onFocus, + onBlur, + isInvalid, + ], + ) + + React.useLayoutEffect(() => { + const root = rootRef.current + if (!root || !isWeb) return + // @ts-ignore web only + root.tabIndex = -1 + }, []) + + return ( + + inputRef.current?.focus()} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut}> + {children} + + + ) +} + +function useSharedInputStyles() { + const t = useTheme() + return React.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.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: + t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, + }, + ] + const errorHover: ViewStyle[] = [ + { + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: tokens.color.red_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 + value: string + onChangeText: (value: string) => void + isInvalid?: boolean +} + +function Input({ + label, + placeholder, + value, + onChangeText, + isInvalid, + ...rest +}: InputProps) { + const t = useTheme() + const ctx = React.useContext(Context) + const withinRoot = Boolean(ctx.inputRef) + + const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = + useSharedInputStyles() + + if (!withinRoot) { + return ( + + + + ) + } + + return ( + <> + + + + + ) +} + +function Icon({icon: Comp}: {icon: React.ComponentType}) { + const t = useTheme() + const ctx = React.useContext(Context) + const {hover, focus, errorHover, errorFocus} = React.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 ( + + + + ) +} + +function Suffix({ + children, + label, + accessibilityHint, +}: React.PropsWithChildren<{ + label: string + accessibilityHint?: AccessibilityProps['accessibilityHint'] +}>) { + const t = useTheme() + const ctx = React.useContext(Context) + return ( + + {children} + + ) +} + +export default { + Root, + Input, + Icon, + Suffix, +} diff --git a/src/components/forms/types.ts b/src/components/forms/types.ts index c43b760b0b..8d94bf0d1c 100644 --- a/src/components/forms/types.ts +++ b/src/components/forms/types.ts @@ -2,17 +2,10 @@ import {AccessibilityProps} from 'react-native' export type RequiredAccessibilityProps = Required -export type BaseProps = Omit< - AccessibilityProps, - 'accessibilityLabel' | 'accessibilityHint' -> & - Pick< - RequiredAccessibilityProps, - 'accessibilityLabel' | 'accessibilityHint' - > & { - value: T - onChange: (value: T) => void - testID: string - label?: string - hasError?: boolean - } +export type BaseProps = AccessibilityProps & { + value: T + onChange: (value: T) => void + testID?: string + label: string + hasError?: boolean +} diff --git a/src/components/icons/ArrowTopRight.tsx b/src/components/icons/ArrowTopRight.tsx index ca29730203..368c45308d 100644 --- a/src/components/icons/ArrowTopRight.tsx +++ b/src/components/icons/ArrowTopRight.tsx @@ -14,11 +14,13 @@ export const ArrowTopRight_Stroke2_Corner0_Rounded = React.forwardRef( // @ts-ignore it's fiiiiine ref={ref} viewBox="0 0 24 24" - style={[{width: size}, style]}> + width={size} + height={size} + style={[style]}> diff --git a/src/components/icons/Globe.tsx b/src/components/icons/Globe.tsx index 3801607ea3..28187266ac 100644 --- a/src/components/icons/Globe.tsx +++ b/src/components/icons/Globe.tsx @@ -16,10 +16,13 @@ export const Globe_Stroke2_Corner0_Rounded = React.forwardRef(function LogoImpl( // @ts-ignore it's fiiiiine ref={ref} viewBox="0 0 24 24" - style={[{width: size}, style]}> + width={size} + height={size} + style={[style]}> diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx index c3e53336bd..c47849aca4 100644 --- a/src/components/icons/TEMPLATE.tsx +++ b/src/components/icons/TEMPLATE.tsx @@ -14,7 +14,9 @@ export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( // @ts-ignore it's fiiiiine ref={ref} viewBox="0 0 24 24" - style={[{width: size}, style]}> + width={size} + height={size} + style={[style]}>

Forms

@@ -24,77 +27,43 @@ export function Forms() {

InputText

- console.log(text)} - /> - console.log(text)} - /> - console.log(text)} - icon={Logo} - /> - console.log(text)} - icon={Logo} - /> - console.log(text)} - icon={Logo} - suffix={() => .bksy.social} - /> - console.log(text)} + + + + + + + + + + @gmail.com + +

InputDate

console.log(date)} - accessibilityLabel="Date" - accessibilityHint="Enter a date" + label="Input" /> console.log(date)} - accessibilityLabel="Date" - accessibilityHint="Enter a date" + label="Input" />
@@ -103,24 +72,21 @@ export function Forms() { console.log(text)} /> console.log(text)} /> console.log(text)}