diff --git a/src/components/ColumnsConfigurator/ColumnsConfiguratorColumn/ColumnConfiguratorColumnNameDetails/ColumnConfiguratorColumnNameDetails.tsx b/src/components/ColumnsConfigurator/ColumnsConfiguratorColumn/ColumnConfiguratorColumnNameDetails/ColumnConfiguratorColumnNameDetails.tsx index ecdbadde91..3e9cb8569c 100644 --- a/src/components/ColumnsConfigurator/ColumnsConfiguratorColumn/ColumnConfiguratorColumnNameDetails/ColumnConfiguratorColumnNameDetails.tsx +++ b/src/components/ColumnsConfigurator/ColumnsConfiguratorColumn/ColumnConfiguratorColumnNameDetails/ColumnConfiguratorColumnNameDetails.tsx @@ -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"; @@ -26,52 +27,53 @@ export type ColumnConfiguratorColumnNameDetailsProps = { export const ColumnConfiguratorColumnNameDetails = (props: ColumnConfiguratorColumnNameDetailsProps) => { const {t} = useTranslation(); - const nameWrapperRef = useRef(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(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(handleBlurNameWrapperContents); + const descriptionConfirmMiniMenu: MiniMenuItem[] = [ { className: "mini-menu-item--cancel", element: , 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: , 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) => { - 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"); @@ -80,13 +82,25 @@ export const ColumnConfiguratorColumnNameDetails = (props: ColumnConfiguratorCol return (
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 ? (
@@ -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} />
diff --git a/src/components/ColumnsConfigurator/ColumnsConfiguratorColumn/ColumnConfiguratorColumnNameDetails/__tests__/ColumnConfiguratorColumnNameDetails.test.tsx b/src/components/ColumnsConfigurator/ColumnsConfiguratorColumn/ColumnConfiguratorColumnNameDetails/__tests__/ColumnConfiguratorColumnNameDetails.test.tsx index 5003983ac0..402dbce2a5 100644 --- a/src/components/ColumnsConfigurator/ColumnsConfiguratorColumn/ColumnConfiguratorColumnNameDetails/__tests__/ColumnConfiguratorColumnNameDetails.test.tsx +++ b/src/components/ColumnsConfigurator/ColumnsConfiguratorColumn/ColumnConfiguratorColumnNameDetails/__tests__/ColumnConfiguratorColumnNameDetails.test.tsx @@ -51,10 +51,11 @@ describe("ColumnConfiguratorColumnNameDetails behaviour", () => { const setOpenStateSpy = jest.fn(); const {container} = renderColumnConfiguratorColumnNameDetails({openState: "nameFirst", setOpenState: setOpenStateSpy}); - const inputElement = container.querySelector(".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"); });