Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/combobox show add new value while autocompleting #3225

Merged
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
Loading