fix: bound formatter memory and improve operations workflows

This commit is contained in:
hectorzhao
2026-09-08 12:46:15 +08:00
parent 50ae37242b
commit 2c228a94e1
27 changed files with 4013 additions and 1230 deletions
+29
View File
@@ -0,0 +1,29 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Modal } from './Modal';
describe('Modal close policy', () => {
it('keeps an explicitly locked form open on mask and Escape but permits the close button', () => {
const close = vi.fn();
render(
<Modal open title="创建通道" closeOnBackdrop={false} closeOnEscape={false} onClose={close}>
</Modal>,
);
fireEvent.mouseDown(document.querySelector('.ui-modal__mask')!);
fireEvent.keyDown(document, { key: 'Escape' });
expect(close).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
expect(close).toHaveBeenCalledOnce();
});
it('preserves mask closing by default for existing consumers', () => {
const close = vi.fn();
render(
<Modal open title="普通弹窗" onClose={close}>
</Modal>,
);
fireEvent.mouseDown(document.querySelector('.ui-modal__mask')!);
expect(close).toHaveBeenCalledOnce();
});
});
+42 -16
View File
@@ -16,6 +16,8 @@ type ModalProps = {
size?: 'md' | 'xl';
onClose: () => void;
dirty?: boolean;
closeOnBackdrop?: boolean;
closeOnEscape?: boolean;
initialFocusRef?: RefObject<HTMLElement | null>;
closeGuardTitle?: string;
closeGuardDescription?: string;
@@ -82,8 +84,9 @@ function unlockDocument() {
}
function focusableElements(root: HTMLElement) {
return Array.from(root.querySelectorAll<HTMLElement>(focusableSelector))
.filter((element) => !element.hidden && element.getClientRects().length > 0);
return Array.from(root.querySelectorAll<HTMLElement>(focusableSelector)).filter(
(element) => !element.hidden && element.getClientRects().length > 0,
);
}
export function Modal({
@@ -94,6 +97,8 @@ export function Modal({
size = 'md',
onClose,
dirty = false,
closeOnBackdrop = true,
closeOnEscape = true,
initialFocusRef,
closeGuardTitle = '放弃未保存的修改?',
closeGuardDescription = '当前内容尚未保存。放弃后无法恢复,请确认是否关闭。',
@@ -108,6 +113,7 @@ export function Modal({
const guardRestoreFocusRef = useRef<HTMLElement | null>(null);
const [showCloseGuard, setShowCloseGuard] = useState(false);
const [layer] = useState(() => modalLayer());
if (!open && showCloseGuard) setShowCloseGuard(false);
const requestClose = useCallback(() => {
if (dirty) {
@@ -124,10 +130,7 @@ export function Modal({
}, [onClose]);
useEffect(() => {
if (!open) {
setShowCloseGuard(false);
return undefined;
}
if (!open) return undefined;
const panel = panelRef.current;
if (!panel) return undefined;
@@ -163,7 +166,7 @@ export function Modal({
event.preventDefault();
event.stopPropagation();
if (showCloseGuard) setShowCloseGuard(false);
else requestClose();
else if (closeOnEscape) requestClose();
return;
}
if (event.key !== 'Tab') return;
@@ -188,7 +191,7 @@ export function Modal({
document.addEventListener('keydown', handleKeyDown, true);
return () => document.removeEventListener('keydown', handleKeyDown, true);
}, [open, requestClose, showCloseGuard]);
}, [closeOnEscape, open, requestClose, showCloseGuard]);
useEffect(() => {
if (!showCloseGuard) return;
@@ -205,13 +208,16 @@ export function Modal({
}, [showCloseGuard]);
if (!open) return null;
const renderedFooter = typeof footer === 'function' ? footer({ requestClose }) : footer;
return createPortal(
<div className="ui-modal" data-ui-modal-root>
<div aria-hidden="true" className="ui-modal__mask" onMouseDown={(event) => {
if (event.target === event.currentTarget) requestClose();
}} />
<div
aria-hidden="true"
className="ui-modal__mask"
onMouseDown={(event) => {
if (closeOnBackdrop && event.target === event.currentTarget) requestClose();
}}
/>
<section
aria-labelledby={titleId}
aria-modal="true"
@@ -221,13 +227,19 @@ export function Modal({
tabIndex={-1}
>
<header className="ui-modal__header">
<div className="ui-modal__title" id={titleId}>{title}</div>
<div className="ui-modal__title" id={titleId}>
{title}
</div>
<Button icon={<X size={17} />} iconOnly variant="ghost" onClick={requestClose}>
</Button>
</header>
<div className="ui-modal__body">{children}</div>
{renderedFooter ? <footer className="ui-modal__footer">{renderedFooter}</footer> : null}
{footer ? (
<footer className="ui-modal__footer">
<ModalFooter footer={footer} requestClose={requestClose} />
</footer>
) : null}
</section>
{showCloseGuard ? (
<div className="ui-modal__guard-layer">
@@ -246,8 +258,12 @@ export function Modal({
<p id={guardDescriptionId}>{closeGuardDescription}</p>
</div>
<footer>
<Button autoFocus onClick={() => setShowCloseGuard(false)} variant="ghost"></Button>
<Button onClick={discardAndClose} variant="danger"></Button>
<Button autoFocus onClick={() => setShowCloseGuard(false)} variant="ghost">
</Button>
<Button onClick={discardAndClose} variant="danger">
</Button>
</footer>
</section>
</div>
@@ -256,3 +272,13 @@ export function Modal({
layer,
);
}
function ModalFooter({
footer,
requestClose,
}: {
footer: NonNullable<ModalProps['footer']>;
requestClose: () => void;
}) {
return typeof footer === 'function' ? footer({ requestClose }) : footer;
}