diff --git a/.changeset/fancy-parents-sit.md b/.changeset/fancy-parents-sit.md new file mode 100644 index 0000000000..4bda11ae4a --- /dev/null +++ b/.changeset/fancy-parents-sit.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added support for grouping options into sections in the Select component. You can now pass section objects with a `title` and a nested `options` array alongside (or instead of) regular options to render grouped dropdowns with section headers. + +**Affected components:** Select diff --git a/docs-ui/src/app/components/select/components.tsx b/docs-ui/src/app/components/select/components.tsx index 1cbc323b84..46cf6b0666 100644 --- a/docs-ui/src/app/components/select/components.tsx +++ b/docs-ui/src/app/components/select/components.tsx @@ -40,6 +40,33 @@ const skills = [ { value: 'swift', label: 'Swift' }, ]; +const sectionedFonts = [ + { + title: 'Serif Fonts', + options: [ + { value: 'times', label: 'Times New Roman' }, + { value: 'georgia', label: 'Georgia' }, + { value: 'garamond', label: 'Garamond' }, + ], + }, + { + title: 'Sans-Serif Fonts', + options: [ + { value: 'arial', label: 'Arial' }, + { value: 'helvetica', label: 'Helvetica' }, + { value: 'verdana', label: 'Verdana' }, + ], + }, + { + title: 'Monospace Fonts', + options: [ + { value: 'courier', label: 'Courier New' }, + { value: 'consolas', label: 'Consolas' }, + { value: 'fira', label: 'Fira Code' }, + ], + }, +]; + export const Preview = () => ( +); + +export const SearchableWithSections = () => ( + `; + +export const selectSectionsSnippet = ``; diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index c90408ef23..7b5d9806fd 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -2181,6 +2181,12 @@ type Option_2 = { }; export { Option_2 as Option }; +// @public (undocumented) +export type OptionSection = { + title: string; + options: Option_2[]; +}; + // @public (undocumented) export interface PaddingProps { // (undocumented) @@ -2667,7 +2673,7 @@ export const SelectDefinition: { export type SelectOwnProps = { icon?: ReactNode; size?: 'small' | 'medium' | Partial>; - options?: Array; + options?: Array; searchable?: boolean; searchPlaceholder?: string; label?: FieldLabelProps['label']; diff --git a/packages/ui/src/components/Select/Select.module.css b/packages/ui/src/components/Select/Select.module.css index c5ce30e363..1016c42c4a 100644 --- a/packages/ui/src/components/Select/Select.module.css +++ b/packages/ui/src/components/Select/Select.module.css @@ -251,6 +251,25 @@ } } + .bui-SelectSection { + &:first-child .bui-SelectSectionHeader { + padding-top: 0; + } + } + + .bui-SelectSectionHeader { + height: 2rem; + display: flex; + align-items: center; + padding-top: var(--bui-space-3); + padding-left: var(--bui-space-3); + color: var(--bui-fg-primary); + font-size: var(--bui-font-size-1); + font-weight: bold; + letter-spacing: 0.05rem; + text-transform: uppercase; + } + .bui-SelectNoResults { padding-inline: var(--bui-space-3); padding-block: var(--bui-space-2); diff --git a/packages/ui/src/components/Select/Select.stories.tsx b/packages/ui/src/components/Select/Select.stories.tsx index cf410e43a1..b4fc111eb9 100644 --- a/packages/ui/src/components/Select/Select.stories.tsx +++ b/packages/ui/src/components/Select/Select.stories.tsx @@ -105,6 +105,51 @@ export const SearchableMultiple = meta.story({ }, }); +const sectionedOptions = [ + { + title: 'Serif Fonts', + options: [ + { value: 'times', label: 'Times New Roman' }, + { value: 'georgia', label: 'Georgia' }, + { value: 'garamond', label: 'Garamond' }, + ], + }, + { + title: 'Sans-Serif Fonts', + options: [ + { value: 'arial', label: 'Arial' }, + { value: 'helvetica', label: 'Helvetica' }, + { value: 'verdana', label: 'Verdana' }, + ], + }, + { + title: 'Monospace Fonts', + options: [ + { value: 'courier', label: 'Courier New' }, + { value: 'consolas', label: 'Consolas' }, + { value: 'fira', label: 'Fira Code' }, + ], + }, +]; + +export const WithSections = meta.story({ + args: { + label: 'Font Family', + options: sectionedOptions, + name: 'font', + }, +}); + +export const SearchableWithSections = meta.story({ + args: { + label: 'Font Family', + searchable: true, + searchPlaceholder: 'Search fonts...', + options: sectionedOptions, + name: 'font', + }, +}); + export const Preview = meta.story({ args: { label: 'Font Family', diff --git a/packages/ui/src/components/Select/SelectContent.tsx b/packages/ui/src/components/Select/SelectContent.tsx index 2797e92d2d..e08fb92d85 100644 --- a/packages/ui/src/components/Select/SelectContent.tsx +++ b/packages/ui/src/components/Select/SelectContent.tsx @@ -25,12 +25,12 @@ import { RiCloseCircleLine } from '@remixicon/react'; import { useDefinition } from '../../hooks/useDefinition'; import { SelectContentDefinition } from './definition'; import { SelectListBox } from './SelectListBox'; -import type { Option } from './types'; +import type { SelectOwnProps } from './types'; interface SelectContentProps { searchable?: boolean; searchPlaceholder?: string; - options?: Array