Skip to content

Commit

Permalink
Feature/combobox show add new value while autocompleting (#3225)
Browse files Browse the repository at this point in the history
* Show AddNewValue button in Combobox based on searchTerm instead of value, so it is displayed when autocompleting words

* Highlight matching text in FilteredOptions

* Add changeset

* Move styling to combobox.css instead of inline JS

* Ensure label options are read as one text, even when highlighting search matches.

This only seemed to be a problem in testing library, so we should be able to remove this if they fix this in their calculation of accessible name.

* Update changeset to a minor version

* Re-introduce controlled value in storybook examples to not change Chromatic snapshots
  • Loading branch information
it-vegard authored Oct 23, 2024
1 parent 9711de9 commit 59dc3e9
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/silver-rings-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": minor
---

Combobox: Enable option to add a new value while autocompleting and highlight matches in FilteredOptions.
5 changes: 5 additions & 0 deletions @navikt/core/css/form/combobox.css
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,11 @@
border-radius: calc(var(--a-border-radius-medium) - 1px);
}

.navds-combobox__list-item mark {
background-color: transparent;
font-weight: bold;
}

/* Mobile */

@media (max-width: 479px) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const AddNewOption = () => {
const {
inputProps: { id },
size,
value,
searchTerm,
} = useInputContext();
const {
setIsMouseLastUsedInputDevice,
Expand All @@ -34,8 +34,8 @@ const AddNewOption = () => {
}
}}
onPointerUp={(event) => {
toggleOption(toComboboxOption(value), event);
if (!isMultiSelect && !isInList(value, selectedOptions))
toggleOption(toComboboxOption(searchTerm), event);
if (!isMultiSelect && !isInList(searchTerm, selectedOptions))
toggleIsListOpen(false);
}}
id={filteredOptionsUtil.getAddNewOptionId(id)}
Expand All @@ -53,7 +53,7 @@ const AddNewOption = () => {
<BodyShort size={size}>
Legg til{" "}
<Label as="span" size={size}>
&#8220;{value}&#8221;
&#8220;{searchTerm}&#8221;
</Label>
</BodyShort>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,24 @@ import { ComboboxOption } from "../types";
import filteredOptionsUtil from "./filtered-options-util";
import { useFilteredOptionsContext } from "./filteredOptionsContext";

const useTextHighlight = (text: string, searchTerm: string) => {
const indexOfHighlightedText = text
.toLowerCase()
.indexOf(searchTerm.toLowerCase());
const start = text.substring(0, indexOfHighlightedText);
const highlight = text.substring(
indexOfHighlightedText,
indexOfHighlightedText + searchTerm.length,
);
const end = text.substring(indexOfHighlightedText + searchTerm.length);
return [start, highlight, end];
};

const FilteredOptionsItem = ({ option }: { option: ComboboxOption }) => {
const {
inputProps: { id },
size,
searchTerm,
} = useInputContext();
const {
setIsMouseLastUsedInputDevice,
Expand All @@ -22,9 +36,11 @@ const FilteredOptionsItem = ({ option }: { option: ComboboxOption }) => {
} = useFilteredOptionsContext();
const { isMultiSelect, maxSelected, selectedOptions, toggleOption } =
useSelectedOptionsContext();
const [start, highlight, end] = useTextHighlight(option.label, searchTerm);

const isDisabled = (_option: ComboboxOption) =>
maxSelected?.isLimitReached && !isInList(_option.value, selectedOptions);

return (
<li
className={cl("navds-combobox__list-item", {
Expand Down Expand Up @@ -64,7 +80,12 @@ const FilteredOptionsItem = ({ option }: { option: ComboboxOption }) => {
aria-selected={isInList(option.value, selectedOptions)}
aria-disabled={isDisabled(option) || undefined}
>
<BodyShort size={size}>{option.label}</BodyShort>
{/* Aria-label is used to fix testing-library wrongly evaluating the accessible name of the option when highlighting text */}
<BodyShort size={size} aria-label={option.label}>
{start}
{highlight && <mark>{highlight}</mark>}
{end}
</BodyShort>
{isInList(option.value, selectedOptions) && <CheckmarkIcon />}
</li>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ const FilteredOptionsProvider = ({

const isValueNew = useMemo(
() =>
Boolean(value) &&
!filteredOptionsMap[filteredOptionsUtils.getOptionId(id, value)],
[filteredOptionsMap, id, value],
Boolean(searchTerm) &&
!filteredOptionsMap[filteredOptionsUtils.getOptionId(id, searchTerm)],
[filteredOptionsMap, id, searchTerm],
);

const ariaDescribedBy = useMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion @navikt/core/react/src/form/combobox/combobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ const complexOptions = [
];

export const WithAddNewOptions: StoryFn = ({ open }: { open?: boolean }) => {
const [value, setValue] = useState<string | undefined>("hello");
const comboboxRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState<string | undefined>("hello");
return (
<UNSAFE_Combobox
id="combobox-with-add-new-options"
Expand Down Expand Up @@ -210,6 +210,7 @@ export const MultiSelectWithAddNewOptions: StoryFn = ({
options={options}
allowNewValues={true}
value={value}
shouldAutocomplete={true}
selectedOptions={selectedOptions}
onChange={setValue}
onToggleSelected={(option, isSelected) =>
Expand Down

0 comments on commit 59dc3e9

Please sign in to comment.