From f720562908ccb0c7c62cabd386920d0c1fdcdaf9 Mon Sep 17 00:00:00 2001 From: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:33:45 +0100 Subject: [PATCH] Combobox: Removed clear button, deprecated props clearButton/clearButtonLabel, changed maxSelected to number. (#3433) Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com> --- .changeset/quick-seas-press.md | 7 ++++ .changeset/strong-carpets-fly.md | 5 +++ @navikt/aksel-stylelint/src/deprecations.ts | 4 +++ .../css/darkside/form/combobox.darkside.css | 21 ------------ @navikt/core/css/form/combobox.css | 24 ++------------ @navikt/core/css/tokens.json | 3 -- .../src/form/combobox/ComboboxWrapper.tsx | 12 +++---- .../FilteredOptions/FilteredOptions.tsx | 8 ++--- .../FilteredOptions/FilteredOptionsItem.tsx | 2 +- .../FilteredOptions/MaxSelectedMessage.tsx | 11 ++----- .../filteredOptionsContext.tsx | 8 ++--- .../src/form/combobox/Input/Input.context.tsx | 2 +- .../react/src/form/combobox/Input/Input.tsx | 2 +- .../form/combobox/Input/InputController.tsx | 27 +++------------- .../selectedOptionsContext.tsx | 17 ++++++---- .../src/form/combobox/combobox.stories.tsx | 21 +++--------- @navikt/core/react/src/form/combobox/types.ts | 32 +++++++------------ .../combobox/with-complex-options.tsx | 2 +- .../combobox/with-max-selected-limit.tsx | 2 +- 19 files changed, 70 insertions(+), 140 deletions(-) create mode 100644 .changeset/quick-seas-press.md create mode 100644 .changeset/strong-carpets-fly.md diff --git a/.changeset/quick-seas-press.md b/.changeset/quick-seas-press.md new file mode 100644 index 0000000000..e03f22ae74 --- /dev/null +++ b/.changeset/quick-seas-press.md @@ -0,0 +1,7 @@ +--- +"@navikt/aksel-stylelint": minor +"@navikt/ds-react": minor +"@navikt/ds-css": minor +--- + +Combobox: Removed clear button, removed tokens staring with `--ac-combobox-clear`, deprecated props `clearButton`/`clearButtonLabel`. diff --git a/.changeset/strong-carpets-fly.md b/.changeset/strong-carpets-fly.md new file mode 100644 index 0000000000..9809f4ac81 --- /dev/null +++ b/.changeset/strong-carpets-fly.md @@ -0,0 +1,5 @@ +--- +"@navikt/ds-react": minor +--- + +Combobox: Changed prop `maxSelected` to number diff --git a/@navikt/aksel-stylelint/src/deprecations.ts b/@navikt/aksel-stylelint/src/deprecations.ts index 96644995ff..0f87270ba8 100644 --- a/@navikt/aksel-stylelint/src/deprecations.ts +++ b/@navikt/aksel-stylelint/src/deprecations.ts @@ -72,4 +72,8 @@ export const deprecations: DeprecatedList = [ classes: ["navds-list--nested", "navds-list__item-content"], message: "Removed in v7.1.1", }, + { + classes: ["navds-combobox__button-clear"], + message: "Removed in v7.8.0", + }, ]; diff --git a/@navikt/core/css/darkside/form/combobox.darkside.css b/@navikt/core/css/darkside/form/combobox.darkside.css index 6df035aa04..710e1bfc74 100644 --- a/@navikt/core/css/darkside/form/combobox.darkside.css +++ b/@navikt/core/css/darkside/form/combobox.darkside.css @@ -69,7 +69,6 @@ } } -.navds-combobox__button-clear svg, .navds-combobox__button-toggle-list svg, .navds-combobox__list svg { width: var(--__axc-combobox-icon-size); @@ -203,19 +202,6 @@ } } -.navds-combobox__button-clear { - border-radius: var(--ax-border-radius-medium); - color: var(--ax-text-subtle); - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - background: none; - border: none; - font-size: 1rem; - padding: 0; -} - .navds-combobox__input::-webkit-search-cancel-button { display: none; } @@ -231,10 +217,7 @@ border: none; font-size: 1rem; padding: 0; -} -.navds-combobox__button-clear, -.navds-combobox__button-toggle-list { &:hover { color: var(--ax-text-accent); @@ -405,10 +388,6 @@ } @media (forced-colors: active) { - .navds-combobox__button-clear:hover { - color: highlight; - } - .navds-combobox__wrapper-inner:has(.navds-combobox__input:focus-visible) { outline-color: highlight; } diff --git a/@navikt/core/css/form/combobox.css b/@navikt/core/css/form/combobox.css index b5e3f6dc59..3f49b89d62 100644 --- a/@navikt/core/css/form/combobox.css +++ b/@navikt/core/css/form/combobox.css @@ -50,7 +50,6 @@ border-color: var(--a-border-subtle); } -.navds-combobox__button-clear svg, .navds-combobox__button-toggle-list svg, .navds-combobox__list svg { width: var(--__ac-combobox-icon-size); @@ -195,19 +194,6 @@ } } -.navds-combobox__button-clear { - border-radius: var(--a-border-radius-medium); - color: var(--ac-combobox-clear-icon, var(--a-text-subtle)); - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - background: none; - border: none; - font-size: 1rem; - padding: 0; -} - .navds-combobox__input::-webkit-search-cancel-button { display: none; } @@ -225,14 +211,12 @@ padding: 0; } -.navds-combobox__button-clear:active:hover, .navds-combobox__button-toggle-list:active:hover { - color: var(--ac-combobox-clear-icon-active, var(--a-text-action)); + color: var(--a-text-action); } -.navds-combobox__button-clear:hover, .navds-combobox__button-toggle-list:hover { - color: var(--ac-combobox-clear-icon-hover, var(--a-text-action-selected)); + color: var(--a-text-action-selected); } .navds-combobox__button-toggle-list:focus-visible { @@ -406,10 +390,6 @@ } @media (forced-colors: active) { - .navds-combobox__button-clear:hover { - color: highlight; - } - .navds-combobox__wrapper-inner:has(.navds-combobox__input:focus-visible) { outline-color: highlight; } diff --git a/@navikt/core/css/tokens.json b/@navikt/core/css/tokens.json index 0cd778ce93..278cf7b37d 100644 --- a/@navikt/core/css/tokens.json +++ b/@navikt/core/css/tokens.json @@ -387,9 +387,6 @@ "--ac-search-error-border": "--a-border-danger" }, "combobox": { - "--ac-combobox-clear-icon": "--a-text-subtle", - "--ac-combobox-clear-icon-hover": "--a-text-action-selected", - "--ac-combobox-clear-icon-active": "--a-text-action", "--ac-combobox-list-bg": "--a-surface-default", "--ac-combobox-list-text": "--a-text-default", "--ac-combobox-list-border-color": "--a-border-divider", diff --git a/@navikt/core/react/src/form/combobox/ComboboxWrapper.tsx b/@navikt/core/react/src/form/combobox/ComboboxWrapper.tsx index 8e44979183..ba8dc603f2 100644 --- a/@navikt/core/react/src/form/combobox/ComboboxWrapper.tsx +++ b/@navikt/core/react/src/form/combobox/ComboboxWrapper.tsx @@ -27,21 +27,21 @@ const ComboboxWrapper = ({ const wrapperRef = useRef(null); const [hasFocusWithin, setHasFocusWithin] = useState(false); - function onFocusInsideWrapper(e) { + function onFocusInsideWrapper(event: React.FocusEvent) { if ( - !wrapperRef.current?.contains(e.relatedTarget) && - toggleOpenButtonRef?.current !== e.target + !wrapperRef.current?.contains(event.relatedTarget) && + toggleOpenButtonRef?.current !== event.target ) { toggleIsListOpen(true); setHasFocusWithin(true); } } - function onBlurWrapper(e) { - if (!wrapperRef.current?.contains(e.relatedTarget)) { + function onBlurWrapper(event: React.FocusEvent) { + if (!wrapperRef.current?.contains(event.relatedTarget)) { toggleIsListOpen(false); setHasFocusWithin(false); - clearInput(e); + clearInput(event); } } diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx index 4db73fb21f..ebbbc41a1a 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptions.tsx @@ -26,12 +26,12 @@ const FilteredOptions = () => { const { maxSelected } = useSelectedOptionsContext(); const shouldRenderNonSelectables = - maxSelected?.isLimitReached || // Render maxSelected message + maxSelected.isLimitReached || // Render maxSelected message isLoading || // Render loading message (!isLoading && filteredOptions.length === 0 && !allowNewValues); // Render no hits message const shouldRenderFilteredOptionsList = - (allowNewValues && isValueNew && !maxSelected?.isLimitReached) || // Render add new option + (allowNewValues && isValueNew && !maxSelected.isLimitReached) || // Render add new option filteredOptions.length > 0; // Render filtered options return ( @@ -45,7 +45,7 @@ const FilteredOptions = () => { > {shouldRenderNonSelectables && (
- {maxSelected?.isLimitReached && } + {maxSelected.isLimitReached && } {isLoading && } {!isLoading && filteredOptions.length === 0 && !allowNewValues && ( @@ -60,7 +60,7 @@ const FilteredOptions = () => { role="listbox" className="navds-combobox__list-options" > - {isValueNew && !maxSelected?.isLimitReached && allowNewValues && ( + {isValueNew && !maxSelected.isLimitReached && allowNewValues && ( )} {filteredOptions.map((option) => ( diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx index 7d99b194df..daab21485d 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx @@ -39,7 +39,7 @@ const FilteredOptionsItem = ({ option }: { option: ComboboxOption }) => { const [start, highlight, end] = useTextHighlight(option.label, searchTerm); const isDisabled = (_option: ComboboxOption) => - maxSelected?.isLimitReached && !isInList(_option.value, selectedOptions); + maxSelected.isLimitReached && !isInList(_option.value, selectedOptions); return (
  • { inputProps: { id }, } = useInputContext(); const { maxSelected, selectedOptions } = useSelectedOptionsContext(); - const translate = useI18n( - "Combobox", - maxSelected?.message ? { maxSelected: maxSelected.message } : undefined, - ); - - if (!maxSelected) { - return null; - } + const translate = useI18n("Combobox"); return (
    { > {translate("maxSelected", { selected: selectedOptions.length, - limit: maxSelected.limit, + limit: maxSelected.limit || 0, })}
    ); diff --git a/@navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx b/@navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx index 7b28c67af3..6038c75ca3 100644 --- a/@navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +++ b/@navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx @@ -142,13 +142,13 @@ const FilteredOptionsProvider = ({ } virtualFocus.resetFocus(); if (newState ?? !isInternalListOpen) { - setHideCaret(!!maxSelected?.isLimitReached); + setHideCaret(maxSelected.isLimitReached); } setInternalListOpen((oldState) => newState ?? !oldState); }, [ virtualFocus, - maxSelected?.isLimitReached, + maxSelected.isLimitReached, isInternalListOpen, setHideCaret, disabled, @@ -178,7 +178,7 @@ const FilteredOptionsProvider = ({ } } const maybeMaxSelectedOptionsId = - maxSelected?.isLimitReached && + maxSelected.isLimitReached && filteredOptionsUtils.getMaxSelectedOptionsId(id); return ( @@ -188,7 +188,7 @@ const FilteredOptionsProvider = ({ }, [ isListOpen, isLoading, - maxSelected?.isLimitReached, + maxSelected.isLimitReached, value, partialAriaDescribedBy, shouldAutocomplete, diff --git a/@navikt/core/react/src/form/combobox/Input/Input.context.tsx b/@navikt/core/react/src/form/combobox/Input/Input.context.tsx index ef41645027..29ec92a664 100644 --- a/@navikt/core/react/src/form/combobox/Input/Input.context.tsx +++ b/@navikt/core/react/src/form/combobox/Input/Input.context.tsx @@ -93,7 +93,7 @@ const InputProvider = ({ children, value: props }: Props) => { ); const clearInput = useCallback( - (event: React.PointerEvent | React.KeyboardEvent | React.MouseEvent) => { + (event: React.PointerEvent | React.KeyboardEvent | React.FocusEvent) => { onClear?.(event); externalOnChange?.(""); setInternalValue(""); diff --git a/@navikt/core/react/src/form/combobox/Input/Input.tsx b/@navikt/core/react/src/form/combobox/Input/Input.tsx index 6339b0d183..febe3d349d 100644 --- a/@navikt/core/react/src/form/combobox/Input/Input.tsx +++ b/@navikt/core/react/src/form/combobox/Input/Input.tsx @@ -279,7 +279,7 @@ const Input = forwardRef( value={value} onBlur={composeEventHandlers(onBlur, virtualFocus.resetFocus)} onClick={() => { - setHideCaret(!!maxSelected?.isLimitReached); + setHideCaret(maxSelected.isLimitReached); value !== searchTerm && onChange(value); }} onInput={onChangeHandler} diff --git a/@navikt/core/react/src/form/combobox/Input/InputController.tsx b/@navikt/core/react/src/form/combobox/Input/InputController.tsx index 82e3e3489c..87965af770 100644 --- a/@navikt/core/react/src/form/combobox/Input/InputController.tsx +++ b/@navikt/core/react/src/form/combobox/Input/InputController.tsx @@ -1,9 +1,7 @@ /* eslint-disable jsx-a11y/no-static-element-interactions */ import cl from "clsx"; import React, { forwardRef } from "react"; -import { XMarkIcon } from "@navikt/aksel-icons"; import { useMergeRefs } from "../../../util/hooks"; -import { useI18n } from "../../../util/i18n/i18n.context"; import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext"; import SelectedOptions from "../SelectedOptions/SelectedOptions"; import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext"; @@ -28,7 +26,9 @@ export const InputController = forwardRef< > >((props, ref) => { const { - clearButton = true, + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Remove when prop has been removed from ComboboxProps. + clearButton, + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Remove when prop has been removed from ComboboxProps. clearButtonLabel, toggleListButton = true, inputClassName, @@ -37,10 +37,8 @@ export const InputController = forwardRef< } = props; const { - clearInput, focusInput, inputProps, - value, size = "medium", inputRef, toggleOpenButtonRef, @@ -52,11 +50,6 @@ export const InputController = forwardRef< const mergedInputRef = useMergeRefs(inputRef, ref); - const translate = useI18n( - "Combobox", - clearButtonLabel ? { clear: clearButtonLabel } : undefined, - ); - return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events
    )} -
    - {value && clearButton && ( -
    - -
    - )} - {toggleListButton && } -
    + {toggleListButton && }
    ); }); diff --git a/@navikt/core/react/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx b/@navikt/core/react/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx index ad0123b7af..96bc82787a 100644 --- a/@navikt/core/react/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx +++ b/@navikt/core/react/src/form/combobox/SelectedOptions/selectedOptionsContext.tsx @@ -12,7 +12,7 @@ type SelectedOptionsContextValue = { removeSelectedOption: (option: ComboboxOption) => void; prevSelectedOptions?: ComboboxOption[]; selectedOptions: ComboboxOption[]; - maxSelected?: ComboboxProps["maxSelected"] & { isLimitReached: boolean }; + maxSelected: { limit: number | undefined; isLimitReached: boolean }; setSelectedOptions: (any) => void; toggleOption: ( option: ComboboxOption, @@ -101,14 +101,17 @@ const SelectedOptionsProvider = ({ [customOptions, onToggleSelected, removeCustomOption], ); + const maxSelectedLimit = + typeof maxSelected === "object" ? maxSelected.limit : maxSelected; const isLimitReached = - (!!maxSelected?.limit && selectedOptions.length >= maxSelected.limit) || - (!isMultiSelect && selectedOptions.length > 0); + !!maxSelectedLimit && selectedOptions.length >= maxSelectedLimit; + const newHideCaret = + isLimitReached || (!isMultiSelect && selectedOptions.length > 0); // biome-ignore lint/correctness/useExhaustiveDependencies: We explicitly want to run this effect when selectedOptions changes to match the view with the selected options. useEffect(() => { - setHideCaret(isLimitReached); - }, [isLimitReached, selectedOptions, setHideCaret]); + setHideCaret(newHideCaret); + }, [newHideCaret, selectedOptions, setHideCaret]); const toggleOption = useCallback( ( @@ -142,8 +145,8 @@ const SelectedOptionsProvider = ({ selectedOptions, setSelectedOptions, toggleOption, - maxSelected: maxSelected && { - ...maxSelected, + maxSelected: { + limit: maxSelectedLimit, isLimitReached, }, }; diff --git a/@navikt/core/react/src/form/combobox/combobox.stories.tsx b/@navikt/core/react/src/form/combobox/combobox.stories.tsx index 2377e941ea..4a28b311f2 100644 --- a/@navikt/core/react/src/form/combobox/combobox.stories.tsx +++ b/@navikt/core/react/src/form/combobox/combobox.stories.tsx @@ -38,16 +38,8 @@ const options = [ "grapefruit", ]; -export const Default: StoryFn< - ComboboxProps & { maxSelected?: number; maxSelectedMessage?: string } -> = ({ maxSelected, maxSelectedMessage, ...rest }) => ( - +export const Default: StoryFn = (props) => ( + ); Default.args = { options, @@ -77,16 +69,13 @@ Default.argTypes = { maxSelected: { control: { type: "number" }, }, - maxSelectedMessage: { - control: { type: "text" }, - }, size: { options: ["medium", "small"], defaultValue: "medium", control: { type: "radio" }, }, - clearButtonLabel: { - control: { type: "text" }, + toggleListButton: { + control: { type: "boolean" }, }, }; @@ -426,7 +415,7 @@ export const MaxSelectedOptions: StoryFn = ({ open }: { open?: boolean }) => { id="combobox-with-max-selected-options" label="Komboboks med begrenset antall valg" options={options} - maxSelected={{ limit: 2 }} + maxSelected={2} selectedOptions={selectedOptions} onToggleSelected={(option, isSelected) => isSelected diff --git a/@navikt/core/react/src/form/combobox/types.ts b/@navikt/core/react/src/form/combobox/types.ts index 487e94b4b1..b5669f267a 100644 --- a/@navikt/core/react/src/form/combobox/types.ts +++ b/@navikt/core/react/src/form/combobox/types.ts @@ -45,15 +45,11 @@ export interface ComboboxProps */ allowNewValues?: boolean; /** - * Adds a button to clear the input value when not empty. - * NB: Will not clear selected values. - * @default true + * @deprecated The clear button has been removed. This prop has no effect. */ clearButton?: boolean; /** - * Custom name for the clear button. Requires `clearButton` to be `true`. - * - * @default "Tøm" + * @deprecated The clear button has been removed. This prop has no effect. */ clearButtonLabel?: string; /** @@ -102,7 +98,7 @@ export interface ComboboxProps * @param event */ onClear?: ( - event: React.PointerEvent | React.KeyboardEvent | React.MouseEvent, + event: React.PointerEvent | React.KeyboardEvent | React.FocusEvent, ) => void; /** * Callback function triggered whenever an option is selected or de-selected. @@ -124,19 +120,15 @@ export interface ComboboxProps */ selectedOptions?: string[] | ComboboxOption[]; /** - * Options for the maximum number of selected options. - */ - maxSelected?: { - /** - * The limit for maximum selected options - */ - limit: number; - /** - * Message to display when the limit for maximum selected options has been reached - * @default "{selected} av maks {limit} er valgt." - */ - message?: string; - }; + * Maximum number of selected options. + * @example maxSelected={3} + */ + maxSelected?: + | { + /** @deprecated Provide a number instead of an object */ + limit: number; + } + | number; /** * Set to `true` to enable inline autocomplete. * diff --git a/aksel.nav.no/website/pages/eksempler/combobox/with-complex-options.tsx b/aksel.nav.no/website/pages/eksempler/combobox/with-complex-options.tsx index 3a1d5b72ef..fd5fafd8df 100644 --- a/aksel.nav.no/website/pages/eksempler/combobox/with-complex-options.tsx +++ b/aksel.nav.no/website/pages/eksempler/combobox/with-complex-options.tsx @@ -15,7 +15,7 @@ const Example = () => { value: o.value, }))} isMultiSelect - maxSelected={{ limit: 3 }} + maxSelected={3} selectedOptions={selectedOptions} onToggleSelected={(option, isSelected) => isSelected diff --git a/aksel.nav.no/website/pages/eksempler/combobox/with-max-selected-limit.tsx b/aksel.nav.no/website/pages/eksempler/combobox/with-max-selected-limit.tsx index 1da9cb47d0..9149723ad6 100644 --- a/aksel.nav.no/website/pages/eksempler/combobox/with-max-selected-limit.tsx +++ b/aksel.nav.no/website/pages/eksempler/combobox/with-max-selected-limit.tsx @@ -12,7 +12,7 @@ const Example = () => { label="Hva er de kuleste transportmidlene? (Velg opptil 3)" options={options} isMultiSelect - maxSelected={{ limit: 3 }} + maxSelected={3} selectedOptions={selectedOptions} onToggleSelected={(option, isSelected) => isSelected