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,
} 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<TextInputProps, 'placeholder'> & {
placeholder: Required<TextInputProps>['placeholder']
placeholder?: Required<TextInputProps>['placeholder']
icon?: React.FunctionComponent<any>
suffix?: React.FunctionComponent<any>
}
@@ -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 (
<View style={[atoms.relative, atoms.w_full]}>
<View style={[a.relative, a.w_full]}>
{label && (
<Text
nativeID={labelId}
style={[
atoms.text_sm,
atoms.font_bold,
a.text_sm,
a.font_bold,
t.atoms.text_contrast_600,
atoms.mb_sm,
a.mb_sm,
]}>
{label}
</Text>
)}
<Input
accessibilityHint={undefined}
{...props}
value={value}
testID={testID}
@@ -168,8 +167,7 @@ export function createTextInput(Input: typeof TextInput) {
aria-label={label}
aria-invalid={hasError}
aria-placeholder={props.placeholder}
accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint}
accessibilityLabel={label}
placeholderTextColor={t.atoms.text_contrast_400.color}
onFocus={onFocus}
onBlur={onBlur}
@@ -180,17 +178,17 @@ export function createTextInput(Input: typeof TextInput) {
})}
style={[
t.atoms.bg_contrast_50,
atoms.w_full,
atoms.px_lg,
atoms.py_md,
atoms.rounded_sm,
atoms.text_md,
a.w_full,
a.px_lg,
a.py_md,
a.rounded_sm,
a.text_md,
t.atoms.text,
web({
paddingTop: atoms.pt_md.paddingTop - 1,
paddingTop: a.pt_md.paddingTop - 1,
}),
{borderColor: 'transparent', paddingRight: suffixPadding},
{borderWidth: 2, lineHeight: atoms.text_md.lineHeight * 1.1875},
{borderWidth: 2, lineHeight: a.text_md.lineHeight * 1.1875},
...inputBaseStyles,
...(hovered ? inputHoverStyles : []),
...(focused ? inputFocusStyles : []),
@@ -201,11 +199,11 @@ export function createTextInput(Input: typeof TextInput) {
{Icon && (
<View
style={[
atoms.absolute,
atoms.inset_0,
atoms.align_center,
atoms.justify_center,
atoms.pl_md,
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
a.pl_md,
{right: 'auto'},
]}>
<Icon
@@ -232,11 +230,11 @@ export function createTextInput(Input: typeof TextInput) {
<View
onLayout={handleSuffixLayout}
style={[
atoms.absolute,
atoms.inset_0,
atoms.align_center,
atoms.justify_center,
atoms.pr_lg,
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
a.pr_lg,
{left: 'auto'},
]}>
<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,
}
+7 -14
View File
@@ -2,17 +2,10 @@ import {AccessibilityProps} from 'react-native'
export type RequiredAccessibilityProps = Required<AccessibilityProps>
export type BaseProps<T = string> = Omit<
AccessibilityProps,
'accessibilityLabel' | 'accessibilityHint'
> &
Pick<
RequiredAccessibilityProps,
'accessibilityLabel' | 'accessibilityHint'
> & {
value: T
onChange: (value: T) => void
testID: string
label?: string
hasError?: boolean
}
export type BaseProps<T = string> = AccessibilityProps & {
value: T
onChange: (value: T) => void
testID?: string
label: string
hasError?: boolean
}
+5 -3
View File
@@ -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]}>
<Path
fill={fill}
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="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"
/>
</Svg>
+5 -2
View File
@@ -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]}>
<Path
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"
/>
</Svg>
+3 -1
View File
@@ -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]}>
<Path
fill={fill}
fill-rule="evenodd"
+33 -67
View File
@@ -2,14 +2,15 @@ import React from 'react'
import {View} from 'react-native'
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 TextField from '#/components/forms/TextField'
import {InputDate, utils} from '#/components/forms/InputDate'
import {InputGroup} from '#/components/forms/InputGroup'
import {Logo} from '#/view/icons/Logo'
import Toggle from '#/components/forms/Toggle'
import ToggleButton from '#/components/forms/ToggleButton'
import {Button} from '#/components/Button'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
export function Forms() {
const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a'])
@@ -17,6 +18,8 @@ export function Forms() {
const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b'])
const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn'])
const [value, setValue] = React.useState('')
return (
<View style={[a.gap_4xl, a.align_start]}>
<H1>Forms</H1>
@@ -24,77 +27,43 @@ export function Forms() {
<View style={[a.gap_md, a.align_start, a.w_full]}>
<H3>InputText</H3>
<InputText
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
placeholder="Type here"
value=""
onChange={text => console.log(text)}
/>
<InputText
hasError
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
placeholder="Type here"
value="Test initial value"
onChange={text => console.log(text)}
/>
<InputText
hasError
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
placeholder="Type here"
value="Test initial 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}
/>
<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.Input
value={value}
onChangeText={setValue}
label="Text field"
/>
<TextField.Root>
<TextField.Icon icon={Globe} />
<TextField.Input
value={value}
onChangeText={setValue}
label="Text field"
/>
</TextField.Root>
<TextField.Root>
<TextField.Icon icon={Globe} />
<TextField.Input
value={value}
onChangeText={setValue}
label="Text field"
/>
<TextField.Suffix label="@gmail.com">@gmail.com</TextField.Suffix>
</TextField.Root>
<H3>InputDate</H3>
<InputDate
testID="date"
value={'2001-01-01'}
onChange={date => console.log(date)}
accessibilityLabel="Date"
accessibilityHint="Enter a date"
label="Input"
/>
<InputDate
testID="date"
value={utils.toSimpleDateString(new Date())}
onChange={date => console.log(date)}
accessibilityLabel="Date"
accessibilityHint="Enter a date"
label="Input"
/>
</View>
@@ -103,24 +72,21 @@ export function Forms() {
<InputGroup>
<InputText
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
label="Input"
placeholder="Type here"
value=""
onChange={text => console.log(text)}
/>
<InputText
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
label="Input"
placeholder="Type here"
value=""
onChange={text => console.log(text)}
/>
<InputText
testID="input"
accessibilityLabel="Input"
accessibilityHint="Enter some text"
label="Input"
placeholder="Type here"
value=""
onChange={text => console.log(text)}