(forms) add a select field

via the <Field> component you can now describe a select input that
matches other field apis
This commit is contained in:
Emmanuel Pelletier
2024-07-24 14:21:34 +02:00
parent c8f96b1f22
commit 786cd3e4c7
4 changed files with 134 additions and 5 deletions

View File

@@ -20,6 +20,7 @@ const box = cva({
popover: {
padding: 'boxPadding.xs',
minWidth: '10rem',
boxShadow: '0 8px 20px #0000001a',
},
dialog: {
width: '30rem',
@@ -38,6 +39,11 @@ const box = cva({
color: 'default.subtle-text',
backgroundColor: 'default.subtle',
},
control: {
border: '1px solid {colors.control.border}',
backgroundColor: 'box.bg',
color: 'control.text',
},
},
size: {
default: {

View File

@@ -10,12 +10,14 @@ import {
type CheckboxProps,
type CheckboxGroupProps,
type RadioGroupProps,
type SelectProps,
} from 'react-aria-components'
import { FieldDescription } from './FieldDescription'
import { FieldErrors } from './FieldErrors'
import { Input } from './Input'
import { Radio } from './Radio'
import { Checkbox } from './Checkbox'
import { Select } from './Select'
import { Div } from './Div'
const FieldWrapper = styled('div', {
@@ -31,12 +33,16 @@ const StyledLabel = styled(Label, {
})
type OmittedRACProps = 'type' | 'label' | 'items' | 'description' | 'validate'
type Items = { items: Array<{ value: string; label: ReactNode }> }
type Items<T = ReactNode> = { items: Array<{ value: string; label: T }> }
type PartialTextFieldProps = Omit<TextFieldProps, OmittedRACProps>
type PartialCheckboxProps = Omit<CheckboxProps, OmittedRACProps>
type PartialCheckboxGroupProps = Omit<CheckboxGroupProps, OmittedRACProps>
type PartialRadioGroupProps = Omit<RadioGroupProps, OmittedRACProps>
type FieldProps = (
type PartialSelectProps<T extends object> = Omit<
SelectProps<T>,
OmittedRACProps
>
type FieldProps<T extends object> = (
| ({
type: 'text'
items?: never
@@ -65,6 +71,11 @@ type FieldProps = (
) => ReactNode | ReactNode[] | true | null | undefined
} & Items &
PartialRadioGroupProps)
| ({
type: 'select'
validate?: (value: T) => ReactNode | ReactNode[] | true | null | undefined
} & Items<string> &
PartialSelectProps<T>)
) & {
label: string
description?: string
@@ -82,14 +93,14 @@ type FieldProps = (
*
* You can directly pass HTML input props if needed (like required, pattern, etc)
*/
export const Field = ({
export const Field = <T extends object>({
type,
label,
description,
items,
validate,
...props
}: FieldProps) => {
}: FieldProps<T>) => {
const LabelAndDescription = (
<>
<StyledLabel>{label}</StyledLabel>
@@ -174,6 +185,20 @@ export const Field = ({
</FieldWrapper>
)
}
if (type === 'select') {
return (
<FieldWrapper>
<Select
validate={validate as unknown as SelectProps<T>['validate']}
{...(props as PartialSelectProps<T>)}
label={LabelAndDescription}
errors={RACFieldErrors}
items={items}
/>
</FieldWrapper>
)
}
}
const FieldItem = ({

View File

@@ -9,7 +9,7 @@ import {
import { styled } from '@/styled-system/jsx'
import { Box } from './Box'
const StyledPopover = styled(RACPopover, {
export const StyledPopover = styled(RACPopover, {
base: {
'&[data-placement="bottom"]': {
marginTop: 0.25,

View File

@@ -0,0 +1,98 @@
import { type ReactNode } from 'react'
import { styled } from '@/styled-system/jsx'
import { RiArrowDropDownLine } from '@remixicon/react'
import {
Button,
ListBox,
ListBoxItem,
Select as RACSelect,
SelectProps,
SelectValue,
} from 'react-aria-components'
import { Box } from './Box'
import { StyledPopover } from './Popover'
const StyledButton = styled(Button, {
base: {
width: 'full',
display: 'flex',
justifyContent: 'space-between',
paddingY: 0.125,
paddingX: 0.25,
border: '1px solid',
borderColor: 'control.border',
color: 'control.text',
borderRadius: 4,
boxShadow: '0 1px 2px rgba(0 0 0 / 0.1)',
'&[data-focus-visible]': {
outline: '2px solid {colors.focusRing}',
outlineOffset: '-1px',
},
'&[data-pressed]': {
backgroundColor: 'control.hover',
},
},
})
const StyledSelectValue = styled(SelectValue, {
base: {
'&[data-placeholder]': {
color: 'default.subtle-text',
fontStyle: 'italic',
},
},
})
const StyledListBoxItem = styled(ListBoxItem, {
base: {
paddingY: 0.125,
paddingX: 0.5,
textAlign: 'left',
width: 'full',
borderRadius: 4,
cursor: 'pointer',
color: 'box.text',
border: '1px solid transparent',
'&[data-selected]': {
fontWeight: 'bold',
},
'&[data-focused]': {
color: 'primary.text',
backgroundColor: 'primary',
outline: 'none!',
},
},
})
export const Select = <T extends string | number>({
label,
items,
errors,
...props
}: Omit<SelectProps<object>, 'items' | 'label' | 'errors'> & {
label: ReactNode
items: Array<{ value: T; label: ReactNode }>
errors?: ReactNode
}) => {
return (
<RACSelect {...props}>
{label}
<StyledButton>
<StyledSelectValue />
<RiArrowDropDownLine aria-hidden="true" />
</StyledButton>
<StyledPopover>
<Box size="sm" type="popover" variant="control">
<ListBox>
{items.map((item) => (
<StyledListBoxItem id={item.value} key={item.value}>
{item.label}
</StyledListBoxItem>
))}
</ListBox>
</Box>
</StyledPopover>
{errors}
</RACSelect>
)
}