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 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..c643f204a7 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx @@ -2,7 +2,9 @@ import cl from "clsx"; import React from "react"; 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"; @@ -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) => ( + 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; 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