Skip to content

Commit

Permalink
Combobox: 🚸 hide caret on select (until options list opened) (#3071)
Browse files Browse the repository at this point in the history
* 🚸 hide caret on select (until list opened)

* 🔥 remove console.log()

* 🎨 invert varname for simpler CSS

* 🚸 also enable caret on "re-click" (while still focused)

* 🚸 DX improvement, placeholders++

* 🚸 (combobox) always show caret for multiple select

* 🚸 hide caret on multiselect + isLimitReached

* 🔥 remove console.log

* 🐛 hide caret when single has a selection (combobox)

* 🐛 hide caret if isLimitReached

* 🎨 remove unnecessary useState()

* 🐛 correct deps for useEffect

* 🐛 add maxSelected as dep

* Update @navikt/core/react/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx

Co-authored-by: Halvor Haugan <[email protected]>

* changeset

* 🐛 avoid setting placeholder twice

* Update .changeset/fresh-boxes-enjoy.md

Co-authored-by: Halvor Haugan <[email protected]>

* 🔥 remove redundant placeholder

---------

Co-authored-by: Halvor Haugan <[email protected]>
  • Loading branch information
JulianNymark and HalvorHaugan authored Sep 2, 2024
1 parent 1cea314 commit c98ab95
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changeset/fresh-boxes-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@navikt/ds-react": patch
"@navikt/ds-css": patch
---

Combobox: :lipstick: hide caret on select
4 changes: 4 additions & 0 deletions @navikt/core/css/form/combobox.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@
height: var(--__ac-combobox-input-height);
}

.navds-combobox__input--hide-caret {
caret-color: transparent;
}

.navds-combobox__input:focus-visible {
outline: none;
border: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const FilteredOptionsProvider = ({
setValue,
setSearchTerm,
shouldAutocomplete,
setHideCaret,
} = useInputContext();
const { maxSelected } = useSelectedOptionsContext();

Expand Down Expand Up @@ -136,9 +137,17 @@ const FilteredOptionsProvider = ({
const toggleIsListOpen = useCallback(
(newState?: boolean) => {
virtualFocus.moveFocusToTop();
if (newState ?? !isInternalListOpen) {
setHideCaret(!!maxSelected?.isLimitReached);
}
setInternalListOpen((oldState) => newState ?? !oldState);
},
[virtualFocus],
[
virtualFocus,
maxSelected?.isLimitReached,
isInternalListOpen,
setHideCaret,
],
);

const isValueNew = useMemo(
Expand Down
5 changes: 5 additions & 0 deletions @navikt/core/react/src/form/combobox/Input/Input.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ interface InputContextValue extends FormFieldType {
setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
shouldAutocomplete?: boolean;
toggleOpenButtonRef: React.RefObject<HTMLButtonElement>;
hideCaret: boolean;
setHideCaret: React.Dispatch<React.SetStateAction<boolean>>;
}

const [InputContextProvider, useInputContext] =
Expand Down Expand Up @@ -69,6 +71,7 @@ const InputProvider = ({ children, value: props }: Props) => {
const inputRef = useRef<HTMLInputElement | null>(null);
const toggleOpenButtonRef = useRef<HTMLButtonElement>(null);
const [internalValue, setInternalValue] = useState<string>(defaultValue);
const [hideCaret, setHideCaret] = useState(false);

const value = useMemo(
() => String(externalValue ?? internalValue),
Expand Down Expand Up @@ -119,6 +122,8 @@ const InputProvider = ({ children, value: props }: Props) => {
setSearchTerm,
shouldAutocomplete,
toggleOpenButtonRef,
hideCaret,
setHideCaret,
};

return (
Expand Down
16 changes: 13 additions & 3 deletions @navikt/core/react/src/form/combobox/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ interface InputProps
}

const Input = forwardRef<HTMLInputElement, InputProps>(
({ inputClassName, shouldShowSelectedOptions, ...rest }, ref) => {
(
{ inputClassName, shouldShowSelectedOptions, placeholder, ...rest },
ref,
) => {
const internalRef = useRef<HTMLInputElement>(null);
const mergedRefs = useMergeRefs(ref, internalRef);
const {
Expand All @@ -32,12 +35,15 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
value,
searchTerm,
setValue,
hideCaret,
setHideCaret,
} = useInputContext();
const {
selectedOptions,
removeSelectedOption,
toggleOption,
isMultiSelect,
maxSelected,
} = useSelectedOptionsContext();
const {
activeDecendantId,
Expand Down Expand Up @@ -220,7 +226,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
ref={mergedRefs}
value={value}
onBlur={() => virtualFocus.moveFocusToTop()}
onClick={() => value !== searchTerm && onChange(value)}
onClick={() => {
setHideCaret(!!maxSelected?.isLimitReached);
value !== searchTerm && onChange(value);
}}
onInput={onChangeHandler}
type="text"
role="combobox"
Expand All @@ -233,12 +242,13 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
aria-activedescendant={activeDecendantId}
aria-describedby={ariaDescribedBy}
aria-invalid={inputProps["aria-invalid"]}
placeholder={selectedOptions.length ? undefined : rest.placeholder}
placeholder={selectedOptions.length ? undefined : placeholder}
className={cl(
inputClassName,
"navds-combobox__input",
"navds-body-short",
`navds-body-short--${size}`,
{ "navds-combobox__input--hide-caret": hideCaret },
)}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { forwardRef } from "react";
import { ChevronDownIcon, ChevronUpIcon } from "@navikt/aksel-icons";
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
import { useInputContext } from "./Input.context";

interface ToggleListButtonProps {
toggleListButtonLabel?: string;
Expand All @@ -11,10 +12,14 @@ export const ToggleListButton = forwardRef<
ToggleListButtonProps
>(({ toggleListButtonLabel }, ref) => {
const { isListOpen, toggleIsListOpen } = useFilteredOptionsContext();
const { focusInput } = useInputContext();
return (
<button
type="button"
onPointerUp={() => toggleIsListOpen()}
onPointerUp={() => {
toggleIsListOpen();
focusInput();
}}
onKeyDown={({ key }) => key === "Enter" && toggleIsListOpen()}
className="navds-combobox__button-toggle-list"
aria-expanded={isListOpen}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { createContext } from "../../../util/create-context";
import { usePrevious } from "../../../util/hooks";
import { useInputContext } from "../Input/Input.context";
Expand Down Expand Up @@ -33,7 +33,7 @@ const SelectedOptionsProvider = ({
"allowNewValues" | "isMultiSelect" | "onToggleSelected" | "maxSelected"
> & { options: ComboboxOption[]; selectedOptions?: ComboboxOption[] };
}) => {
const { clearInput, focusInput } = useInputContext();
const { clearInput, focusInput, setHideCaret } = useInputContext();
const {
customOptions,
removeCustomOption,
Expand Down Expand Up @@ -101,6 +101,14 @@ const SelectedOptionsProvider = ({
[customOptions, onToggleSelected, removeCustomOption],
);

const isLimitReached =
(!!maxSelected?.limit && selectedOptions.length >= maxSelected.limit) ||
(!isMultiSelect && selectedOptions.length > 0);

useEffect(() => {
setHideCaret(isLimitReached);
}, [selectedOptions, setHideCaret, isLimitReached]);

const toggleOption = useCallback(
(
option: ComboboxOption,
Expand All @@ -125,9 +133,6 @@ const SelectedOptionsProvider = ({

const prevSelectedOptions = usePrevious<ComboboxOption[]>(selectedOptions);

const isLimitReached =
!!maxSelected?.limit && selectedOptions.length >= maxSelected.limit;

const selectedOptionsState = {
addSelectedOption,
isMultiSelect,
Expand Down
32 changes: 31 additions & 1 deletion @navikt/core/react/src/form/combobox/combobox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Meta, StoryFn } from "@storybook/react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Alert } from "../../alert";
import { Button } from "../../button";
import { Chips } from "../../chips";
import { VStack } from "../../layout/stack";
import { Modal } from "../../modal";
import { BodyLong } from "../../typography";
import { TextField } from "../textfield";
import { UNSAFE_Combobox } from "./index";

Expand Down Expand Up @@ -44,7 +46,7 @@ export const Default: StoryFunction = (props) => (

Default.args = {
options,
label: "Hva er dine favorittfrukter?",
label: "Hva er din favorittfrukt?",
shouldAutocomplete: true,
isLoading: false,
isMultiSelect: false,
Expand Down Expand Up @@ -74,6 +76,34 @@ Default.argTypes = {
},
};

export const WithPlaceholder: StoryFunction = () => {
const props = {
options,
label: "Hva er din favorittfrukt?",
shouldAutocomplete: true,
isLoading: false,
isMultiSelect: false,
allowNewValues: false,
onChange: console.log,
};
return (
<VStack gap="8" align="center">
<Alert variant="warning" style={{ width: "70ch" }}>
<VStack gap="4">
<BodyLong>
{`We don't endorse placeholders, but they shouldn't break either!`}
</BodyLong>
<BodyLong>
{`Don't
tell anyone that they work (or that they exist).`}
</BodyLong>
</VStack>
</Alert>
<UNSAFE_Combobox {...props} id="combobox" placeholder="placeholder" />
</VStack>
);
};

export const MultiSelect: StoryFn = () => {
const [selectedOptions, setSelectedOptions] = useState<string[]>([
"pear",
Expand Down

0 comments on commit c98ab95

Please sign in to comment.