-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat: Automatically manage focus tree tab indexes #9079
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
feat: Automatically manage focus tree tab indexes #9079
Conversation
This is still a work-in-progress change, and it may be adapted to include managing node tab indexes.
@BenHenning what are the next steps for this to be mergeable? |
@rachel-fenichel it can probably go into review now (and I will do that later today). I want to spend a bit of time seeing if I can get automatically managed node tab indexes working before fully giving up on that. Edit: Actually there is one bug I'm re-remembering now that I'm testing this locally that still needs to be fixed yet (sometimes the flyout doesn't auto-collapse). |
The following scenario breaks still:
This is related to google/blockly-keyboard-experimentation#547. |
Uh oh. |
This removes Flyout's ability to be tab-focused and insteads relies on treating it as a workspace, removing the extra tab stop. This fixes an issue with FocusManager getting semi-permanently out of sync with the DOM. This reintroduces automatic tab management for nodes, and fixes an issue with auto tab management for trees that would break tabbing order.
NB: This should be fully working now, but tests & some clean-up need to come yet. I also think that I'm going to be a bit stricter on tab management in Edit: Actually, I just realized that |
Decided to include a fix for #8970 since I was in the neighborhood. |
From @rachel-fenichel's testing:
|
Also fix a potential edge case with the auto tab index management overwriting an existing tab index (since it can sometimes be necessary as indicated by the doc update for IFocusableNode).
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.
Self-reviewed (mostly a spot check for anything obvious).
Things left to do in/for this PR:
|
I think this can be reviewed now, though please note there will need to probably be 1 more pass once all of the above items are addressed. |
} | ||
|
||
/** See IFocusableTree.getRootFocusableNode. */ | ||
getRootFocusableNode(): IFocusableNode { | ||
return this; | ||
throw new Error('Flyouts are not directly focusable.'); |
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.
Can you add a deprecation warning to these so we can remove them in v13?
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.
Done in latest.
core/focus_manager.ts
Outdated
* not currently registered. | ||
*/ | ||
private lookUpRegistration(tree: IFocusableTree): TreeRegistration | null { | ||
const index = this.registeredTrees.findIndex((reg) => reg.tree === tree); |
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.
Why are you using findIndex
instead of find
since you're returning the element anyway, not the index?
Though I'm wondering if this should be a map of registered trees to IDs and not an array. Does the order in which trees are registered matter?
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.
No reason not to use find
honestly--updated.
As for order, it doesn't matter* and the size of the array will be quite small (there aren't many trees). I'm also not keen on relying on the stability of the tree's root's element ID for registration correctness (though it should technically be reliable per contracts).
* The order can theoretically matter, but only if trees are violating their contracts.
core/focus_manager.ts
Outdated
* @param focusableNode The node that should receive active focus. | ||
*/ | ||
focusNode(focusableNode: IFocusableNode): void { | ||
this.ensureManagerIsUnlocked(); | ||
if (!this.currentlyHoldsEphemeralFocus) { | ||
const mustRestoreUpdatingNodeFlag = !this.currentlyHoldsEphemeralFocus; |
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.
nit: don't use the word flag
in the name, we already know it's a boolean
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.
Ah nice catch--surprised I did that. Fixed!
core/focus_manager.ts
Outdated
} | ||
node.onNodeFocus(); | ||
this.lockFocusStateChanges = false; | ||
|
||
// Only overwrite the tab index if it isn't a tree root that's auto managed. |
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.
I find this confusing, could you explain it and consider rewriting the comment?
You say you're going to overwrite the tab index, but you only set it if there wasn't a tab index attribute to start with. It's hard for me to call that overwriting. I think you have to add this because you can't focus an element that doesn't have a tabindex, and you removed the tabindex from a bunch of the html for core blockly elements like fields. Is that right? If so, that would be helpful context to put in the comment instead of saying "overwrite"
On a separate confusion, it would help if you explained why you don't set it if it is a tree root that's auto managed. Is the only reason because you already set it to -1 above?
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.
This is, admittedly, a very confusing comment. :)
... I think you have to add this because you can't focus an element that doesn't have a tabindex, and you removed the tabindex from a bunch of the html for core blockly elements like fields. Is that right? If so, that would be helpful context to put in the comment instead of saying "overwrite"
This is correct. It's documented in IFocusableNode
(where I think it's much more important to explain), but I completely agree that 'overwrite' is the wrong terminology here and the context is important. I've tried to clarify the different cases that this bit affects, and why.
On a separate confusion, it would help if you explained why you don't set it if it is a tree root that's auto managed. Is the only reason because you already set it to -1 above?
Yep, this is the reason, and I've attempted to also clarify it in the latest changes.
Hopefully this is much more clear now.
NB: The plan to fix #9105 is no longer part of this PR. It's not blocking MakeCode (since they use their own flyout implementation), and the fix is actually likely to be a bit tricky with the way we automatically open/close the flyout based on focus. |
Main documentation and code clarity improvements.
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.
Self-reviewed all the latest changes.
With google/blockly-keyboard-experimentation#557 in place and all the tasks listed above addressed (including reviewer comments), I think this is ready again for review. Fortunately, there weren't many changes needed here. PTAL @maribethb. |
Thanks @maribethb for the review, and @rachel-fenichel for helping with testing! |
Fixes part of google/blockly#8915 Fixes part of google/blockly#9020 This PR does a few things: - It introduces some new helpers to simplify arrow key inputs across all of the webdriver tests. - It introduces a new focusable div element that exists in the tab order before toolbox (so that back navigation can be tested). - It renames a few functions for clarity: `setCurrentCursorNodeById` -> `focusOnBlock` and `setCurrentCursorNodeByIdAndFieldName` -> `focusOnBlockField`. This is closer to what the functions are actually doing, and it moves away from cursor verbiage (which could become confusing in the future once the cursor is removed). - It introduces some robustness sanity checks for test utility functions. These should fail if an assumption isn't met rather than return null or undefined--failing fast is really useful in tests to avoid hiding legitimate failures. - It introduces a whole test suite for toolbox and flyout (though a lot more tests can be added). This specifically emphasizes regressions fixed by google/blockly#9079.
The basics
The details
Resolves
Fixes #8965
Fixes #8978
Fixes #8970
Fixes google/blockly-keyboard-experimentation#523
Fixes google/blockly-keyboard-experimentation#547
Fixes part of #8910
Proposed Changes
Fives groups of changes are included in this PR:
FocusManager
losing DOM syncing that was introduced in fix: UpdatefocusNode
to self correct focus #9082.FocusManager
.Reason for Changes
Infrastructure changes reasoning:
FocusManager
. This facilitates a more automated navigation experience.try{} finally{}
could be warranted, but the code will stay as-is unless requested otherwise.Flyout
changes:Flyout
no longer needs to be a focusable tree, but removing that would be an API breakage. Instead, it throws for most of the normal tree/node calls as it should no longer be used as such. Instead, its workspace has been made top-level tabbable (in addition to the main workspace) which solves the extra tab stop issues and general confusing inconsistencies between the flyout, toolbox, and workspace.Flyout
now correctly auto-selects the first block (Flyout only default focuses blocks #9103 notwithstanding). Technically it did before, however the extraFlyout
tabstop before its workspace caused the inconsistency (since focusing theFlyout
itself did not auto-select, only selecting its workspace did).Important caveats:
getAttribute
is used in place of directly fetching.tabIndex
since the latter can apparently default to-1
(and possibly0
) in cases when it's not actually set. This is a very surprising behavior that leads to incorrect test results.focus()
calls or clicking). This is demonstrated both by updates toFocusManager
's tests as well as toolbox's category and separator. This can be slightly tricky to miss as large parts of Blockly now depend on focus to represent their state, so clicking either needs to be managed by Blockly (with correspondingfocusNode
calls) or automatic (with a tab index defined for the element that can be clicked, or which has a child that can be clicked).Note that nearly all elements used for testing focus in the test
index.html
page have had their tab indexes removed to lean onFocusManager
's automatic tab management (though as mentioned above there is still some manual tab index management required forfocus()
-specific tests).Test Coverage
New tests were added for all of the updated behaviors to
FocusManager
, including a new need to explicitly provide (and reset) tab indexes for allfocus()
-esque tests. This also includes adding new tests for some behaviors introduced in past PRs (a la #8910).Note that all of the new and affected conditionals in
FocusManager
have been verified as having at least 1 test that breaks when it's removed (inverted conditions weren't thoroughly tested, but it's expected that they should also be well covered now).Additional tests to cover the actual navigation flows will be added to the keyboard experimentation plugin repository as part of google/blockly-keyboard-experimentation#557 (this PR needs to be merged first).
For manual testing, I mainly verified keyboard navigation with some cursory mouse & click testing in the simple playground. @rachel-fenichel also performed more thorough mouse & click testing (that yielded an actual issue that was fixed--see discussion below).
The core webdriver tests have been verified to have seemingly the same existing failures with and without these changes.
All of the following new keyboard navigation plugin tests have been verified as failing without the fixes introduced in this branch (and passing with them):
Tab navigating to flyout should auto-select first block
Keyboard nav to different toolbox category should auto-select first block
Keyboard nav to different toolbox category and block should select different block
Tab navigate away from toolbox restores focus to initial element
Tab navigate away from toolbox closes flyout
Tab navigate away from flyout to toolbox and away closes flyout
Tabbing to the workspace after selecting flyout block should close the flyout
Tabbing to the workspace after selecting flyout block via workspace toolbox shortcut should close the flyout
Tabbing back from workspace should reopen the flyout
Navigation position in workspace should be retained when tabbing to flyout and back
Clicking outside Blockly with focused toolbox closes the flyout
Clicking outside Blockly with focused flyout closes the flyout
Clicking on toolbox category focuses it and opens flyout
Documentation
No documentation changes are needed beyond the code doc changes included in the PR.
Additional Information
An additional PR will be introduced for the keyboard experimentation plugin repository to add tests there (see test coverage above). This description will be updated with a link to that PR once it exists.