fix: bound formatter memory and improve operations workflows
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user