From 46f561d41f4d2e54649583da8500880e44f9c2a7 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 5 Feb 2024 12:00:47 -0600 Subject: [PATCH 1/8] 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}
)} From e3303301478f8b6fc869fbfc6c9cf1f17b166ee2 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 5 Feb 2024 12:51:53 -0600 Subject: [PATCH 2/8] Fix reverse-tab auto-select behavior in tag input --- src/components/TagInput.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index d09115a3..5f348e12 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -218,6 +218,7 @@ export default function TagInput({ autoComplete="off" autoCapitalize="off" readOnly={readOnly} + onFocus={() => setSelectedOptionIndex(undefined)} />
From e5efc3614d7cc338bc4a5669ba107fd4da08b75b Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 5 Feb 2024 19:37:07 -0600 Subject: [PATCH 3/8] Scope tag input query selector --- src/components/TagInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index 5f348e12..c7f7b4b3 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -89,7 +89,7 @@ export default function TagInput({ // Focus option in the DOM when selected index changes useEffect(() => { if (selectedOptionIndex !== undefined) { - const options = optionsRef.current?.querySelectorAll('div'); + const options = optionsRef.current?.querySelectorAll(':scope > div'); const option = options?.[selectedOptionIndex] as HTMLElement | undefined; option?.focus(); } From 1da28079e61d5bb14def473526ba1bd85d782de7 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 6 Feb 2024 17:46:43 -0600 Subject: [PATCH 4/8] Display tag counts in photo form --- src/app/admin/photos/[photoId]/edit/page.tsx | 2 +- src/app/admin/uploads/[uploadPath]/page.tsx | 2 +- src/components/FieldSetWithStatus.tsx | 10 +- src/components/TagInput.tsx | 70 +++++++++---- src/photo/PhotoEditPageClient.tsx | 3 +- src/photo/form/PhotoForm.tsx | 104 ++++++++++--------- src/photo/form/index.ts | 34 +++--- src/tag/index.ts | 3 + 8 files changed, 136 insertions(+), 92 deletions(-) diff --git a/src/app/admin/photos/[photoId]/edit/page.tsx b/src/app/admin/photos/[photoId]/edit/page.tsx index 27762339..7e78b30b 100644 --- a/src/app/admin/photos/[photoId]/edit/page.tsx +++ b/src/app/admin/photos/[photoId]/edit/page.tsx @@ -12,7 +12,7 @@ export default async function PhotoEditPage({ if (!photo) { redirect(PATH_ADMIN); } - const uniqueTags = (await getUniqueTagsCached()).map(tag => tag.tag); + const uniqueTags = await getUniqueTagsCached(); return ( diff --git a/src/app/admin/uploads/[uploadPath]/page.tsx b/src/app/admin/uploads/[uploadPath]/page.tsx index f2354591..17b43b52 100644 --- a/src/app/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/admin/uploads/[uploadPath]/page.tsx @@ -15,7 +15,7 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { photoFormExif, } = await extractExifDataFromBlobPath(uploadPath); - const uniqueTags = (await getUniqueTagsCached()).map(tag => tag.tag); + const uniqueTags = await getUniqueTagsCached(); if (!photoFormExif) { redirect(PATH_ADMIN); } diff --git a/src/components/FieldSetWithStatus.tsx b/src/components/FieldSetWithStatus.tsx index a680704c..3f2bd4ab 100644 --- a/src/components/FieldSetWithStatus.tsx +++ b/src/components/FieldSetWithStatus.tsx @@ -4,7 +4,7 @@ import { LegacyRef } from 'react'; import { useFormStatus } from 'react-dom'; import Spinner from './Spinner'; import { clsx } from 'clsx/lite'; -import { FieldSetType } from '@/photo/form'; +import { FieldSetType, AnnotatedTag } from '@/photo/form'; import TagInput from './TagInput'; export default function FieldSetWithStatus({ @@ -16,7 +16,7 @@ export default function FieldSetWithStatus({ onChange, selectOptions, selectOptionsDefaultLabel, - commaSeparatedOptions, + tagOptions, placeholder, loading, required, @@ -33,7 +33,7 @@ export default function FieldSetWithStatus({ onChange?: (value: string) => void selectOptions?: { value: string, label: string }[] selectOptionsDefaultLabel?: string - commaSeparatedOptions?: string[] + tagOptions?: AnnotatedTag [] placeholder?: string loading?: boolean required?: boolean @@ -91,11 +91,11 @@ export default function FieldSetWithStatus({ {optionLabel} )} - : commaSeparatedOptions + : tagOptions ? void className?: string readOnly?: boolean @@ -28,6 +29,10 @@ export default function TagInput({ const [inputText, setInputText] = useState(''); const [selectedOptionIndex, setSelectedOptionIndex] = useState(); + const optionValues = useMemo(() => + options.map(({ value }) => value) + , [options]); + const selectedOptions = useMemo(() => convertStringToArray(value) ?? [] , [value]); @@ -35,18 +40,21 @@ export default function TagInput({ const inputTextFormatted = parameterize(inputText); const isInputTextUnique = inputTextFormatted && - !options.includes(inputTextFormatted) && + !optionValues.includes(inputTextFormatted) && !selectedOptions.includes(inputTextFormatted); - const optionsFiltered = (isInputTextUnique - ? [`${CREATE_LABEL} "${inputTextFormatted}"`] - : []).concat(options - .filter(option => - !selectedOptions.includes(option) && - ( - !inputTextFormatted || - option.includes(inputTextFormatted) - ))); + const optionsFiltered = useMemo(() => + (isInputTextUnique + ? [{ value: `${CREATE_LABEL} "${inputTextFormatted}"` }] + : [] + ).concat(options + .filter(({ value }) => + !selectedOptions.includes(value) && + ( + !inputTextFormatted || + value.includes(inputTextFormatted) + ))) + , [inputTextFormatted, isInputTextUnique, options, selectedOptions]); const hideMenu = useCallback((shouldBlurInput?: boolean) => { setShouldShowMenu(false); @@ -117,7 +125,7 @@ export default function TagInput({ e.stopImmediatePropagation(); e.preventDefault(); } - addOption(optionsFiltered[selectedOptionIndex ?? 0]); + addOption(optionsFiltered[selectedOptionIndex ?? 0].value); setInputText(''); break; case ',': @@ -137,7 +145,12 @@ export default function TagInput({ break; case 'ArrowUp': setSelectedOptionIndex(i => { - if (i === undefined || i === 0) { + if ( + document.activeElement === inputRef.current && + optionsFiltered.length > 0 + ) { + return optionsFiltered.length - 1; + } else if (i === undefined || i === 0) { inputRef.current?.focus(); return undefined; } else { @@ -197,8 +210,8 @@ export default function TagInput({ 'cursor-pointer select-none', 'whitespace-nowrap', 'px-1.5 py-0.5', - 'bg-gray-100 dark:bg-gray-800', - 'active:bg-gray-50 dark:active:bg-gray-900', + 'bg-gray-200/60 dark:bg-gray-800', + 'active:bg-gray-200 dark:active:bg-gray-900', 'rounded-sm', )} onClick={() => removeOption(option)} @@ -233,27 +246,40 @@ export default function TagInput({ 'text-xl shadow-lg dark:shadow-xl', )} > - {optionsFiltered.map((option, index) => + {optionsFiltered.map(({ value, annotation }, index) =>
{ - addOption(option); + addOption(value); setInputText(''); }} onFocus={() => setSelectedOptionIndex(index)} > - {option} + + {value} + + {annotation && + + {annotation} + }
)} diff --git a/src/photo/PhotoEditPageClient.tsx b/src/photo/PhotoEditPageClient.tsx index 0bf00b99..0969d8c0 100644 --- a/src/photo/PhotoEditPageClient.tsx +++ b/src/photo/PhotoEditPageClient.tsx @@ -10,13 +10,14 @@ import { useFormState } from 'react-dom'; import { areSimpleObjectsEqual } from '@/utility/object'; import IconGrSync from '@/site/IconGrSync'; import { getExifDataAction } from './actions'; +import { Tags } from '@/tag'; export default function PhotoEditPageClient({ photo, uniqueTags, }: { photo: Photo - uniqueTags?: string[] + uniqueTags?: Tags }) { const seedExifData = { url: photo.url }; diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 61b78a2d..5d083b72 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -5,7 +5,7 @@ import { FORM_METADATA_ENTRIES, PhotoFormData, convertFormKeysToLabels, - getInitialErrors, + getFormErrors, isFormValid, } from '.'; import FieldSetWithStatus from '@/components/FieldSetWithStatus'; @@ -23,7 +23,7 @@ import { toastSuccess, toastWarning } from '@/toast'; import { getDimensionsFromSize } from '@/utility/size'; import ImageBlurFallback from '@/components/ImageBlurFallback'; import { BLUR_ENABLED } from '@/site/config'; -import { sortTagsWithoutFavs } from '@/tag'; +import { Tags, sortTagsObjectWithoutFavs } from '@/tag'; const THUMBNAIL_SIZE = 300; @@ -37,13 +37,13 @@ export default function PhotoForm({ initialPhotoForm: Partial updatedExifData?: Partial type?: 'create' | 'edit' - uniqueTags?: string[] + uniqueTags?: Tags debugBlur?: boolean }) { const [formData, setFormData] = useState>(initialPhotoForm); const [formErrors, setFormErrors] = - useState(getInitialErrors(initialPhotoForm)); + useState(getFormErrors(initialPhotoForm)); // Update form when EXIF data // is refreshed by parent @@ -146,51 +146,57 @@ export default function PhotoForm({ onSubmit={() => blur()} className="space-y-6" > - {FORM_METADATA_ENTRIES.map(([key, { - label, - note, - required, - options, - optionsDefaultLabel, - readOnly, - validate, - capitalize, - hideIfEmpty, - hideBasedOnCamera, - loadingMessage, - type, - }]) => - ( - (!hideIfEmpty || formData[key]) && - !hideBasedOnCamera?.(formData.make) - ) && - { - setFormData({ ...formData, [key]: value }); - if (validate) { - setFormErrors({ ...formErrors, [key]: validate(value) }); - } - }} - selectOptions={options} - selectOptionsDefaultLabel={optionsDefaultLabel} - commaSeparatedOptions={key === 'tags' - ? sortTagsWithoutFavs(uniqueTags ?? []) - : undefined} - required={required} - readOnly={readOnly} - capitalize={capitalize} - placeholder={loadingMessage && !formData[key] - ? loadingMessage - : undefined} - loading={loadingMessage && !formData[key] ? true : false} - type={type} - />)} + {FORM_METADATA_ENTRIES( + sortTagsObjectWithoutFavs(uniqueTags ?? []) + .map(({ tag, count }) => ({ + value: tag, + annotation: `× ${count}`, + })) + ) + .map(([key, { + label, + note, + required, + selectOptions, + selectOptionsDefaultLabel, + tagOptions, + readOnly, + validate, + capitalize, + hideIfEmpty, + hideBasedOnCamera, + loadingMessage, + type, + }]) => + ( + (!hideIfEmpty || formData[key]) && + !hideBasedOnCamera?.(formData.make) + ) && + { + setFormData({ ...formData, [key]: value }); + if (validate) { + setFormErrors({ ...formErrors, [key]: validate(value) }); + } + }} + selectOptions={selectOptions} + selectOptionsDefaultLabel={selectOptionsDefaultLabel} + tagOptions={tagOptions} + required={required} + readOnly={readOnly} + capitalize={capitalize} + placeholder={loadingMessage && !formData[key] + ? loadingMessage + : undefined} + loading={loadingMessage && !formData[key] ? true : false} + type={type} + />)}
boolean loadingMessage?: string type?: FieldSetType - options?: { value: string, label: string }[] - optionsDefaultLabel?: string + selectOptions?: { value: string, label: string }[] + selectOptionsDefaultLabel?: string + tagOptions?: AnnotatedTag[] }; -const FORM_METADATA: Record = { +const FORM_METADATA = ( + tagOptions?: AnnotatedTag[] +): Record => ({ title: { label: 'title', capitalize: true }, tags: { label: 'tags', + tagOptions, validate: tags => doesTagsStringIncludeFavs(tags) ? `'${TAG_FAVS}' is a reserved tag` : undefined, @@ -66,8 +72,8 @@ const FORM_METADATA: Record = { model: { label: 'camera model' }, filmSimulation: { label: 'fujifilm simulation', - options: FILM_SIMULATION_FORM_INPUT_OPTIONS, - optionsDefaultLabel: 'Unknown', + selectOptions: FILM_SIMULATION_FORM_INPUT_OPTIONS, + selectOptionsDefaultLabel: 'Unknown', hideBasedOnCamera: make => make !== MAKE_FUJIFILM, }, focalLength: { label: 'focal length' }, @@ -84,26 +90,28 @@ const FORM_METADATA: Record = { priorityOrder: { label: 'priority order' }, favorite: { label: 'favorite', type: 'checkbox', virtual: true }, hidden: { label: 'hidden', type: 'checkbox' }, -}; +}); -export const FORM_METADATA_ENTRIES = - (Object.entries(FORM_METADATA) as [keyof PhotoFormData, FormMeta][]) +export const FORM_METADATA_ENTRIES = ( + ...args: Parameters +) => + (Object.entries(FORM_METADATA(...args)) as [keyof PhotoFormData, FormMeta][]) .filter(([_, meta]) => !meta.hide); export const convertFormKeysToLabels = (keys: (keyof PhotoFormData)[]) => - keys.map(key => FORM_METADATA[key].label.toUpperCase()); + keys.map(key => FORM_METADATA()[key].label.toUpperCase()); -export const getInitialErrors = ( +export const getFormErrors = ( formData: Partial ): Partial> => Object.keys(formData).reduce((acc, key) => ({ ...acc, - [key]: FORM_METADATA_ENTRIES.find(([k]) => k === key)?.[1] + [key]: FORM_METADATA_ENTRIES().find(([k]) => k === key)?.[1] .validate?.(formData[key as keyof PhotoFormData]), }), {}); export const isFormValid = (formData: Partial) => - FORM_METADATA_ENTRIES.every( + FORM_METADATA_ENTRIES().every( ([key, { required, validate }]) => (!required || Boolean(formData[key])) && (validate?.(formData[key]) === undefined) @@ -191,7 +199,7 @@ export const convertFormDataToPhotoDbInsert = ( if ( key.startsWith('$ACTION_ID_') || (photoForm as any)[key] === '' || - FORM_METADATA[key as keyof PhotoFormData]?.virtual + FORM_METADATA()[key as keyof PhotoFormData]?.virtual ) { delete (photoForm as any)[key]; } diff --git a/src/tag/index.ts b/src/tag/index.ts index 03cf7699..58384d0a 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -46,6 +46,9 @@ export const sortTagsObject = ( export const sortTagsWithoutFavs = (tags: string[]) => sortTags(tags, TAG_FAVS); +export const sortTagsObjectWithoutFavs = (tags: Tags) => + sortTagsObject(tags, TAG_FAVS); + export const descriptionForTaggedPhotos = ( photos: Photo[], dateBased?: boolean, From bdc9dcb1205d8df4dbba0973bd0c24412be9df7f Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 6 Feb 2024 21:38:06 -0600 Subject: [PATCH 5/8] Outline tag control when children have focus --- src/components/TagInput.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index 5eacb072..af2e82e5 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -186,7 +186,7 @@ export default function TagInput({ return (
setShouldShowMenu(true)} onBlur={e => { if (!e.currentTarget.contains(e.relatedTarget)) { @@ -197,6 +197,7 @@ export default function TagInput({
Date: Tue, 6 Feb 2024 22:03:08 -0600 Subject: [PATCH 6/8] Fix enter key behavior --- src/components/TagInput.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index af2e82e5..fa73ae15 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -121,12 +121,12 @@ export default function TagInput({ case 'Enter': // Only trap focus if there are options to select // otherwise allow form to submit - if (optionsFiltered.length > 0) { + if (shouldShowMenu && optionsFiltered.length > 0) { e.stopImmediatePropagation(); e.preventDefault(); + addOption(optionsFiltered[selectedOptionIndex ?? 0].value); + setInputText(''); } - addOption(optionsFiltered[selectedOptionIndex ?? 0].value); - setInputText(''); break; case ',': addOption(inputText); @@ -181,6 +181,7 @@ export default function TagInput({ selectedOptionIndex, optionsFiltered, addOption, + shouldShowMenu, ]); return ( @@ -272,13 +273,7 @@ export default function TagInput({ {value} {annotation && - + {annotation} }
)} From 8237693fdd117dd3a6c149eabb420196a7d6de2e Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 6 Feb 2024 22:38:00 -0600 Subject: [PATCH 7/8] Improve tag input support for screen readers --- src/components/TagInput.tsx | 131 ++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 50 deletions(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index fa73ae15..9a682a58 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -6,6 +6,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; const KEY_KEYDOWN = 'keydown'; const CREATE_LABEL = 'Create'; +const ARIA_ID_TAG_CONTROL = 'tag-control'; +const ARIA_ID_TAG_OPTIONS = 'tag-options'; + export default function TagInput({ name, value = '', @@ -195,19 +198,35 @@ export default function TagInput({ } }} > -
+
+ {selectedOptions.length === 0 + ? 'No tags selected' + : selectedOptions.join(', ') + + ` tag${selectedOptions.length !== 1 ? 's' : ''} selected`} +
+
{selectedOptions .filter(Boolean) .map(option => setSelectedOptionIndex(undefined)} + aria-autocomplete="list" + aria-expanded={shouldShowMenu} + aria-haspopup="true" + aria-controls={shouldShowMenu ? ARIA_ID_TAG_OPTIONS : undefined} + role="combobox" />
-
-
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', - 'text-xl shadow-lg dark:shadow-xl', - )} - > - {optionsFiltered.map(({ value, annotation }, index) => -
{ - addOption(value); - setInputText(''); - }} - onFocus={() => setSelectedOptionIndex(index)} - > - - {value} - - {annotation && - - {annotation} - } -
)} -
-
+ {shouldShowMenu && optionsFiltered.length > 0 && +
+
+ {optionsFiltered.map(({ value, annotation }, index) => +
{ + addOption(value); + setInputText(''); + }} + onFocus={() => setSelectedOptionIndex(index)} + > + + {value} + + {annotation && + + {annotation} + } +
)} +
+
}
); } From 618ba90508858925d1e47495d42d951f4b0e45bf Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 6 Feb 2024 22:44:50 -0600 Subject: [PATCH 8/8] Fix outline flashes in tag input --- src/components/TagInput.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index 9a682a58..a1e6ebf2 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -214,7 +214,8 @@ export default function TagInput({ className={clsx( className, 'w-full control !px-2 !py-2', - 'group-focus-within:outline outline-1 outline-blue-600', + 'outline-1 outline-blue-600', + 'group-focus-within:outline group-active:outline', 'inline-flex flex-wrap items-center gap-2', readOnly && 'cursor-not-allowed', readOnly && 'bg-gray-100 dark:bg-gray-900 dark:text-gray-400',