Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 48 additions & 11 deletions src/components/shared/wizard/RenderMultiField.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<HTMLDivElement>();

/**
* This component renders an editable field for multiple values depending on the type of the corresponding metadata
*/
Expand All @@ -25,6 +23,8 @@ const RenderMultiField = ({
form: FieldProps["form"]
showCheck?: boolean,
}) => {
// One ref per rendered field
const childRef = useRef<HTMLDivElement>(null);
// Indicator if currently edit mode is activated
const { editMode, setEditMode } = useClickOutsideField(childRef);
// Temporary storage for value user currently types in
Expand Down Expand Up @@ -93,21 +93,32 @@ 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)
editMode ? (
<>
{fieldInfo.type === "mixed_text" && (
<EditMultiSelect
containerRef={childRef}
collection={fieldInfo.collection ? fieldInfo.collection : []}
field={field}
fieldValue={fieldValue}
inputValue={inputValue}
removeItem={removeItem}
handleChange={handleChange}
handleKeyDown={handleKeyDown}
handleBlur={submitValue}
// Route through the ref, not submitValue directly
commitOnUnmount={input => submitValueRef.current(input)}
exitEditMode={() => setEditMode(false)}
/>
)}
</>
Expand All @@ -128,42 +139,64 @@ const RenderMultiField = ({

// Renders multi select
const EditMultiSelect = ({
containerRef,
collection,
handleKeyDown,
handleChange,
handleBlur,
commitOnUnmount,
exitEditMode,
inputValue,
removeItem,
field,
fieldValue,
}: {
containerRef: React.RefObject<HTMLDivElement | null>
collection: { [key: string]: unknown }[]
handleKeyDown: (event: React.KeyboardEvent) => void
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void
handleBlur: (refCurrent: string) => void
commitOnUnmount: (typedValue: string) => void
exitEditMode: () => void
inputValue: HTMLInputElement["value"]
removeItem: (key: number) => void
field: FieldProps["field"]
fieldValue: FieldInputProps<unknown>["value"]
}) => {
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<HTMLInputElement>(null);
const leaveTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
React.useEffect(() => {
return () => handleBlur(textRef.current);
return () => {
clearTimeout(leaveTimeout.current);
commitOnUnmount(textRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<>
<div ref={childRef}>
<div
ref={containerRef}
// Tabbing out: leave edit mode; the unmount cleanup commits the typed
// value. Clicks are handled by useClickOutsideField instead.
onBlur={e => {
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);
}
Comment thread
Arnei marked this conversation as resolved.
}}
>
<div>
<input
ref={inputRef}
type="text"
name={field.name}
value={inputValue}
Expand All @@ -187,7 +220,11 @@ const EditMultiSelect = ({
<span className="multi-value" key={key}>
{item}
<ButtonLikeAnchor
onClick={() => removeItem(key)}
onClick={() => {
removeItem(key);
// The pressed button is about to unmount; keep focus in the field.
inputRef.current?.focus();
}}
>
<LuX />
</ButtonLikeAnchor>
Expand Down
Loading