Skip to content
This repository has been archived by the owner on Dec 21, 2023. It is now read-only.

Commit

Permalink
feat: add Input component (#587)
Browse files Browse the repository at this point in the history
* wip input

* wip input

* wip input with labels

* wip input with labels

Co-authored-by: Axel Delafosse <[email protected]>
  • Loading branch information
intergalacticspacehighway and axeldelafosse authored Dec 13, 2021
1 parent 4e6b1ca commit a139a59
Show file tree
Hide file tree
Showing 13 changed files with 665 additions and 42 deletions.
10 changes: 6 additions & 4 deletions packages/design-system/checkbox/checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ export const Primary: React.VFC<{}> = () => {

return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Label style={{ flexDirection: 'row', alignItems: 'center' }}>
<Checkbox accesibilityLabel="I agree" checked={checked} onChange={setChecked} />
<Text style={{ marginLeft: 10 }}>I agree</Text>
</Label>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Checkbox id="checkbox" accesibilityLabel="I agree" checked={checked} onChange={setChecked} />
<Label htmlFor="checkbox" sx={{ flexDirection: 'row', marginLeft: 10, alignItems: 'center' }}>
I agree
</Label>
</View>
</View>
)
}
6 changes: 5 additions & 1 deletion packages/design-system/checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ type CheckboxProps = {
checked: boolean
hitSlop?: number
accesibilityLabel: string
id?: string
}

export const Checkbox = ({ checked, onChange, hitSlop = 14, accesibilityLabel }: CheckboxProps) => {
export const Checkbox = ({ checked, onChange, id, hitSlop = 14, accesibilityLabel }: CheckboxProps) => {
const handleChange = useCallback(() => {
onChange(!checked)
}, [onChange, checked])
Expand Down Expand Up @@ -68,6 +69,9 @@ export const Checkbox = ({ checked, onChange, hitSlop = 14, accesibilityLabel }:
</Svg>
</MotiView>
</View>
{Platform.OS === 'web' && (
<input type="checkbox" id={id} hidden onChange={handleChange} checked={checked} />
)}
</Pressable>
)
}
30 changes: 29 additions & 1 deletion packages/design-system/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
import { useColorScheme } from 'react-native'
import React from 'react'
import { Platform, useColorScheme } from 'react-native'
import { useSharedValue } from 'react-native-reanimated'

export const useIsDarkMode = () => {
return useColorScheme() === 'dark'
}

export const useOnFocus = () => {
const focused = useSharedValue(0)
// use state on web for now till useAnimatedStyle bug is resolved
const [state, setFocused] = React.useState(0)

const focusHandler = React.useMemo(() => {
return {
onFocus: () => {
focused.value = 1
if (Platform.OS === 'web') {
setFocused(1)
}
},
onBlur: () => {
focused.value = 0
if (Platform.OS === 'web') {
setFocused(0)
}
},
focused: Platform.select({ default: focused, web: { value: state } }),
}
}, [state])

return focusHandler
}
177 changes: 177 additions & 0 deletions packages/design-system/input/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { useMemo } from 'react'
import { TextInput } from 'dripsy'
import { Pressable, Props as PressableProps } from '../pressable-scale'
import { View } from '../view'
import { Text } from '../text'
import { tw } from '../tailwind'
import { Platform, TextInputProps, useColorScheme } from 'react-native'
import { useOnFocus, useIsDarkMode } from '../hooks'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { colors } from '../tailwind/colors'
import { Label } from '../label'

type InputProps = {
leftElement?: React.ReactElement
rightElement?: React.ReactElement
placeholder?: string
onChangeText?: (text: string) => void
value?: string
isInvalid?: boolean
id?: string
disabled?: boolean
type?: TextInputProps['keyboardType']
label?: string
errorText?: string
helperText?: string
accessibilityLabel?: string
}

const borderColor = {
dark: '#52525B',
light: '#E4E4E7',
}

const boxShadow = {
dark: borderColor.dark + ' 0px 0px 0px 4px',
light: borderColor.light + ' 0px 0px 0px 4px',
}

let idCounter = 0
// Replace this with useId from React 18. Currently we're doing client side rendering, so probably this is safe!
const useId = (id?: string) => {
const newId = useMemo(() => {
if (id) {
return id
} else {
idCounter++
return idCounter.toString()
}
}, [id])

return newId
}

export const Input = (props: InputProps) => {
const {
leftElement,
rightElement,
placeholder,
onChangeText,
value,
label,
helperText,
errorText,
disabled,
type,
isInvalid,
accessibilityLabel,
} = props
const { onFocus, onBlur, focused } = useOnFocus()
const colorScheme = useColorScheme()
const isDark = useIsDarkMode()

const inputId = useId(props.id)
const helperTextId = useId()
const errorTextId = useId()

const animatableStyle = useAnimatedStyle(() => {
return {
boxShadow: Platform.OS === 'web' && focused.value ? boxShadow[colorScheme] : undefined,
opacity: disabled ? 0.75 : 1,
}
}, [focused, disabled])

return (
<View>
{label ? (
<Label
variant="text-sm"
htmlFor={inputId}
tw="dark:text-white text-gray-900"
sx={{ marginBottom: 4, fontWeight: '700' }}
>
{label}
</Label>
) : null}
<Animated.View
style={[
{
flexDirection: 'row',
alignItems: 'center',
borderRadius: 999,
...tw.style(`bg-gray-100 dark:bg-gray-900 ${isInvalid ? 'border-red-500 border' : ''}`),
},
// @ts-ignore
animatableStyle,
]}
>
{leftElement}
<TextInput
sx={{
flexGrow: 1,
paddingY: 12,
paddingLeft: leftElement ? 0 : 16,
paddingRight: rightElement ? 0 : 16,
fontWeight: '500',
...tw.style('text-gray-900 dark:text-white'),
}}
// @ts-ignore remove focus outline on web as we'll control the focus styling
style={Platform.select({
web: {
outline: 'none',
},
default: undefined,
})}
placeholderTextColor={isDark ? colors.gray['400'] : colors.gray['500']}
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
editable={!disabled}
onFocus={onFocus}
onBlur={onBlur}
nativeID={inputId}
selectionColor={isDark ? colors.gray['300'] : colors.gray['700']}
keyboardType={type}
disabled={disabled}
accessibilityLabel={accessibilityLabel}
accessibilityDescribedBy={Platform.select({
web: helperText ? helperTextId : undefined,
default: undefined,
})}
accessibilityErrorMessage={Platform.select({
web: errorText ? errorTextId : undefined,
default: undefined,
})}
accessibilityInvalid={Platform.select({ web: isInvalid, default: undefined })}
/>
{rightElement && <View sx={{ marginLeft: 'auto' }}>{rightElement}</View>}
</Animated.View>
{helperText ? (
<Text
variant="text-sm"
nativeID={helperTextId}
tw="text-gray-600 dark:text-gray-400"
sx={{ marginTop: 4, fontWeight: '600' }}
>
{helperText}
</Text>
) : null}
{errorText ? (
<Text
nativeID={errorTextId}
variant="text-sm"
tw="text-red-500"
sx={{ marginTop: 4, fontWeight: '600' }}
>
{errorText}
</Text>
) : null}
</View>
)
}

// This component adds appropriate padding to match our design system and increase the pressable area
// Usage - with rightElement and leftElement
export const InputPressable = (props: PressableProps) => {
return <Pressable {...props} sx={{ padding: 10, ...props.sx }} />
}
84 changes: 84 additions & 0 deletions packages/design-system/input/input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Meta } from '@storybook/react'
import { Input, InputPressable } from './index'
import { View } from '../view'
import { Svg, Path } from 'react-native-svg'
import { useIsDarkMode } from '../hooks'

const LinkIcon = () => {
const isDark = useIsDarkMode()
return (
<InputPressable onPress={() => {}}>
<Svg width="22" height="22" viewBox="0 0 22 22" fill="none">
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M15.9956 0.0666941C14.4225 0.0530236 12.9069 0.65779 11.7752 1.75074L11.7649 1.76086L10.0449 3.47086C9.65325 3.86025 9.6514 4.49341 10.0408 4.88507C10.4302 5.27673 11.0633 5.27858 11.455 4.88919L13.1696 3.18456C13.9235 2.45893 14.9318 2.05752 15.9783 2.06662C17.0271 2.07573 18.0303 2.49641 18.7719 3.23804C19.5136 3.97967 19.9342 4.98292 19.9434 6.03171C19.9525 7.07776 19.5514 8.08563 18.8264 8.83941L15.8328 11.8329L15.8327 11.833C15.4272 12.2387 14.9392 12.5524 14.4018 12.7529C13.8644 12.9533 13.2902 13.0359 12.718 12.9949C12.1459 12.9539 11.5893 12.7904 11.086 12.5154C10.5826 12.2404 10.1443 11.8604 9.80072 11.401C9.46991 10.9588 8.84323 10.8685 8.40098 11.1993C7.95873 11.5301 7.86838 12.1568 8.19919 12.599C8.71453 13.288 9.37202 13.858 10.127 14.2705C10.8821 14.683 11.717 14.9283 12.5752 14.9898C13.4333 15.0513 14.2947 14.9274 15.1008 14.6267C15.9069 14.326 16.6389 13.8555 17.2472 13.247L20.2471 10.2471L20.2592 10.2347C21.3522 9.10313 21.957 7.58751 21.9433 6.01433C21.9296 4.44115 21.2986 2.93628 20.1862 1.82383C19.0737 0.711378 17.5688 0.0803646 15.9956 0.0666941ZM9.42474 7.01025C8.56657 6.94879 7.70522 7.07261 6.89911 7.37331C6.09305 7.67399 5.36109 8.1445 4.75285 8.75292L4.75272 8.75305L1.75285 11.7529L1.74067 11.7653C0.647719 12.8969 0.0429528 14.4125 0.0566233 15.9857C0.0702938 17.5589 0.701307 19.0638 1.81376 20.1762C2.9262 21.2887 4.43108 21.9197 6.00426 21.9334C7.57744 21.947 9.09305 21.3423 10.2247 20.2493L10.2371 20.2371L11.9471 18.5271C12.3376 18.1366 12.3376 17.5034 11.9471 17.1129C11.5565 16.7224 10.9234 16.7224 10.5328 17.1129L8.82932 18.8164C8.07555 19.5415 7.06768 19.9425 6.02164 19.9334C4.97285 19.9243 3.9696 19.5036 3.22797 18.762C2.48634 18.0204 2.06566 17.0171 2.05655 15.9683C2.04746 14.9223 2.44851 13.9144 3.17355 13.1606L6.16706 10.1671L6.16719 10.167C6.5727 9.76135 7.06071 9.44765 7.59811 9.24718C8.13552 9.04671 8.70976 8.96416 9.28187 9.00514C9.85398 9.04611 10.4106 9.20965 10.9139 9.48465C11.4173 9.75966 11.8556 10.1397 12.1992 10.599C12.53 11.0413 13.1567 11.1316 13.5989 10.8008C14.0412 10.47 14.1315 9.8433 13.8007 9.40105C13.2854 8.71209 12.6279 8.14203 11.8729 7.72952C11.1178 7.31701 10.2829 7.07171 9.42474 7.01025Z"
fill={isDark ? '#B2B4CC' : '#3D3E5C'}
/>
</Svg>
</InputPressable>
)
}

const CopyIcon = () => {
const isDark = useIsDarkMode()

return (
<InputPressable>
<Svg width="22" height="22" viewBox="0 0 22 22" fill="none">
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M2.29289 2.29289C2.48043 2.10536 2.73478 2 3 2H12C12.2652 2 12.5196 2.10536 12.7071 2.29289C12.8946 2.48043 13 2.73478 13 3V4C13 4.55228 13.4477 5 14 5C14.5523 5 15 4.55228 15 4V3C15 2.20435 14.6839 1.44129 14.1213 0.87868C13.5587 0.31607 12.7956 0 12 0H3C2.20435 0 1.44129 0.31607 0.87868 0.87868C0.31607 1.44129 0 2.20435 0 3V12C0 12.7956 0.31607 13.5587 0.87868 14.1213C1.44129 14.6839 2.20435 15 3 15H4C4.55228 15 5 14.5523 5 14C5 13.4477 4.55228 13 4 13H3C2.73478 13 2.48043 12.8946 2.29289 12.7071C2.10536 12.5196 2 12.2652 2 12V3C2 2.73478 2.10536 2.48043 2.29289 2.29289ZM9 10C9 9.44771 9.44771 9 10 9H19C19.5523 9 20 9.44771 20 10V19C20 19.5523 19.5523 20 19 20H10C9.44771 20 9 19.5523 9 19V10ZM10 7C8.34315 7 7 8.34315 7 10V19C7 20.6569 8.34315 22 10 22H19C20.6569 22 22 20.6569 22 19V10C22 8.34315 20.6569 7 19 7H10Z"
fill={isDark ? '#B2B4CC' : '#3D3E5C'}
/>
</Svg>
</InputPressable>
)
}

export const Inputs = () => {
const isDark = useIsDarkMode()
return (
<View sx={{ padding: 10, flex: 1, minHeight: 500 }} tw={isDark ? 'bg-black' : 'bg-white'}>
<Input label="First Name" leftElement={<LinkIcon />} placeholder="First name" />
<Spacer />
<Input label="Last Name" leftElement={<LinkIcon />} rightElement={<CopyIcon />} placeholder="Last name" />
<Spacer />
<Input label="Email" type="email-address" placeholder="Email" />
<Spacer />
<Input label="Disabled" disabled placeholder="Last name" />
<Spacer />
<Input label="Amount" type="number-pad" isInvalid placeholder="Amount" />
<Spacer />
<Spacer />
</View>
)
}

export const InputFormElements = () => {
const isDark = useIsDarkMode()
return (
<View sx={{ padding: 10, flex: 1, minHeight: 500 }} tw={isDark ? 'bg-black' : 'bg-white'}>
<Input helperText="enter your username" label="Username" placeholder="@showtime" />
<Spacer />
<Input
errorText="Email not found"
isInvalid
helperText="Enter email linked to this account"
label="Email"
// use id when placeholders don't help browser detect auto complete.
id="Email"
placeholder="[email protected]"
/>
</View>
)
}

const Spacer = () => {
return <View sx={{ height: 20 }} />
}
export default {
component: Input,
title: 'Components/Input',
} as Meta
4 changes: 2 additions & 2 deletions packages/design-system/label/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { View } from 'react-native'
import { Text, Props as TextProps } from '../text'

export const Label = View
export const Label = ({ htmlFor, ...rest }: TextProps & { htmlFor?: string }) => <Text {...rest} />
21 changes: 2 additions & 19 deletions packages/design-system/label/index.web.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
import { unstable_createElement } from 'react-native-web'
import { Text } from '../text'

// https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/View/index.js#L133
const viewStyle = {
alignItems: 'stretch',
border: '0 solid black',
boxSizing: 'border-box',
display: 'flex',
flexBasis: 'auto',
flexDirection: 'column',
flexShrink: 0,
margin: 0,
minHeight: 0,
minWidth: 0,
padding: 0,
position: 'relative',
zIndex: 0,
}

export const Label = props => unstable_createElement('label', { ...props, style: [viewStyle, props.style] })
export const Label = props => <Text {...props} accessibilityRole="label" />
Loading

0 comments on commit a139a59

Please sign in to comment.