diff --git a/src/components/select/index.tsx b/src/components/select/index.tsx new file mode 100644 index 00000000..1ee4d4fe --- /dev/null +++ b/src/components/select/index.tsx @@ -0,0 +1,76 @@ +import { Select as BaseSelect, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' +import { Label } from '../ui/label' +import { cn } from '../../lib/utils' + +export type SelectOption = { + label: string + value: T +} + +export type SelectProps = { + className?: string + defaultValue?: T + disabled?: boolean + handleChange?: (value: T) => void + label?: string + options?: SelectOption[] + placeholder?: string + value?: T +} + +// Avoid throwing an error if has an empty string value. +// https://github.com/radix-ui/primitives/blob/main/packages/react/select/src/select.tsx#L1277 +const EMPTY_VALUE_PLACEHOLDER = '__empty__' + +export const Select = ({ + className, + defaultValue, + disabled = false, + handleChange, + label, + options = [], + placeholder = 'Select an option', + value, +}: SelectProps) => { + const normalizeValue = (val: T | undefined) => { + if (val === '') return EMPTY_VALUE_PLACEHOLDER + return val + } + + const denormalizeValue = (val: string): T => { + if (val === EMPTY_VALUE_PLACEHOLDER) return '' as T + return val as T + } + + const handleValueChange = (newValue: string) => { + if (handleChange) { + handleChange(denormalizeValue(newValue)) + } + } + + return ( +
+ {label && } + + + + + + {options.map(option => { + const itemValue = option.value === '' ? EMPTY_VALUE_PLACEHOLDER : String(option.value) + return ( + + {option.label} + + ) + })} + + +
+ ) +} diff --git a/src/stories/Select/Select.stories.tsx b/src/stories/Select/Select.stories.tsx index 0d25895f..ee90f1ec 100644 --- a/src/stories/Select/Select.stories.tsx +++ b/src/stories/Select/Select.stories.tsx @@ -1,28 +1,11 @@ import type { Meta, StoryObj } from '@storybook/react-vite' -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from '../../components/ui/select.tsx' -import { expect, within, userEvent } from 'storybook/test' +import { Select } from '../../components/select' const meta: Meta = { title: 'Components/Select', component: Select, parameters: { - docs: { - description: { - component: 'Displays a list of options for the user to pick from—triggered by a button.', - }, - }, layout: 'centered', - design: { - type: 'figma', - url: 'https://www.figma.com/design/dSsI9L6NSpNCorbSdiYd1k/Oasis-Design-System---shadcn-ui---Default---December-2024?node-id=118-1264&p=f&t=wiAnBZzlnMC9rGYE-0', - }, }, tags: ['autodocs'], } @@ -30,31 +13,17 @@ const meta: Meta = { export default meta type Story = StoryObj +const options = [ + { value: 'rofl_create', label: 'ROFL Create' }, + { value: 'rofl_register', label: 'ROFL Register' }, + { value: 'rofl_remove', label: 'ROFL Remove' }, + { value: 'rofl_update', label: 'ROFL Update' }, + { value: '', label: 'Unknown' }, +] + export const Default: Story = { args: { - defaultValue: '', - }, - render: args => ( - - ), - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - const selectTrigger = canvas.getByRole('combobox') - await expect(selectTrigger).toBeInTheDocument() - await userEvent.click(selectTrigger) - const options = document.querySelectorAll('[role="option"]') - await expect(options).toBeTruthy() + options, + placeholder: 'Select type', }, }