Skip to content

fix: keep nested fixed targets focusable - #615

Open
Amber (YuanboXue-Amber) wants to merge 2 commits into
microsoft:masterfrom
YuanboXue-Amber:users/yuanboxue/fixed-target-visibility
Open

Amber (YuanboXue-Amber) wants to merge 2 commits into
microsoft:masterfrom
YuanboXue-Amber:users/yuanboxue/fixed-target-visibility

Conversation

@YuanboXue-Amber

@YuanboXue-Amber Amber (YuanboXue-Amber) commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Use a fixed-position target's own client rectangles instead of its parent's offsetParent to detect missing rendered boxes. Visible fixed and display: contents parents can also have a null offsetParent, incorrectly rejecting focusable targets and causing observed focus requests to time out.

Existing visibility, body-size and accessibility guards remain unchanged. No API or retry behavior changes.

Before / after

All rows describe fixed-position targets. "Allowed" means passing the visibility gate; other focusability and accessibility checks still apply.

Situation Before After
Visible, ordinary parent Allowed Allowed
Visible, fixed parent with null offsetParent Incorrectly rejected Allowed
Visible, display: contents parent Incorrectly rejected Allowed
display: none on target or ancestor Rejected Rejected
Target's computed visibility is hidden Rejected Rejected

Validation

  • Five new browser regression cases fail without the fix. Targeted observed-focus, focusability and focus-recovery suites: 48 tests passed in both normal and Shadow DOM modes.
  • Implementation CI passed build, typecheck, lint, formatting and all six browser-test configurations.

Performance

Measured before/after results (Chromium 150)

Synthetic benchmark of the exact before/after helper and visibility caller: 1,684-element document, warmed-up code, alternating execution order, 15-30 samples per version, and 50,000 checks per clean-layout sample.

Median time per visibility check, in microseconds:

Scenario Before After Impact
Fixed target, ordinary parent 1.10 1.25 +14%, about 0.15 microseconds
Fixed target directly under body 1.09 1.24 +14%, about 0.15 microseconds
Non-fixed target (unchanged path) 0.74 0.73 Effectively unchanged
Fixed target, width changed before each check 34.1 34.4 +0.9%; within run variation

A scan of 180 ordinary and 20 fixed targets took 0.153 -> 0.156 ms, with substantial timing overlap.

For the corrected cases, fixed-parent checks took 0.83 -> 1.26 microseconds, and display: contents parent checks took 0.81 -> 1.26 microseconds. These are not equivalent-work comparisons: the old code incorrectly rejected early; the new code also reaches the existing body-size guard.

Chromium layout counters:

Workload Before After
10,000 checks, clean layout 0 0
200 checks, a width change before each 200 200
20 scans, one container-width change before each 20 20

Style-recalculation counts matched too. The new read added processing cost but no additional layouts in these cases.

Conclusion: about 0.15 ms extra per 1,000 previously valid fixed-target checks in this benchmark. This is a Chromium/Electron microbenchmark on one machine, not an end-to-end or cross-browser performance guarantee.

A null parent offsetParent does not establish that a fixed target is hidden. Use the target's rendered client rectangles and preserve existing visibility/accessibility checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
tabster
all exports
110.948 kB
30.14 kB
110.915 kB
30.134 kB
-33 B
-6 B
tabster
createTabster (core)
38.569 kB
11.569 kB
38.536 kB
11.568 kB
-33 B
-1 B
tabster
focusable.findAll
38.592 kB
11.577 kB
38.559 kB
11.577 kB
-33 B
tabster
focusable.findLast
38.593 kB
11.577 kB
38.56 kB
11.576 kB
-33 B
-1 B
tabster
focusable.findNext
38.593 kB
11.577 kB
38.56 kB
11.576 kB
-33 B
-1 B
tabster
focusable.findPrev
38.593 kB
11.577 kB
38.56 kB
11.576 kB
-33 B
-1 B
tabster
getCrossOrigin
107.881 kB
29.426 kB
107.848 kB
29.423 kB
-33 B
-3 B
tabster
getDeloser
47.93 kB
13.853 kB
47.897 kB
13.849 kB
-33 B
-4 B
tabster
getGroupper
45.671 kB
13.236 kB
45.638 kB
13.235 kB
-33 B
-1 B
tabster
getModalizer
47.596 kB
13.995 kB
47.563 kB
13.995 kB
-33 B
tabster
getMover
53.128 kB
15.489 kB
53.095 kB
15.491 kB
-33 B
2 B
tabster
getObservedElement
44.334 kB
13.138 kB
44.301 kB
13.135 kB
-33 B
-3 B
tabster
getOutline
47.204 kB
13.807 kB
47.171 kB
13.81 kB
-33 B
3 B
tabster
getRestorer
41.154 kB
12.166 kB
41.121 kB
12.165 kB
-33 B
-1 B

🤖 This report was generated against cc60426d5ab690bc33f01fb39144f2afd9d3ef0e

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@YuanboXue-Amber
Amber (YuanboXue-Amber) marked this pull request as ready for review November 12, 2026 14:56

This branch has not been deployed

No deployments
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