Refactor input

This commit is contained in:
Eric Bailey
2024-01-18 11:06:54 -06:00
parent 13409f889c
commit 1e5f213f12
7 changed files with 394 additions and 114 deletions
+25 -27
View File
@@ -7,7 +7,7 @@ import {
LayoutChangeEvent, LayoutChangeEvent,
} from 'react-native' } 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 {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
@@ -15,7 +15,7 @@ import {BaseProps} from '#/components/forms/types'
type Props = BaseProps & type Props = BaseProps &
Omit<TextInputProps, 'placeholder'> & { Omit<TextInputProps, 'placeholder'> & {
placeholder: Required<TextInputProps>['placeholder'] placeholder?: Required<TextInputProps>['placeholder']
icon?: React.FunctionComponent<any> icon?: React.FunctionComponent<any>
suffix?: React.FunctionComponent<any> suffix?: React.FunctionComponent<any>
} }
@@ -25,8 +25,6 @@ export function createTextInput(Input: typeof TextInput) {
value: initialValue, value: initialValue,
onChange, onChange,
testID, testID,
accessibilityLabel,
accessibilityHint,
label, label,
hasError, hasError,
icon: Icon, icon: Icon,
@@ -146,21 +144,22 @@ export function createTextInput(Input: typeof TextInput) {
) )
return ( return (
<View style={[atoms.relative, atoms.w_full]}> <View style={[a.relative, a.w_full]}>
{label && ( {label && (
<Text <Text
nativeID={labelId} nativeID={labelId}
style={[ style={[
atoms.text_sm, a.text_sm,
atoms.font_bold, a.font_bold,
t.atoms.text_contrast_600, t.atoms.text_contrast_600,
atoms.mb_sm, a.mb_sm,
]}> ]}>
{label} {label}
</Text> </Text>
)} )}
<Input <Input
accessibilityHint={undefined}
{...props} {...props}
value={value} value={value}
testID={testID} testID={testID}
@@ -168,8 +167,7 @@ export function createTextInput(Input: typeof TextInput) {
aria-label={label} aria-label={label}
aria-invalid={hasError} aria-invalid={hasError}
aria-placeholder={props.placeholder} aria-placeholder={props.placeholder}
accessibilityLabel={accessibilityLabel} accessibilityLabel={label}
accessibilityHint={accessibilityHint}
placeholderTextColor={t.atoms.text_contrast_400.color} placeholderTextColor={t.atoms.text_contrast_400.color}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
@@ -180,17 +178,17 @@ export function createTextInput(Input: typeof TextInput) {
})} })}
style={[ style={[
t.atoms.bg_contrast_50, t.atoms.bg_contrast_50,
atoms.w_full, a.w_full,
atoms.px_lg, a.px_lg,
atoms.py_md, a.py_md,
atoms.rounded_sm, a.rounded_sm,
atoms.text_md, a.text_md,
t.atoms.text, t.atoms.text,
web({ web({
paddingTop: atoms.pt_md.paddingTop - 1, paddingTop: a.pt_md.paddingTop - 1,
}), }),
{borderColor: 'transparent', paddingRight: suffixPadding}, {borderColor: 'transparent', paddingRight: suffixPadding},
{borderWidth: 2, lineHeight: atoms.text_md.lineHeight * 1.1875}, {borderWidth: 2, lineHeight: a.text_md.lineHeight * 1.1875},
...inputBaseStyles, ...inputBaseStyles,
...(hovered ? inputHoverStyles : []), ...(hovered ? inputHoverStyles : []),
...(focused ? inputFocusStyles : []), ...(focused ? inputFocusStyles : []),
@@ -201,11 +199,11 @@ export function createTextInput(Input: typeof TextInput) {
{Icon && ( {Icon && (
<View <View
style={[ style={[
atoms.absolute, a.absolute,
atoms.inset_0, a.inset_0,
atoms.align_center, a.align_center,
atoms.justify_center, a.justify_center,
atoms.pl_md, a.pl_md,
{right: 'auto'}, {right: 'auto'},
]}> ]}>
<Icon <Icon
@@ -232,11 +230,11 @@ export function createTextInput(Input: typeof TextInput) {
<View <View
onLayout={handleSuffixLayout} onLayout={handleSuffixLayout}
style={[ style={[
atoms.absolute, a.absolute,
atoms.inset_0, a.inset_0,
atoms.align_center, a.align_center,
atoms.justify_center, a.justify_center,
atoms.pr_lg, a.pr_lg,
{left: 'auto'}, {left: 'auto'},
]}> ]}>
<Suffix /> <Suffix />
+316
View File
@@ -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<TextInput> | 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<TextInput>(null)
const rootRef = React.useRef<View>(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 (
<Context.Provider value={context}>
<Pressable
accessibilityRole="button"
ref={rootRef}
role="none"
style={[
a.flex_row,
a.align_center,
a.relative,
a.w_full,
a.px_md,
{
paddingVertical: 14,
},
]}
onPressIn={() => inputRef.current?.focus()}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}>
{children}
</Pressable>
</Context.Provider>
)
}
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<TextInputProps, 'value' | 'onChangeText'> & {
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 (
<Root isInvalid={isInvalid}>
<Input
label={label}
placeholder={placeholder}
value={value}
onChangeText={onChangeText}
isInvalid={isInvalid}
{...rest}
/>
</Root>
)
}
return (
<>
<TextInput
accessibilityHint={undefined}
{...rest}
aria-label={label}
accessibilityLabel={label}
ref={ctx.inputRef}
value={value}
onChangeText={onChangeText}
onFocus={ctx.onFocus}
onBlur={ctx.onBlur}
placeholder={placeholder || label}
placeholderTextColor={t.palette.contrast_500}
style={[
a.relative,
a.z_20,
a.flex_1,
a.text_md,
t.atoms.text,
a.px_xs,
{lineHeight: a.text_md.lineHeight * 1.1875},
]}
/>
<View
style={[
a.z_10,
a.absolute,
a.inset_0,
a.rounded_sm,
t.atoms.bg_contrast_50,
{borderColor: 'transparent', borderWidth: 2},
ctx.hovered ? chromeHover : {},
ctx.focused ? chromeFocus : {},
ctx.isInvalid ? chromeError : {},
ctx.isInvalid && (ctx.hovered || ctx.focused) ? chromeErrorHover : {},
]}
/>
</>
)
}
function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
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 (
<View style={[a.z_20, a.pr_xs]}>
<Comp
size="md"
style={[
{color: t.palette.contrast_500},
ctx.hovered ? hover : {},
ctx.focused ? focus : {},
ctx.isInvalid && ctx.hovered ? errorHover : {},
ctx.isInvalid && ctx.focused ? errorFocus : {},
]}
/>
</View>
)
}
function Suffix({
children,
label,
accessibilityHint,
}: React.PropsWithChildren<{
label: string
accessibilityHint?: AccessibilityProps['accessibilityHint']
}>) {
const t = useTheme()
const ctx = React.useContext(Context)
return (
<Text
aria-label={label}
accessibilityLabel={label}
accessibilityHint={accessibilityHint}
style={[
a.z_20,
a.pr_sm,
a.text_md,
t.atoms.text_contrast_400,
{
pointerEvents: 'none',
},
web({
marginTop: -2,
}),
ctx.hovered || ctx.focused
? {
color: t.palette.contrast_800,
}
: {},
]}>
{children}
</Text>
)
}
export default {
Root,
Input,
Icon,
Suffix,
}
+3 -10
View File
@@ -2,17 +2,10 @@ import {AccessibilityProps} from 'react-native'
export type RequiredAccessibilityProps = Required<AccessibilityProps> export type RequiredAccessibilityProps = Required<AccessibilityProps>
export type BaseProps<T = string> = Omit< export type BaseProps<T = string> = AccessibilityProps & {
AccessibilityProps,
'accessibilityLabel' | 'accessibilityHint'
> &
Pick<
RequiredAccessibilityProps,
'accessibilityLabel' | 'accessibilityHint'
> & {
value: T value: T
onChange: (value: T) => void onChange: (value: T) => void
testID: string testID?: string
label?: string label: string
hasError?: boolean hasError?: boolean
} }
+5 -3
View File
@@ -14,11 +14,13 @@ export const ArrowTopRight_Stroke2_Corner0_Rounded = React.forwardRef(
// @ts-ignore it's fiiiiine // @ts-ignore it's fiiiiine
ref={ref} ref={ref}
viewBox="0 0 24 24" viewBox="0 0 24 24"
style={[{width: size}, style]}> width={size}
height={size}
style={[style]}>
<Path <Path
fill={fill} fill={fill}
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z" d="M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z"
/> />
</Svg> </Svg>
+5 -2
View File
@@ -16,10 +16,13 @@ export const Globe_Stroke2_Corner0_Rounded = React.forwardRef(function LogoImpl(
// @ts-ignore it's fiiiiine // @ts-ignore it's fiiiiine
ref={ref} ref={ref}
viewBox="0 0 24 24" viewBox="0 0 24 24"
style={[{width: size}, style]}> width={size}
height={size}
style={[style]}>
<Path <Path
fill={fill} fill={fill}
fill-rule="evenodd" fillRule="evenodd"
clipRule="evenodd"
d="M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z" d="M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z"
/> />
</Svg> </Svg>
+3 -1
View File
@@ -14,7 +14,9 @@ export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef(
// @ts-ignore it's fiiiiine // @ts-ignore it's fiiiiine
ref={ref} ref={ref}
viewBox="0 0 24 24" viewBox="0 0 24 24"
style={[{width: size}, style]}> width={size}
height={size}
style={[style]}>
<Path <Path
fill={fill} fill={fill}
fill-rule="evenodd" fill-rule="evenodd"
+31 -65
View File
@@ -2,14 +2,15 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Text, H1, H3} from '#/components/Typography' import {H1, H3} from '#/components/Typography'
import {InputText} from '#/components/forms/InputText' import {InputText} from '#/components/forms/InputText'
import TextField from '#/components/forms/TextField'
import {InputDate, utils} from '#/components/forms/InputDate' import {InputDate, utils} from '#/components/forms/InputDate'
import {InputGroup} from '#/components/forms/InputGroup' import {InputGroup} from '#/components/forms/InputGroup'
import {Logo} from '#/view/icons/Logo'
import Toggle from '#/components/forms/Toggle' import Toggle from '#/components/forms/Toggle'
import ToggleButton from '#/components/forms/ToggleButton' import ToggleButton from '#/components/forms/ToggleButton'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
export function Forms() { export function Forms() {
const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a']) const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a'])
@@ -17,6 +18,8 @@ export function Forms() {
const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b']) const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b'])
const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn']) const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn'])
const [value, setValue] = React.useState('')
return ( return (
<View style={[a.gap_4xl, a.align_start]}> <View style={[a.gap_4xl, a.align_start]}>
<H1>Forms</H1> <H1>Forms</H1>
@@ -24,77 +27,43 @@ export function Forms() {
<View style={[a.gap_md, a.align_start, a.w_full]}> <View style={[a.gap_md, a.align_start, a.w_full]}>
<H3>InputText</H3> <H3>InputText</H3>
<InputText <TextField.Input
testID="input" value={value}
accessibilityLabel="Input" onChangeText={setValue}
accessibilityHint="Enter some text" label="Text field"
placeholder="Type here"
value=""
onChange={text => console.log(text)}
/> />
<InputText
hasError <TextField.Root>
testID="input" <TextField.Icon icon={Globe} />
accessibilityLabel="Input" <TextField.Input
accessibilityHint="Enter some text" value={value}
placeholder="Type here" onChangeText={setValue}
value="Test initial value" label="Text field"
onChange={text => console.log(text)}
/> />
<InputText </TextField.Root>
hasError
testID="input" <TextField.Root>
accessibilityLabel="Input" <TextField.Icon icon={Globe} />
accessibilityHint="Enter some text" <TextField.Input
placeholder="Type here" value={value}
value="Test initial value" onChangeText={setValue}
onChange={text => console.log(text)} label="Text field"
icon={Logo}
/>
<InputText
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
placeholder="Type here"
value=""
onChange={text => console.log(text)}
icon={Logo}
/>
<InputText
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
placeholder="Type here"
value=""
onChange={text => console.log(text)}
icon={Logo}
suffix={() => <Text>.bksy.social</Text>}
/>
<InputText
multiline
numberOfLines={3}
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
placeholder="Type here"
value=""
onChange={text => console.log(text)}
/> />
<TextField.Suffix label="@gmail.com">@gmail.com</TextField.Suffix>
</TextField.Root>
<H3>InputDate</H3> <H3>InputDate</H3>
<InputDate <InputDate
testID="date" testID="date"
value={'2001-01-01'} value={'2001-01-01'}
onChange={date => console.log(date)} onChange={date => console.log(date)}
accessibilityLabel="Date" label="Input"
accessibilityHint="Enter a date"
/> />
<InputDate <InputDate
testID="date" testID="date"
value={utils.toSimpleDateString(new Date())} value={utils.toSimpleDateString(new Date())}
onChange={date => console.log(date)} onChange={date => console.log(date)}
accessibilityLabel="Date" label="Input"
accessibilityHint="Enter a date"
/> />
</View> </View>
@@ -103,24 +72,21 @@ export function Forms() {
<InputGroup> <InputGroup>
<InputText <InputText
testID="input" testID="input"
accessibilityLabel="Input" label="Input"
accessibilityHint="Enter some text"
placeholder="Type here" placeholder="Type here"
value="" value=""
onChange={text => console.log(text)} onChange={text => console.log(text)}
/> />
<InputText <InputText
testID="input" testID="input"
accessibilityLabel="Input" label="Input"
accessibilityHint="Enter some text"
placeholder="Type here" placeholder="Type here"
value="" value=""
onChange={text => console.log(text)} onChange={text => console.log(text)}
/> />
<InputText <InputText
testID="input" testID="input"
accessibilityLabel="Input" label="Input"
accessibilityHint="Enter some text"
placeholder="Type here" placeholder="Type here"
value="" value=""
onChange={text => console.log(text)} onChange={text => console.log(text)}