From a1a7ad9300a3550a4fe48578881b1b860f6745dd Mon Sep 17 00:00:00 2001 From: Naffah Abdulla Rasheed Date: Mon, 2 Nov 2026 11:58:02 +0500 Subject: [PATCH 1/8] Keep editors from throwing when unmounted with a dialog open Radix calls onCloseAutoFocus from a timer, which can run after Tiptap has destroyed the editor. WysiwygEditor and BlockEditor now check for that first. Tests cover both, and drop the workaround that closed the dialog before unmounting. Fixes #57 Co-Authored-By: Claude Opus 5.5 --- .changeset/editor-unmount-dialog.md | 5 +++++ packages/onu/src/editor/block-editor.test.tsx | 14 ++++++++++++++ packages/onu/src/editor/block-editor.tsx | 3 ++- packages/onu/src/editor/wysiwyg-editor.test.tsx | 17 ++++++++++++++--- packages/onu/src/editor/wysiwyg-editor.tsx | 3 ++- 5 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 .changeset/editor-unmount-dialog.md diff --git a/.changeset/editor-unmount-dialog.md b/.changeset/editor-unmount-dialog.md new file mode 100644 index 00000000..78f8f680 --- /dev/null +++ b/.changeset/editor-unmount-dialog.md @@ -0,0 +1,5 @@ +--- +"@javaabu/onu": patch +--- + +`WysiwygEditor` and `BlockEditor` no longer throw when they unmount with a dialog open. diff --git a/packages/onu/src/editor/block-editor.test.tsx b/packages/onu/src/editor/block-editor.test.tsx index d91b6e8e..fa430c8a 100644 --- a/packages/onu/src/editor/block-editor.test.tsx +++ b/packages/onu/src/editor/block-editor.test.tsx @@ -194,6 +194,20 @@ describe("BlockEditor", () => { expect(editor.getHTML()).not.toContain(" { + const onError = vi.fn<(event: ErrorEvent) => void>() + window.addEventListener("error", onError) + const { editor, unmount } = await setup() + const list = await openSlash(editor, "image") + fireEvent.click(within(list).getByRole("option", { name: /Image/ })) + await screen.findByRole("dialog", { name: "Insert image" }) + unmount() + // Radix calls onCloseAutoFocus from a timer after the unmount. + await act(() => new Promise((resolve) => setTimeout(resolve, 50))) + window.removeEventListener("error", onError) + expect(onError).not.toHaveBeenCalled() + }) + it("does not open inside code blocks", async () => { const { editor } = await setup({ defaultValue: "
a
" }) act(() => { diff --git a/packages/onu/src/editor/block-editor.tsx b/packages/onu/src/editor/block-editor.tsx index 90668e48..14700b64 100644 --- a/packages/onu/src/editor/block-editor.tsx +++ b/packages/onu/src/editor/block-editor.tsx @@ -232,7 +232,8 @@ export function BlockEditor({ event.preventDefault() const target = returnFocus.current if (target?.isConnected) target.focus() - else editor?.commands.focus() + // Radix calls this from a timer, which can run after the editor unmounts. + else if (editor && !editor.isDestroyed) editor.commands.focus() } return ( diff --git a/packages/onu/src/editor/wysiwyg-editor.test.tsx b/packages/onu/src/editor/wysiwyg-editor.test.tsx index 82ea3c57..73e6b804 100644 --- a/packages/onu/src/editor/wysiwyg-editor.test.tsx +++ b/packages/onu/src/editor/wysiwyg-editor.test.tsx @@ -298,9 +298,20 @@ describe("WysiwygEditor", () => { const dialog = await screen.findByRole("dialog", { name: "Insert image" }) expect(within(dialog).getByLabelText(/Alternative text/)).toHaveAttribute("data-thaana") expect(within(dialog).getByLabelText(/Image URL/)).not.toHaveAttribute("data-thaana") - // Close it and let focus return, because the dialog would otherwise focus the editor after it unmounts. - await user.keyboard("{Escape}") - await waitFor(() => expect(button("Image")).toHaveFocus()) + }) + + it("unmounts with a dialog open without touching the destroyed editor", async () => { + const onError = vi.fn<(event: ErrorEvent) => void>() + window.addEventListener("error", onError) + const user = userEvent.setup() + const { unmount } = await setup() + await user.click(button("Link")) + await screen.findByRole("dialog", { name: "Insert link" }) + unmount() + // Radix calls onCloseAutoFocus from a timer after the unmount. + await act(() => new Promise((resolve) => setTimeout(resolve, 50))) + window.removeEventListener("error", onError) + expect(onError).not.toHaveBeenCalled() }) it("uploads images through the upload callback and reports failures", async () => { diff --git a/packages/onu/src/editor/wysiwyg-editor.tsx b/packages/onu/src/editor/wysiwyg-editor.tsx index a946cf9d..d8eca730 100644 --- a/packages/onu/src/editor/wysiwyg-editor.tsx +++ b/packages/onu/src/editor/wysiwyg-editor.tsx @@ -169,7 +169,8 @@ export function WysiwygEditor({ event.preventDefault() const target = returnFocus.current if (target?.isConnected) target.focus() - else editor?.commands.focus() + // Radix calls this from a timer, which can run after the editor unmounts. + else if (editor && !editor.isDestroyed) editor.commands.focus() } return ( From 0fb8cb0f93c76a4fc9b5217cd2005ed0e2bd6aff Mon Sep 17 00:00:00 2001 From: Naffah Abdulla Rasheed Date: Mon, 2 Nov 2026 11:58:28 +0500 Subject: [PATCH 2/8] Let keyboard users scroll the theme builder's CSS The code block overflows at phone width, so it is now a focusable, named region, like CodeBlock. Fixes #51 Co-Authored-By: Claude Opus 5.5 --- web/src/showcase/foundations/theme-builder.demo.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/showcase/foundations/theme-builder.demo.tsx b/web/src/showcase/foundations/theme-builder.demo.tsx index 5d772930..f9e751c2 100644 --- a/web/src/showcase/foundations/theme-builder.demo.tsx +++ b/web/src/showcase/foundations/theme-builder.demo.tsx @@ -117,7 +117,14 @@ export default function ThemeBuilder() { -
+        
           {css}