Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import classNames from "classnames";
import {useOnBlur} from "utils/hooks/useOnBlur";
import {useTranslation} from "react-i18next";
import {ReactComponent as CheckDoneIcon} from "assets/icons/check-done.svg";
import {ReactComponent as CloseIcon} from "assets/icons/close.svg";
import {TextArea} from "components/TextArea/TextArea";
import {MiniMenu, MiniMenuItem} from "components/MiniMenu/MiniMenu";
import {Dispatch, SetStateAction, useRef, useState, FocusEvent} from "react";
import "./ColumnConfiguratorColumnNameDetails.scss";
import {Dispatch, SetStateAction, useRef, useState} from "react";
import {MAX_COLUMN_DESCRIPTION_LENGTH} from "constants/misc";
import "./ColumnConfiguratorColumnNameDetails.scss";

export type OpenState = "closed" | "visualFeedback" | "nameFirst" | "descriptionFirst";

Expand All @@ -26,52 +27,53 @@ export type ColumnConfiguratorColumnNameDetailsProps = {
export const ColumnConfiguratorColumnNameDetails = (props: ColumnConfiguratorColumnNameDetailsProps) => {
const {t} = useTranslation();

const nameWrapperRef = useRef<HTMLDivElement>(null);

// temporary state for name and description text as the changes have to be confirmed before applying
const [name, setName] = useState(props.name);
const [description, setDescription] = useState(props.description);

const isEditing = props.openState === "nameFirst" || props.openState === "descriptionFirst";

const nameInputRef = useRef<HTMLInputElement>(null);

const cancelChanges = () => {
props.setOpenState("closed");
nameInputRef.current?.blur(); // leave input (or we can keep typing inside it)
};

const saveChanges = () => {
props.updateColumnTitle(name, description);
// show visual feedback for 2s before displaying menu options again
nameInputRef.current?.blur(); // leave input (or we can keep typing inside it)
props.setOpenState("visualFeedback");
setTimeout(() => {
props.setOpenState("closed");
}, 2000);
};

// if we leave the wrapper, reset and close
const handleBlurNameWrapperContents = () => {
props.setOpenState("closed");
setName(props.name);
setDescription(props.description);
};

const nameWrapperRef = useOnBlur<HTMLDivElement>(handleBlurNameWrapperContents);

const descriptionConfirmMiniMenu: MiniMenuItem[] = [
{
className: "mini-menu-item--cancel",
element: <CloseIcon />,
label: t("Templates.ColumnsConfiguratorColumn.cancel"),
onClick(): void {
props.setOpenState("closed");
(document.activeElement as HTMLElement)?.blur(); // leave input (or we can keep typing inside it)
},
onClick: cancelChanges,
},
{
className: "mini-menu-item--save",
element: <CheckDoneIcon />,
label: t("Templates.ColumnsConfiguratorColumn.save"),
onClick(): void {
props.updateColumnTitle(name, description);
// show visual feedback for 2s before displaying menu options again
props.setOpenState("visualFeedback");
setTimeout(() => {
props.setOpenState("closed");
}, 2000);
},
onClick: saveChanges,
},
];

// if we leave the wrapper close, otherwise leave open
const handleBlurNameWrapperContents = (e: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const isFocusInsideTitleHeaderWrapper = nameWrapperRef.current?.contains(e.relatedTarget);

if (!isFocusInsideTitleHeaderWrapper) {
props.setOpenState("closed");

// reset name and description to actual
setName(props.name);
setDescription(props.description);
}
};

const openDescriptionWithCurrentValue = () => {
setDescription(props.description);
props.setOpenState("descriptionFirst");
Expand All @@ -80,13 +82,25 @@ export const ColumnConfiguratorColumnNameDetails = (props: ColumnConfiguratorCol
return (
<div className={classNames(props.className, "column-configurator-column-name-details__name-wrapper")} ref={nameWrapperRef}>
<input
ref={nameInputRef}
className={classNames("column-configurator-column-name-details__name", {"column-configurator-column-name-details__name--editing": isEditing})}
value={name}
placeholder={t("Templates.ColumnsConfiguratorColumn.namePlaceholder")}
onInput={(e) => setName(e.currentTarget.value)}
onFocus={() => props.setOpenState("nameFirst")}
onBlur={handleBlurNameWrapperContents}
autoComplete="off"
onKeyDown={(e) => {
// handle Enter key submission
if (e.key === "Enter") {
e.preventDefault();
saveChanges();
}
// escape to cancel
else if (e.key === "Escape") {
e.preventDefault();
cancelChanges();
}
}}
/>
{isEditing ? (
<div className="column-configurator-column-name-details__description-wrapper">
Expand All @@ -98,8 +112,9 @@ export const ColumnConfiguratorColumnNameDetails = (props: ColumnConfiguratorCol
embedded
fitted
autoFocus={props.openState === "descriptionFirst"}
onBlur={handleBlurNameWrapperContents}
maxLength={MAX_COLUMN_DESCRIPTION_LENGTH}
onSubmit={saveChanges}
onCancel={cancelChanges}
/>
<MiniMenu className="column-configurator-column-name-details__description-mini-menu" items={descriptionConfirmMiniMenu} small transparent />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ describe("ColumnConfiguratorColumnNameDetails behaviour", () => {
const setOpenStateSpy = jest.fn();
const {container} = renderColumnConfiguratorColumnNameDetails({openState: "nameFirst", setOpenState: setOpenStateSpy});

const inputElement = container.querySelector<HTMLInputElement>(".column-configurator-column-name-details__name")!;

fireEvent.focus(inputElement);
fireEvent.blur(inputElement);
// note: useOnBlur does not actually use the native blur event, but scans for clicks outside the element instead.
// this is why to simulate the blur, we just click somewhere on the document.
// const wrapperElement = container.firstChild as HTMLDivElement
// fireEvent.blur(wrapperElement);
fireEvent.click(document);

expect(setOpenStateSpy).toHaveBeenCalledWith("closed");
});
Expand Down
Loading