Skip to content

Commit

Permalink
Upgrade downshift version, fix combobox options highlighting (#839)
Browse files Browse the repository at this point in the history
* Update Selects

* Update Combobox

* Update Multiselect

* Bump version

* update lock

* add changeset

* Fire onInputChange only onChange event

* fix import

* Fix import

* Use downshift highlight index

* Revert useHighlightedIndex and check log

* log higlight index

* more logs

* add higlight to key

* impove use highlight

* calculate hightlight index on open menu

* Remove logs and add select highlight

* Set highlight index on focus

* Add log check

* Set highlight index on arrow down

* Remove logs and handleSetHighlightedIndex on focus

* Add form event types adapter

* Fix changeset

* Handle undefined onChange and remove undefined from formEventTypeAdapter
  • Loading branch information
poulch authored Aug 21, 2024
1 parent 87113d9 commit e577c41
Show file tree
Hide file tree
Showing 17 changed files with 5,112 additions and 8,892 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-glasses-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@saleor/macaw-ui": patch
---

You can now navigate on dropdown list. Dropdown stays close on focus.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
"@radix-ui/react-tooltip": "^1.0.6",
"@vanilla-extract/css-utils": "^0.1.3",
"@vanilla-extract/recipes": "^0.5.0",
"downshift": "^7.6.0"
"downshift": "^9.0.8"
},
"peerDependencies": {
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
Expand Down
13,799 changes: 4,982 additions & 8,817 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/components/BaseSelect/NoOptions/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Children, ReactNode, isValidElement } from "react";
import { Children, ReactNode, isValidElement, ReactElement } from "react";
import { NoOptions } from "./NoOptions";

export const hasNoOptions = (children: ReactNode): boolean => {
let hasNoOptions = false;

Children.forEach(children, (child) => {
if (isValidElement(child) && child.type === NoOptions) {
if (isValidElement(child) && (child as ReactElement).type === NoOptions) {
hasNoOptions = true;
}
});
Expand Down
29 changes: 19 additions & 10 deletions src/components/BaseSelect/useHighlightedIndex.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
UseComboboxStateChangeTypes,
UseSelectStateChangeTypes,
useCombobox as useDownshiftCombobox,
useSelect as useDownshiftSelect,
UseComboboxStateChange,
UseSelectStateChange,
} from "downshift";
import { useEffect, useState } from "react";

Expand All @@ -14,37 +14,37 @@ export function useHighlightedIndex<T extends Option>(
): {
highlightedIndex: number | undefined;
onHighlightedIndexChange: (
index: number | undefined,
type: UseComboboxStateChangeTypes | UseSelectStateChangeTypes
change: UseComboboxStateChange<T> | UseSelectStateChange<T>
) => void;
} {
// Initially we don't show any item as highlighted
const [highlightedIndex, setHighlightedIndex] = useState<number | undefined>(
-1
);

// When data from API comes we can calulate intial highlighted index
// When data from API comes we can calculate initial highlighted index
// Or when we change selected item
useEffect(() => {
// If we don't have selected item leave highlighted index as -1
if (!selectedItem) {
return;
}

// Find hilighted index in items to select base on selected item value
// Find highlighted index in items to select base on selected item value
// If there is no match, leave highlighted index as -1
setHighlightedIndex(getIndexToHighlight(items, selectedItem));
}, [items, selectedItem]);

const handleHighlightedIndexChange = (
highlightedIndex: number | undefined,
type: UseComboboxStateChangeTypes | UseSelectStateChangeTypes
) => {
const handleHighlightedIndexChange = ({
type,
highlightedIndex,
}: UseComboboxStateChange<T> | UseSelectStateChange<T>) => {
switch (type) {
// Restore highlighted index to -1 when input value is changed and there is no selected item
case useDownshiftCombobox.stateChangeTypes.InputChange:
setHighlightedIndex(!selectedItem ? -1 : highlightedIndex);
break;

// Restore highlighted index to last selected item when leaving menu
case useDownshiftCombobox.stateChangeTypes.MenuMouseLeave:
case useDownshiftSelect.stateChangeTypes.MenuMouseLeave:
Expand All @@ -58,8 +58,17 @@ export function useHighlightedIndex<T extends Option>(
case useDownshiftSelect.stateChangeTypes.ItemClick:
case useDownshiftCombobox.stateChangeTypes.ItemMouseMove:
case useDownshiftSelect.stateChangeTypes.ItemMouseMove:
case useDownshiftCombobox.stateChangeTypes.InputKeyDownArrowUp:
case useDownshiftSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
case useDownshiftSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
setHighlightedIndex(highlightedIndex);
break;
case useDownshiftCombobox.stateChangeTypes.InputKeyDownArrowDown:
if (selectedItem && highlightedIndex === -1) {
setHighlightedIndex(getIndexToHighlight(items, selectedItem));
} else {
setHighlightedIndex(highlightedIndex);
}
}
};

Expand Down
30 changes: 16 additions & 14 deletions src/components/Combobox/Common/useCombobox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
GetPropsCommonOptions,
UseComboboxGetInputPropsOptions,
useCombobox as useDownshiftCombobox,
UseComboboxGetInputPropsOptions,
} from "downshift";
import { FocusEvent, useState } from "react";

Expand Down Expand Up @@ -65,18 +65,8 @@ export const useCombobox = <T extends Option, V extends string | Option>({
itemToString: (item) => item?.label ?? "",
selectedItem,
highlightedIndex,
onHighlightedIndexChange: ({ highlightedIndex, type }) => {
onHighlightedIndexChange(highlightedIndex, type);
},
onSelectedItemChange: ({ selectedItem }) => {
if (selectedItem) {
const selectedValue = isValuePassedAsString
? selectedItem.value
: selectedItem;
setInputValue("");
onChange?.(selectedValue as V);
}
},
onHighlightedIndexChange,
isItemDisabled: (item) => item.disabled ?? false,
onStateChange: ({ inputValue: newInputValue, type }) => {
switch (type) {
case useDownshiftCombobox.stateChangeTypes.InputChange:
Expand All @@ -89,6 +79,15 @@ export const useCombobox = <T extends Option, V extends string | Option>({
break;
}
},
onSelectedItemChange: ({ selectedItem }) => {
if (selectedItem) {
const selectedValue = isValuePassedAsString
? selectedItem.value
: selectedItem;
setInputValue("");
onChange?.(selectedValue as V);
}
},
});

return {
Expand All @@ -103,7 +102,10 @@ export const useCombobox = <T extends Option, V extends string | Option>({
options?: UseComboboxGetInputPropsOptions,
otherOptions?: GetPropsCommonOptions
) =>
_getInputProps(
_getInputProps<{
onFocus: (e: FocusEvent<HTMLInputElement>) => void;
onBlur: (e: FocusEvent<HTMLInputElement>) => void;
}>(
{
onFocus: (e) => {
onFocus?.(e);
Expand Down
17 changes: 11 additions & 6 deletions src/components/Combobox/Dynamic/DynamicCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { classNames } from "~/utils";

import { useFloating } from "~/hooks/useFloating";
import { useInfinityScroll } from "~/hooks/useInfinityScroll";
import { formEventTypeAdapter } from "~/utils/formEventTypeAdapter";
import { Box, List, PropsWithBox, Text } from "../..";
import { HelperText, inputRecipe, InputVariants } from "../../BaseInput";
import {
Expand Down Expand Up @@ -110,6 +111,11 @@ const DynamicComboboxInner = <T extends Option>(

const scrollRef = useInfinityScroll(onScrollEnd);

const inputProps = getInputProps({
id,
ref,
});

return (
<Box display="flex" flexDirection="column">
<ComboboxWrapper
Expand All @@ -129,16 +135,15 @@ const DynamicComboboxInner = <T extends Option>(
{startAdornment && typed && <Box>{startAdornment(value)}</Box>}

<Box
id={id}
as="input"
type="text"
className={classNames(inputRecipe({ size, error }))}
disabled={disabled}
{...props}
{...getInputProps({
id,
ref,
})}
{...inputProps}
onChange={
inputProps.onChange && formEventTypeAdapter(inputProps.onChange)
}
/>

{endAdornment && typed && <Box>{endAdornment(value)}</Box>}
Expand Down Expand Up @@ -167,7 +172,7 @@ const DynamicComboboxInner = <T extends Option>(
itemsToSelect?.map((item, index) => (
<List.Item
data-test-id="select-option"
key={`${id}-${item.value}-${index}`}
key={`${id}-${item.value}-${index}-${highlightedIndex}`}
className={listItemStyle}
{...getItemProps({
item,
Expand Down
17 changes: 11 additions & 6 deletions src/components/Combobox/Static/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { classNames, isString } from "~/utils";

import { useFloating } from "~/hooks/useFloating";
import { formEventTypeAdapter } from "~/utils/formEventTypeAdapter";
import { ComboboxWrapper } from "../Common";
import { useCombobox } from "../Common/useCombobox";

Expand Down Expand Up @@ -99,6 +100,11 @@ const ComboboxInner = <T extends Option, V extends Option | string>(

const { refs, floatingStyles } = useFloating();

const inputProps = getInputProps({
id,
ref,
});

return (
<Box display="flex" flexDirection="column">
<ComboboxWrapper
Expand All @@ -118,7 +124,6 @@ const ComboboxInner = <T extends Option, V extends Option | string>(
{startAdornment && typed && <Box>{startAdornment(value)}</Box>}

<Box
id={id}
as="input"
type="text"
className={classNames(inputRecipe({ size, error }))}
Expand All @@ -127,10 +132,10 @@ const ComboboxInner = <T extends Option, V extends Option | string>(
textOverflow="ellipsis"
title={isString(value) ? value : value?.label}
{...props}
{...getInputProps({
id,
ref,
})}
{...inputProps}
onChange={
inputProps.onChange && formEventTypeAdapter(inputProps.onChange)
}
/>

{endAdornment && typed && <Box>{endAdornment(value)}</Box>}
Expand Down Expand Up @@ -160,11 +165,11 @@ const ComboboxInner = <T extends Option, V extends Option | string>(
<List.Item
data-test-id="select-option"
key={`${id}-${item.value}-${index}`}
disabled={item.disabled}
className={listItemStyle}
{...getItemProps({
item,
index,
disabled: item.disabled,
})}
active={highlightedIndex === index}
>
Expand Down
12 changes: 6 additions & 6 deletions src/components/Multiselect/Common/Adornment.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { UseComboboxPropGetters } from "downshift";

import { sprinkles } from "~/theme";
import { classNames } from "~/utils";

import { Option, toggleIconStyle } from "../../BaseSelect";
import { toggleIconStyle } from "../../BaseSelect";
import { ArrowDownIcon } from "../../Icons";
import { RenderEndAdornmentType } from "./useMultiselect";
import { RenderEndAdornmentType, useMultiselect } from "./useMultiselect";

export type AdornmentProps = {
size?: "small" | "medium" | "large";
getToggleButtonProps: UseComboboxPropGetters<Option>["getToggleButtonProps"];
getToggleButtonProps: ReturnType<
typeof useMultiselect
>["getToggleButtonProps"];
renderEndAdornment?: RenderEndAdornmentType;
disabled?: boolean;
};
Expand All @@ -21,7 +21,7 @@ export const Adornment = ({
disabled,
}: AdornmentProps) => {
return renderEndAdornment ? (
<>{renderEndAdornment(getToggleButtonProps({ disabled }))}</>
<>{renderEndAdornment(getToggleButtonProps())}</>
) : (
<ArrowDownIcon
className={classNames(toggleIconStyle, sprinkles({ cursor: "pointer" }))}
Expand Down
6 changes: 4 additions & 2 deletions src/components/Multiselect/Common/MultiselectWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { LabelVariants, labelRecipe, spanRecipe } from "../../BaseInput";
import { Option } from "../../BaseSelect";

import { Adornment } from "./Adornment";
import { RenderEndAdornmentType } from "./useMultiselect";
import { RenderEndAdornmentType, useMultiselect } from "./useMultiselect";
import { multiselectSpanRecipe } from "./Multiselect.css";

type MultiselectWrapperProps = LabelVariants & {
Expand All @@ -21,7 +21,9 @@ type MultiselectWrapperProps = LabelVariants & {
error?: boolean;
children: ReactNode;
getLabelProps: UseComboboxPropGetters<Option>["getLabelProps"];
getToggleButtonProps: UseComboboxPropGetters<Option>["getToggleButtonProps"];
getToggleButtonProps: ReturnType<
typeof useMultiselect
>["getToggleButtonProps"];
renderEndAdornment?: RenderEndAdornmentType;
hasItemsToSelect?: boolean;
};
Expand Down
20 changes: 12 additions & 8 deletions src/components/Multiselect/Common/useMultiselect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import {
GetPropsCommonOptions,
UseComboboxGetInputPropsOptions,
UseComboboxGetToggleButtonPropsOptions,
UseComboboxPropGetters,
UseComboboxGetToggleButtonPropsReturnValue,
useCombobox,
useMultipleSelection,
} from "downshift";
import { FocusEvent, ReactNode, useState } from "react";
import { FocusEvent, ReactNode, useState, MouseEventHandler } from "react";

import { MultiChangeHandler, Option } from "~/components/BaseSelect";
import { isStringArray } from "~/utils";

export type RenderEndAdornmentType = (
...props: ReturnType<UseComboboxPropGetters<Option>["getToggleButtonProps"]>
props: UseComboboxGetToggleButtonPropsReturnValue
) => ReactNode;

const getItemsFilter = <T extends Option>(
Expand Down Expand Up @@ -104,6 +104,7 @@ export const useMultiselect = <T extends Option, V extends Option | string>({
items: itemsToSelect,
itemToString: (item) => item?.label ?? "",
defaultHighlightedIndex: 0,
isItemDisabled: (item) => item?.disabled ?? false,
selectedItem: null,
stateReducer(_state, actionAndChanges) {
const { changes, type } = actionAndChanges;
Expand Down Expand Up @@ -161,7 +162,10 @@ export const useMultiselect = <T extends Option, V extends Option | string>({
options?: UseComboboxGetInputPropsOptions,
otherOptions?: GetPropsCommonOptions
) =>
_getInputProps(
_getInputProps<{
onFocus: (e: FocusEvent<HTMLInputElement>) => void;
onBlur: (e: FocusEvent<HTMLInputElement>) => void;
}>(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
getDropdownProps({
onFocus: (e: FocusEvent<HTMLInputElement, Element>) => {
Expand All @@ -184,10 +188,10 @@ export const useMultiselect = <T extends Option, V extends Option | string>({
selectedItems,
inputValue,
showInput,
getToggleButtonProps: (
options?: UseComboboxGetToggleButtonPropsOptions | undefined
) =>
_getToggleButtonProps({
getToggleButtonProps: (options?: UseComboboxGetToggleButtonPropsOptions) =>
_getToggleButtonProps<{
onClick?: MouseEventHandler<HTMLButtonElement>;
}>({
onClick: (event) => {
event.preventDefault();
},
Expand Down
Loading

0 comments on commit e577c41

Please sign in to comment.