From 608e1f39442890415e7fc551f1fcfa9466983443 Mon Sep 17 00:00:00 2001 From: Vegard Haugstvedt Date: Thu, 17 Aug 2023 13:26:10 +0200 Subject: [PATCH] Feature/combobox error message (#2182) Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com> --- .changeset/beige-keys-roll.md | 5 +++ .../core/react/src/form/combobox/Combobox.tsx | 13 ++++++- .../filteredOptionsContext.tsx | 25 ++++++++----- .../react/src/form/combobox/Input/Input.tsx | 1 + .../src/form/combobox/Input/inputContext.tsx | 2 ++ .../src/form/combobox/combobox.stories.tsx | 27 ++++++++++++++ .../pages/eksempler/combobox/with-error.tsx | 36 +++++++++++++++++++ 7 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 .changeset/beige-keys-roll.md create mode 100644 aksel.nav.no/website/pages/eksempler/combobox/with-error.tsx diff --git a/.changeset/beige-keys-roll.md b/.changeset/beige-keys-roll.md new file mode 100644 index 0000000000..d1491ab274 --- /dev/null +++ b/.changeset/beige-keys-roll.md @@ -0,0 +1,5 @@ +--- +"@navikt/ds-react": patch +--- + +Add support for error messages to combobox diff --git a/@navikt/core/react/src/form/combobox/Combobox.tsx b/@navikt/core/react/src/form/combobox/Combobox.tsx index 2744f21b10..48f47b4051 100644 --- a/@navikt/core/react/src/form/combobox/Combobox.tsx +++ b/@navikt/core/react/src/form/combobox/Combobox.tsx @@ -1,6 +1,6 @@ import cl from "clsx"; import React, { forwardRef, useMemo, useRef } from "react"; -import { BodyShort, Label, mergeRefs } from "../.."; +import { BodyShort, ErrorMessage, Label, mergeRefs } from "../.."; import ClearButton from "./ClearButton"; import FilteredOptions from "./FilteredOptions/FilteredOptions"; import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext"; @@ -39,12 +39,15 @@ export const Combobox = forwardRef< const { clearInput, + error, + errorId, focusInput, hasError, inputDescriptionId, inputProps, inputRef, value, + showErrorMsg, size = "medium", } = useInputContext(); @@ -129,6 +132,14 @@ export const Combobox = forwardRef< +
+ {showErrorMsg && {error}} +
); }); diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx index b317dcf522..df0635af47 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx @@ -8,6 +8,7 @@ import React, { useRef, useLayoutEffect, } from "react"; +import cl from "clsx"; import { useCustomOptionsContext } from "../customOptionsContext"; import { useInputContext } from "../Input/inputContext"; import usePrevious from "../../../util/usePrevious"; @@ -58,7 +59,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => { } = props; const filteredOptionsRef = useRef(null); const { - inputProps: { id }, + inputProps: { "aria-describedby": partialAriaDescribedBy, id }, value, searchTerm, setValue, @@ -124,18 +125,26 @@ export const FilteredOptionsProvider = ({ children, value: props }) => { }, [allowNewValues, isValueNew]); const ariaDescribedBy = useMemo(() => { + let activeOption; if (!isLoading && filteredOptions.length === 0) { - return `${id}-no-hits`; + activeOption = `${id}-no-hits`; } else if ((value && value !== "") || isLoading) { if (shouldAutocomplete && filteredOptions[0]) { - return `${id}-option-${filteredOptions[0].replace(" ", "-")}`; - } else if (isLoading) { - return `${id}-is-loading`; + activeOption = `${id}-option-${filteredOptions[0].replace(" ", "-")}`; + } else if (isListOpen && isLoading) { + activeOption = `${id}-is-loading`; } - } else { - return undefined; } - }, [isLoading, value, shouldAutocomplete, filteredOptions, id]); + return cl(activeOption, partialAriaDescribedBy) || undefined; + }, [ + isListOpen, + isLoading, + value, + partialAriaDescribedBy, + shouldAutocomplete, + filteredOptions, + id, + ]); const currentOption = useMemo(() => { if (filteredOptionsIndex == null) { diff --git a/@navikt/core/react/src/form/combobox/Input/Input.tsx b/@navikt/core/react/src/form/combobox/Input/Input.tsx index 4e77f0bb91..d4fbafb520 100644 --- a/@navikt/core/react/src/form/combobox/Input/Input.tsx +++ b/@navikt/core/react/src/form/combobox/Input/Input.tsx @@ -170,6 +170,7 @@ const Input = forwardRef( aria-autocomplete={shouldAutocomplete ? "both" : "list"} aria-activedescendant={activeDecendantId} aria-describedby={ariaDescribedBy} + aria-invalid={inputProps["aria-invalid"]} className={cl( inputClassName, "navds-combobox__input", diff --git a/@navikt/core/react/src/form/combobox/Input/inputContext.tsx b/@navikt/core/react/src/form/combobox/Input/inputContext.tsx index 1073680e7f..35851f7b3c 100644 --- a/@navikt/core/react/src/form/combobox/Input/inputContext.tsx +++ b/@navikt/core/react/src/form/combobox/Input/inputContext.tsx @@ -13,6 +13,7 @@ import { useFormField, FormFieldType } from "../../useFormField"; interface InputContextType extends FormFieldType { clearInput: (event: React.PointerEvent | React.KeyboardEvent) => void; + error?: string; focusInput: () => void; inputRef: React.RefObject; value: string; @@ -101,6 +102,7 @@ export const InputContextProvider = ({ children, value: props }) => { value={{ ...formFieldProps, clearInput, + error, focusInput, inputRef, value, diff --git a/@navikt/core/react/src/form/combobox/combobox.stories.tsx b/@navikt/core/react/src/form/combobox/combobox.stories.tsx index 6cec3cbaea..1783f0cd1f 100644 --- a/@navikt/core/react/src/form/combobox/combobox.stories.tsx +++ b/@navikt/core/react/src/form/combobox/combobox.stories.tsx @@ -316,6 +316,33 @@ ComboboxSizes.args = { options, }; +export const WithError = { + args: { + error: "Du må velge en favorittfrukt.", + isLoading: true, + }, + render: (props) => { + const [hasSelectedValue, setHasSelectedValue] = useState(false); + const [isLoading, setIsLoading] = useState(false); + return ( + + { + setIsLoading(true); + setTimeout(() => setIsLoading(false), 2000); + }} + onToggleSelected={(_, isSelected) => setHasSelectedValue(isSelected)} + /> + + ); + }, +}; + function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/aksel.nav.no/website/pages/eksempler/combobox/with-error.tsx b/aksel.nav.no/website/pages/eksempler/combobox/with-error.tsx new file mode 100644 index 0000000000..db6d2b7b8f --- /dev/null +++ b/aksel.nav.no/website/pages/eksempler/combobox/with-error.tsx @@ -0,0 +1,36 @@ +import { UNSAFE_Combobox } from "@navikt/ds-react"; +import { withDsExample } from "components/website-modules/examples/withDsExample"; + +const initialOptions = [ + "banana", + "apple", + "tangerine", + "pear", + "grape", + "kiwi", + "mango", + "passion fruit", + "pineapple", + "strawberry", + "watermelon", + "grape fruit", +]; + +export const Example = () => { + return ( +
+ +
+ ); +}; + +export default withDsExample(Example, "static"); + +export const args = { + index: 0, + desc: "Ved Single Select velger brukeren kun ett valg fra nedtrekkslisten.", +};