diff --git a/src/components/shared/wizard/RenderMultiField.tsx b/src/components/shared/wizard/RenderMultiField.tsx index 213b23a46a..037a61cbe6 100644 --- a/src/components/shared/wizard/RenderMultiField.tsx +++ b/src/components/shared/wizard/RenderMultiField.tsx @@ -7,8 +7,6 @@ import { MetadataField } from "../../../slices/eventSlice"; import ButtonLikeAnchor from "../ButtonLikeAnchor"; import { LuCheck, LuSquarePen, LuX } from "react-icons/lu"; -const childRef = React.createRef(); - /** * This component renders an editable field for multiple values depending on the type of the corresponding metadata */ @@ -25,6 +23,8 @@ const RenderMultiField = ({ form: FieldProps["form"] showCheck?: boolean, }) => { + // One ref per rendered field + const childRef = useRef(null); // Indicator if currently edit mode is activated const { editMode, setEditMode } = useClickOutsideField(childRef); // Temporary storage for value user currently types in @@ -45,6 +45,13 @@ const RenderMultiField = ({ submitValue(); } + + // Backspace on an empty input drops the last value. + if (event.key === "Backspace" && inputValue === "" && fieldValue.length > 0) { + event.preventDefault(); + + removeItem(fieldValue.length - 1); + } }; const submitValue = (alternativeInput?: string) => { @@ -93,6 +100,17 @@ const RenderMultiField = ({ form.setFieldValue(field.name, fieldValue); }; + // Always points at the latest submitValue, so late callers (e.g. unmount + // cleanup) don't commit against a stale field value. + const submitValueRef = useRef(submitValue); + submitValueRef.current = submitValue; + + // Commit the typed value and leave edit mode (keyboard exit). + const leaveEditMode = (typedValue: string) => { + submitValueRef.current(typedValue); + setEditMode(false); + }; + return ( // Render editable field for multiple values depending on type of metadata field // (types: see metadata.json retrieved from backend) @@ -100,6 +118,7 @@ const RenderMultiField = ({ <> {fieldInfo.type === "mixed_text" && ( submitValueRef.current(input)} + handleLeave={leaveEditMode} /> )} @@ -128,19 +149,23 @@ const RenderMultiField = ({ // Renders multi select const EditMultiSelect = ({ + containerRef, collection, handleKeyDown, handleChange, handleBlur, + handleLeave, inputValue, removeItem, field, fieldValue, }: { + containerRef: React.RefObject collection: { [key: string]: unknown }[] handleKeyDown: (event: React.KeyboardEvent) => void handleChange: (event: React.ChangeEvent) => void handleBlur: (refCurrent: string) => void + handleLeave: (typedValue: string) => void inputValue: HTMLInputElement["value"] removeItem: (key: number) => void field: FieldProps["field"] @@ -154,14 +179,39 @@ const EditMultiSelect = ({ React.useEffect(() => { textRef.current = inputValue; }, [inputValue]); + // Pending deferred blur handling, see onBlur below. + const blurTimeout = useRef>(undefined); React.useEffect(() => { - return () => handleBlur(textRef.current); + return () => { + clearTimeout(blurTimeout.current); + handleBlur(textRef.current); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> -
+
{ + // Tabbing out: commit the typed value and leave edit mode. Clicks + // are handled by useClickOutsideField instead. + const leavingField = !!e.relatedTarget && !e.currentTarget.contains(e.relatedTarget); + const typedValue = inputValue; + + // Defer until the browser has finished moving focus; re-rendering + // mid-transfer would otherwise lose focus entirely. + clearTimeout(blurTimeout.current); + blurTimeout.current = setTimeout(() => { + if (leavingField) { + handleLeave(typedValue); + } else { + handleBlur(typedValue); + } + }); + }} + >
{item} removeItem(key)} > diff --git a/src/hooks/wizardHooks.ts b/src/hooks/wizardHooks.ts index 43555a302b..2a007f5cef 100644 --- a/src/hooks/wizardHooks.ts +++ b/src/hooks/wizardHooks.ts @@ -132,6 +132,9 @@ export const useClickOutsideField = ( // childRef.current.addEventListener("blur", handleBlur, true); // capture phase // } + // Keyboard exit is handled by the component's own onBlur (see + // RenderMultiField); a listener here can't tell Tab from a click. + // Adding event listener for detecting click outside window.addEventListener("mousedown", handleClickOutside); diff --git a/src/styles/components/modals/_footer.scss b/src/styles/components/modals/_footer.scss index b06180a25a..d8efc6b438 100644 --- a/src/styles/components/modals/_footer.scss +++ b/src/styles/components/modals/_footer.scss @@ -49,6 +49,14 @@ @include button.btn(red); } + // Grey out disabled wizard buttons. Must stay after the btn() includes + // to override their :hover/:focus rules. + &.inactive, + &:disabled { + opacity: 0.5; + cursor: default; + } + min-width: 100px; margin: 10px 3% 0 15px; display: inline-block;