Custom add and create flow polish
This commit is contained in:
@@ -28,6 +28,9 @@ const CreateContainer = memo<CreateProps>(
|
||||
ariaLabelledBy,
|
||||
backdropVariant = "default",
|
||||
stepper,
|
||||
kebabTriggerAriaLabel,
|
||||
kebabMenuAriaLabel,
|
||||
kebabMenuItems,
|
||||
}) => {
|
||||
const createRef = useRef<HTMLDivElement>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
@@ -60,6 +63,9 @@ const CreateContainer = memo<CreateProps>(
|
||||
overlayRef={overlayRef}
|
||||
backdropVariant={backdropVariant}
|
||||
stepper={stepper}
|
||||
kebabTriggerAriaLabel={kebabTriggerAriaLabel}
|
||||
kebabMenuAriaLabel={kebabMenuAriaLabel}
|
||||
kebabMenuItems={kebabMenuItems}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RefObject } from "react";
|
||||
import type { CreateModalBackdropVariant } from "./CreateModalFrame.view";
|
||||
import type { ModalHeaderMenuItem } from "../ModalHeader/ModalHeader.types";
|
||||
|
||||
export interface CreateProps {
|
||||
isOpen: boolean;
|
||||
@@ -37,6 +38,9 @@ export interface CreateProps {
|
||||
backdropVariant?: CreateModalBackdropVariant;
|
||||
/** Passed through to ModalFooter; set explicitly when step visibility must not infer from steps alone. */
|
||||
stepper?: boolean;
|
||||
kebabTriggerAriaLabel?: string;
|
||||
kebabMenuAriaLabel?: string;
|
||||
kebabMenuItems?: ModalHeaderMenuItem[];
|
||||
}
|
||||
|
||||
export interface CreateViewProps {
|
||||
@@ -63,4 +67,7 @@ export interface CreateViewProps {
|
||||
overlayRef: RefObject<HTMLDivElement | null>;
|
||||
backdropVariant: CreateModalBackdropVariant;
|
||||
stepper?: boolean;
|
||||
kebabTriggerAriaLabel?: string;
|
||||
kebabMenuAriaLabel?: string;
|
||||
kebabMenuItems?: ModalHeaderMenuItem[];
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ export function CreateView({
|
||||
overlayRef,
|
||||
backdropVariant,
|
||||
stepper,
|
||||
kebabTriggerAriaLabel,
|
||||
kebabMenuAriaLabel,
|
||||
kebabMenuItems,
|
||||
}: CreateViewProps) {
|
||||
return (
|
||||
<CreateModalFrameView
|
||||
@@ -42,7 +45,13 @@ export function CreateView({
|
||||
overlayRef={overlayRef}
|
||||
dialogRef={createRef}
|
||||
>
|
||||
<ModalHeader onClose={onClose} onMoreOptions={onClose} />
|
||||
<ModalHeader
|
||||
onClose={onClose}
|
||||
moreOptionsAriaLabel={kebabTriggerAriaLabel}
|
||||
menuAriaLabel={kebabMenuAriaLabel}
|
||||
menuItems={kebabMenuItems}
|
||||
showMoreOptionsButton={(kebabMenuItems?.length ?? 0) > 0}
|
||||
/>
|
||||
|
||||
{headerContent !== undefined ? (
|
||||
<div className="shrink-0">{headerContent}</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useEffect, useId, useRef, useState } from "react";
|
||||
import { ModalHeaderView } from "./ModalHeader.view";
|
||||
import type { ModalHeaderProps } from "./ModalHeader.types";
|
||||
|
||||
@@ -10,7 +10,55 @@ import type { ModalHeaderProps } from "./ModalHeader.types";
|
||||
* (right) icon buttons.
|
||||
*/
|
||||
const ModalHeaderContainer = memo<ModalHeaderProps>((props) => {
|
||||
return <ModalHeaderView {...props} />;
|
||||
const { menuItems = [] } = props;
|
||||
const hasMenu = menuItems.length > 0;
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const menuId = useId();
|
||||
const menuWrapRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen || !hasMenu) return;
|
||||
const onDoc = (event: MouseEvent) => {
|
||||
if (
|
||||
menuWrapRef.current &&
|
||||
!menuWrapRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
}, [hasMenu, menuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen || !hasMenu) return;
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [hasMenu, menuOpen]);
|
||||
|
||||
return (
|
||||
<div ref={menuWrapRef}>
|
||||
<ModalHeaderView
|
||||
{...props}
|
||||
menuId={menuId}
|
||||
menuOpen={menuOpen}
|
||||
onToggleMenu={hasMenu ? () => setMenuOpen((open) => !open) : undefined}
|
||||
onMenuItemClick={
|
||||
hasMenu
|
||||
? (item) => {
|
||||
item.onClick?.();
|
||||
setMenuOpen(false);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ModalHeaderContainer.displayName = "ModalHeader";
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import type { IconName } from "../../asset/icon";
|
||||
|
||||
export interface ModalHeaderMenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
leadingIcon: IconName;
|
||||
onClick?: () => void;
|
||||
/** Kebab rows only; omit for default lockup styling. */
|
||||
variant?: "default" | "destructive";
|
||||
}
|
||||
|
||||
export interface ModalHeaderProps {
|
||||
onClose?: () => void;
|
||||
onMoreOptions?: () => void;
|
||||
@@ -7,5 +18,11 @@ export interface ModalHeaderProps {
|
||||
closeButtonAriaLabel?: string;
|
||||
/** When set, used for the more-options control’s accessible name (e.g. localized). */
|
||||
moreOptionsAriaLabel?: string;
|
||||
menuAriaLabel?: string;
|
||||
menuItems?: ModalHeaderMenuItem[];
|
||||
menuId?: string;
|
||||
menuOpen?: boolean;
|
||||
onToggleMenu?: () => void;
|
||||
onMenuItemClick?: (_item: ModalHeaderMenuItem) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import ListItem from "../../layout/ListItem";
|
||||
import Popover from "../Popover";
|
||||
import { getAssetPath } from "../../../../lib/assetUtils";
|
||||
import type { ModalHeaderProps } from "./ModalHeader.types";
|
||||
|
||||
@@ -11,8 +13,16 @@ export function ModalHeaderView({
|
||||
showMoreOptionsButton = true,
|
||||
closeButtonAriaLabel = "Close dialog",
|
||||
moreOptionsAriaLabel = "More options",
|
||||
menuAriaLabel = "More options menu",
|
||||
menuItems = [],
|
||||
menuId,
|
||||
menuOpen = false,
|
||||
onToggleMenu,
|
||||
onMenuItemClick,
|
||||
className = "",
|
||||
}: ModalHeaderProps) {
|
||||
const hasMenu = menuItems.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border-b border-[var(--color-border-default-secondary)] h-[48px] shrink-0 sticky top-0 bg-[var(--color-surface-default-primary)] z-[2] ${className}`}
|
||||
@@ -41,9 +51,12 @@ export function ModalHeaderView({
|
||||
{showMoreOptionsButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onMoreOptions}
|
||||
onClick={hasMenu ? onToggleMenu : onMoreOptions}
|
||||
className={`${iconButtonClass} right-[24px] top-[12px]`}
|
||||
aria-label={moreOptionsAriaLabel}
|
||||
aria-haspopup={hasMenu ? "menu" : undefined}
|
||||
aria-expanded={hasMenu ? menuOpen : undefined}
|
||||
aria-controls={hasMenu ? menuId : undefined}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
@@ -58,6 +71,22 @@ export function ModalHeaderView({
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{showMoreOptionsButton && hasMenu && menuOpen ? (
|
||||
<div className="absolute right-[24px] top-[44px] z-[300]">
|
||||
<Popover id={menuId} menuAriaLabel={menuAriaLabel}>
|
||||
{menuItems.map((item, index) => (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
showDivider={index < menuItems.length - 1}
|
||||
leadingIcon={item.leadingIcon}
|
||||
label={item.label}
|
||||
variant={item.variant}
|
||||
onClick={() => onMenuItemClick?.(item)}
|
||||
/>
|
||||
))}
|
||||
</Popover>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user