diff --git a/src/components/shared/wizard/RenderMultiField.tsx b/src/components/shared/wizard/RenderMultiField.tsx index 213b23a46a..02cf64832d 100644 --- a/src/components/shared/wizard/RenderMultiField.tsx +++ b/src/components/shared/wizard/RenderMultiField.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useRef, useState } from "react"; +import React, { ReactNode, useLayoutEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import cn from "classnames"; import { useClickOutsideField } from "../../../hooks/wizardHooks"; @@ -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 @@ -93,6 +93,14 @@ const RenderMultiField = ({ form.setFieldValue(field.name, fieldValue); }; + // Always points at the latest submitValue, so the unmount cleanup doesn't + // commit against a stale field value. Must be a layout effect: the child's + // unmount cleanup runs before this component's passive effects. + const submitValueRef = useRef(submitValue); + useLayoutEffect(() => { + submitValueRef.current = submitValue; + }); + return ( // Render editable field for multiple values depending on type of metadata field // (types: see metadata.json retrieved from backend) @@ -100,6 +108,7 @@ const RenderMultiField = ({ <> {fieldInfo.type === "mixed_text" && ( submitValueRef.current(input)} + exitEditMode={() => setEditMode(false)} /> )} @@ -128,19 +139,23 @@ const RenderMultiField = ({ // Renders multi select const EditMultiSelect = ({ + containerRef, collection, handleKeyDown, handleChange, - handleBlur, + commitOnUnmount, + exitEditMode, 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 + commitOnUnmount: (typedValue: string) => void + exitEditMode: () => void inputValue: HTMLInputElement["value"] removeItem: (key: number) => void field: FieldProps["field"] @@ -148,22 +163,40 @@ const EditMultiSelect = ({ }) => { const { t } = useTranslation(); - // onBlur does not get called if a component unmounts for some reason - // Instead, we achieve the same effect with useEffect + // Commit the typed value whenever the editor unmounts, which covers every way + // of leaving it (tabbing out, clicking outside, closing the modal/wizard page). const textRef = useRef(inputValue); React.useEffect(() => { textRef.current = inputValue; }, [inputValue]); + const inputRef = useRef(null); + const leaveTimeout = useRef>(undefined); React.useEffect(() => { - return () => handleBlur(textRef.current); + return () => { + clearTimeout(leaveTimeout.current); + commitOnUnmount(textRef.current); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> -
+
{ + if (e.relatedTarget && !e.currentTarget.contains(e.relatedTarget)) { + // Wait until focus has landed on the next field. Unmounting this + // editor mid-transfer makes the modal's focus trap (focus-trap >= 8.2) + // pull focus back to the start of the modal. + leaveTimeout.current = setTimeout(exitEditMode); + } + }} + >
{item} removeItem(key)} + onClick={() => { + removeItem(key); + // The pressed button is about to unmount; keep focus in the field. + inputRef.current?.focus(); + }} >