From cb54195aae95155831363d6b0cd496ab6ba47f7e Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 21 Aug 2025 18:23:55 +0200 Subject: [PATCH 1/4] Create custom Select component --- src/components/select/index.tsx | 48 +++++++++++++++++++++++++ src/stories/Select/Select.stories.tsx | 52 ++++++--------------------- 2 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 src/components/select/index.tsx diff --git a/src/components/select/index.tsx b/src/components/select/index.tsx new file mode 100644 index 00000000..d279a024 --- /dev/null +++ b/src/components/select/index.tsx @@ -0,0 +1,48 @@ +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: string +} + +export type SelectProps = { + className?: string + defaultValue?: string + disabled?: boolean + handleChange?: (value: string) => void + label?: string + options: SelectOption[] + placeholder?: string + value?: string +} + +export const Select = ({ + className, + defaultValue, + disabled = false, + handleChange, + label, + options, + placeholder = 'Select an option', + value, +}: SelectProps) => { + return ( +
+ {label && } + + + + + + {options.map(option => ( + + {option.label} + + ))} + + +
+ ) +} diff --git a/src/stories/Select/Select.stories.tsx b/src/stories/Select/Select.stories.tsx index 0d25895f..bd4d084a 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,16 @@ 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' }, +] + 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', }, } From c85b051d621403bcf2ccbf07ba4fe703b98b2dba Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 21 Aug 2025 17:08:40 +0200 Subject: [PATCH 2/4] Use generic for SelectOption value --- src/components/select/index.tsx | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/select/index.tsx b/src/components/select/index.tsx index d279a024..19379d8e 100644 --- a/src/components/select/index.tsx +++ b/src/components/select/index.tsx @@ -2,42 +2,47 @@ import { Select as BaseSelect, SelectContent, SelectItem, SelectTrigger, SelectV import { Label } from '../ui/label' import { cn } from '../../lib/utils' -export type SelectOption = { +export type SelectOption = { label: string - value: string + value: T } -export type SelectProps = { +export type SelectProps = { className?: string - defaultValue?: string + defaultValue?: T disabled?: boolean - handleChange?: (value: string) => void + handleChange?: (value: T) => void label?: string - options: SelectOption[] + options?: SelectOption[] placeholder?: string - value?: string + value?: T } -export const Select = ({ +export const Select = ({ className, defaultValue, disabled = false, handleChange, label, - options, + options = [], placeholder = 'Select an option', value, -}: SelectProps) => { +}: SelectProps) => { return (
{label && } - + void) | undefined} + defaultValue={defaultValue as string} + disabled={disabled} + > {options.map(option => ( - + {option.label} ))} From 4d74071e0e65ddf95cf52bc98ac74c9ea3cfdd9c Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 21 Aug 2025 17:36:26 +0200 Subject: [PATCH 3/4] Handle empty string option value --- src/components/select/index.tsx | 39 +++++++++++++++++++++------ src/stories/Select/Select.stories.tsx | 1 + 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/components/select/index.tsx b/src/components/select/index.tsx index 19379d8e..9f32fd03 100644 --- a/src/components/select/index.tsx +++ b/src/components/select/index.tsx @@ -18,6 +18,10 @@ export type SelectProps = { 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, @@ -28,24 +32,43 @@ export const Select = ({ 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 && } void) | undefined} - defaultValue={defaultValue as string} + value={normalizeValue(value)} + onValueChange={handleValueChange} + defaultValue={normalizeValue(defaultValue)} disabled={disabled} > - {options.map(option => ( - - {option.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 bd4d084a..ee90f1ec 100644 --- a/src/stories/Select/Select.stories.tsx +++ b/src/stories/Select/Select.stories.tsx @@ -18,6 +18,7 @@ const options = [ { 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 = { From abfb4f7348431c267608d2a00a395467079a43fa Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 22 Aug 2025 11:12:38 +0200 Subject: [PATCH 4/4] Adjust icon based on design feedback --- src/components/select/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/select/index.tsx b/src/components/select/index.tsx index 9f32fd03..1ee4d4fe 100644 --- a/src/components/select/index.tsx +++ b/src/components/select/index.tsx @@ -57,7 +57,7 @@ export const Select = ({ defaultValue={normalizeValue(defaultValue)} disabled={disabled} > - +