Skip to content

Commit 13bca08

Browse files
authored
Merge pull request #58 from oasisprotocol/mz/select
Create custom Select component
2 parents 41e4db7 + abfb4f7 commit 13bca08

2 files changed

Lines changed: 87 additions & 42 deletions

File tree

src/components/select/index.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Select as BaseSelect, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
2+
import { Label } from '../ui/label'
3+
import { cn } from '../../lib/utils'
4+
5+
export type SelectOption<T = string> = {
6+
label: string
7+
value: T
8+
}
9+
10+
export type SelectProps<T = string> = {
11+
className?: string
12+
defaultValue?: T
13+
disabled?: boolean
14+
handleChange?: (value: T) => void
15+
label?: string
16+
options?: SelectOption<T>[]
17+
placeholder?: string
18+
value?: T
19+
}
20+
21+
// Avoid throwing an error if <Select.Item /> has an empty string value.
22+
// https://github.com/radix-ui/primitives/blob/main/packages/react/select/src/select.tsx#L1277
23+
const EMPTY_VALUE_PLACEHOLDER = '__empty__'
24+
25+
export const Select = <T extends string = string>({
26+
className,
27+
defaultValue,
28+
disabled = false,
29+
handleChange,
30+
label,
31+
options = [],
32+
placeholder = 'Select an option',
33+
value,
34+
}: SelectProps<T>) => {
35+
const normalizeValue = (val: T | undefined) => {
36+
if (val === '') return EMPTY_VALUE_PLACEHOLDER
37+
return val
38+
}
39+
40+
const denormalizeValue = (val: string): T => {
41+
if (val === EMPTY_VALUE_PLACEHOLDER) return '' as T
42+
return val as T
43+
}
44+
45+
const handleValueChange = (newValue: string) => {
46+
if (handleChange) {
47+
handleChange(denormalizeValue(newValue))
48+
}
49+
}
50+
51+
return (
52+
<div className={cn('space-y-2', className)}>
53+
{label && <Label>{label}</Label>}
54+
<BaseSelect
55+
value={normalizeValue(value)}
56+
onValueChange={handleValueChange}
57+
defaultValue={normalizeValue(defaultValue)}
58+
disabled={disabled}
59+
>
60+
<SelectTrigger className="[&_svg:not([class*='text-'])]:text-foreground [&_svg]:opacity-100 text-foreground font-medium data-[size=default]:h-10 w-full bg-background focus-visible:ring-ring/20">
61+
<SelectValue placeholder={placeholder} />
62+
</SelectTrigger>
63+
<SelectContent>
64+
{options.map(option => {
65+
const itemValue = option.value === '' ? EMPTY_VALUE_PLACEHOLDER : String(option.value)
66+
return (
67+
<SelectItem key={itemValue} value={itemValue}>
68+
{option.label}
69+
</SelectItem>
70+
)
71+
})}
72+
</SelectContent>
73+
</BaseSelect>
74+
</div>
75+
)
76+
}
Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,29 @@
11
import type { Meta, StoryObj } from '@storybook/react-vite'
2-
import {
3-
Select,
4-
SelectContent,
5-
SelectGroup,
6-
SelectItem,
7-
SelectTrigger,
8-
SelectValue,
9-
} from '../../components/ui/select.tsx'
10-
import { expect, within, userEvent } from 'storybook/test'
2+
import { Select } from '../../components/select'
113

124
const meta: Meta<typeof Select> = {
135
title: 'Components/Select',
146
component: Select,
157
parameters: {
16-
docs: {
17-
description: {
18-
component: 'Displays a list of options for the user to pick from—triggered by a button.',
19-
},
20-
},
218
layout: 'centered',
22-
design: {
23-
type: 'figma',
24-
url: 'https://www.figma.com/design/dSsI9L6NSpNCorbSdiYd1k/Oasis-Design-System---shadcn-ui---Default---December-2024?node-id=118-1264&p=f&t=wiAnBZzlnMC9rGYE-0',
25-
},
269
},
2710
tags: ['autodocs'],
2811
}
2912

3013
export default meta
3114
type Story = StoryObj<typeof meta>
3215

16+
const options = [
17+
{ value: 'rofl_create', label: 'ROFL Create' },
18+
{ value: 'rofl_register', label: 'ROFL Register' },
19+
{ value: 'rofl_remove', label: 'ROFL Remove' },
20+
{ value: 'rofl_update', label: 'ROFL Update' },
21+
{ value: '', label: 'Unknown' },
22+
]
23+
3324
export const Default: Story = {
3425
args: {
35-
defaultValue: '',
36-
},
37-
render: args => (
38-
<Select {...args}>
39-
<SelectTrigger className="w-[180px]">
40-
<SelectValue placeholder="Select type" />
41-
</SelectTrigger>
42-
<SelectContent>
43-
<SelectGroup>
44-
<SelectItem value="rofl_create">ROFL Create</SelectItem>
45-
<SelectItem value="rofl_register">ROFL Register</SelectItem>
46-
<SelectItem value="rofl_remove">ROFL Remove</SelectItem>
47-
<SelectItem value="rofl_update">ROFL Update</SelectItem>
48-
</SelectGroup>
49-
</SelectContent>
50-
</Select>
51-
),
52-
play: async ({ canvasElement }) => {
53-
const canvas = within(canvasElement)
54-
const selectTrigger = canvas.getByRole('combobox')
55-
await expect(selectTrigger).toBeInTheDocument()
56-
await userEvent.click(selectTrigger)
57-
const options = document.querySelectorAll('[role="option"]')
58-
await expect(options).toBeTruthy()
26+
options,
27+
placeholder: 'Select type',
5928
},
6029
}

0 commit comments

Comments
 (0)