Merged in feat/SW-1509-new-select-component (pull request #1766)
Feat/SW-1509 new select component Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
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>
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ label: 'Foo', value: 'foo' },
|
||||||
|
{ label: 'Bar', value: 'bar' },
|
||||||
|
{ label: 'Baz', value: 'baz' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
items,
|
||||||
|
label: 'Select an item',
|
||||||
|
name: 'foo',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultSelected: Story = {
|
||||||
|
args: {
|
||||||
|
items,
|
||||||
|
label: 'Select an item',
|
||||||
|
name: 'foo',
|
||||||
|
defaultSelectedKey: 'foo',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Icons: Story = {
|
||||||
|
args: {
|
||||||
|
items,
|
||||||
|
label: 'Select an item',
|
||||||
|
name: 'foo',
|
||||||
|
icon: 'star',
|
||||||
|
itemIcon: 'check',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Filtering: Story = {
|
||||||
|
args: {
|
||||||
|
items,
|
||||||
|
label: 'Select an item',
|
||||||
|
name: 'foo',
|
||||||
|
enableFiltering: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilteringSelected: Story = {
|
||||||
|
args: {
|
||||||
|
items,
|
||||||
|
label: 'Select an item',
|
||||||
|
name: 'foo',
|
||||||
|
enableFiltering: true,
|
||||||
|
defaultSelectedKey: 'foo',
|
||||||
|
},
|
||||||
|
}
|
||||||
107
packages/design-system/lib/components/Select/Select.tsx
Normal file
107
packages/design-system/lib/components/Select/Select.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import {
|
||||||
|
Select as AriaSelect,
|
||||||
|
SelectValue,
|
||||||
|
Popover,
|
||||||
|
ListBox,
|
||||||
|
Button,
|
||||||
|
} from 'react-aria-components'
|
||||||
|
import { cx } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||||
|
import { Typography } from '../Typography'
|
||||||
|
import { SelectItem } from './SelectItem'
|
||||||
|
import { SelectFilter } from './SelectFilter'
|
||||||
|
|
||||||
|
import type { SelectProps, SelectFilterProps } from './types'
|
||||||
|
|
||||||
|
import styles from './select.module.css'
|
||||||
|
|
||||||
|
export function Select({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
items,
|
||||||
|
isDisabled,
|
||||||
|
icon,
|
||||||
|
itemIcon,
|
||||||
|
...props
|
||||||
|
}: SelectProps | SelectFilterProps) {
|
||||||
|
if ('enableFiltering' in props) {
|
||||||
|
return (
|
||||||
|
<SelectFilter
|
||||||
|
name={name}
|
||||||
|
label={label}
|
||||||
|
items={items}
|
||||||
|
icon={icon}
|
||||||
|
itemIcon={itemIcon}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaSelect
|
||||||
|
className={styles.select}
|
||||||
|
name={name}
|
||||||
|
aria-label={label}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Button className={cx(styles.inner, styles.button)}>
|
||||||
|
{icon ? (
|
||||||
|
<MaterialIcon
|
||||||
|
icon={icon}
|
||||||
|
size={24}
|
||||||
|
color={iconColor}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<SelectValue className={cx(styles.displayText, styles.selectValue)}>
|
||||||
|
{({ selectedText }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
variant={
|
||||||
|
selectedText
|
||||||
|
? 'Label/xsRegular'
|
||||||
|
: 'Body/Paragraph/mdRegular'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className={styles.label}>{label}</span>
|
||||||
|
</Typography>
|
||||||
|
{selectedText ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<span>{selectedText}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</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) => (
|
||||||
|
<SelectItem
|
||||||
|
key={item.value}
|
||||||
|
id={item.value}
|
||||||
|
icon={item.icon || itemIcon}
|
||||||
|
isDisabled={item.isDisabled}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</ListBox>
|
||||||
|
</Popover>
|
||||||
|
</AriaSelect>
|
||||||
|
)
|
||||||
|
}
|
||||||
105
packages/design-system/lib/components/Select/SelectFilter.tsx
Normal file
105
packages/design-system/lib/components/Select/SelectFilter.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ComboBox,
|
||||||
|
Input,
|
||||||
|
Key,
|
||||||
|
ListBox,
|
||||||
|
Popover,
|
||||||
|
} from 'react-aria-components'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||||
|
import { Typography } from '../Typography'
|
||||||
|
import { SelectItem } from './SelectItem'
|
||||||
|
|
||||||
|
import type { SelectFilterProps } from './types'
|
||||||
|
|
||||||
|
import styles from './select.module.css'
|
||||||
|
|
||||||
|
export function SelectFilter({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
items,
|
||||||
|
isDisabled,
|
||||||
|
icon,
|
||||||
|
itemIcon,
|
||||||
|
defaultSelectedKey,
|
||||||
|
onSelectionChange,
|
||||||
|
...props
|
||||||
|
}: SelectFilterProps) {
|
||||||
|
const [focus, setFocus] = useState(false)
|
||||||
|
const [value, setValue] = useState<Key | null>(defaultSelectedKey ?? null)
|
||||||
|
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComboBox
|
||||||
|
className={styles.select}
|
||||||
|
name={name}
|
||||||
|
aria-label={label}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onSelectionChange={(val) => {
|
||||||
|
setValue(val)
|
||||||
|
if (onSelectionChange) {
|
||||||
|
onSelectionChange(val)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => setFocus(true)}
|
||||||
|
onBlur={() => setFocus(false)}
|
||||||
|
defaultSelectedKey={defaultSelectedKey}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<label className={styles.inner}>
|
||||||
|
{icon ? (
|
||||||
|
<MaterialIcon
|
||||||
|
icon={icon}
|
||||||
|
size={24}
|
||||||
|
color={iconColor}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<span className={styles.displayText}>
|
||||||
|
<Typography
|
||||||
|
variant={
|
||||||
|
focus || value ? 'Label/xsRegular' : 'Body/Paragraph/mdRegular'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className={styles.label}>{label}</span>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<Input className={styles.input} />
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
<Button className={styles.button}>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="chevron_right"
|
||||||
|
size={24}
|
||||||
|
color={iconColor}
|
||||||
|
aria-hidden="true"
|
||||||
|
className={styles.chevron}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
className={styles.popover}
|
||||||
|
shouldFlip={false}
|
||||||
|
crossOffset={icon ? -40 : -8}
|
||||||
|
offset={22}
|
||||||
|
>
|
||||||
|
<ListBox className={styles.listBox}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<SelectItem
|
||||||
|
key={item.value}
|
||||||
|
id={item.value}
|
||||||
|
icon={item.icon || itemIcon}
|
||||||
|
isDisabled={item.isDisabled}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</ListBox>
|
||||||
|
</Popover>
|
||||||
|
</ComboBox>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
packages/design-system/lib/components/Select/SelectItem.tsx
Normal file
43
packages/design-system/lib/components/Select/SelectItem.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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,
|
||||||
|
...props
|
||||||
|
}: SelectItemProps) {
|
||||||
|
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListBoxItem
|
||||||
|
className={styles.listBoxItem}
|
||||||
|
textValue={children}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{({ isSelected }) => (
|
||||||
|
<>
|
||||||
|
{icon ? (
|
||||||
|
<MaterialIcon
|
||||||
|
icon={icon}
|
||||||
|
size={24}
|
||||||
|
color={iconColor}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Typography
|
||||||
|
variant={
|
||||||
|
isSelected ? 'Body/Paragraph/mdBold' : 'Body/Paragraph/mdRegular'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{children}</span>
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ListBoxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
3
packages/design-system/lib/components/Select/index.ts
Normal file
3
packages/design-system/lib/components/Select/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { Select } from './Select'
|
||||||
|
export { SelectItem } from './SelectItem'
|
||||||
|
export { SelectFilter } from './SelectFilter'
|
||||||
134
packages/design-system/lib/components/Select/select.module.css
Normal file
134
packages/design-system/lib/components/Select/select.module.css
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
.select {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--Surface-UI-Fill-Default);
|
||||||
|
border: 1px solid var(--Border-Interactive-Default);
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
|
||||||
|
&[data-required] .label::after {
|
||||||
|
content: ' *';
|
||||||
|
}
|
||||||
|
&[data-open] .chevron {
|
||||||
|
rotate: -90deg;
|
||||||
|
}
|
||||||
|
&[data-focused] {
|
||||||
|
border-color: var(--Border-Interactive-Focus);
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.input {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
color: var(--Text-Interactive-Focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-disabled] {
|
||||||
|
.inner {
|
||||||
|
background-color: var(--Surface-Primary-Disabled);
|
||||||
|
color: var(--Text-Interactive-Disabled);
|
||||||
|
}
|
||||||
|
.button,
|
||||||
|
.input,
|
||||||
|
.label,
|
||||||
|
.selectValue {
|
||||||
|
color: var(--Text-Interactive-Disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-invalid] {
|
||||||
|
border-color: var(--Border-Interactive-Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
rotate: 90deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
width: 100%;
|
||||||
|
height: 56px;
|
||||||
|
padding: var(--Space-x15);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.input {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
position: absolute;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&[value]:not([value='']) {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.displayText {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(var(--Space-x05) / 2);
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&:has(.input) {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectValue {
|
||||||
|
align-items: flex-start;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/design-system/lib/components/Select/types.ts
Normal file
34
packages/design-system/lib/components/Select/types.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ComponentProps } from 'react'
|
||||||
|
import { ComboBox, 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: Item[]
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
onSelectionChange?: (key: Key | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectItemProps extends ComponentProps<typeof ListBoxItem> {
|
||||||
|
icon?: MaterialIconProps['icon']
|
||||||
|
children: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectFilterProps extends ComponentProps<typeof ComboBox> {
|
||||||
|
icon?: MaterialIconProps['icon']
|
||||||
|
itemIcon?: MaterialIconProps['icon']
|
||||||
|
items: Item[]
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
onSelectionChange?: (key: Key | null) => void
|
||||||
|
enableFiltering: boolean
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"./ChipButton": "./dist/components/ChipButton/index.js",
|
"./ChipButton": "./dist/components/ChipButton/index.js",
|
||||||
"./ChipLink": "./dist/components/ChipLink/index.js",
|
"./ChipLink": "./dist/components/ChipLink/index.js",
|
||||||
"./Chips": "./dist/components/Chips/index.js",
|
"./Chips": "./dist/components/Chips/index.js",
|
||||||
|
"./Select": "./dist/components/Select/index.js",
|
||||||
"./Typography": "./dist/components/Typography/index.js",
|
"./Typography": "./dist/components/Typography/index.js",
|
||||||
"./RegularRateCard": "./dist/components/RateCard/Regular/index.js",
|
"./RegularRateCard": "./dist/components/RateCard/Regular/index.js",
|
||||||
"./CampaignRateCard": "./dist/components/RateCard/Campaign/index.js",
|
"./CampaignRateCard": "./dist/components/RateCard/Campaign/index.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user