From df82ca995f6a6ae9f6cbf6b15846f615f0470bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Mon, 22 Jul 2024 09:33:43 +0200 Subject: [PATCH] Fix showing proper highlight index when value is object in Combobox (#815) * Fix show highlight index * Add changeset * Fix highlighted index * Fix return * Small fixes * Remove index check * Improvments * Clear input when select value * Allow select label to be ReactNode * Fix dependency useCombobox * Refactor to hooks, add support for select highlight * Refactor to use single hook for comobobox and select * Restore select option react node * Add handle to clear input to useHighlightedIndex * Update src/components/BaseSelect/useHighlightedIndex.ts Co-authored-by: Wojciech Mista * Update src/components/BaseSelect/useHighlightedIndex.ts Co-authored-by: Wojciech Mista --------- Co-authored-by: Wojciech Mista --- .changeset/brave-rockets-poke.md | 5 ++ src/components/BaseSelect/index.ts | 1 + .../BaseSelect/useHighlightedIndex.ts | 81 +++++++++++++++++++ .../Combobox/Common/useCombobox.tsx | 16 +++- src/components/Select/useSelect.tsx | 13 ++- 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 .changeset/brave-rockets-poke.md create mode 100644 src/components/BaseSelect/useHighlightedIndex.ts diff --git a/.changeset/brave-rockets-poke.md b/.changeset/brave-rockets-poke.md new file mode 100644 index 00000000..7274f155 --- /dev/null +++ b/.changeset/brave-rockets-poke.md @@ -0,0 +1,5 @@ +--- +"@saleor/macaw-ui": patch +--- + +You can now see selected option in combobox dropdown when provided value does not match same referance with options diff --git a/src/components/BaseSelect/index.ts b/src/components/BaseSelect/index.ts index 9c028d6c..3870e06c 100644 --- a/src/components/BaseSelect/index.ts +++ b/src/components/BaseSelect/index.ts @@ -3,3 +3,4 @@ export * from "./LoadingListItem"; export * from "./types"; export * from "./helpers"; export * from "./NoOptions"; +export * from "./useHighlightedIndex"; diff --git a/src/components/BaseSelect/useHighlightedIndex.ts b/src/components/BaseSelect/useHighlightedIndex.ts new file mode 100644 index 00000000..549ece17 --- /dev/null +++ b/src/components/BaseSelect/useHighlightedIndex.ts @@ -0,0 +1,81 @@ +import { + UseComboboxStateChangeTypes, + UseSelectStateChangeTypes, + useCombobox as useDownshiftCombobox, + useSelect as useDownshiftSelect, +} from "downshift"; +import { useEffect, useState } from "react"; + +import { Option } from "~/components/BaseSelect"; + +export function useHighlightedIndex( + items: T[], + selectedItem: T | null | undefined +): { + highlightedIndex: number | undefined; + onHighlightedIndexChange: ( + index: number | undefined, + type: UseComboboxStateChangeTypes | UseSelectStateChangeTypes + ) => void; +} { + // Initially we don't show any item as highlighted + const [highlightedIndex, setHighlightedIndex] = useState( + -1 + ); + + // When data from API comes we can calulate intial 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 + // If there is no match, leave highlighted index as -1 + setHighlightedIndex(getIndexToHighlight(items, selectedItem)); + }, [items, selectedItem]); + + const handleHighlightedIndexChange = ( + highlightedIndex: number | undefined, + type: UseComboboxStateChangeTypes | UseSelectStateChangeTypes + ) => { + 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: + setHighlightedIndex( + selectedItem + ? getIndexToHighlight(items, selectedItem) + : highlightedIndex + ); + break; + case useDownshiftCombobox.stateChangeTypes.ItemClick: + case useDownshiftSelect.stateChangeTypes.ItemClick: + case useDownshiftCombobox.stateChangeTypes.ItemMouseMove: + case useDownshiftSelect.stateChangeTypes.ItemMouseMove: + setHighlightedIndex(highlightedIndex); + break; + } + }; + + return { + highlightedIndex, + onHighlightedIndexChange: handleHighlightedIndexChange, + }; +} + +function getIndexToHighlight( + items: T[], + selectedItem: T +): number { + if (typeof selectedItem === "string") { + return items.findIndex((item) => item.value === selectedItem); + } + + return items.findIndex((item) => item.value === selectedItem?.value); +} diff --git a/src/components/Combobox/Common/useCombobox.tsx b/src/components/Combobox/Common/useCombobox.tsx index 477f55e3..7239cbcc 100644 --- a/src/components/Combobox/Common/useCombobox.tsx +++ b/src/components/Combobox/Common/useCombobox.tsx @@ -5,7 +5,11 @@ import { } from "downshift"; import { FocusEvent, useState } from "react"; -import { Option, SingleChangeHandler } from "~/components/BaseSelect"; +import { + Option, + SingleChangeHandler, + useHighlightedIndex, +} from "~/components/BaseSelect"; const getItemsFilter = ( inputValue: string | undefined, @@ -44,6 +48,10 @@ export const useCombobox = ({ const typed = Boolean(selectedItem || active || inputValue); const itemsToSelect = getItemsFilter(inputValue, options); + const { highlightedIndex, onHighlightedIndexChange } = useHighlightedIndex( + itemsToSelect, + selectedItem + ); const { isOpen, @@ -51,17 +59,21 @@ export const useCombobox = ({ getLabelProps, getMenuProps, getInputProps: _getInputProps, - highlightedIndex, getItemProps, } = useDownshiftCombobox({ items: itemsToSelect, 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); } }, diff --git a/src/components/Select/useSelect.tsx b/src/components/Select/useSelect.tsx index 65cdf7c9..eaa53719 100644 --- a/src/components/Select/useSelect.tsx +++ b/src/components/Select/useSelect.tsx @@ -5,7 +5,7 @@ import { } from "downshift"; import { FocusEvent, useState } from "react"; -import { Option } from "../BaseSelect"; +import { Option, useHighlightedIndex } from "../BaseSelect"; import { SelectProps } from "./Select"; export const useSelect = ({ @@ -25,19 +25,26 @@ export const useSelect = ({ }) => { const [active, setActive] = useState(false); const typed = Boolean(value || active); + const { highlightedIndex, onHighlightedIndexChange } = useHighlightedIndex( + options, + value + ); const { isOpen, getToggleButtonProps: _getToggleButtonProps, getLabelProps, getMenuProps, - highlightedIndex, getItemProps, selectedItem, } = useDownshiftSelect({ items: options, selectedItem: value ?? null, - itemToString: (item) => item?.label ?? "", + highlightedIndex, + onHighlightedIndexChange({ highlightedIndex, type }) { + onHighlightedIndexChange(highlightedIndex, type); + }, + itemToString: (item) => item?.value ?? "", onSelectedItemChange: ({ selectedItem }) => { if (selectedItem) { const selectedValue = isValuePassedAsString