✨(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:
@@ -20,6 +20,7 @@ const box = cva({
|
|||||||
popover: {
|
popover: {
|
||||||
padding: 'boxPadding.xs',
|
padding: 'boxPadding.xs',
|
||||||
minWidth: '10rem',
|
minWidth: '10rem',
|
||||||
|
boxShadow: '0 8px 20px #0000001a',
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
width: '30rem',
|
width: '30rem',
|
||||||
@@ -38,6 +39,11 @@ const box = cva({
|
|||||||
color: 'default.subtle-text',
|
color: 'default.subtle-text',
|
||||||
backgroundColor: 'default.subtle',
|
backgroundColor: 'default.subtle',
|
||||||
},
|
},
|
||||||
|
control: {
|
||||||
|
border: '1px solid {colors.control.border}',
|
||||||
|
backgroundColor: 'box.bg',
|
||||||
|
color: 'control.text',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import {
|
|||||||
type CheckboxProps,
|
type CheckboxProps,
|
||||||
type CheckboxGroupProps,
|
type CheckboxGroupProps,
|
||||||
type RadioGroupProps,
|
type RadioGroupProps,
|
||||||
|
type SelectProps,
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { FieldDescription } from './FieldDescription'
|
import { FieldDescription } from './FieldDescription'
|
||||||
import { FieldErrors } from './FieldErrors'
|
import { FieldErrors } from './FieldErrors'
|
||||||
import { Input } from './Input'
|
import { Input } from './Input'
|
||||||
import { Radio } from './Radio'
|
import { Radio } from './Radio'
|
||||||
import { Checkbox } from './Checkbox'
|
import { Checkbox } from './Checkbox'
|
||||||
|
import { Select } from './Select'
|
||||||
import { Div } from './Div'
|
import { Div } from './Div'
|
||||||
|
|
||||||
const FieldWrapper = styled('div', {
|
const FieldWrapper = styled('div', {
|
||||||
@@ -31,12 +33,16 @@ const StyledLabel = styled(Label, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
type OmittedRACProps = 'type' | 'label' | 'items' | 'description' | 'validate'
|
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 PartialTextFieldProps = Omit<TextFieldProps, OmittedRACProps>
|
||||||
type PartialCheckboxProps = Omit<CheckboxProps, OmittedRACProps>
|
type PartialCheckboxProps = Omit<CheckboxProps, OmittedRACProps>
|
||||||
type PartialCheckboxGroupProps = Omit<CheckboxGroupProps, OmittedRACProps>
|
type PartialCheckboxGroupProps = Omit<CheckboxGroupProps, OmittedRACProps>
|
||||||
type PartialRadioGroupProps = Omit<RadioGroupProps, OmittedRACProps>
|
type PartialRadioGroupProps = Omit<RadioGroupProps, OmittedRACProps>
|
||||||
type FieldProps = (
|
type PartialSelectProps<T extends object> = Omit<
|
||||||
|
SelectProps<T>,
|
||||||
|
OmittedRACProps
|
||||||
|
>
|
||||||
|
type FieldProps<T extends object> = (
|
||||||
| ({
|
| ({
|
||||||
type: 'text'
|
type: 'text'
|
||||||
items?: never
|
items?: never
|
||||||
@@ -65,6 +71,11 @@ type FieldProps = (
|
|||||||
) => ReactNode | ReactNode[] | true | null | undefined
|
) => ReactNode | ReactNode[] | true | null | undefined
|
||||||
} & Items &
|
} & Items &
|
||||||
PartialRadioGroupProps)
|
PartialRadioGroupProps)
|
||||||
|
| ({
|
||||||
|
type: 'select'
|
||||||
|
validate?: (value: T) => ReactNode | ReactNode[] | true | null | undefined
|
||||||
|
} & Items<string> &
|
||||||
|
PartialSelectProps<T>)
|
||||||
) & {
|
) & {
|
||||||
label: string
|
label: string
|
||||||
description?: string
|
description?: string
|
||||||
@@ -82,14 +93,14 @@ type FieldProps = (
|
|||||||
*
|
*
|
||||||
* You can directly pass HTML input props if needed (like required, pattern, etc)
|
* You can directly pass HTML input props if needed (like required, pattern, etc)
|
||||||
*/
|
*/
|
||||||
export const Field = ({
|
export const Field = <T extends object>({
|
||||||
type,
|
type,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
items,
|
items,
|
||||||
validate,
|
validate,
|
||||||
...props
|
...props
|
||||||
}: FieldProps) => {
|
}: FieldProps<T>) => {
|
||||||
const LabelAndDescription = (
|
const LabelAndDescription = (
|
||||||
<>
|
<>
|
||||||
<StyledLabel>{label}</StyledLabel>
|
<StyledLabel>{label}</StyledLabel>
|
||||||
@@ -174,6 +185,20 @@ export const Field = ({
|
|||||||
</FieldWrapper>
|
</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 = ({
|
const FieldItem = ({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { Box } from './Box'
|
import { Box } from './Box'
|
||||||
|
|
||||||
const StyledPopover = styled(RACPopover, {
|
export const StyledPopover = styled(RACPopover, {
|
||||||
base: {
|
base: {
|
||||||
'&[data-placement="bottom"]': {
|
'&[data-placement="bottom"]': {
|
||||||
marginTop: 0.25,
|
marginTop: 0.25,
|
||||||
|
|||||||
98
src/frontend/src/primitives/Select.tsx
Normal file
98
src/frontend/src/primitives/Select.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user