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',
itemIcon: 'check',
items: ['Foo', 'Bar', 'Baz'],
// items: new Array(30).fill(null).map((_, idx) => idx),
label: 'Select an item',
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,
Button,
} 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 { Typography } from '../Typography'
import { SelectItem } from './SelectItem'
import { SelectFilter } from './SelectFilter'
import type { SelectProps } from './types'
import styles from './select.module.css'
export function Select({
name,
label,
@@ -21,7 +24,21 @@ export function Select({
isDisabled,
icon,
itemIcon,
enableFiltering,
}: 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'
return (
@@ -32,7 +49,7 @@ export function Select({
isRequired={isRequired}
isDisabled={isDisabled}
>
<Button className={styles.button}>
<Button className={cx(styles.inner, styles.button)}>
{icon ? (
<MaterialIcon
icon={icon}
@@ -41,7 +58,7 @@ export function Select({
aria-hidden="true"
/>
) : null}
<SelectValue className={styles.selectValue}>
<SelectValue className={cx(styles.displayText, styles.selectValue)}>
{({ isPlaceholder, selectedText }) => (
<>
<Typography
@@ -51,7 +68,11 @@ export function Select({
>
<span className={styles.label}>{label}</span>
</Typography>
{!isPlaceholder && selectedText}
{selectedText ? (
<Typography variant="Body/Paragraph/mdRegular">
<span>{selectedText}</span>
</Typography>
) : null}
</>
)}
</SelectValue>
@@ -64,6 +85,7 @@ export function Select({
className={styles.chevron}
/>
</Button>
<Popover className={styles.popover} shouldFlip={false}>
<ListBox className={styles.listBox}>
{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 { SelectItem } from './SelectItem'
export { SelectFilter } from './SelectFilter'

View File

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

View File

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