ForgeWP

Solution: Arrival Dialog

Use native modal behavior with explicit labels, initial focus, and focus restoration.

This solution completes the arrival dialog practice. The browser handles modal focus containment and Escape; React opens the dialog and restores focus after closing.

1. Create the island

src/components/ArrivalDialog.tsx
1import { useId, useRef } from "react"
2
3export function ArrivalDialog() {
4  const dialogRef = useRef<HTMLDialogElement>(null)
5  const triggerRef = useRef<HTMLButtonElement>(null)
6  const titleId = useId()
7
8  return (
9    <div>
10      <button
11        ref={triggerRef}
12        type="button"
13        aria-haspopup="dialog"
14        className="rounded-lg border px-4 py-2 focus-visible:outline-2 focus-visible:outline-offset-4"
15        onClick={() => dialogRef.current?.showModal()}
16      >
17        Arrival information
18      </button>
19      <dialog
20        ref={dialogRef}
21        aria-labelledby={titleId}
22        onClose={() => triggerRef.current?.focus()}
23        className="m-auto max-h-[85dvh] w-[calc(100%-2rem)] max-w-lg overflow-y-auto rounded-2xl bg-white p-6 text-slate-950 shadow-xl backdrop:bg-black/50"
24      >
25        <h2 id={titleId} className="text-2xl font-semibold">
26          Plan your arrival
27        </h2>
28        <p className="mt-4">
29          Check-in starts at 3 pm. Contact reception if you expect to arrive
30          after 10 pm.
31        </p>
32        <button
33          type="button"
34          autoFocus
35          className="mt-6 rounded-lg border px-4 py-2 focus-visible:outline-2 focus-visible:outline-offset-4"
36          onClick={() => dialogRef.current?.close()}
37        >
38          Close
39        </button>
40      </dialog>
41    </div>
42  )
43}

useId() connects this instance's title to its dialog. showModal() opens it in the browser's top layer and makes the background inert. The Close button's autoFocus supplies the initial focus target. Both close() and the native Escape action lead to onClose, where focus returns to the opener.

Keep the dialog element in the markup without an open attribute. React does not need a second state variable to mirror the browser's dialog state. No document-level keyboard listeners or manual focus trap are needed for this example.

2. Mount both islands

Replace the demonstration Page with:

src/app/pages/NavigationDemo.tsx
1import { Hydrate } from "@forgewp/react"
2import { MobileMenu } from "../../components/MobileMenu"
3import { ArrivalDialog } from "../../components/ArrivalDialog"
4
5export function NavigationDemo() {
6  return (
7    <main className="mx-auto max-w-5xl space-y-8 px-6 py-12">
8      <h1 className="text-3xl font-semibold">Explore your stay</h1>
9      <nav aria-label="Desktop navigation" className="hidden gap-6 md:flex">
10        <a href="/rooms/">Rooms</a>
11        <a href="/contact/">Contact</a>
12      </nav>
13      <Hydrate trigger="load">
14        <MobileMenu />
15      </Hydrate>
16      <p>Check-in starts at 3 pm. Contact reception for late arrivals.</p>
17      <Hydrate trigger="load">
18        <ArrivalDialog />
19      </Hydrate>
20    </main>
21  )
22}

The short arrival message stays available in ordinary HTML. The modal adds a convenient focused view when JavaScript is available. Replace the example times and links with your property's real information before publishing.

3. Verify on WordPress

Export again, update the test theme, and visit the Page using the Navigation Demo template. Complete the practice acceptance checklist, including keyboard focus, a screen reader, narrow viewports, zoom, and reduced motion.

Inspect the page for separate mobile-menu and arrival-dialog hydration wrappers with the load trigger. Open, close, and reopen the dialog several times. Check that navigation and the visible arrival message remain usable with JavaScript disabled.

The lesson's automated check verifies source discovery and initial server-rendered markup. Keyboard focus and native modal behavior still need the browser checks above on the exported site.