Full cleanup pass
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCreateFlowSm2Up } from "../../../(app)/create/hooks/useCreateFlowSm2Up";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { CreateFlowTopNavView } from "./CreateFlowTopNav.view";
|
||||
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||
import type {
|
||||
CreateFlowTopNavActionMenuItem,
|
||||
CreateFlowTopNavProps,
|
||||
} from "./CreateFlowTopNav.types";
|
||||
|
||||
/**
|
||||
* Figma: Utility / CreateFlowTopNav — wizard header (create-flow chrome).
|
||||
@@ -34,15 +38,168 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
||||
const router = useRouter();
|
||||
const t = useTranslation("create.topNav");
|
||||
const tPopover = useTranslation("modals.popoverExport");
|
||||
const sm2Up = useCreateFlowSm2Up();
|
||||
const exitButtonText =
|
||||
exitLabel ?? (saveDraftOnExit ? t("saveAndExit") : t("exit"));
|
||||
const [exportMenuOpen, setExportMenuOpen] = useState(false);
|
||||
const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
|
||||
const exportWrapRef = useRef<HTMLDivElement>(null);
|
||||
const actionsWrapRef = useRef<HTMLDivElement>(null);
|
||||
const exportMenuId = useId();
|
||||
const actionsMenuId = useId();
|
||||
|
||||
const handleExit = (options?: { saveDraft?: boolean }) => {
|
||||
if (onExit) {
|
||||
onExit(options);
|
||||
} else {
|
||||
// Default behavior: navigate to home
|
||||
router.push("/");
|
||||
const handleExit = useCallback(
|
||||
(options?: { saveDraft?: boolean }) => {
|
||||
if (onExit) {
|
||||
onExit(options);
|
||||
} else {
|
||||
// Default behavior: navigate to home
|
||||
router.push("/");
|
||||
}
|
||||
},
|
||||
[onExit, router],
|
||||
);
|
||||
|
||||
const hasSecondaryActions =
|
||||
hasShare ||
|
||||
hasExport ||
|
||||
hasEdit ||
|
||||
hasDuplicate ||
|
||||
hasManageStakeholders;
|
||||
const useKebabMenu = hasSecondaryActions && !sm2Up;
|
||||
|
||||
const actionMenuItems = useMemo((): CreateFlowTopNavActionMenuItem[] => {
|
||||
const items: CreateFlowTopNavActionMenuItem[] = [];
|
||||
|
||||
if (hasShare && onShare) {
|
||||
items.push({
|
||||
id: "share",
|
||||
label: t("share"),
|
||||
leadingIcon: "mail",
|
||||
onClick: onShare,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (hasExport && onSelectExportFormat) {
|
||||
items.push(
|
||||
{
|
||||
id: "export-pdf",
|
||||
label: tPopover("downloadPdf"),
|
||||
leadingIcon: "picture_as_pdf",
|
||||
onClick: () => onSelectExportFormat("pdf"),
|
||||
},
|
||||
{
|
||||
id: "export-csv",
|
||||
label: tPopover("downloadCsv"),
|
||||
leadingIcon: "csv",
|
||||
onClick: () => onSelectExportFormat("csv"),
|
||||
},
|
||||
{
|
||||
id: "export-markdown",
|
||||
label: tPopover("downloadMarkdown"),
|
||||
leadingIcon: "markdown_copy",
|
||||
onClick: () => onSelectExportFormat("markdown"),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (hasDuplicate && onDuplicate) {
|
||||
items.push({
|
||||
id: "duplicate",
|
||||
label: duplicateLabel ?? t("edit"),
|
||||
leadingIcon: "content_copy",
|
||||
onClick: onDuplicate,
|
||||
});
|
||||
} else if (hasEdit && onEdit) {
|
||||
items.push({
|
||||
id: "edit",
|
||||
label: t("edit"),
|
||||
leadingIcon: "edit",
|
||||
onClick: onEdit,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasManageStakeholders && onManageStakeholders) {
|
||||
items.push({
|
||||
id: "manage-stakeholders",
|
||||
label: t("manageStakeholders"),
|
||||
leadingIcon: "tags",
|
||||
onClick: onManageStakeholders,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: "exit",
|
||||
label: exitButtonText,
|
||||
leadingIcon: "log_out",
|
||||
onClick: () => void handleExit({ saveDraft: saveDraftOnExit }),
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [
|
||||
duplicateLabel,
|
||||
exitButtonText,
|
||||
handleExit,
|
||||
hasDuplicate,
|
||||
hasEdit,
|
||||
hasExport,
|
||||
hasManageStakeholders,
|
||||
hasShare,
|
||||
onDuplicate,
|
||||
onEdit,
|
||||
onManageStakeholders,
|
||||
onSelectExportFormat,
|
||||
onShare,
|
||||
saveDraftOnExit,
|
||||
t,
|
||||
tPopover,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!exportMenuOpen) return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (
|
||||
exportWrapRef.current &&
|
||||
!exportWrapRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setExportMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
}, [exportMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionsMenuOpen) return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (
|
||||
actionsWrapRef.current &&
|
||||
!actionsWrapRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setActionsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
}, [actionsMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!exportMenuOpen) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setExportMenuOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [exportMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionsMenuOpen) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setActionsMenuOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [actionsMenuOpen]);
|
||||
|
||||
return (
|
||||
<CreateFlowTopNavView
|
||||
@@ -63,6 +220,17 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
||||
duplicateAriaLabel={duplicateAriaLabel}
|
||||
buttonPalette={buttonPalette}
|
||||
className={className}
|
||||
exitButtonText={exitButtonText}
|
||||
useKebabMenu={useKebabMenu}
|
||||
exportMenuOpen={exportMenuOpen}
|
||||
setExportMenuOpen={setExportMenuOpen}
|
||||
actionsMenuOpen={actionsMenuOpen}
|
||||
setActionsMenuOpen={setActionsMenuOpen}
|
||||
exportWrapRef={exportWrapRef}
|
||||
actionsWrapRef={actionsWrapRef}
|
||||
exportMenuId={exportMenuId}
|
||||
actionsMenuId={actionsMenuId}
|
||||
actionMenuItems={actionMenuItems}
|
||||
exportPopoverMenuAriaLabel={tPopover("menuAriaLabel")}
|
||||
exportPopoverPdfLabel={tPopover("downloadPdf")}
|
||||
exportPopoverCsvLabel={tPopover("downloadCsv")}
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
* Includes logo and action buttons (Share, Export, Edit, Exit).
|
||||
*/
|
||||
|
||||
import type { Dispatch, RefObject, SetStateAction } from "react";
|
||||
import type { IconName } from "../../asset/icon";
|
||||
|
||||
export type CreateFlowTopNavActionMenuItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
leadingIcon: IconName;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export interface CreateFlowTopNavProps {
|
||||
/**
|
||||
* Whether to show the Share button
|
||||
@@ -81,8 +91,19 @@ export interface CreateFlowTopNavProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Resolved copy for the export popover; supplied by the container. */
|
||||
/** Resolved copy and menu state; supplied by the container. */
|
||||
export type CreateFlowTopNavViewProps = CreateFlowTopNavProps & {
|
||||
exitButtonText: string;
|
||||
useKebabMenu: boolean;
|
||||
exportMenuOpen: boolean;
|
||||
setExportMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
actionsMenuOpen: boolean;
|
||||
setActionsMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
exportWrapRef: RefObject<HTMLDivElement | null>;
|
||||
actionsWrapRef: RefObject<HTMLDivElement | null>;
|
||||
exportMenuId: string;
|
||||
actionsMenuId: string;
|
||||
actionMenuItems: CreateFlowTopNavActionMenuItem[];
|
||||
exportPopoverMenuAriaLabel: string;
|
||||
exportPopoverPdfLabel: string;
|
||||
exportPopoverCsvLabel: string;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import type { IconName } from "../../asset/icon";
|
||||
import Logo from "../../asset/Logo";
|
||||
import Button from "../../buttons/Button";
|
||||
import ListItem from "../../layout/ListItem";
|
||||
import Popover from "../../modals/Popover";
|
||||
import { useCreateFlowSm2Up } from "../../../(app)/create/hooks/useCreateFlowSm2Up";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import type { CreateFlowTopNavViewProps } from "./CreateFlowTopNav.types";
|
||||
|
||||
@@ -16,13 +13,6 @@ const outlineButtonClass =
|
||||
const exitButtonFigmaClass =
|
||||
"!rounded-[var(--radius-measures-radius-full,9999px)] !border-[1.25px] !px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!text-[12px] md:!leading-[14px]";
|
||||
|
||||
type ActionMenuItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
leadingIcon: IconName;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
function KebabIcon({ className = "" }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
@@ -54,11 +44,21 @@ export function CreateFlowTopNavView({
|
||||
onDuplicate,
|
||||
onManageStakeholders,
|
||||
onExit,
|
||||
exitLabel,
|
||||
duplicateLabel,
|
||||
duplicateAriaLabel,
|
||||
buttonPalette = "default",
|
||||
className = "",
|
||||
exitButtonText,
|
||||
useKebabMenu,
|
||||
exportMenuOpen,
|
||||
setExportMenuOpen,
|
||||
actionsMenuOpen,
|
||||
setActionsMenuOpen,
|
||||
exportWrapRef,
|
||||
actionsWrapRef,
|
||||
exportMenuId,
|
||||
actionsMenuId,
|
||||
actionMenuItems,
|
||||
exportPopoverMenuAriaLabel,
|
||||
exportPopoverPdfLabel,
|
||||
exportPopoverCsvLabel,
|
||||
@@ -67,15 +67,6 @@ export function CreateFlowTopNavView({
|
||||
actionsMenuAriaLabel,
|
||||
}: CreateFlowTopNavViewProps) {
|
||||
const t = useTranslation("create.topNav");
|
||||
const sm2Up = useCreateFlowSm2Up();
|
||||
const exitButtonText =
|
||||
exitLabel ?? (saveDraftOnExit ? t("saveAndExit") : t("exit"));
|
||||
const [exportMenuOpen, setExportMenuOpen] = useState(false);
|
||||
const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
|
||||
const exportWrapRef = useRef<HTMLDivElement>(null);
|
||||
const actionsWrapRef = useRef<HTMLDivElement>(null);
|
||||
const exportMenuId = useId();
|
||||
const actionsMenuId = useId();
|
||||
|
||||
const hasSecondaryActions =
|
||||
hasShare ||
|
||||
@@ -83,142 +74,6 @@ export function CreateFlowTopNavView({
|
||||
hasEdit ||
|
||||
hasDuplicate ||
|
||||
hasManageStakeholders;
|
||||
const useKebabMenu = hasSecondaryActions && !sm2Up;
|
||||
|
||||
const actionMenuItems = useMemo((): ActionMenuItem[] => {
|
||||
const items: ActionMenuItem[] = [];
|
||||
|
||||
if (hasShare && onShare) {
|
||||
items.push({
|
||||
id: "share",
|
||||
label: t("share"),
|
||||
leadingIcon: "mail",
|
||||
onClick: onShare,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasExport && onSelectExportFormat) {
|
||||
items.push(
|
||||
{
|
||||
id: "export-pdf",
|
||||
label: exportPopoverPdfLabel,
|
||||
leadingIcon: "picture_as_pdf",
|
||||
onClick: () => onSelectExportFormat("pdf"),
|
||||
},
|
||||
{
|
||||
id: "export-csv",
|
||||
label: exportPopoverCsvLabel,
|
||||
leadingIcon: "csv",
|
||||
onClick: () => onSelectExportFormat("csv"),
|
||||
},
|
||||
{
|
||||
id: "export-markdown",
|
||||
label: exportPopoverMarkdownLabel,
|
||||
leadingIcon: "markdown_copy",
|
||||
onClick: () => onSelectExportFormat("markdown"),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (hasDuplicate && onDuplicate) {
|
||||
items.push({
|
||||
id: "duplicate",
|
||||
label: duplicateLabel ?? t("edit"),
|
||||
leadingIcon: "content_copy",
|
||||
onClick: onDuplicate,
|
||||
});
|
||||
} else if (hasEdit && onEdit) {
|
||||
items.push({
|
||||
id: "edit",
|
||||
label: t("edit"),
|
||||
leadingIcon: "edit",
|
||||
onClick: onEdit,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasManageStakeholders && onManageStakeholders) {
|
||||
items.push({
|
||||
id: "manage-stakeholders",
|
||||
label: t("manageStakeholders"),
|
||||
leadingIcon: "tags",
|
||||
onClick: onManageStakeholders,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: "exit",
|
||||
label: exitButtonText,
|
||||
leadingIcon: "log_out",
|
||||
onClick: () => void onExit?.({ saveDraft: saveDraftOnExit }),
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [
|
||||
duplicateLabel,
|
||||
exitButtonText,
|
||||
exportPopoverCsvLabel,
|
||||
exportPopoverMarkdownLabel,
|
||||
exportPopoverPdfLabel,
|
||||
hasDuplicate,
|
||||
hasEdit,
|
||||
hasExport,
|
||||
hasManageStakeholders,
|
||||
hasShare,
|
||||
onDuplicate,
|
||||
onEdit,
|
||||
onExit,
|
||||
onManageStakeholders,
|
||||
onSelectExportFormat,
|
||||
onShare,
|
||||
saveDraftOnExit,
|
||||
t,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!exportMenuOpen) return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (
|
||||
exportWrapRef.current &&
|
||||
!exportWrapRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setExportMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
}, [exportMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionsMenuOpen) return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (
|
||||
actionsWrapRef.current &&
|
||||
!actionsWrapRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setActionsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
}, [actionsMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!exportMenuOpen) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setExportMenuOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [exportMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionsMenuOpen) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setActionsMenuOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [actionsMenuOpen]);
|
||||
|
||||
const inlineActions = (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user