Skip to content
Open
Show file tree
Hide file tree
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
63 changes: 58 additions & 5 deletions src/components/shared/wizard/RenderMultiField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 All @@ -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) => {
Expand Down Expand Up @@ -93,21 +100,35 @@ 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)
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
handleBlur={input => submitValueRef.current(input)}
handleLeave={leaveEditMode}
/>
)}
</>
Expand All @@ -128,19 +149,23 @@ const RenderMultiField = ({

// Renders multi select
const EditMultiSelect = ({
containerRef,
collection,
handleKeyDown,
handleChange,
handleBlur,
handleLeave,
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
handleLeave: (typedValue: string) => void
inputValue: HTMLInputElement["value"]
removeItem: (key: number) => void
field: FieldProps["field"]
Expand All @@ -154,14 +179,39 @@ const EditMultiSelect = ({
React.useEffect(() => {
textRef.current = inputValue;
}, [inputValue]);
// Pending deferred blur handling, see onBlur below.
const blurTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
React.useEffect(() => {
return () => handleBlur(textRef.current);
return () => {
clearTimeout(blurTimeout.current);
handleBlur(textRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<>
<div ref={childRef}>
<div
ref={containerRef}
// Handle leaving the field via keyboard navigation.
onBlur={e => {
// 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);
}
});
}}
>
<div>
<input
type="text"
Expand All @@ -187,6 +237,9 @@ const EditMultiSelect = ({
<span className="multi-value" key={key}>
{item}
<ButtonLikeAnchor
// Keep out of the tab order so Tab goes to the next field
// (Backspace still removes values via keyboard).
tabIndex={-1}
onClick={() => removeItem(key)}
>
<LuX />
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/wizardHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
8 changes: 8 additions & 0 deletions src/styles/components/modals/_footer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading