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"}