feat(SW-1509): new select component in design-system
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import 'react-material-symbols/rounded'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Select } from './Select'
|
||||
|
||||
const meta: Meta<typeof Select> = {
|
||||
title: 'Components/Select',
|
||||
component: Select,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Select>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
icon: 'star',
|
||||
itemIcon: 'check',
|
||||
items: ['Foo', 'Bar', 'Baz'],
|
||||
// items: new Array(30).fill(null).map((_, idx) => idx),
|
||||
label: 'Select an item',
|
||||
name: 'foo',
|
||||
},
|
||||
}
|
||||
89
packages/design-system/lib/components/Select/Select.tsx
Normal file
89
packages/design-system/lib/components/Select/Select.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
Select as AriaSelect,
|
||||
SelectValue,
|
||||
Popover,
|
||||
ListBox,
|
||||
Button,
|
||||
} from 'react-aria-components'
|
||||
import { Typography } from '../Typography'
|
||||
|
||||
import styles from './select.module.css'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { SelectItem } from './SelectItem'
|
||||
|
||||
import type { SelectProps } from './types'
|
||||
|
||||
export function Select({
|
||||
name,
|
||||
label,
|
||||
items,
|
||||
isRequired,
|
||||
isDisabled,
|
||||
icon,
|
||||
itemIcon,
|
||||
}: SelectProps) {
|
||||
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
|
||||
|
||||
return (
|
||||
<AriaSelect
|
||||
className={styles.select}
|
||||
name={name}
|
||||
aria-label={label}
|
||||
isRequired={isRequired}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<Button className={styles.button}>
|
||||
{icon ? (
|
||||
<MaterialIcon
|
||||
icon={icon}
|
||||
size={24}
|
||||
color={iconColor}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<SelectValue className={styles.selectValue}>
|
||||
{({ isPlaceholder, selectedText }) => (
|
||||
<>
|
||||
<Typography
|
||||
variant={
|
||||
isPlaceholder ? 'Body/Paragraph/mdRegular' : 'Label/xsRegular'
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{label}</span>
|
||||
</Typography>
|
||||
{!isPlaceholder && selectedText}
|
||||
</>
|
||||
)}
|
||||
</SelectValue>
|
||||
|
||||
<MaterialIcon
|
||||
icon="chevron_right"
|
||||
size={24}
|
||||
color={iconColor}
|
||||
aria-hidden="true"
|
||||
className={styles.chevron}
|
||||
/>
|
||||
</Button>
|
||||
<Popover className={styles.popover} shouldFlip={false}>
|
||||
<ListBox className={styles.listBox}>
|
||||
{items.map((item, idx) => (
|
||||
<Typography variant="Body/Paragraph/mdRegular" key={idx}>
|
||||
{typeof item === 'object' ? (
|
||||
<SelectItem
|
||||
icon={item.icon || itemIcon}
|
||||
isDisabled={item.isDisabled}
|
||||
>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
) : (
|
||||
<SelectItem icon={itemIcon} isDisabled={isDisabled}>
|
||||
{item.toString()}
|
||||
</SelectItem>
|
||||
)}
|
||||
</Typography>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</AriaSelect>
|
||||
)
|
||||
}
|
||||
35
packages/design-system/lib/components/Select/SelectItem.tsx
Normal file
35
packages/design-system/lib/components/Select/SelectItem.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ListBoxItem } from 'react-aria-components'
|
||||
import { Typography } from '../Typography'
|
||||
import styles from './select.module.css'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { SelectItemProps } from './types'
|
||||
|
||||
export function SelectItem({ children, icon, isDisabled }: SelectItemProps) {
|
||||
return (
|
||||
<ListBoxItem
|
||||
className={styles.listBoxItem}
|
||||
textValue={children}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
{icon ? (
|
||||
<MaterialIcon
|
||||
icon={icon}
|
||||
size={24}
|
||||
color={isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<Typography
|
||||
variant={
|
||||
isSelected ? 'Body/Paragraph/mdBold' : 'Body/Paragraph/mdRegular'
|
||||
}
|
||||
>
|
||||
<span>{children}</span>
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</ListBoxItem>
|
||||
)
|
||||
}
|
||||
2
packages/design-system/lib/components/Select/index.ts
Normal file
2
packages/design-system/lib/components/Select/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Select } from './Select'
|
||||
export { SelectItem } from './SelectItem'
|
||||
@@ -0,0 +1,96 @@
|
||||
.select {
|
||||
position: relative;
|
||||
max-width: 300px;
|
||||
|
||||
&[data-required] .label::after {
|
||||
content: '*';
|
||||
}
|
||||
&[data-open] .chevron {
|
||||
rotate: -90deg;
|
||||
}
|
||||
&[data-focused] {
|
||||
.button {
|
||||
border: 1px solid var(--Border-Interactive-Focus);
|
||||
outline: none;
|
||||
}
|
||||
.selectValue {
|
||||
color: var(--Text-Interactive-Focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
rotate: 90deg;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: var(--Surface-UI-Fill-Default);
|
||||
border: 1px solid var(--Border-Default);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
padding: var(--Space-x1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
|
||||
&[disabled] {
|
||||
color: var(--Text-Interactive-Disabled);
|
||||
background-color: var(--Surface-Primary-Disabled);
|
||||
|
||||
.label,
|
||||
.selectValue {
|
||||
color: var(--Text-Interactive-Disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selectValue {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--Space-x05) / 2);
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--Text-Interactive-Placeholder);
|
||||
}
|
||||
|
||||
.popover {
|
||||
background-color: var(--Surface-Primary-Default);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
box-shadow: 0 0 14px 6px rgb(0 0 0 / 10%);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
overflow: auto;
|
||||
padding: var(--Space-x2);
|
||||
outline: none;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.listBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.listBoxItem {
|
||||
padding: var(--Space-x1) var(--Space-x1) var(--Space-x1) var(--Space-x15);
|
||||
color: var(--Text-Default);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
|
||||
&[data-focused] {
|
||||
outline: none;
|
||||
}
|
||||
&[data-focused],
|
||||
&[data-hovered] {
|
||||
background-color: var(--Surface-Primary-Hover);
|
||||
}
|
||||
}
|
||||
25
packages/design-system/lib/components/Select/types.ts
Normal file
25
packages/design-system/lib/components/Select/types.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ComponentProps } from 'react'
|
||||
import { Key, ListBoxItem, Select } from 'react-aria-components'
|
||||
import { MaterialIconProps } from '../Icons/MaterialIcon'
|
||||
|
||||
interface Item extends Record<string, unknown> {
|
||||
label: string
|
||||
value: Key
|
||||
isDisabled?: boolean
|
||||
icon?: MaterialIconProps['icon']
|
||||
}
|
||||
|
||||
export interface SelectProps extends ComponentProps<typeof Select> {
|
||||
icon?: MaterialIconProps['icon']
|
||||
itemIcon?: MaterialIconProps['icon']
|
||||
items: (Key | Item)[]
|
||||
name: string
|
||||
label: string
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
export interface SelectItemProps extends ComponentProps<typeof ListBoxItem> {
|
||||
icon?: MaterialIconProps['icon']
|
||||
children: string
|
||||
}
|
||||
Reference in New Issue
Block a user