diff --git a/.changeset/wise-carrots-hug.md b/.changeset/wise-carrots-hug.md new file mode 100644 index 00000000000..40421d675db --- /dev/null +++ b/.changeset/wise-carrots-hug.md @@ -0,0 +1,5 @@ +--- +"@navikt/ds-react": patch +--- + +Refactored Combobox FilteredOptions diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/AddNewOption.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/AddNewOption.tsx new file mode 100644 index 00000000000..75c70eccb85 --- /dev/null +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/AddNewOption.tsx @@ -0,0 +1,63 @@ +import cl from "clsx"; +import React from "react"; +import { PlusIcon } from "@navikt/aksel-icons"; +import { BodyShort, Label } from "../../../typography"; +import { useInputContext } from "../Input/Input.context"; +import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext"; +import { isInList, toComboboxOption } from "../combobox-utils"; +import filteredOptionsUtil from "./filtered-options-util"; +import { useFilteredOptionsContext } from "./filteredOptionsContext"; + +const AddNewOption = () => { + const { + inputProps: { id }, + size, + value, + } = useInputContext(); + const { + setIsMouseLastUsedInputDevice, + toggleIsListOpen, + activeDecendantId, + virtualFocus, + } = useFilteredOptionsContext(); + const { isMultiSelect, selectedOptions, toggleOption } = + useSelectedOptionsContext(); + return ( +
  • { + if (activeDecendantId !== filteredOptionsUtil.getAddNewOptionId(id)) { + virtualFocus.moveFocusToElement( + filteredOptionsUtil.getAddNewOptionId(id), + ); + setIsMouseLastUsedInputDevice(true); + } + }} + onPointerUp={(event) => { + toggleOption(toComboboxOption(value), event); + if (!isMultiSelect && !isInList(value, selectedOptions)) + toggleIsListOpen(false); + }} + id={filteredOptionsUtil.getAddNewOptionId(id)} + className={cl( + "navds-combobox__list-item navds-combobox__list-item--new-option", + { + "navds-combobox__list-item--new-option--focus": + activeDecendantId === filteredOptionsUtil.getAddNewOptionId(id), + }, + )} + role="option" + aria-selected={false} + > + + + Legg til{" "} + + +
  • + ); +}; + +export default AddNewOption; diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx index 9e9b5e4ddc6..dd7d32cdb0c 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx @@ -1,20 +1,18 @@ import cl from "clsx"; import React from "react"; -import { CheckmarkIcon, PlusIcon } from "@navikt/aksel-icons"; -import { Loader } from "../../../loader"; -import { BodyShort, Label } from "../../../typography"; import { useInputContext } from "../Input/Input.context"; import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext"; -import { isInList, toComboboxOption } from "../combobox-utils"; -import { ComboboxOption } from "../types"; +import AddNewOption from "./AddNewOption"; +import FilteredOptionsItem from "./FilteredOptionsItem"; +import LoadingMessage from "./LoadingMessage"; +import MaxSelectedMessage from "./MaxSelectedMessage"; +import NoSearchHitsMessage from "./NoSearchHitsMessage"; import filteredOptionsUtil from "./filtered-options-util"; import { useFilteredOptionsContext } from "./filteredOptionsContext"; const FilteredOptions = () => { const { inputProps: { id }, - size, - value, } = useInputContext(); const { allowNewValues, @@ -23,17 +21,9 @@ const FilteredOptions = () => { filteredOptions, setFilteredOptionsRef, isMouseLastUsedInputDevice, - setIsMouseLastUsedInputDevice, isValueNew, - toggleIsListOpen, - activeDecendantId, - virtualFocus, } = useFilteredOptionsContext(); - const { isMultiSelect, selectedOptions, toggleOption, maxSelected } = - useSelectedOptionsContext(); - - const isDisabled = (option: ComboboxOption) => - maxSelected?.isLimitReached && !isInList(option.value, selectedOptions); + const { maxSelected } = useSelectedOptionsContext(); const shouldRenderNonSelectables = maxSelected?.isLimitReached || // Render maxSelected message @@ -55,30 +45,10 @@ const FilteredOptions = () => { > {shouldRenderNonSelectables && (
    - {maxSelected?.isLimitReached && ( -
    - {maxSelected.message ?? - `${selectedOptions.length} av ${maxSelected.limit} er valgt.`} -
    - )} - {isLoading && ( -
    - -
    - )} + {maxSelected?.isLimitReached && } + {isLoading && } {!isLoading && filteredOptions.length === 0 && !allowNewValues && ( -
    - Ingen søketreff -
    + )}
    )} @@ -90,90 +60,10 @@ const FilteredOptions = () => { className="navds-combobox__list-options" > {isValueNew && !maxSelected?.isLimitReached && allowNewValues && ( -
  • { - if ( - activeDecendantId !== - filteredOptionsUtil.getAddNewOptionId(id) - ) { - virtualFocus.moveFocusToElement( - filteredOptionsUtil.getAddNewOptionId(id), - ); - setIsMouseLastUsedInputDevice(true); - } - }} - onPointerUp={(event) => { - toggleOption(toComboboxOption(value), event); - if (!isMultiSelect && !isInList(value, selectedOptions)) - toggleIsListOpen(false); - }} - id={filteredOptionsUtil.getAddNewOptionId(id)} - className={cl( - "navds-combobox__list-item navds-combobox__list-item--new-option", - { - "navds-combobox__list-item--new-option--focus": - activeDecendantId === - filteredOptionsUtil.getAddNewOptionId(id), - }, - )} - role="option" - aria-selected={false} - > - - - Legg til{" "} - - -
  • + )} {filteredOptions.map((option) => ( -
  • { - if ( - activeDecendantId !== - filteredOptionsUtil.getOptionId(id, option.label) - ) { - virtualFocus.moveFocusToElement( - filteredOptionsUtil.getOptionId(id, option.label), - ); - setIsMouseLastUsedInputDevice(true); - } - }} - onPointerUp={(event) => { - if (isDisabled(option)) { - return; - } - toggleOption(option, event); - if ( - !isMultiSelect && - !isInList(option.value, selectedOptions) - ) { - toggleIsListOpen(false); - } - }} - role="option" - aria-selected={isInList(option.value, selectedOptions)} - aria-disabled={isDisabled(option) || undefined} - > - {option.label} - {isInList(option.value, selectedOptions) && } -
  • + ))} )} diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx new file mode 100644 index 00000000000..cb2d085edf6 --- /dev/null +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx @@ -0,0 +1,73 @@ +import cl from "clsx"; +import React from "react"; +import { CheckmarkIcon } from "@navikt/aksel-icons"; +import { BodyShort } from "../../../typography"; +import { useInputContext } from "../Input/Input.context"; +import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext"; +import { isInList } from "../combobox-utils"; +import { ComboboxOption } from "../types"; +import filteredOptionsUtil from "./filtered-options-util"; +import { useFilteredOptionsContext } from "./filteredOptionsContext"; + +const FilteredOptionsItem = ({ option }: { option: ComboboxOption }) => { + const { + inputProps: { id }, + size, + } = useInputContext(); + const { + setIsMouseLastUsedInputDevice, + toggleIsListOpen, + activeDecendantId, + virtualFocus, + } = useFilteredOptionsContext(); + const { isMultiSelect, maxSelected, selectedOptions, toggleOption } = + useSelectedOptionsContext(); + + const isDisabled = (_option: ComboboxOption) => + maxSelected?.isLimitReached && !isInList(_option.value, selectedOptions); + return ( +
  • { + if ( + activeDecendantId !== + filteredOptionsUtil.getOptionId(id, option.label) + ) { + virtualFocus.moveFocusToElement( + filteredOptionsUtil.getOptionId(id, option.label), + ); + setIsMouseLastUsedInputDevice(true); + } + }} + onPointerUp={(event) => { + if (isDisabled(option)) { + return; + } + toggleOption(option, event); + if (!isMultiSelect && !isInList(option.value, selectedOptions)) { + toggleIsListOpen(false); + } + }} + role="option" + aria-selected={isInList(option.value, selectedOptions)} + aria-disabled={isDisabled(option) || undefined} + > + {option.label} + {isInList(option.value, selectedOptions) && } +
  • + ); +}; + +export default FilteredOptionsItem; diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/LoadingMessage.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/LoadingMessage.tsx new file mode 100644 index 00000000000..19e3910e644 --- /dev/null +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/LoadingMessage.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { Loader } from "../../../loader"; +import { useInputContext } from "../Input/Input.context"; +import filteredOptionsUtil from "./filtered-options-util"; + +const LoadingMessage = () => { + const { + inputProps: { id }, + } = useInputContext(); + return ( +
    + +
    + ); +}; + +export default LoadingMessage; diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/MaxSelectedMessage.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/MaxSelectedMessage.tsx new file mode 100644 index 00000000000..ab916c8f792 --- /dev/null +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/MaxSelectedMessage.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { useInputContext } from "../Input/Input.context"; +import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext"; +import filteredOptionsUtil from "./filtered-options-util"; + +const MaxSelectedMessage = () => { + const { + inputProps: { id }, + } = useInputContext(); + const { maxSelected, selectedOptions } = useSelectedOptionsContext(); + + if (!maxSelected) { + return null; + } + + return ( +
    + {maxSelected.message ?? + `${selectedOptions.length} av ${maxSelected.limit} er valgt.`} +
    + ); +}; + +export default MaxSelectedMessage; diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/NoSearchHitsMessage.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/NoSearchHitsMessage.tsx new file mode 100644 index 00000000000..0d016dbb735 --- /dev/null +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/NoSearchHitsMessage.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { useInputContext } from "../Input/Input.context"; +import filteredOptionsUtil from "./filtered-options-util"; + +const NoSearchHitsMessage = () => { + const { + inputProps: { id }, + } = useInputContext(); + return ( +
    + Ingen søketreff +
    + ); +}; + +export default NoSearchHitsMessage;