From 77e4e9d2032c0fa4bc0e6568f52fa78209c9ef67 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Tue, 8 Apr 2025 12:54:48 +0200 Subject: [PATCH 01/10] feat(SW-1509): new select component in design-system --- .../lib/components/Select/Select.stories.tsx | 26 +++++ .../lib/components/Select/Select.tsx | 89 +++++++++++++++++ .../lib/components/Select/SelectItem.tsx | 35 +++++++ .../lib/components/Select/index.ts | 2 + .../lib/components/Select/select.module.css | 96 +++++++++++++++++++ .../lib/components/Select/types.ts | 25 +++++ 6 files changed, 273 insertions(+) create mode 100644 packages/design-system/lib/components/Select/Select.stories.tsx create mode 100644 packages/design-system/lib/components/Select/Select.tsx create mode 100644 packages/design-system/lib/components/Select/SelectItem.tsx create mode 100644 packages/design-system/lib/components/Select/index.ts create mode 100644 packages/design-system/lib/components/Select/select.module.css create mode 100644 packages/design-system/lib/components/Select/types.ts diff --git a/packages/design-system/lib/components/Select/Select.stories.tsx b/packages/design-system/lib/components/Select/Select.stories.tsx new file mode 100644 index 000000000..a04fb7e75 --- /dev/null +++ b/packages/design-system/lib/components/Select/Select.stories.tsx @@ -0,0 +1,26 @@ +import 'react-material-symbols/rounded' + +import type { Meta, StoryObj } from '@storybook/react' + +import { Select } from './Select' + +const meta: Meta = { + title: 'Components/Select', + component: Select, + argTypes: {}, +} + +export default meta + +type Story = StoryObj + +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', + }, +} diff --git a/packages/design-system/lib/components/Select/Select.tsx b/packages/design-system/lib/components/Select/Select.tsx new file mode 100644 index 000000000..343c07f54 --- /dev/null +++ b/packages/design-system/lib/components/Select/Select.tsx @@ -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 ( + + + + + {items.map((item, idx) => ( + + {typeof item === 'object' ? ( + + {item.label} + + ) : ( + + {item.toString()} + + )} + + ))} + + + + ) +} diff --git a/packages/design-system/lib/components/Select/SelectItem.tsx b/packages/design-system/lib/components/Select/SelectItem.tsx new file mode 100644 index 000000000..2e8efbc81 --- /dev/null +++ b/packages/design-system/lib/components/Select/SelectItem.tsx @@ -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 ( + + {({ isSelected }) => ( + <> + {icon ? ( + + ) +} diff --git a/packages/design-system/lib/components/Select/index.ts b/packages/design-system/lib/components/Select/index.ts new file mode 100644 index 000000000..65e5c3fbf --- /dev/null +++ b/packages/design-system/lib/components/Select/index.ts @@ -0,0 +1,2 @@ +export { Select } from './Select' +export { SelectItem } from './SelectItem' diff --git a/packages/design-system/lib/components/Select/select.module.css b/packages/design-system/lib/components/Select/select.module.css new file mode 100644 index 000000000..35c839131 --- /dev/null +++ b/packages/design-system/lib/components/Select/select.module.css @@ -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); + } +} diff --git a/packages/design-system/lib/components/Select/types.ts b/packages/design-system/lib/components/Select/types.ts new file mode 100644 index 000000000..a92502315 --- /dev/null +++ b/packages/design-system/lib/components/Select/types.ts @@ -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 { + label: string + value: Key + isDisabled?: boolean + icon?: MaterialIconProps['icon'] +} + +export interface SelectProps extends ComponentProps { + icon?: MaterialIconProps['icon'] + itemIcon?: MaterialIconProps['icon'] + items: (Key | Item)[] + name: string + label: string + isRequired?: boolean + isDisabled?: boolean +} + +export interface SelectItemProps extends ComponentProps { + icon?: MaterialIconProps['icon'] + children: string +} From bc7cec215cd0bc490ebc7b654fa6e40603f03522 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Tue, 8 Apr 2025 15:59:58 +0200 Subject: [PATCH 02/10] feat(SW-1509): enable filtering select --- .../lib/components/Select/Select.stories.tsx | 26 ++++- .../lib/components/Select/Select.tsx | 32 +++++- .../lib/components/Select/SelectFilter.tsx | 104 ++++++++++++++++++ .../lib/components/Select/index.ts | 1 + .../lib/components/Select/select.module.css | 74 +++++++++---- .../lib/components/Select/types.ts | 1 + 6 files changed, 213 insertions(+), 25 deletions(-) create mode 100644 packages/design-system/lib/components/Select/SelectFilter.tsx diff --git a/packages/design-system/lib/components/Select/Select.stories.tsx b/packages/design-system/lib/components/Select/Select.stories.tsx index a04fb7e75..daf9e067a 100644 --- a/packages/design-system/lib/components/Select/Select.stories.tsx +++ b/packages/design-system/lib/components/Select/Select.stories.tsx @@ -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, + }, +} diff --git a/packages/design-system/lib/components/Select/Select.tsx b/packages/design-system/lib/components/Select/Select.tsx index 343c07f54..299d542de 100644 --- a/packages/design-system/lib/components/Select/Select.tsx +++ b/packages/design-system/lib/components/Select/Select.tsx @@ -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 ( + + ) + } const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default' return ( @@ -32,7 +49,7 @@ export function Select({ isRequired={isRequired} isDisabled={isDisabled} > - + {items.map((item, idx) => ( diff --git a/packages/design-system/lib/components/Select/SelectFilter.tsx b/packages/design-system/lib/components/Select/SelectFilter.tsx new file mode 100644 index 000000000..33c9886f1 --- /dev/null +++ b/packages/design-system/lib/components/Select/SelectFilter.tsx @@ -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(null) + const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default' + + return ( + setValue(val)} + onFocus={() => setFocus(true)} + onBlur={() => setFocus(false)} + > + + + + + {items.map((item, idx) => ( + + {typeof item === 'object' ? ( + + {item.label} + + ) : ( + + {item.toString()} + + )} + + ))} + + + + ) +} diff --git a/packages/design-system/lib/components/Select/index.ts b/packages/design-system/lib/components/Select/index.ts index 65e5c3fbf..20fbbe724 100644 --- a/packages/design-system/lib/components/Select/index.ts +++ b/packages/design-system/lib/components/Select/index.ts @@ -1,2 +1,3 @@ export { Select } from './Select' export { SelectItem } from './SelectItem' +export { SelectFilter } from './SelectFilter' diff --git a/packages/design-system/lib/components/Select/select.module.css b/packages/design-system/lib/components/Select/select.module.css index 35c839131..a4eefbebf 100644 --- a/packages/design-system/lib/components/Select/select.module.css +++ b/packages/design-system/lib/components/Select/select.module.css @@ -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); } diff --git a/packages/design-system/lib/components/Select/types.ts b/packages/design-system/lib/components/Select/types.ts index a92502315..9eeabfa22 100644 --- a/packages/design-system/lib/components/Select/types.ts +++ b/packages/design-system/lib/components/Select/types.ts @@ -17,6 +17,7 @@ export interface SelectProps extends ComponentProps { label: string isRequired?: boolean isDisabled?: boolean + enableFiltering?: boolean } export interface SelectItemProps extends ComponentProps { From bacd57a735de5787eb8ea5e0a1a1aabb4b6fa20c Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Wed, 9 Apr 2025 11:51:37 +0200 Subject: [PATCH 03/10] fix(SW-1509): new select filter props interface --- .../design-system/lib/components/Select/SelectFilter.tsx | 4 ++-- packages/design-system/lib/components/Select/types.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/design-system/lib/components/Select/SelectFilter.tsx b/packages/design-system/lib/components/Select/SelectFilter.tsx index 33c9886f1..ec65f91c3 100644 --- a/packages/design-system/lib/components/Select/SelectFilter.tsx +++ b/packages/design-system/lib/components/Select/SelectFilter.tsx @@ -13,7 +13,7 @@ import { MaterialIcon } from '../Icons/MaterialIcon' import { Typography } from '../Typography' import { SelectItem } from './SelectItem' -import type { SelectProps } from './types' +import type { SelectFilterProps } from './types' import styles from './select.module.css' @@ -25,7 +25,7 @@ export function SelectFilter({ isDisabled, icon, itemIcon, -}: SelectProps) { +}: SelectFilterProps) { const [focus, setFocus] = useState(false) const [value, setValue] = useState(null) const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default' diff --git a/packages/design-system/lib/components/Select/types.ts b/packages/design-system/lib/components/Select/types.ts index 9eeabfa22..78708943d 100644 --- a/packages/design-system/lib/components/Select/types.ts +++ b/packages/design-system/lib/components/Select/types.ts @@ -24,3 +24,8 @@ export interface SelectItemProps extends ComponentProps { icon?: MaterialIconProps['icon'] children: string } + +// Disabling rule because we're just omitting one prop while keeping interface +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface SelectFilterProps + extends Omit {} From 158c2501a5eb7e90e1c80776b41289b20f5f6cb1 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Wed, 9 Apr 2025 13:27:45 +0200 Subject: [PATCH 04/10] feat(SW-1509): add select to exports --- packages/design-system/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 051a1bcb5..1aad0512e 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -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", From 9ef292709b81eebedb34e1f318683c3b916b4434 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Wed, 9 Apr 2025 17:33:44 +0200 Subject: [PATCH 05/10] feat(SW-1509): enable support for more react aria props fix invalid border corrected default border --- .../design-system/lib/components/Select/Select.tsx | 8 ++++---- .../lib/components/Select/select.module.css | 11 +++++++---- packages/design-system/lib/components/Select/types.ts | 2 -- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/design-system/lib/components/Select/Select.tsx b/packages/design-system/lib/components/Select/Select.tsx index 299d542de..9a3d67998 100644 --- a/packages/design-system/lib/components/Select/Select.tsx +++ b/packages/design-system/lib/components/Select/Select.tsx @@ -20,11 +20,11 @@ export function Select({ name, label, items, - isRequired, isDisabled, icon, itemIcon, enableFiltering, + ...props }: SelectProps) { if (enableFiltering) { return ( @@ -32,10 +32,10 @@ export function Select({ name={name} label={label} items={items} - isRequired={isRequired} - isDisabled={isDisabled} icon={icon} itemIcon={itemIcon} + isDisabled={isDisabled} + {...props} /> ) } @@ -46,8 +46,8 @@ export function Select({ className={styles.select} name={name} aria-label={label} - isRequired={isRequired} isDisabled={isDisabled} + {...props} >