Skip to content

fix(overlay): take focus when opening and hand it back when closing - #2423

Merged
alexander-akait merged 5 commits into
mainfrom
feat/overlay-merge
Sep 26, 2026
Merged

alexander-akait merged 5 commits into
mainfrom
feat/overlay-merge

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Rebased onto main now that #2421 has merged, so this stands on its own — one commit.

Before webpack-dev-server can drop its own overlay and use this one, this one has to do everything that one does. Comparing them turned up two things it did not, both accessibility:

  1. Focus never moved. The overlay opened without touching it, so reaching the overlay meant clicking into the frame first — Escape and the arrow keys did nothing until you did, and a screen reader stayed wherever the page already was. webpack-dev-server's focuses its close button on open, saves what had focus, and gives it back on close. This one now does the same, so closing no longer leaves focus on a removed element with the next Tab starting from the top of the document.

  2. The frame had no accessible name. webpack-dev-server sets title on its iframe; this one did not, leaving it announced by its about:blank url.

One subtlety worth pointing out, because it is where the two overlays genuinely differ: this one paginates, and paginating re-renders the card. Focusing on every render would take focus off the ‹ › button being clicked, so only the render that opens the overlay takes focus.

On the wider "merge the overlays" step

I had said this would be a dedupe, with this package's overlay being the superset that wins. Having actually compared them, that was wrong, and I would rather correct it than let it shape the next PR:

webpack-dev-server webpack-dev-middleware
lines 738 873
architecture state machine module-level state, no machine
extra options — styles, ansiColors, openEditorEndpoint, paginate
shared trustedTypesPolicyName, catchRuntimeError, errors/warnings/runtimeErrors filters same

They are two implementations with overlapping, not nested, feature sets. This PR closes the behavioral gaps so nothing is lost by switching; it deliberately does not attempt to reconcile the architectures.

formatProblem, which webpack-dev-server exports from its overlay, is not a gap: this package formats build problems server-side in src/hot.js and runtime errors inside the overlay, so the same work happens in a different place.

What kind of change does this PR introduce?

fix

Did you add tests for your changes?

Yes — one end-to-end case that focuses an input, triggers a build error, and checks that focus lands on the close button inside the frame and returns to the input after Escape, plus that the frame has a title.

Teeth-checked in both halves: removing the closeButton.focus() fails it, and separately removing the focus restoration fails it on the focus-me assertion. All 36 overlay cases and all 82 end-to-end cases pass, re-run after the rebase.

A correction to what I claimed on #2420 and #2421: I said the browser end-to-end suites could not run in my sandbox, and that this was environmental because they failed the same way on a clean main. That was wrong. They fail when jest is invoked without --experimental-vm-modules — which npm run test:e2e passes and I was not — and puppeteer then needs pointing at a browser. With both fixed they run fine, and I have now run the whole suite locally, including the two client.test.js snapshots that CI caught on #2421 and that I had only reasoned about rather than executed.

test/logging.test.js still fails 74/74 on a clean main, unrelated to any of this.

Does this PR introduce a breaking change?

No. The overlay gains focus behavior it did not have; nothing about its appearance, options or dismissal changes.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

Nothing — this restores expected behavior rather than adding a knob.

Use of AI

AI-assisted (Claude Code). It was used to compare the two overlays, write the fix and the test, and verify it: the test was run against both halves of the change reverted in turn to confirm it fails, and the full end-to-end suite was run locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

Summary by CodeRabbit

  • Improvements
    • The error overlay now keeps keyboard focus on the relevant navigation control when paging through problems and restores focus to the previously active control when closed, if it is still available.
    • The overlay frame has an accessible name to help assistive technologies identify it.
    • Connection feedback is now consistent across supported transports.
  • Bug Fixes
    • Events received after a connection closes are ignored, preventing stale activity from being processed.

@changeset-bot

changeset-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3eb4580

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-middleware Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Base automatically changed from feat/client-transports to main September 24, 2026 18:57
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 997c1a80-5162-4c73-9f4a-34a14a6dc25d

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1baa1 and 422e901.

📒 Files selected for processing (6)
  • .changeset/overlay-focus.md
  • client-src/overlay.js
  • test/client-transports.test.js
  • test/e2e/overlay.test.js
  • test/e2e/transport.test.js
  • test/helpers/hot-app.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • test/client-transports.test.js
  • test/e2e/overlay.test.js
  • .changeset/overlay-focus.md
  • test/e2e/transport.test.js
  • client-src/overlay.js
  • test/helpers/hot-app.js

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


Walkthrough

The error overlay saves the host page’s deepest active element when it opens and focuses its Close button after the opening render. When cleared, it restores focus if the saved element remains connected and supports focus. Later renders preserve focus on the corresponding in-card control or move it to Close. The iframe receives an accessible title. Client transport handlers suppress specified events after closure and no longer log raw connection errors. The shared socket open path logs “connected.” Test helpers and end-to-end tests cover overlay behavior and both SSE and WebSocket transports. Changesets describe the updates.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 422e9

The overlay test may not catch error markup disappearing. Confirm that it verifies the markup is displayed as text before merging.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary overlay accessibility change: moving focus to the overlay on open and restoring it on close. It does not mention the additional transport changes, but the title…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 9 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 60de74d1-aae2-4bf1-81b1-f716bc931dc2

📥 Commits

Reviewing files that changed from the base of the PR and between eed5f19 and df7df50.

📒 Files selected for processing (3)
  • .changeset/overlay-focus.md
  • client-src/overlay.js
  • test/e2e/overlay.test.js

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread client-src/overlay.js Outdated
Comment thread client-src/overlay.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/e2e/overlay.test.js (1)

1016-1019: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that the markup appears as text.

The current assertions pass when the overlay drops the submitted <img> markup. Assert that the frame text contains the markup while retaining the checks that no image renders or executes.

Suggested fix
+    expect(await frame.evaluate(() => document.body.textContent)).toContain(
+      "<img src=x onerror='globalThis.xss=1'>",
+    );
     expect(
       await frame.evaluate(() => document.querySelectorAll("img").length),

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: cbebb2d4-2d29-4657-918d-1c1506454b9f

📥 Commits

Reviewing files that changed from the base of the PR and between df7df50 and cdc7aa1.

📒 Files selected for processing (1)
  • test/e2e/overlay.test.js

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 90749554-0828-4da5-a83b-d7cf168f188a

📥 Commits

Reviewing files that changed from the base of the PR and between cdc7aa1 and fda4561.

📒 Files selected for processing (5)
  • .changeset/connected-log-both-transports.md
  • client-src/clients/EventSourceClient.js
  • client-src/clients/createSocket.js
  • test/e2e/transport.test.js
  • test/helpers/hot-app.js
💤 Files with no reviewable changes (1)
  • client-src/clients/EventSourceClient.js

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread test/helpers/hot-app.js Outdated
Comment thread test/helpers/hot-app.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 043d3f56-11d7-4483-922a-5fb04f2ab87e

📥 Commits

Reviewing files that changed from the base of the PR and between fda4561 and 2d1baa1.

📒 Files selected for processing (5)
  • .changeset/transport-parity.md
  • client-src/clients/EventSourceClient.js
  • client-src/clients/WebSocketClient.js
  • test/client-socket.test.js
  • test/client-transports.test.js

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.

Comment thread test/client-transports.test.js
@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.19%. Comparing base (eed5f19) to head (3eb4580).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2423      +/-   ##
==========================================
+ Coverage   95.45%   96.19%   +0.73%     
==========================================
  Files          17       17              
  Lines        1849     1890      +41     
==========================================
+ Hits         1765     1818      +53     
+ Misses         84       72      -12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

alexander-akait and others added 5 commits September 26, 2026 11:56
The overlay opened without touching focus, so reaching it meant clicking
into the frame first: Escape and the arrow keys did nothing until then, and
a screen reader stayed wherever the page was. Its frame had no accessible
name either, leaving it announced by its `about:blank` url.

Only the render that opens it takes focus — paginating re-renders the card,
and moving focus then would take it off the button being clicked.

These are the parts webpack-dev-server's own overlay has and this one did
not, which it would otherwise lose by switching to this one.
…d no test for

Mapping webpack-dev-server's 39 end-to-end overlay cases onto this one's 36
found six behaviors it relies on that nothing here pinned: an `errors`
filter keeping one problem and rejecting all of them, a failure in the very
first build (which arrives through the catch-up sync rather than a rebuild),
one broken rebuild replacing another's overlay, Escape working a second
time, and markup in a build error reaching the card as text.

All six already behave the way webpack-dev-server's do — these are the
tests, not fixes.
`connected` was logged by the Server-Sent Events transport itself, so a page
using the WebSocket one got no sign it had connected at all. It is said by
the shared socket now, on the same event, so neither transport can be the
one that stays quiet.

Found by running the same four behaviors over both: connecting and applying
a hot update, falling back to a reload when nothing accepts it, showing and
clearing a build error, and catching up a client on what it missed. The
harness takes `transport: "ws"`, which also exercises `attach` — the
WebSocket path had no browser coverage before this.
The WebSocket one logged a raw `error`, which the Server-Sent Events one
never did. Making both say it would have been the wrong way to agree:
`EventSource` fires `error` on every routine reconnection, and a WebSocket
`error` carries no detail by specification, so it prints an opaque object.
Neither logs it now — the shared socket already reports the reconnection,
which is the part worth reading.

The WebSocket one also had no equivalent of the other's `closed` guard, so
a close or message the browser had already queued still reached the caller
after it had closed, and the shared socket would reconnect a connection
nobody wanted.

The contract they share is now tested once and run against each, which is
what turned both of these up, and all three files are fully covered.
Two gaps in the focus handling, both found in review.

`document.activeElement` names a shadow host rather than the control
focused inside it, and focusing a host that does not delegate focus does
nothing at all — so the overlay dropped focus instead of handing it back.
Descend into open roots before remembering what to restore.

Paging through problems rebuilds the card, which destroys the very button
that was clicked; focus then fell back to the frame's body, so a keyboard
user lost the navigation after the first press. Each control is named
across the re-render and the equivalent one takes focus again.

The test helper could not have caught either over a WebSocket, because it
could not restart a server at all: an upgraded socket is no longer the
HTTP server's to close, so `stopHttp()` waited on a connected client
forever, and the replacement server was never handed back to the
transport. Both fixed, and the reconnect story now runs over both
transports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@alexander-akait
alexander-akait merged commit 8bb0fdd into main Sep 26, 2026
19 checks passed
@alexander-akait
alexander-akait deleted the feat/overlay-merge branch September 26, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant