-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tabs: sync browser focus to selected tab in controlled mode #56658
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for working on this!
It would be great if we had unit tests, so that we could also test for alternative solution much quicker!
I would also love to hear from @diegohaz in case he had any thoughts on alternative solutions
Flaky tests detected in e4a28c0. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7143785893
|
Tricky one. Refocusing can be very disorienting, particularly when you don't know why it happened, so should be avoided. But at the same time, being out of sync with a control can also be confusing, particularly when focus is tied to selection state. I'd err on not switching focus over staying in sync, but if we can prevent that from happening too then so much the better. Looks like you're already accounting for |
308a036
to
ea1e323
Compare
Perfect, thank you for the input! And yes, this PR should address that issue. |
The code is looking good! Could you also add a unit test for the desired behaviour (both controlled/uncontrolled, selectOnMove true/false) ? |
I've added the new tests, but they're currently flakey. It doesn't look like the tests themselves, as they pass consistently when run individually. It's when I run them together that they fail. There are two new uncontrolled mode tests, and I only see the second one occasionally being flaky, not the first. There are four new controlled mode tests, and the flakiness there also appears isolated to the second, third, and/or fourth but not the first. All of that to say, it really feels like something is leaking between tests but I can't tell what, and I haven't yet figured out how to prevent it. One other note: there isn't really a way to test the new behavior of this PR in uncontrolled mode, due to the nature of the changes, so the uncontrolled tests are really only testing that I'm going to switch tasks for a bit, but I'll circle back to continue investigating what's leaking between these tests later. |
09ce42d
to
0738fd7
Compare
The |
CI checks are finally passing! One of them was oddly stubborn, but passes consistently locally, and is now fine here as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not something for this PR but rather for a potential follow-up:
I may be forgetting some context about past decisions, but is there a reason why the "Accessibility and semantics", "Focus Behavior", "Tab Attributes", and "Tab Activation" sub-sections in the tests are only testing the uncontrolled version of the component? And why do we have then dedicated "Uncontrolled mode" and "Controlled mode" sections?
I feel like a better structure could be to:
- have a top-level
describe.each
which makes sure that we're testing both uncontrolled and controlled version of the components - for controlled-only and uncontrolled-only, we could have separate sections (or subsections)
If I recall correctly, that structure grew out of how we implemented Controlled Mode initially, with a focus on how tab activation would differ (or not) between the two modes. When I added the "Controlled" and "Uncontrolled" sections originally, it was to test those differences in behavior between the two modes. The tests in those first sections you've mentioned are things that shouldn't be impacted by the behavioral changes between the two modes.
A problem with a top-level All of that said, I'm more than happy to look into reworking the testing suite's structure! At the very least, we can probably run more of the tests through both modes, making sure controlled/uncontrolled doesn't impact them in ways they shouldn't. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
Regarding the structure of the test file, definitely not a priority right now anyway.
What?
In controlled mode, make browser focus follow any changes in tab selection that occur while a rendered tab currently has browser focus.
Why?
In some cases, it's possible for focus to be passed to the selected tab, only to have that selection change immediately afterwards. This could leave keyboard navigation and/or assistive tech in a confusing state where the focused tab does not reflect the content actually being displayed.
Real life example in the editor when tabbing to the sidebar without having a block actively selected.
How?
The main
Tabs
component now wraps its content provider in a div that we can attach a ref to. Using that ref, we can identify what DOM element currently has focus in the browser. Comparing that element's id to our tabs ids, we can then then make sure that the focus remains on the selected tab at the right times (specifically, only if a tab was actively focused when the change occurred).I wasn't able to use Ariakit's
activeId
value for this, because it appears to already follow the selected tab, which can leave it out of sync with browser focus if conditions are right as in the example aboveTesting Instructions
Apply the following diff to perform this test in Storybook:
Notes
In most implementations, if a tab has focus it will automatically be the selected tab, because by default focusing a tab selects it. The exception to this is when
selectOnMove = false
. This means that in controlled mode, with that prop set tofalse
it is possible for a user to be navigating tabs with their arrow keys at the same time that a controlling component forces a selection change. I expect that to be pretty rare, and this change will mostly take effect during race conditions like the one in the example above. I don't anticipate it happening often while someone's casually navigating a bit of UI. It is technically possible though, and with this PR in effect that would move the focus to the newly selected tab. I went back and forth on how to approach that.On the one hand, we could limit this change to only fire when
selectOnMove = true
, or fine-tune it a bit to only take effect if the selected tab specifically was focused when the controlled selection change took effect.On the other hand, I thought the change in focus might be a good way to notify assistive tech that something had just happened, rather than letting it happen silently in the background with no warning. Very much open to feedback on this point! cc @andrewhayward in case you have thoughts.