✨(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: {
|
||||
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: {
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
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