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