Portal: full mock-driven surfaces, demonolithed components, backend-ready mocks (#6686)

This commit is contained in:
Reece Browne
2026-06-16 12:20:35 +01:00
committed by GitHub
parent 6716398ccb
commit 96accea984
146 changed files with 12221 additions and 286 deletions
+55 -91
View File
@@ -1,5 +1,6 @@
import { useEffect, useId, useRef, type ReactNode } from "react";
import { useEffect, useId, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { FocusTrap } from "@mantine/core";
import "@shared/components/Modal.css";
export type ModalWidth = "sm" | "md" | "lg" | "xl";
@@ -25,16 +26,12 @@ export interface ModalProps {
children?: ReactNode;
}
const FOCUSABLE_SELECTOR =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
/**
* Portal-rendered modal. Backdrop fades in, dialog scales in. ESC and
* click-on-backdrop close by default. Each instance is self-contained — the
* caller owns the open state and the close handler.
*
* Focus is trapped inside the dialog while open and returned to the
* previously-focused element on close.
* Portal-rendered modal with our own brand shell (header / body / footer,
* width presets, backdrop). The hard part — trapping Tab focus inside the
* dialog, initial focus, and restoring focus to the opener on close — is
* delegated to Mantine's <FocusTrap> rather than hand-rolled. ESC and
* backdrop click close by default; the caller owns the open state.
*/
export function Modal({
open,
@@ -49,7 +46,6 @@ export function Modal({
className,
children,
}: ModalProps) {
const dialogRef = useRef<HTMLDivElement | null>(null);
const titleId = useId();
useEffect(() => {
@@ -70,40 +66,6 @@ export function Modal({
};
}, [open]);
useEffect(() => {
if (!open) return;
const previouslyFocused = document.activeElement as HTMLElement | null;
const dialog = dialogRef.current;
const first = dialog?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
first?.focus();
function onKey(e: KeyboardEvent) {
if (e.key !== "Tab" || !dialog) return;
const focusables =
dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
if (focusables.length === 0) {
e.preventDefault();
return;
}
const firstEl = focusables[0];
const lastEl = focusables[focusables.length - 1];
const active = document.activeElement;
if (e.shiftKey && active === firstEl) {
e.preventDefault();
lastEl.focus();
} else if (!e.shiftKey && active === lastEl) {
e.preventDefault();
firstEl.focus();
}
}
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("keydown", onKey);
previouslyFocused?.focus?.();
};
}, [open]);
if (!open) return null;
function onBackdropClick() {
@@ -118,53 +80,55 @@ export function Modal({
onClick={onBackdropClick}
role="presentation"
>
<div
ref={dialogRef}
className={["sui-modal", `sui-modal--${width}`, className ?? ""]
.filter(Boolean)
.join(" ")}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby={hasTitle ? titleId : undefined}
aria-label={!hasTitle ? ariaLabel : undefined}
>
{(title || subtitle) && (
<header className="sui-modal__header">
<div className="sui-modal__header-text">
{title && (
<div id={titleId} className="sui-modal__title">
{title}
</div>
)}
{subtitle && <div className="sui-modal__sub">{subtitle}</div>}
</div>
<button
type="button"
className="sui-modal__close"
onClick={onClose}
aria-label="Close"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
<FocusTrap active>
<div
className={["sui-modal", `sui-modal--${width}`, className ?? ""]
.filter(Boolean)
.join(" ")}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby={hasTitle ? titleId : undefined}
aria-label={!hasTitle ? ariaLabel : undefined}
tabIndex={-1}
>
{(title || subtitle) && (
<header className="sui-modal__header">
<div className="sui-modal__header-text">
{title && (
<div id={titleId} className="sui-modal__title">
{title}
</div>
)}
{subtitle && <div className="sui-modal__sub">{subtitle}</div>}
</div>
<button
type="button"
className="sui-modal__close"
onClick={onClose}
aria-label="Close"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</header>
)}
<div className="sui-modal__body">{children}</div>
{footer && <footer className="sui-modal__footer">{footer}</footer>}
</div>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</header>
)}
<div className="sui-modal__body">{children}</div>
{footer && <footer className="sui-modal__footer">{footer}</footer>}
</div>
</FocusTrap>
</div>,
document.body,
);