From e901e7307db3a6dd8fe9b6fd9def60d9e33e9f81 Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Tue, 16 Jul 2024 11:04:52 +0300 Subject: [PATCH 1/8] Add story --- src/components/input/input.js | 46 +++++++++++++++++++++++++-- src/components/input/input.stories.js | 16 ++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/components/input/input.js b/src/components/input/input.js index b0ea557e..d5967c0f 100644 --- a/src/components/input/input.js +++ b/src/components/input/input.js @@ -1,7 +1,10 @@ -import React from "react" +import React, { useRef } from "react" import Flex from "@/components/templates/flex" import { TextMicro } from "@/components/typography" +import Drop from "@/components/drops/drop" import { Input, LabelText } from "./styled" +import { useEffect } from "react" +import { useState } from "react" const Error = ({ error }) => { const errorMessage = error === true ? "invalid" : error @@ -13,6 +16,18 @@ const Error = ({ error }) => { ) } +const Suggestions = ({ suggestions = [] } = {}) => { + return ( + + ) +} + export const TextInput = ({ error, disabled, @@ -32,12 +47,23 @@ export const TextInput = ({ containerStyles, inputContainerStyles, hideErrorMessage, + autocompleteProps, ...props }) => { + const inputContainerRef = useRef() + const [autocompleteOpen, setAutocompleteOpen] = useState() + const { suggestions = [] } = autocompleteProps || {} + + useEffect(() => { + if (suggestions.length) { + setAutocompleteOpen(!!value.length) + } + }, [suggestions, value, setAutocompleteOpen]) + return ( {typeof label === "string" ? {label} : label} - + {iconLeft && ( {iconLeft} @@ -71,6 +97,22 @@ export const TextInput = ({ {typeof hint === "string" ? {hint} : !!hint && hint} {!hideErrorMessage ? : null} + {autocompleteOpen && inputContainerRef?.current && ( + {}} + onClickOutside={() => {}} + onEsc={() => {}} + > + + + )} ) } diff --git a/src/components/input/input.stories.js b/src/components/input/input.stories.js index e917fd82..b9bfbbbb 100644 --- a/src/components/input/input.stories.js +++ b/src/components/input/input.stories.js @@ -1,6 +1,7 @@ import React from "react" import { Icon } from "@/components/icon" import { TextInput } from "." +import { useState } from "react" export const WithIcons = args => ( ( export const Basic = args => +export const WithAutocomplete = () => { + const [value, setValue] = useState("") + const autocompleteProps = { + suggestions: [{ value: "one", label: "one" }], + } + + return ( + setValue(e.target.value)} + autocompleteProps={autocompleteProps} + /> + ) +} + export default { component: TextInput, args: { From 90cedd49b5b3efb66ce951e48eaeb628e38d50af Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Wed, 24 Jul 2024 11:52:20 +0300 Subject: [PATCH 2/8] wip --- src/components/input/autocomplete/index.js | 30 ++++++++++ src/components/input/autocomplete/options.js | 16 ++++++ src/components/input/autocomplete/styled.js | 11 ++++ .../input/autocomplete/useAutocomplete.js | 16 ++++++ src/components/input/input.js | 57 ++++++------------- src/components/input/input.stories.js | 6 +- 6 files changed, 96 insertions(+), 40 deletions(-) create mode 100644 src/components/input/autocomplete/index.js create mode 100644 src/components/input/autocomplete/options.js create mode 100644 src/components/input/autocomplete/styled.js create mode 100644 src/components/input/autocomplete/useAutocomplete.js diff --git a/src/components/input/autocomplete/index.js b/src/components/input/autocomplete/index.js new file mode 100644 index 00000000..efab07e5 --- /dev/null +++ b/src/components/input/autocomplete/index.js @@ -0,0 +1,30 @@ +import React from "react" +import Drop from "@/components/drops/drop" +import Options from "./options" +import useAutocomplete from "./useAutocomplete" + +const Autocomplete = ({ value, autocompleteProps, tagretRef }) => { + const { autocompleteOpen, suggestions } = useAutocomplete({ value, autocompleteProps }) + + return ( + autocompleteOpen && + tagretRef?.current && ( + {}} + onClickOutside={() => {}} + onEsc={() => {}} + > + + + ) + ) +} + +export default Autocomplete diff --git a/src/components/input/autocomplete/options.js b/src/components/input/autocomplete/options.js new file mode 100644 index 00000000..bd2758c7 --- /dev/null +++ b/src/components/input/autocomplete/options.js @@ -0,0 +1,16 @@ +import React from "react" +import { StyledOption } from "./styled" + +const Options = ({ suggestions = [] } = {}) => { + return ( +
    + {suggestions.map(({ value, label }) => ( + + {label} + + ))} +
+ ) +} + +export default Options diff --git a/src/components/input/autocomplete/styled.js b/src/components/input/autocomplete/styled.js new file mode 100644 index 00000000..27c843a8 --- /dev/null +++ b/src/components/input/autocomplete/styled.js @@ -0,0 +1,11 @@ +import styled from "styled-components" + +export const StyledOption = styled.li` + &:hover { + background-color: red; + } + + &:focus { + background-color: red; + } +` diff --git a/src/components/input/autocomplete/useAutocomplete.js b/src/components/input/autocomplete/useAutocomplete.js new file mode 100644 index 00000000..9cb65b3d --- /dev/null +++ b/src/components/input/autocomplete/useAutocomplete.js @@ -0,0 +1,16 @@ +import { useState, useEffect } from "react" + +const useAutocomplete = ({ value, autocompleteProps = {} } = {}) => { + const [autocompleteOpen, setAutocompleteOpen] = useState() + const { suggestions = [] } = autocompleteProps || {} + + useEffect(() => { + if (suggestions.length) { + setAutocompleteOpen(!!value.length) + } + }, [suggestions, value, setAutocompleteOpen]) + + return { autocompleteOpen, suggestions } +} + +export default useAutocomplete diff --git a/src/components/input/input.js b/src/components/input/input.js index d5967c0f..69f5d63a 100644 --- a/src/components/input/input.js +++ b/src/components/input/input.js @@ -1,10 +1,8 @@ -import React, { useRef } from "react" +import React, { useRef, useMemo } from "react" import Flex from "@/components/templates/flex" import { TextMicro } from "@/components/typography" -import Drop from "@/components/drops/drop" import { Input, LabelText } from "./styled" -import { useEffect } from "react" -import { useState } from "react" +import Autocomplete from "./autocomplete" const Error = ({ error }) => { const errorMessage = error === true ? "invalid" : error @@ -16,18 +14,6 @@ const Error = ({ error }) => { ) } -const Suggestions = ({ suggestions = [] } = {}) => { - return ( -
    - {suggestions.map(({ value, label }) => ( -
  • - {label} -
  • - ))} -
- ) -} - export const TextInput = ({ error, disabled, @@ -51,14 +37,17 @@ export const TextInput = ({ ...props }) => { const inputContainerRef = useRef() - const [autocompleteOpen, setAutocompleteOpen] = useState() - const { suggestions = [] } = autocompleteProps || {} - useEffect(() => { - if (suggestions.length) { - setAutocompleteOpen(!!value.length) - } - }, [suggestions, value, setAutocompleteOpen]) + const autocompleteInputProps = useMemo( + () => + autocompleteProps + ? { + "aria-autocomplete": "list", + "aria-controls": "autocomplete-list", + } + : {}, + [] + ) return ( @@ -85,6 +74,7 @@ export const TextInput = ({ ref={inputRef} error={error} hasValue={!!value} + {...autocompleteInputProps} {...props} /> @@ -97,22 +87,11 @@ export const TextInput = ({ {typeof hint === "string" ? {hint} : !!hint && hint} {!hideErrorMessage ? : null} - {autocompleteOpen && inputContainerRef?.current && ( - {}} - onClickOutside={() => {}} - onEsc={() => {}} - > - - - )} +
) } diff --git a/src/components/input/input.stories.js b/src/components/input/input.stories.js index b9bfbbbb..16383c45 100644 --- a/src/components/input/input.stories.js +++ b/src/components/input/input.stories.js @@ -16,7 +16,11 @@ export const Basic = args => export const WithAutocomplete = () => { const [value, setValue] = useState("") const autocompleteProps = { - suggestions: [{ value: "one", label: "one" }], + suggestions: [ + { value: "one", label: "one" }, + { value: "two", label: "two" }, + { value: "three", label: "three" }, + ], } return ( From bbde2e90524fd64633d365e1f2cdac8d39853246 Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Fri, 26 Jul 2024 15:50:02 +0300 Subject: [PATCH 3/8] Use Dropdown to render suggestions --- src/components/input/autocomplete/index.js | 32 +++++++------------ src/components/input/autocomplete/options.js | 16 ---------- src/components/input/autocomplete/styled.js | 14 ++++---- .../input/autocomplete/useAutocomplete.js | 5 ++- src/components/input/input.js | 21 ++++++------ 5 files changed, 33 insertions(+), 55 deletions(-) delete mode 100644 src/components/input/autocomplete/options.js diff --git a/src/components/input/autocomplete/index.js b/src/components/input/autocomplete/index.js index efab07e5..22d7742f 100644 --- a/src/components/input/autocomplete/index.js +++ b/src/components/input/autocomplete/index.js @@ -1,28 +1,20 @@ import React from "react" -import Drop from "@/components/drops/drop" -import Options from "./options" +import { StyledOptionsContainer } from "./styled" +import Dropdown from "@/components/drops/menu/dropdown" +import DropdownItem from "@/components/drops/menu/dropdownItem" import useAutocomplete from "./useAutocomplete" -const Autocomplete = ({ value, autocompleteProps, tagretRef }) => { - const { autocompleteOpen, suggestions } = useAutocomplete({ value, autocompleteProps }) +const Autocomplete = ({ value, autocompleteProps, Item = DropdownItem }) => { + const { autocompleteOpen, suggestions, onItemClick } = useAutocomplete({ + value, + autocompleteProps, + }) return ( - autocompleteOpen && - tagretRef?.current && ( - {}} - onClickOutside={() => {}} - onEsc={() => {}} - > - - + autocompleteOpen && ( + + + ) ) } diff --git a/src/components/input/autocomplete/options.js b/src/components/input/autocomplete/options.js deleted file mode 100644 index bd2758c7..00000000 --- a/src/components/input/autocomplete/options.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react" -import { StyledOption } from "./styled" - -const Options = ({ suggestions = [] } = {}) => { - return ( -
    - {suggestions.map(({ value, label }) => ( - - {label} - - ))} -
- ) -} - -export default Options diff --git a/src/components/input/autocomplete/styled.js b/src/components/input/autocomplete/styled.js index 27c843a8..6f621d1d 100644 --- a/src/components/input/autocomplete/styled.js +++ b/src/components/input/autocomplete/styled.js @@ -1,11 +1,9 @@ import styled from "styled-components" +import Flex from "@/components/templates/flex" -export const StyledOption = styled.li` - &:hover { - background-color: red; - } - - &:focus { - background-color: red; - } +export const StyledOptionsContainer = styled(Flex)` + width: 300px; + position: absolute; + left: 0; + top: 36px; ` diff --git a/src/components/input/autocomplete/useAutocomplete.js b/src/components/input/autocomplete/useAutocomplete.js index 9cb65b3d..2a4cecd3 100644 --- a/src/components/input/autocomplete/useAutocomplete.js +++ b/src/components/input/autocomplete/useAutocomplete.js @@ -1,16 +1,19 @@ +import { useCallback } from "react" import { useState, useEffect } from "react" const useAutocomplete = ({ value, autocompleteProps = {} } = {}) => { const [autocompleteOpen, setAutocompleteOpen] = useState() const { suggestions = [] } = autocompleteProps || {} + const onItemClick = useCallback(e => console.log(e), []) + useEffect(() => { if (suggestions.length) { setAutocompleteOpen(!!value.length) } }, [suggestions, value, setAutocompleteOpen]) - return { autocompleteOpen, suggestions } + return { autocompleteOpen, suggestions, onItemClick } } export default useAutocomplete diff --git a/src/components/input/input.js b/src/components/input/input.js index 69f5d63a..f05b1b7e 100644 --- a/src/components/input/input.js +++ b/src/components/input/input.js @@ -1,4 +1,4 @@ -import React, { useRef, useMemo } from "react" +import React, { useMemo } from "react" import Flex from "@/components/templates/flex" import { TextMicro } from "@/components/typography" import { Input, LabelText } from "./styled" @@ -36,8 +36,6 @@ export const TextInput = ({ autocompleteProps, ...props }) => { - const inputContainerRef = useRef() - const autocompleteInputProps = useMemo( () => autocompleteProps @@ -50,9 +48,16 @@ export const TextInput = ({ ) return ( - + {typeof label === "string" ? {label} : label} - + {iconLeft && ( {iconLeft} @@ -87,11 +92,7 @@ export const TextInput = ({ {typeof hint === "string" ? {hint} : !!hint && hint} {!hideErrorMessage ? : null} - + ) } From d1573087419f0f50b76b6dcd4c902c81273d1038 Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Fri, 26 Jul 2024 23:45:42 +0300 Subject: [PATCH 4/8] Add dropdown key navigation --- src/components/drops/menu/dropdown.js | 44 +++++++++++++++++++ src/components/drops/menu/dropdownItem.js | 6 +++ src/components/input/autocomplete/index.js | 36 ++++++++++++--- src/components/input/autocomplete/styled.js | 1 + .../input/autocomplete/useAutocomplete.js | 16 +++++-- src/components/input/input.js | 34 ++++++++++++-- src/components/input/input.stories.js | 18 +++----- 7 files changed, 131 insertions(+), 24 deletions(-) diff --git a/src/components/drops/menu/dropdown.js b/src/components/drops/menu/dropdown.js index 1886c7ee..11e890c1 100644 --- a/src/components/drops/menu/dropdown.js +++ b/src/components/drops/menu/dropdown.js @@ -5,6 +5,7 @@ import Flex from "@/components/templates/flex" import Search from "@/components/search" import Box from "@/components/templates/box" import { mergeRefs } from "@/utils" +import { useCallback } from "react" const Container = styled(Flex)` ${({ hideShadow }) => @@ -15,6 +16,19 @@ const Container = styled(Flex)` const defaultEstimateSize = () => 28 +const indexCalculatorByKey = { + ArrowDown: (index, length) => Math.min(index + 1, length - 1), + ArrowUp: index => Math.max(index - 1, 0), + Home: () => 0, + End: (_, length) => length - 1, + default: index => index, +} + +const getNextIndex = (currentIndex, key, itemsLength) => { + const calculator = indexCalculatorByKey[key] || indexCalculatorByKey.default + return calculator(currentIndex, itemsLength) +} + const Dropdown = forwardRef( ( { @@ -32,6 +46,9 @@ const Dropdown = forwardRef( gap = 0, estimateSize = defaultEstimateSize, close, + enableKeyNavigation, + activeIndex, + setActiveIndex, ...rest }, forwardedRef @@ -61,6 +78,31 @@ const Dropdown = forwardRef( estimateSize, }) + const handleKeyDown = useCallback( + event => { + if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.code)) { + setActiveIndex(prevIndex => { + const nextIndex = getNextIndex(prevIndex, event.code, items.length) + rowVirtualizer.scrollToIndex(nextIndex) + return nextIndex + }) + } + }, + [rowVirtualizer, items, setActiveIndex] + ) + + const virtualContainerProps = useMemo(() => { + if (enableKeyNavigation) + return { + tabIndex: 0, + role: "listbox", + "aria-activedescendant": `item-${activeIndex}`, + onKeyDown: handleKeyDown, + } + + return {} + }, [enableKeyNavigation, activeIndex, handleKeyDown]) + return (
))} diff --git a/src/components/drops/menu/dropdownItem.js b/src/components/drops/menu/dropdownItem.js index 4aae3bb8..8ba2b1ef 100644 --- a/src/components/drops/menu/dropdownItem.js +++ b/src/components/drops/menu/dropdownItem.js @@ -13,6 +13,8 @@ export const ItemContainer = styled(Flex).attrs(props => ({ cursor: ${({ cursor }) => cursor ?? "pointer"}; opacity: ${({ disabled, selected }) => (selected ? 0.9 : disabled ? 0.4 : 1)}; pointer-events: ${({ disabled }) => (disabled ? "none" : "auto")}; + background-color: ${props => + props.activeIndex == props.index ? getColor("borderSecondary")(props) : "none"}; &:hover { background-color: ${props => getColor("borderSecondary")(props)}; @@ -43,6 +45,7 @@ const DropdownItem = ({ onItemClick, index, style, + enableKeyNavigation, ...rest }) => { const selected = selectedValue === value @@ -54,11 +57,14 @@ const DropdownItem = ({ return ( { - const { autocompleteOpen, suggestions, onItemClick } = useAutocomplete({ +const Autocomplete = forwardRef(({ value, autocompleteProps, onInputChange, onEsc }, ref) => { + const [activeIndex, setActiveIndex] = useState(0) + const { autocompleteOpen, close, suggestions, onItemClick } = useAutocomplete({ value, + onInputChange, autocompleteProps, }) + const onKeyDown = useCallback( + e => { + if (e.code == "Escape") { + onEsc() + close() + } else if (e.code == "Enter") { + onItemClick(suggestions[activeIndex]?.value) + onEsc() + close() + } + }, + [activeIndex, suggestions, onItemClick, onEsc, close] + ) + return ( autocompleteOpen && ( - + ) ) -} +}) export default Autocomplete diff --git a/src/components/input/autocomplete/styled.js b/src/components/input/autocomplete/styled.js index 6f621d1d..5b2096b2 100644 --- a/src/components/input/autocomplete/styled.js +++ b/src/components/input/autocomplete/styled.js @@ -3,6 +3,7 @@ import Flex from "@/components/templates/flex" export const StyledOptionsContainer = styled(Flex)` width: 300px; + max-height: 300px; position: absolute; left: 0; top: 36px; diff --git a/src/components/input/autocomplete/useAutocomplete.js b/src/components/input/autocomplete/useAutocomplete.js index 2a4cecd3..891325ee 100644 --- a/src/components/input/autocomplete/useAutocomplete.js +++ b/src/components/input/autocomplete/useAutocomplete.js @@ -1,11 +1,21 @@ import { useCallback } from "react" import { useState, useEffect } from "react" -const useAutocomplete = ({ value, autocompleteProps = {} } = {}) => { +const useAutocomplete = ({ value, onInputChange, autocompleteProps = {} }) => { const [autocompleteOpen, setAutocompleteOpen] = useState() const { suggestions = [] } = autocompleteProps || {} - const onItemClick = useCallback(e => console.log(e), []) + const close = useCallback(() => setAutocompleteOpen(false), [setAutocompleteOpen]) + + const onItemClick = useCallback( + val => { + if (typeof onInputChange == "function") { + onInputChange({ target: { value: val } }) + close() + } + }, + [close, onInputChange] + ) useEffect(() => { if (suggestions.length) { @@ -13,7 +23,7 @@ const useAutocomplete = ({ value, autocompleteProps = {} } = {}) => { } }, [suggestions, value, setAutocompleteOpen]) - return { autocompleteOpen, suggestions, onItemClick } + return { autocompleteOpen, close, suggestions, onItemClick } } export default useAutocomplete diff --git a/src/components/input/input.js b/src/components/input/input.js index f05b1b7e..9b979236 100644 --- a/src/components/input/input.js +++ b/src/components/input/input.js @@ -1,8 +1,9 @@ -import React, { useMemo } from "react" +import React, { useMemo, useRef, useCallback } from "react" import Flex from "@/components/templates/flex" import { TextMicro } from "@/components/typography" import { Input, LabelText } from "./styled" import Autocomplete from "./autocomplete" +import { mergeRefs } from "@/utils" const Error = ({ error }) => { const errorMessage = error === true ? "invalid" : error @@ -36,15 +37,34 @@ export const TextInput = ({ autocompleteProps, ...props }) => { + const ref = useRef() + const autocompleteMenuRef = useRef() + + const onKeyDown = useCallback( + e => { + if (autocompleteMenuRef.current && ["ArrowDown", "ArrowUp"].includes(e.key)) { + autocompleteMenuRef.current.focus() + } + }, + [autocompleteMenuRef?.current] + ) + + const onAutocompleteEscape = useCallback(() => { + if (ref?.current) { + ref.current.focus() + } + }, [ref]) + const autocompleteInputProps = useMemo( () => autocompleteProps ? { "aria-autocomplete": "list", "aria-controls": "autocomplete-list", + onKeyDown, } : {}, - [] + [autocompleteProps, onKeyDown] ) return ( @@ -76,7 +96,7 @@ export const TextInput = ({ type="text" value={value} size={size} - ref={inputRef} + ref={mergeRefs(inputRef, ref)} error={error} hasValue={!!value} {...autocompleteInputProps} @@ -92,7 +112,13 @@ export const TextInput = ({
{typeof hint === "string" ? {hint} : !!hint && hint} {!hideErrorMessage ? : null} - +
) } diff --git a/src/components/input/input.stories.js b/src/components/input/input.stories.js index 16383c45..dc6790a5 100644 --- a/src/components/input/input.stories.js +++ b/src/components/input/input.stories.js @@ -16,20 +16,14 @@ export const Basic = args => export const WithAutocomplete = () => { const [value, setValue] = useState("") const autocompleteProps = { - suggestions: [ - { value: "one", label: "one" }, - { value: "two", label: "two" }, - { value: "three", label: "three" }, - ], + suggestions: Array.from(Array(10000).keys()).map(i => ({ value: i, label: `Label ${i}` })), } - return ( - setValue(e.target.value)} - autocompleteProps={autocompleteProps} - /> - ) + const onChange = e => { + setValue(e.target.value) + } + + return } export default { From f6a3b039f160a5dff27155f24448b88c518847ff Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Mon, 29 Jul 2024 15:43:00 +0300 Subject: [PATCH 5/8] Use filtered suggestions --- src/components/input/autocomplete/index.js | 8 ++++---- src/components/input/autocomplete/useAutocomplete.js | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/input/autocomplete/index.js b/src/components/input/autocomplete/index.js index aec1394b..98aff005 100644 --- a/src/components/input/autocomplete/index.js +++ b/src/components/input/autocomplete/index.js @@ -6,7 +6,7 @@ import useAutocomplete from "./useAutocomplete" const Autocomplete = forwardRef(({ value, autocompleteProps, onInputChange, onEsc }, ref) => { const [activeIndex, setActiveIndex] = useState(0) - const { autocompleteOpen, close, suggestions, onItemClick } = useAutocomplete({ + const { autocompleteOpen, close, filteredSuggestions, onItemClick } = useAutocomplete({ value, onInputChange, autocompleteProps, @@ -18,12 +18,12 @@ const Autocomplete = forwardRef(({ value, autocompleteProps, onInputChange, onEs onEsc() close() } else if (e.code == "Enter") { - onItemClick(suggestions[activeIndex]?.value) + onItemClick(filteredSuggestions[activeIndex]?.value) onEsc() close() } }, - [activeIndex, suggestions, onItemClick, onEsc, close] + [activeIndex, filteredSuggestions, onItemClick, onEsc, close] ) return ( @@ -31,7 +31,7 @@ const Autocomplete = forwardRef(({ value, autocompleteProps, onInputChange, onEs { const [autocompleteOpen, setAutocompleteOpen] = useState() const { suggestions = [] } = autocompleteProps || {} + const [filteredSuggestions, setFilteredSuggestions] = useState(suggestions) const close = useCallback(() => setAutocompleteOpen(false), [setAutocompleteOpen]) @@ -18,12 +19,14 @@ const useAutocomplete = ({ value, onInputChange, autocompleteProps = {} }) => { ) useEffect(() => { - if (suggestions.length) { - setAutocompleteOpen(!!value.length) + if (suggestions.length && !!value) { + const filtered = suggestions.filter(({ label }) => label.includes(value)) + setFilteredSuggestions(filtered) + setAutocompleteOpen(!!filtered.length) } - }, [suggestions, value, setAutocompleteOpen]) + }, [value, suggestions, setAutocompleteOpen, setFilteredSuggestions]) - return { autocompleteOpen, close, suggestions, onItemClick } + return { autocompleteOpen, close, filteredSuggestions, onItemClick } } export default useAutocomplete From 293c1138729b22b7cccad519774a776b83a10b3c Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Tue, 30 Jul 2024 10:25:46 +0300 Subject: [PATCH 6/8] wip --- src/components/input/autocomplete/index.js | 4 +++- .../input/autocomplete/useAutocomplete.js | 23 +++++++++++++------ src/components/input/input.stories.js | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/components/input/autocomplete/index.js b/src/components/input/autocomplete/index.js index 98aff005..d0dc7494 100644 --- a/src/components/input/autocomplete/index.js +++ b/src/components/input/autocomplete/index.js @@ -2,6 +2,7 @@ import React, { forwardRef, useCallback, useState } from "react" import { StyledOptionsContainer } from "./styled" import Dropdown from "@/components/drops/menu/dropdown" import DropdownItem from "@/components/drops/menu/dropdownItem" +import useOutsideClick from "@/hooks/useOutsideClick" import useAutocomplete from "./useAutocomplete" const Autocomplete = forwardRef(({ value, autocompleteProps, onInputChange, onEsc }, ref) => { @@ -20,12 +21,13 @@ const Autocomplete = forwardRef(({ value, autocompleteProps, onInputChange, onEs } else if (e.code == "Enter") { onItemClick(filteredSuggestions[activeIndex]?.value) onEsc() - close() } }, [activeIndex, filteredSuggestions, onItemClick, onEsc, close] ) + useOutsideClick(ref, close, ref?.current) + return ( autocompleteOpen && ( diff --git a/src/components/input/autocomplete/useAutocomplete.js b/src/components/input/autocomplete/useAutocomplete.js index 7d882c0c..a328ab2c 100644 --- a/src/components/input/autocomplete/useAutocomplete.js +++ b/src/components/input/autocomplete/useAutocomplete.js @@ -1,10 +1,17 @@ -import { useCallback } from "react" -import { useState, useEffect } from "react" +import { useState, useEffect, useMemo, useCallback } from "react" const useAutocomplete = ({ value, onInputChange, autocompleteProps = {} }) => { const [autocompleteOpen, setAutocompleteOpen] = useState() const { suggestions = [] } = autocompleteProps || {} - const [filteredSuggestions, setFilteredSuggestions] = useState(suggestions) + const items = useMemo( + () => + suggestions.map(suggestion => ({ + value: suggestion, + label: suggestion, + })), + [suggestions] + ) + const [filteredSuggestions, setFilteredSuggestions] = useState(items) const close = useCallback(() => setAutocompleteOpen(false), [setAutocompleteOpen]) @@ -12,19 +19,21 @@ const useAutocomplete = ({ value, onInputChange, autocompleteProps = {} }) => { val => { if (typeof onInputChange == "function") { onInputChange({ target: { value: val } }) - close() + setTimeout(() => close(), 100) } }, [close, onInputChange] ) useEffect(() => { - if (suggestions.length && !!value) { - const filtered = suggestions.filter(({ label }) => label.includes(value)) + if (items.length && !!value) { + const filtered = items.filter(({ label }) => + label.toLowerCase().includes(value.toLowerCase()) + ) setFilteredSuggestions(filtered) setAutocompleteOpen(!!filtered.length) } - }, [value, suggestions, setAutocompleteOpen, setFilteredSuggestions]) + }, [value, items, setAutocompleteOpen, setFilteredSuggestions]) return { autocompleteOpen, close, filteredSuggestions, onItemClick } } diff --git a/src/components/input/input.stories.js b/src/components/input/input.stories.js index dc6790a5..846356d0 100644 --- a/src/components/input/input.stories.js +++ b/src/components/input/input.stories.js @@ -16,7 +16,7 @@ export const Basic = args => export const WithAutocomplete = () => { const [value, setValue] = useState("") const autocompleteProps = { - suggestions: Array.from(Array(10000).keys()).map(i => ({ value: i, label: `Label ${i}` })), + suggestions: Array.from(Array(10000).keys()).map(i => `Label ${i}`), } const onChange = e => { From 66829734a67f8073bab4c9d79a490a45b94cafaf Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Tue, 30 Jul 2024 12:31:41 +0300 Subject: [PATCH 7/8] Fix autocomplete useEffect --- src/components/input/autocomplete/useAutocomplete.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/input/autocomplete/useAutocomplete.js b/src/components/input/autocomplete/useAutocomplete.js index a328ab2c..57c33906 100644 --- a/src/components/input/autocomplete/useAutocomplete.js +++ b/src/components/input/autocomplete/useAutocomplete.js @@ -26,14 +26,16 @@ const useAutocomplete = ({ value, onInputChange, autocompleteProps = {} }) => { ) useEffect(() => { - if (items.length && !!value) { + if (!value) { + close() + } else if (items.length) { const filtered = items.filter(({ label }) => label.toLowerCase().includes(value.toLowerCase()) ) setFilteredSuggestions(filtered) setAutocompleteOpen(!!filtered.length) } - }, [value, items, setAutocompleteOpen, setFilteredSuggestions]) + }, [value, items, setAutocompleteOpen, setFilteredSuggestions, close]) return { autocompleteOpen, close, filteredSuggestions, onItemClick } } From 9c9a76da6cec435932aae4ebb6537317e9a622f4 Mon Sep 17 00:00:00 2001 From: John Kapantzakis Date: Tue, 30 Jul 2024 12:56:59 +0300 Subject: [PATCH 8/8] Accept loadable suggestions --- src/components/input/autocomplete/useAutocomplete.js | 11 +++++++++-- src/components/input/input.stories.js | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/input/autocomplete/useAutocomplete.js b/src/components/input/autocomplete/useAutocomplete.js index 57c33906..6e61bad7 100644 --- a/src/components/input/autocomplete/useAutocomplete.js +++ b/src/components/input/autocomplete/useAutocomplete.js @@ -1,11 +1,18 @@ import { useState, useEffect, useMemo, useCallback } from "react" +const defaultSuggestions = { + loading: false, + loaded: true, + value: [], + error: null, +} + const useAutocomplete = ({ value, onInputChange, autocompleteProps = {} }) => { const [autocompleteOpen, setAutocompleteOpen] = useState() - const { suggestions = [] } = autocompleteProps || {} + const { suggestions = defaultSuggestions } = autocompleteProps || {} const items = useMemo( () => - suggestions.map(suggestion => ({ + suggestions.value.map(suggestion => ({ value: suggestion, label: suggestion, })), diff --git a/src/components/input/input.stories.js b/src/components/input/input.stories.js index 846356d0..dd969b9a 100644 --- a/src/components/input/input.stories.js +++ b/src/components/input/input.stories.js @@ -16,7 +16,11 @@ export const Basic = args => export const WithAutocomplete = () => { const [value, setValue] = useState("") const autocompleteProps = { - suggestions: Array.from(Array(10000).keys()).map(i => `Label ${i}`), + suggestions: { + loading: false, + value: Array.from(Array(10000).keys()).map(i => `Label ${i}`), + error: null, + }, } const onChange = e => {