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:
Christian Andolf
2025-04-11 10:44:27 +00:00
8 changed files with 494 additions and 0 deletions

View File

@@ -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',
},
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -0,0 +1,3 @@
export { Select } from './Select'
export { SelectItem } from './SelectItem'
export { SelectFilter } from './SelectFilter'

View 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);
}
}

View 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
}

View File

@@ -9,6 +9,7 @@
"./ChipButton": "./dist/components/ChipButton/index.js",
"./ChipLink": "./dist/components/ChipLink/index.js",
"./Chips": "./dist/components/Chips/index.js",
"./Select": "./dist/components/Select/index.js",
"./Typography": "./dist/components/Typography/index.js",
"./RegularRateCard": "./dist/components/RateCard/Regular/index.js",
"./CampaignRateCard": "./dist/components/RateCard/Campaign/index.js",