From 46f561d41f4d2e54649583da8500880e44f9c2a7 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 5 Feb 2024 12:00:47 -0600 Subject: [PATCH] Refine tag component behavior --- src/components/TagInput.tsx | 47 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index b4a69eaa..d09115a3 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -2,8 +2,8 @@ import { convertStringToArray, parameterize } from '@/utility/string'; import { clsx } from 'clsx/lite'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -const KEYDOWN_KEY = 'keydown'; -const CREATE_LABEL = 'Create '; +const KEY_KEYDOWN = 'keydown'; +const CREATE_LABEL = 'Create'; export default function TagInput({ name, @@ -24,7 +24,7 @@ export default function TagInput({ const inputRef = useRef(null); const optionsRef = useRef(null); - const [hasFocus, setHasFocus] = useState(false); + const [shouldShowMenu, setShouldShowMenu] = useState(false); const [inputText, setInputText] = useState(''); const [selectedOptionIndex, setSelectedOptionIndex] = useState(); @@ -39,7 +39,7 @@ export default function TagInput({ !selectedOptions.includes(inputTextFormatted); const optionsFiltered = (isInputTextUnique - ? [`${CREATE_LABEL}"${inputTextFormatted}"`] + ? [`${CREATE_LABEL} "${inputTextFormatted}"`] : []).concat(options .filter(option => !selectedOptions.includes(option) && @@ -48,12 +48,20 @@ export default function TagInput({ option.includes(inputTextFormatted) ))); + const hideMenu = useCallback((shouldBlurInput?: boolean) => { + setShouldShowMenu(false); + setSelectedOptionIndex(undefined); + if (shouldBlurInput) { + inputRef.current?.blur(); + } + }, []); + const addOption = useCallback((option?: string) => { if (option && !selectedOptions.includes(option)) { onChange?.([ ...selectedOptions, option.startsWith(CREATE_LABEL) - ? option.slice(CREATE_LABEL.length + 1, -1) + ? option.slice(CREATE_LABEL.length, -1) : option, ] .filter(Boolean) @@ -71,10 +79,12 @@ export default function TagInput({ inputRef.current?.focus(); }, [onChange, selectedOptions]); - // Reset selected option index when focus is lost + // Show options when input text changes useEffect(() => { - if (!hasFocus) { setSelectedOptionIndex(undefined); } - }, [hasFocus]); + if (inputText) { + setShouldShowMenu(true); + } + }, [inputText]); // Focus option in the DOM when selected index changes useEffect(() => { @@ -88,6 +98,7 @@ export default function TagInput({ // Setup keyboard listener useEffect(() => { const ref = containerRef.current; + const listener = (e: KeyboardEvent) => { // Keys which always trap focus switch (e.key) { @@ -137,20 +148,22 @@ export default function TagInput({ case 'Backspace': if (inputText === '' && selectedOptions.length > 0) { removeOption(selectedOptions[selectedOptions.length - 1]); + hideMenu(); } break; case 'Escape': - inputRef.current?.blur(); - setHasFocus(false); + hideMenu(true); break; } }; - ref?.addEventListener(KEYDOWN_KEY, listener); - return () => ref?.removeEventListener(KEYDOWN_KEY, listener); + + ref?.addEventListener(KEY_KEYDOWN, listener); + + return () => ref?.removeEventListener(KEY_KEYDOWN, listener); }, [ inputText, removeOption, - hasFocus, + hideMenu, selectedOptions, selectedOptionIndex, optionsFiltered, @@ -161,11 +174,10 @@ export default function TagInput({
setHasFocus(true)} + onFocus={() => setShouldShowMenu(true)} onBlur={e => { if (!e.currentTarget.contains(e.relatedTarget)) { - setHasFocus(false); - setSelectedOptionIndex(undefined); + hideMenu(); } }} > @@ -213,7 +225,7 @@ export default function TagInput({
0) && 'hidden', + !(shouldShowMenu && optionsFiltered.length > 0) && 'hidden', 'control absolute top-0 mt-3 w-full z-10 !px-1.5 !py-1.5', 'max-h-[8rem] overflow-y-auto', 'flex flex-col gap-y-1', @@ -238,6 +250,7 @@ export default function TagInput({ addOption(option); setInputText(''); }} + onFocus={() => setSelectedOptionIndex(index)} > {option}
)}