feat(SW-1509): enable filtering select

This commit is contained in:
Christian Andolf
2025-04-08 15:59:58 +02:00
parent 77e4e9d203
commit bc7cec215c
6 changed files with 213 additions and 25 deletions

View File

@@ -19,8 +19,32 @@ export const Default: Story = {
icon: 'star', icon: 'star',
itemIcon: 'check', itemIcon: 'check',
items: ['Foo', 'Bar', 'Baz'], items: ['Foo', 'Bar', 'Baz'],
// items: new Array(30).fill(null).map((_, idx) => idx),
label: 'Select an item', label: 'Select an item',
name: 'foo', name: 'foo',
}, },
} }
export const ObjectItem: Story = {
args: {
icon: 'star',
itemIcon: 'check',
items: [
{ label: 'Foo', value: 'foo' },
{ label: 'Bar', value: 'bar' },
{ label: 'Baz', value: 'baz' },
],
label: 'Select an item',
name: 'foo',
},
}
export const Filtering: Story = {
args: {
icon: 'star',
itemIcon: 'check',
items: ['Foo', 'Bar', 'Baz'],
label: 'Select an item',
name: 'foo',
enableFiltering: true,
},
}

View File

@@ -5,14 +5,17 @@ import {
ListBox, ListBox,
Button, Button,
} from 'react-aria-components' } from 'react-aria-components'
import { Typography } from '../Typography' import { cx } from 'class-variance-authority'
import styles from './select.module.css'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography'
import { SelectItem } from './SelectItem' import { SelectItem } from './SelectItem'
import { SelectFilter } from './SelectFilter'
import type { SelectProps } from './types' import type { SelectProps } from './types'
import styles from './select.module.css'
export function Select({ export function Select({
name, name,
label, label,
@@ -21,7 +24,21 @@ export function Select({
isDisabled, isDisabled,
icon, icon,
itemIcon, itemIcon,
enableFiltering,
}: SelectProps) { }: SelectProps) {
if (enableFiltering) {
return (
<SelectFilter
name={name}
label={label}
items={items}
isRequired={isRequired}
isDisabled={isDisabled}
icon={icon}
itemIcon={itemIcon}
/>
)
}
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default' const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
return ( return (
@@ -32,7 +49,7 @@ export function Select({
isRequired={isRequired} isRequired={isRequired}
isDisabled={isDisabled} isDisabled={isDisabled}
> >
<Button className={styles.button}> <Button className={cx(styles.inner, styles.button)}>
{icon ? ( {icon ? (
<MaterialIcon <MaterialIcon
icon={icon} icon={icon}
@@ -41,7 +58,7 @@ export function Select({
aria-hidden="true" aria-hidden="true"
/> />
) : null} ) : null}
<SelectValue className={styles.selectValue}> <SelectValue className={cx(styles.displayText, styles.selectValue)}>
{({ isPlaceholder, selectedText }) => ( {({ isPlaceholder, selectedText }) => (
<> <>
<Typography <Typography
@@ -51,7 +68,11 @@ export function Select({
> >
<span className={styles.label}>{label}</span> <span className={styles.label}>{label}</span>
</Typography> </Typography>
{!isPlaceholder && selectedText} {selectedText ? (
<Typography variant="Body/Paragraph/mdRegular">
<span>{selectedText}</span>
</Typography>
) : null}
</> </>
)} )}
</SelectValue> </SelectValue>
@@ -64,6 +85,7 @@ export function Select({
className={styles.chevron} className={styles.chevron}
/> />
</Button> </Button>
<Popover className={styles.popover} shouldFlip={false}> <Popover className={styles.popover} shouldFlip={false}>
<ListBox className={styles.listBox}> <ListBox className={styles.listBox}>
{items.map((item, idx) => ( {items.map((item, idx) => (

View File

@@ -0,0 +1,104 @@
import {
Button,
ComboBox,
Input,
Key,
ListBox,
Popover,
} from 'react-aria-components'
import { cx } from 'class-variance-authority'
import { useState } from 'react'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography'
import { SelectItem } from './SelectItem'
import type { SelectProps } from './types'
import styles from './select.module.css'
export function SelectFilter({
name,
label,
items,
isRequired,
isDisabled,
icon,
itemIcon,
}: SelectProps) {
const [focus, setFocus] = useState(false)
const [value, setValue] = useState<Key | null>(null)
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
return (
<ComboBox
className={styles.select}
name={name}
aria-label={label}
isRequired={isRequired}
isDisabled={isDisabled}
onSelectionChange={(val) => setValue(val)}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
>
<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={cx(styles.input, { [styles.hasValue]: value })} />
</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, 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>
</ComboBox>
)
}

View File

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

View File

@@ -1,5 +1,8 @@
.select { .select {
position: relative; position: relative;
background-color: var(--Surface-UI-Fill-Default);
border: 1px solid var(--Border-Default);
border-radius: var(--Corner-radius-md);
max-width: 300px; max-width: 300px;
&[data-required] .label::after { &[data-required] .label::after {
@@ -9,48 +12,81 @@
rotate: -90deg; rotate: -90deg;
} }
&[data-focused] { &[data-focused] {
.button { border: 1px solid var(--Border-Interactive-Focus);
border: 1px solid var(--Border-Interactive-Focus);
.button,
.input {
outline: none; outline: none;
} }
.selectValue { .input {
position: unset;
}
.label {
color: var(--Text-Interactive-Focus); 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);
}
}
} }
.chevron { .chevron {
rotate: 90deg; rotate: 90deg;
} }
.button { .inner {
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; display: flex;
align-items: center; align-items: center;
gap: var(--Space-x1); gap: var(--Space-x1);
width: 100%; width: 100%;
height: 56px; height: 56px;
padding: var(--Space-x1);
box-sizing: border-box;
&[disabled] { .button {
color: var(--Text-Interactive-Disabled); padding: 0;
background-color: var(--Surface-Primary-Disabled); }
}
.label, .button,
.selectValue { .input {
color: var(--Text-Interactive-Disabled); background: none;
} border: 0;
}
.input {
position: absolute;
padding: 0;
&.hasValue {
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 { .selectValue {
display: flex;
flex-direction: column;
gap: calc(var(--Space-x05) / 2);
align-items: flex-start; align-items: flex-start;
flex: 1;
color: var(--Text-Default); color: var(--Text-Default);
} }

View File

@@ -17,6 +17,7 @@ export interface SelectProps extends ComponentProps<typeof Select> {
label: string label: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean isDisabled?: boolean
enableFiltering?: boolean
} }
export interface SelectItemProps extends ComponentProps<typeof ListBoxItem> { export interface SelectItemProps extends ComponentProps<typeof ListBoxItem> {