From f977eee2e97de9e44dff6403b854a3f4f67f59f0 Mon Sep 17 00:00:00 2001 From: Vegard Haugstvedt Date: Fri, 1 Nov 2024 13:55:32 +0100 Subject: [PATCH 1/3] Combobox: Add options grouping. Searchable and creates headings in FilteredOptions. --- @navikt/core/css/form/combobox.css | 11 +++++ .../FilteredOptions/FilteredOptions.tsx | 42 +++++++++++++++++-- .../FilteredOptions/filtered-options-util.ts | 6 ++- .../FilteredOptions/useVirtualFocus.ts | 10 ++++- .../react/src/form/combobox/combobox-utils.ts | 11 +++-- .../src/form/combobox/combobox.stories.tsx | 40 ++++++++++++++++++ @navikt/core/react/src/form/combobox/types.ts | 5 +++ 7 files changed, 117 insertions(+), 8 deletions(-) diff --git a/@navikt/core/css/form/combobox.css b/@navikt/core/css/form/combobox.css index a6fcae859e..548d56c927 100644 --- a/@navikt/core/css/form/combobox.css +++ b/@navikt/core/css/form/combobox.css @@ -309,6 +309,17 @@ cursor: default; } +/* Group / category */ +.navds-combobox__list__group { + width: 100%; +} + +.navds-combobox__list__group__heading { + background-color: var(--a-surface-subtle); + padding-block: var(--a-spacing-05); + padding-inline: var(--a-spacing-3); +} + /* ul-list and selectable li-items */ .navds-combobox__list-options { diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx index 4db73fb21f..9f8db958a1 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx @@ -1,7 +1,9 @@ import cl from "clsx"; import React from "react"; +import { Detail } from "../../../typography"; import { useInputContext } from "../Input/Input.context"; import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext"; +import { ComboboxOption } from "../types"; import AddNewOption from "./AddNewOption"; import FilteredOptionsItem from "./FilteredOptionsItem"; import LoadingMessage from "./LoadingMessage"; @@ -34,6 +36,16 @@ const FilteredOptions = () => { (allowNewValues && isValueNew && !maxSelected?.isLimitReached) || // Render add new option filteredOptions.length > 0; // Render filtered options + const groups = filteredOptions.reduce( + (_groups: string[], option: ComboboxOption): string[] => { + if (option.group && !_groups.includes(option.group)) { + return [..._groups, option.group]; + } + return _groups; + }, + [], + ); + return (
{ {isValueNew && !maxSelected?.isLimitReached && allowNewValues && ( )} - {filteredOptions.map((option) => ( - - ))} + {groups.length > 0 && + groups.map((group) => ( +
+ + {group} + + {filteredOptions + .filter((option) => option.group === group) + .map((option) => ( + + ))} +
+ ))} + {groups.length === 0 && + filteredOptions.map((option) => ( + + ))} )}
diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/filtered-options-util.ts b/@navikt/core/react/src/form/combobox/FilteredOptions/filtered-options-util.ts index fb2b5eaac0..bd4eecf20e 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/filtered-options-util.ts +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/filtered-options-util.ts @@ -7,7 +7,11 @@ const isPartOfText = (value: string, text: string) => normalizeText(text).includes(normalizeText(value ?? "")); const getMatchingValuesFromList = (value: string, list: ComboboxOption[]) => - list.filter((listItem) => isPartOfText(value, listItem.label)); + list.filter( + (listItem) => + isPartOfText(value, listItem.label) || + (listItem.group && isPartOfText(value, listItem.group)), + ); const getFirstValueStartingWith = (text: string, list: ComboboxOption[]) => { return list.find((listItem) => diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/useVirtualFocus.ts b/@navikt/core/react/src/form/combobox/FilteredOptions/useVirtualFocus.ts index 9512e69a04..bb882e9785 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/useVirtualFocus.ts +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/useVirtualFocus.ts @@ -23,7 +23,15 @@ const useVirtualFocus = ( ); const getListOfAllChildren = (): HTMLElement[] => - Array.from(containerRef?.children ?? []) as HTMLElement[]; + (Array.from(containerRef?.children ?? []) as HTMLElement[]).reduce( + (acc: HTMLElement[], el) => { + if (el.role === "group") { + return [...acc, ...(Array.from(el.children) as HTMLElement[])]; + } + return [...acc, el]; + }, + [], + ); const getElementsAbleToReceiveFocus = () => getListOfAllChildren().filter( (child) => child.getAttribute("data-no-focus") !== "true", diff --git a/@navikt/core/react/src/form/combobox/combobox-utils.ts b/@navikt/core/react/src/form/combobox/combobox-utils.ts index 58f9c7da73..62e3ffdf98 100644 --- a/@navikt/core/react/src/form/combobox/combobox-utils.ts +++ b/@navikt/core/react/src/form/combobox/combobox-utils.ts @@ -24,9 +24,14 @@ const toComboboxOption = (value: string): ComboboxOption => ({ }); const mapToComboboxOptionArray = (options?: string[] | ComboboxOption[]) => { - return options?.map((option: string | ComboboxOption) => - typeof option === "string" ? toComboboxOption(option) : option, - ); + return options + ?.map((option: string | ComboboxOption) => + typeof option === "string" ? toComboboxOption(option) : option, + ) + .map((option: ComboboxOption) => ({ + ...option, + group: option.group, + })); }; export { isInList, mapToComboboxOptionArray, toComboboxOption }; diff --git a/@navikt/core/react/src/form/combobox/combobox.stories.tsx b/@navikt/core/react/src/form/combobox/combobox.stories.tsx index 0761c0c8ed..d946a18b20 100644 --- a/@navikt/core/react/src/form/combobox/combobox.stories.tsx +++ b/@navikt/core/react/src/form/combobox/combobox.stories.tsx @@ -578,3 +578,43 @@ Chromatic.parameters = { disable: false, }, }; + +export const GroupedOptions: StoryFn = ({ ...rest }) => ( + +); diff --git a/@navikt/core/react/src/form/combobox/types.ts b/@navikt/core/react/src/form/combobox/types.ts index 27a57f7e6b..50c997085f 100644 --- a/@navikt/core/react/src/form/combobox/types.ts +++ b/@navikt/core/react/src/form/combobox/types.ts @@ -14,6 +14,11 @@ export type ComboboxOption = { * The programmatic value of the option, for use internally. Will be returned from onToggleSelected. */ value: string; + /** + * Group options under a "heading" by adding this prop. + * Can also be searched for. + */ + group?: string; }; export interface ComboboxProps From 69c0d08084886cd4cb6b3a8c0863c4d7b08fb638 Mon Sep 17 00:00:00 2001 From: Vegard Haugstvedt Date: Fri, 1 Nov 2024 14:00:40 +0100 Subject: [PATCH 2/3] Refactor FilteredOptionsGroup into a separate component --- .../FilteredOptions/FilteredOptions.tsx | 27 +++++-------------- .../FilteredOptions/FilteredOptionsGroup.tsx | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 @navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsGroup.tsx diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx index 9f8db958a1..c643f204a7 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx @@ -1,10 +1,10 @@ import cl from "clsx"; import React from "react"; -import { Detail } from "../../../typography"; import { useInputContext } from "../Input/Input.context"; import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext"; import { ComboboxOption } from "../types"; import AddNewOption from "./AddNewOption"; +import FilteredOptionsGroup from "./FilteredOptionsGroup"; import FilteredOptionsItem from "./FilteredOptionsItem"; import LoadingMessage from "./LoadingMessage"; import MaxSelectedMessage from "./MaxSelectedMessage"; @@ -77,26 +77,13 @@ const FilteredOptions = () => { )} {groups.length > 0 && groups.map((group) => ( -
- - {group} - - {filteredOptions - .filter((option) => option.group === group) - .map((option) => ( - - ))} -
+ group={group} + options={filteredOptions.filter( + (option) => option.group === group, + )} + /> ))} {groups.length === 0 && filteredOptions.map((option) => ( diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsGroup.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsGroup.tsx new file mode 100644 index 0000000000..1c561f42f0 --- /dev/null +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsGroup.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { Detail } from "../../../typography"; +import { ComboboxOption } from "../types"; +import FilteredOptionsItem from "./FilteredOptionsItem"; + +const FilteredOptionsGroup = ({ group, options }) => ( +
+ + {group} + + {options.map((option: ComboboxOption) => ( + + ))} +
+); + +export default FilteredOptionsGroup; From ed232a93f8247dbf19e196bd8c1e4af7d0d77497 Mon Sep 17 00:00:00 2001 From: Vegard Haugstvedt Date: Fri, 1 Nov 2024 14:03:06 +0100 Subject: [PATCH 3/3] Add changeset --- .changeset/nasty-tigers-sparkle.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/nasty-tigers-sparkle.md diff --git a/.changeset/nasty-tigers-sparkle.md b/.changeset/nasty-tigers-sparkle.md new file mode 100644 index 0000000000..8d96ae1371 --- /dev/null +++ b/.changeset/nasty-tigers-sparkle.md @@ -0,0 +1,6 @@ +--- +"@navikt/ds-react": minor +"@navikt/ds-css": minor +--- + +Combobox: Group options with a heading for when several types of content is used within one Combobox