chore: Add internal component to transparently proxy focus through portals
Description
The problem statement
There are a number of components that could render content in portals outside of the standard DOM order. Notably, these are popovers (with renderWithPortal) and dropdown-based components (with expandToViewport). When focus gets moved to these floating components, tabbing past the end causes focus to hit the end of the DOM instead of "resuming" past the trigger. Popovers normally render a focus trap, but the ones used in onboarding don't have a focus trap, so they're affected as well.
My proposal
A generic component, that behaves very much like a portal, but also moves focus through the portal'ed content as if it was rendered inside the DOM. It works by detecting tabbable elements before and after the portal and determining focus direction. Try it locally!
Maybe this logic could be integrated into the standard portal component, but I'm not sure yet.
Also
I renamed <TabTrap /> to <FocusDetector /> (cause it ain't a tab trap), and moved the tabbable utils out into the general utils folder. Also renamed focusables to tabbables because I'm explicitly ignoring elements with tabindex="-1".
In the future, if the portal is the final tabbable element, it would be nice to be able to disable the final tab trap so that the focus naturally moves into the address bar instead of back to the start of the DOM.
How has this been tested?
Only locally, and manually. An integration test is definitely in order, but let's validate the approach first.
Documentation changes
[Do the changes include any API documentation changes?]
- [ ] Yes, this change contains documentation changes.
- [x] No.
Related Links
[Attach any related links/pull request for this change]
Review checklist
The following items are to be evaluated by the author(s) and the reviewer(s).
Correctness
- [x] Changes are backward-compatible if not indicated, see
CONTRIBUTING.md. - [x] Changes do not include unsupported browser features, see
CONTRIBUTING.md. - [ ] Changes were manually tested for accessibility, see accessibility guidelines.
Security
- [x] If the code handles URLs: all URLs are validated through the
checkSafeUrlfunction.
Testing
- [ ] Changes are covered with new/existing unit tests?
- [ ] Changes are covered with new/existing integration tests?
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
Codecov Report
Base: 92.61% // Head: 92.55% // Decreases project coverage by -0.05% :warning:
Coverage data is based on head (
ca3efc1) compared to base (a6e49a9). Patch coverage: 50.00% of modified lines in pull request are covered.
:exclamation: Current head ca3efc1 differs from pull request most recent head 6887662. Consider uploading reports for the commit 6887662 to get more accurate results
Additional details and impacted files
@@ Coverage Diff @@
## main #201 +/- ##
==========================================
- Coverage 92.61% 92.55% -0.06%
==========================================
Files 559 552 -7
Lines 15822 15712 -110
Branches 4328 4301 -27
==========================================
- Hits 14654 14543 -111
Misses 1086 1086
- Partials 82 83 +1
| Impacted Files | Coverage Δ | |
|---|---|---|
| src/internal/components/dropdown/index.tsx | 91.90% <0.00%> (+0.04%) |
:arrow_up: |
| src/internal/components/focus-lock/index.tsx | 83.33% <50.00%> (ø) |
|
| src/internal/utils/tabbables.ts | 70.00% <60.00%> (ø) |
|
| src/internal/components/focus-detector/index.tsx | 100.00% <100.00%> (ø) |
|
| src/breadcrumb-group/internal.tsx | 83.33% <0.00%> (-6.92%) |
:arrow_down: |
| src/internal/utils/scrollable-containers.ts | 93.75% <0.00%> (-6.25%) |
:arrow_down: |
| src/autosuggest/internal.tsx | 89.36% <0.00%> (-4.19%) |
:arrow_down: |
| src/autosuggest/options-list.tsx | 97.22% <0.00%> (-2.78%) |
:arrow_down: |
| src/autosuggest/virtual-list.tsx | 90.90% <0.00%> (-2.43%) |
:arrow_down: |
| ...rc/property-filter/property-filter-autosuggest.tsx | 87.50% <0.00%> (-0.84%) |
:arrow_down: |
| ... and 92 more |
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.
:umbrella: View full report at Codecov.
:loudspeaker: Do you have feedback about the report comment? Let us know in this issue.
"In the future, if the portal is the final tabbable element, it would be nice to be able to disable the final tab trap so that the focus naturally moves into the address bar instead of back to the start of the DOM."
What is the problem with adding this already?
The more I update this component, the more I'm starting to feel that us mere mortals do not deserve to steal control of the natural tab order from the heavens this way. I'm no Prometheus, so I'll leave this back where I found it.
For portals, the focus behavior should probably decided on a component-by-component level. Modals and popovers are tab traps. I'll probably just take a "tab loop" approach with dropdowns. At least I gave this experiment a solid try...