fix: keep draft authoring dialogs visible

This commit is contained in:
lda
2026-08-09 13:48:10 +07:00 Verified
parent 53606fdb84
commit 98861a7fc2
4 changed files with 51 additions and 13 deletions
@@ -18,7 +18,7 @@ export const CapabilityPalette = ({
<header className="capability-palette__header">
<p className="workspace-route-pending__eyebrow">Available interfaces</p>
<h2>Capabilities</h2>
<p>Choose a capability to inspect its contract before adding it in the next authoring slice.</p>
<p>Choose a capability to inspect its contract, configure a step, and add it to this graph.</p>
</header>
{capabilities.length > 0 ? (
<ul className="capability-palette__list">
@@ -128,6 +128,50 @@ beforeEach(() => {
afterEach(() => cleanup());
describe("CreateDraftDialog", () => {
it("stays open when StrictMode replays effects with native dialog events", async () => {
const originalShowModal = HTMLDialogElement.prototype.showModal;
const originalClose = HTMLDialogElement.prototype.close;
Object.defineProperty(HTMLDialogElement.prototype, "showModal", {
configurable: true,
value(this: HTMLDialogElement) {
this.setAttribute("open", "");
},
});
Object.defineProperty(HTMLDialogElement.prototype, "close", {
configurable: true,
value(this: HTMLDialogElement) {
this.removeAttribute("open");
queueMicrotask(() => this.dispatchEvent(new Event("close")));
},
});
try {
renderStrictDialog(null);
await Promise.resolve();
expect(screen.getByRole("dialog", { name: "Create a draft workspace" })).toHaveAttribute(
"open",
);
} finally {
if (originalShowModal === undefined) {
Reflect.deleteProperty(HTMLDialogElement.prototype, "showModal");
} else {
Object.defineProperty(HTMLDialogElement.prototype, "showModal", {
configurable: true,
value: originalShowModal,
});
}
if (originalClose === undefined) {
Reflect.deleteProperty(HTMLDialogElement.prototype, "close");
} else {
Object.defineProperty(HTMLDialogElement.prototype, "close", {
configurable: true,
value: originalClose,
});
}
}
});
it("keeps successful creation navigable after StrictMode effect replay", async () => {
const user = userEvent.setup();
vi.mocked(authoringClient.createEmpty).mockResolvedValueOnce(workspace("strict-created"));
@@ -44,15 +44,6 @@ const showModal = (dialog: HTMLDialogElement): void => {
dialog.setAttribute("open", "");
};
const closeModal = (dialog: HTMLDialogElement): void => {
if (!dialog.open) return;
if (typeof dialog.close === "function") {
dialog.close();
return;
}
dialog.removeAttribute("open");
};
export const CreateDraftDialog = ({
capability,
onClose,
@@ -95,7 +86,10 @@ export const CreateDraftDialog = ({
lifecycleTokenRef.current = null;
}
requestGenerationRef.current += 1;
closeModal(dialog);
// Do not call dialog.close() here. React Strict Mode replays this cleanup
// while the same dialog remains mounted; Chrome dispatches that native
// close event asynchronously and the remounted instance can mistake it
// for an operator close. Removing the DOM node closes a real unmount.
if (previouslyFocused !== null && document.contains(previouslyFocused)) {
previouslyFocused.focus();
}
@@ -38,7 +38,7 @@ export const DraftIndexRoute = () => {
<header className="draft-workspaces__header">
<p className="workspace-route-pending__eyebrow">Authoring inventory</p>
<h1>Draft workspaces</h1>
<p>Inspect saved workflow drafts without changing their definitions.</p>
<p>Create a workspace or open an existing draft to build and validate its graph.</p>
<div className="draft-workspaces__actions">
<button onClick={() => setCreateDialogOpen(true)} type="button">
New draft
@@ -52,7 +52,7 @@ export const DraftIndexRoute = () => {
<section aria-labelledby="draft-workspaces-list-heading" className="draft-workspaces__panel">
<div className="draft-workspaces__section-heading">
<div>
<p className="workspace-route-pending__eyebrow">Read-only index</p>
<p className="workspace-route-pending__eyebrow">Authoring workspaces</p>
<h2 id="draft-workspaces-list-heading">Available drafts</h2>
</div>
{drafts.items.length > 0 && (