From abf9c2c771f8c3b2452d11f0705f454c4ecdf9a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 10:15:53 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=EB=B0=94?= =?UTF-8?q?=ED=80=B4=2085?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Difa7hNLiCp9rFtK3R7Xtt From c72ccdac03c1324fcd63f26e7aaff48b12aebc41 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 10:20:33 +0000 Subject: [PATCH 2/4] =?UTF-8?q?web:=20Pattern=20Lab=20=EC=9D=98=20Share=20?= =?UTF-8?q?=EC=B0=BD=EC=9D=B4=20Esc=20=EB=A1=9C=20=EB=8B=AB=ED=9E=88?= =?UTF-8?q?=EA=B3=A0=20=EC=B4=88=EC=A0=90=EC=9D=84=20Share=20=EB=8B=A8?= =?UTF-8?q?=EC=B6=94=EB=A1=9C=20=EB=8F=8C=EB=A0=A4=EC=A4=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Difa7hNLiCp9rFtK3R7Xtt --- .../app/pattern-lab/HardwareModal.test.tsx | 11 +++- web/src/app/pattern-lab/HardwareModal.tsx | 6 +- web/src/components/community/PublishModal.tsx | 23 +++++++- .../components/community/dialogKeys.test.tsx | 58 ++++++++++++++++++- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/web/src/app/pattern-lab/HardwareModal.test.tsx b/web/src/app/pattern-lab/HardwareModal.test.tsx index 94a9feeb4..d75836116 100644 --- a/web/src/app/pattern-lab/HardwareModal.test.tsx +++ b/web/src/app/pattern-lab/HardwareModal.test.tsx @@ -3,8 +3,9 @@ // on up to this backdrop and closed To hardware as well. The header the model // translated lives only in this modal's state, so it went with it. // -// Esc goes the same way. Send and Publish listen for no keys, so this modal -// hears the Esc meant for them, and it closes the one on top. +// Esc goes the same way. This modal hears the Esc meant for the window on +// top, Send or Publish, and closes that one alone. Publish hears the same +// Esc itself and closes itself, which leaves this modal open all the same. // // The modal is mounted the way the lab mounts it, behind an open flag that // onClose clears, so a stray close really unmounts it. The pasted header is @@ -403,12 +404,16 @@ describe("HardwareModal", () => { it("Esc over the Publish modal closes Publish alone and keeps the header", async () => { render(); pasteHeaderAndContinue(); - fireEvent.click(screen.getByText("Upload to the community")); + const upload = screen.getByText("Upload to the community"); + upload.focus(); + fireEvent.click(upload); await screen.findByText("Share to Community"); + expect(screen.getByRole("textbox", { name: "Title" })).toHaveFocus(); fireEvent.keyDown(document.activeElement!, { key: "Escape" }); await waitFor(() => expect(screen.queryByText("Share to Community")).toBeNull()); + expect(upload).toHaveFocus(); expectHeaderKept(); }); diff --git a/web/src/app/pattern-lab/HardwareModal.tsx b/web/src/app/pattern-lab/HardwareModal.tsx index 919dd26ae..e9ff44ff1 100644 --- a/web/src/app/pattern-lab/HardwareModal.tsx +++ b/web/src/app/pattern-lab/HardwareModal.tsx @@ -217,9 +217,9 @@ export default function HardwareModal({ // here as well, and that one began in the card. const closeOnBackdrop = useBackdropClose(onClose); - // Esc closes the window on top. Send and Publish open over this one and - // hear no keys of their own, so the first Esc is theirs and the header - // stays. + // Esc closes the window on top and the header stays. Send opens over this + // one and hears no keys of its own, so this listener closes it. Publish + // hears Esc itself, and both listeners close Publish and nothing else. const cardRef = useDialog(() => { if (sendOpen) setSendOpen(false); else if (publishCode !== null) setPublishCode(null); diff --git a/web/src/components/community/PublishModal.tsx b/web/src/components/community/PublishModal.tsx index 3bdc40152..30dea52f4 100644 --- a/web/src/components/community/PublishModal.tsx +++ b/web/src/components/community/PublishModal.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; +import { useDialog } from "@/lib/hooks/useDialog"; import { authClient } from "@/lib/community/auth-client"; import { COMMUNITY_FETCH_INIT, communityApiUrl } from "@/lib/community/apiBase"; import { useSubmitLatch } from "@/lib/community/submitLatch"; @@ -121,6 +122,17 @@ export default function PublishModal({ const latch = useSubmitLatch(); const [includeShow, setIncludeShow] = useState(true); + // The card takes the focus first, so the hook sees the Share button that + // opened the window and hands the focus back to it on close. The title + // field's autoFocus used to move it before the hook could look, and the + // field is not there yet while the session is being checked. + const cardRef = useDialog(onClose); + const titleRef = useRef(null); + const formShown = !isPending && Boolean(session); + useEffect(() => { + if (formShown) titleRef.current?.focus({ preventScroll: true }); + }, [formShown]); + const licenseChoices = parentLicense ? forkLicenseOptions(parentLicense) : LICENSE_OPTIONS; // Default to the recommended licence, unless the parent's terms rule it out. const [licenseId, setLicenseId] = useState( @@ -252,7 +264,12 @@ export default function PublishModal({ return (
-
event.stopPropagation()}> +
event.stopPropagation()} + >
{editOf ? "Update your pattern" : "Share to Community"}