Fix arrow key navigation not reaching filter button in notebook find widget#308429
Fix arrow key navigation not reaching filter button in notebook find widget#308429yogeshwaran-c wants to merge 2 commits intomicrosoft:mainfrom
Conversation
…widget In the notebook find widget, arrow key navigation among toggle buttons only cycled through the three standard toggles (case sensitive, whole words, regex) but skipped the filter (funnel) button added by NotebookFindInput. The root cause was that FindInput hardcoded its arrow-key navigation index list as a local constant in the constructor, making it impossible for subclasses to extend the list with additional controls. Fix: extract the index computation into a protected getOptionNavigationIndexes() method that is called dynamically on each keydown event. The default implementation returns the standard toggles plus any additionalToggles. NotebookFindInput overrides this method to also include the filter button's focusable element, so left/right arrow keys now cycle through all four buttons. Fixes microsoft#207765
|
Please attach a gif of this tested and working, the prior PR you've submitted for this appears to have not fixed the issue. I can review once we've confirmed working functionality, thank you! |
There was a problem hiding this comment.
Pull request overview
This PR fixes keyboard (left/right arrow) navigation in the notebook find widget so that focus can reach the filter (funnel) button, by making the set of navigable option controls extensible from FindInput subclasses.
Changes:
- Refactors
FindInputarrow-key option navigation to compute navigable elements dynamically via a new protectedgetOptionNavigationIndexes()method. - Extends
NotebookFindInputnavigation targets to include the filter button’s focusable element.
Show a summary per file
| File | Description |
|---|---|
src/vs/base/browser/ui/findinput/findInput.ts |
Introduces an overridable method to compute arrow-key navigation targets for option toggles. |
src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts |
Overrides the navigation target list to include the notebook filter button. |
Copilot's findings
Comments suppressed due to low confidence (2)
src/vs/base/browser/ui/findinput/findInput.ts:268
getOptionNavigationIndexes()currently includes controls regardless of whether they are actually meant to be focusable (e.g. in notebooksregex.domNode.tabIndexis set to -1 when filters are active). Because the arrow-key handler calls.focus()unconditionally, navigation can land on elements that were intentionally removed from the tab order. Consider filtering the returned list to only include elements that are focusable (e.g.tabIndex >= 0and notdisplay:none) so arrow navigation matches the current focus model.
protected getOptionNavigationIndexes(): HTMLElement[] {
const indexes: HTMLElement[] = [];
if (this.caseSensitive) {
indexes.push(this.caseSensitive.domNode);
}
if (this.wholeWords) {
indexes.push(this.wholeWords.domNode);
}
if (this.regex) {
indexes.push(this.regex.domNode);
}
for (const toggle of this.additionalToggles) {
indexes.push(toggle.domNode);
}
return indexes;
src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts:266
querySelector('a.action-label')couples the navigation logic to ActionBar’s internal DOM structure/class names. To make this more robust, consider exposing a dedicated focus element fromNotebookFindInputFilterButton(or caching the element when the ActionBar is created) and using that here, rather than querying by a CSS selector on every keydown.
// Include the filter button's focusable element in arrow-key navigation
// so users can reach it with left/right arrow keys in addition to Tab.
const filterFocusable = this._findFilter.container.querySelector<HTMLElement>('a.action-label');
if (filterFocusable) {
indexes.push(filterFocusable);
- Files reviewed: 2/2 changed files
- Comments generated: 2
| /** | ||
| * Returns the list of DOM elements that participate in left/right arrow key | ||
| * navigation among the find input toggle buttons. Subclasses can override | ||
| * this to include additional toggle-like controls. | ||
| */ | ||
| protected getOptionNavigationIndexes(): HTMLElement[] { | ||
| const indexes: HTMLElement[] = []; | ||
| if (this.caseSensitive) { |
There was a problem hiding this comment.
getOptionNavigationIndexes() returns a list of HTMLElements (not numeric indexes). The name/variable naming here is misleading and makes the navigation logic harder to follow. Consider renaming this API to something like getOptionNavigationElements()/getOptionNavigationTargets() (and updating local variable names accordingly).
This issue also appears on line 254 of the same file.
| protected override getOptionNavigationIndexes(): HTMLElement[] { | ||
| const indexes = super.getOptionNavigationIndexes(); | ||
| // Include the filter button's focusable element in arrow-key navigation | ||
| // so users can reach it with left/right arrow keys in addition to Tab. | ||
| const filterFocusable = this._findFilter.container.querySelector<HTMLElement>('a.action-label'); | ||
| if (filterFocusable) { |
There was a problem hiding this comment.
This override always returns super.getOptionNavigationIndexes() (which includes the regex toggle) even when regex has been removed from the tab order (e.g. regex.domNode.tabIndex = -1 when filters are active). That means left/right arrows can still move focus onto a disabled/unfocusable control. Consider filtering out any elements that are not currently focusable (tabIndex < 0 / aria-disabled=true / display:none) before returning the list.
This issue also appears on line 262 of the same file.
| protected override getOptionNavigationIndexes(): HTMLElement[] { | |
| const indexes = super.getOptionNavigationIndexes(); | |
| // Include the filter button's focusable element in arrow-key navigation | |
| // so users can reach it with left/right arrow keys in addition to Tab. | |
| const filterFocusable = this._findFilter.container.querySelector<HTMLElement>('a.action-label'); | |
| if (filterFocusable) { | |
| private isOptionNavigationFocusable(element: HTMLElement | null): element is HTMLElement { | |
| if (!element) { | |
| return false; | |
| } | |
| if (element.tabIndex < 0 || element.getAttribute('aria-disabled') === 'true') { | |
| return false; | |
| } | |
| return dom.getWindow(element).getComputedStyle(element).display !== 'none'; | |
| } | |
| protected override getOptionNavigationIndexes(): HTMLElement[] { | |
| const indexes = super.getOptionNavigationIndexes().filter(element => this.isOptionNavigationFocusable(element)); | |
| // Include the filter button's focusable element in arrow-key navigation | |
| // so users can reach it with left/right arrow keys in addition to Tab. | |
| const filterFocusable = this._findFilter.container.querySelector<HTMLElement>('a.action-label'); | |
| if (this.isOptionNavigationFocusable(filterFocusable)) { |
|
Thanks for the review @Yoyokrazy! I don't have a GUI environment to record a gif right now, but here's exactly how to test:
The fix changes |
Address Copilot review feedback on the notebook find widget arrow-key navigation fix: - Rename `getOptionNavigationIndexes()` -> `getOptionNavigationElements()` in `FindInput` and update local variable names. The method returns `HTMLElement[]`, not numeric indexes, so the prior name was misleading. - In `NotebookFindInput.getOptionNavigationElements()`, filter out controls that are not currently focusable (tabIndex < 0, aria-disabled=true, or display:none) before participating in arrow-key navigation. The notebook sets `regex.domNode.tabIndex = -1` when filters are active, and the previous override could move focus to that disabled control.
|
@Yoyokrazy unfortunately I don't have a desktop session available to capture a gif, but here are the manual steps I've stepped through against this branch and what I expect at each step:
If a gif is a hard requirement before review I'm happy to find a way to record one — but I'd also gladly add an automated DOM-level test (driving keydown events through Separately, addressed both Copilot suggestions in 3d2b180:
|
Fixes #207765
Problem
In the notebook find widget, arrow key navigation among the toggle buttons only cycles through the three standard toggles (case sensitive, whole words, regex) but skips the filter (funnel) button added by
NotebookFindInput.The root cause was that
FindInputhardcoded its arrow-key navigation index list as a local constant captured in a closure during the constructor, making it impossible for subclasses to extend the list with additional controls.Solution
Extract the index computation into a protected
getOptionNavigationIndexes()method that is called dynamically on each keydown event. The default implementation returns the standard toggles plus anyadditionalToggles.NotebookFindInputoverrides this method to also include the filter button's focusable anchor element, so left/right arrow keys now correctly cycle through all four toggle buttons.Files changed
src/vs/base/browser/ui/findinput/findInput.ts— add protectedgetOptionNavigationIndexes()methodsrc/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts— override to include filter button