Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 58 additions & 24 deletions src/vs/base/browser/ui/findinput/findInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class FindInput extends Widget {
protected additionalToggles: Toggle[] = [];
public readonly domNode: HTMLElement;
public readonly inputBox: HistoryInputBox;
private allTogglesDomNodes: HTMLElement[] = [];

private readonly _onDidOptionChange = this._register(new Emitter<boolean>());
public get onDidOptionChange(): Event<boolean /* via keyboard */> { return this._onDidOptionChange.event; }
Expand Down Expand Up @@ -164,36 +165,41 @@ export class FindInput extends Widget {
this._register(this.caseSensitive.onKeyDown(e => {
this._onCaseSensitiveKeyDown.fire(e);
}));
}

// Arrow-Key support to navigate between options
const indexes = [this.caseSensitive.domNode, this.wholeWords.domNode, this.regex.domNode];
this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Escape)) {
const index = indexes.indexOf(<HTMLElement>this.domNode.ownerDocument.activeElement);
if (index >= 0) {
let newIndex: number = -1;
if (event.equals(KeyCode.RightArrow)) {
newIndex = (index + 1) % indexes.length;
} else if (event.equals(KeyCode.LeftArrow)) {
if (index === 0) {
newIndex = indexes.length - 1;
} else {
newIndex = index - 1;
}
}
// Arrow-Key support to navigate between all toggle options (common + additional)
// This will be set up after additional toggles are added
this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Escape)) {
// Only handle navigation if we have toggles
if (this.allTogglesDomNodes.length === 0) {
return;
}

if (event.equals(KeyCode.Escape)) {
indexes[index].blur();
this.inputBox.focus();
} else if (newIndex >= 0) {
indexes[newIndex].focus();
const index = this.allTogglesDomNodes.indexOf(<HTMLElement>this.domNode.ownerDocument.activeElement);
if (index >= 0) {
let newIndex: number = -1;
if (event.equals(KeyCode.RightArrow)) {
newIndex = (index + 1) % this.allTogglesDomNodes.length;
} else if (event.equals(KeyCode.LeftArrow)) {
if (index === 0) {
newIndex = this.allTogglesDomNodes.length - 1;
} else {
newIndex = index - 1;
}
}

dom.EventHelper.stop(event, true);
if (event.equals(KeyCode.Escape)) {
this.allTogglesDomNodes[index].blur();
this.inputBox.focus();
} else if (newIndex >= 0) {
this.allTogglesDomNodes[newIndex].focus();
}

dom.EventHelper.stop(event, true);
}
});
}
}
});

this.controls = document.createElement('div');
this.controls.className = 'controls';
Expand Down Expand Up @@ -305,6 +311,34 @@ export class FindInput extends Widget {
}

this.updateInputBoxPadding();
this.updateToggleTabIndexes();
}

/**
* Updates the tabIndex of all toggles to implement a focus group.
* Only the first toggle is tabbable (tabIndex=0), others are not (tabIndex=-1).
* Arrow keys can be used to navigate between toggles within the group.
*/
private updateToggleTabIndexes(): void {
// Collect all toggle DOM nodes in order
this.allTogglesDomNodes = [];
if (this.caseSensitive) {
this.allTogglesDomNodes.push(this.caseSensitive.domNode);
}
if (this.wholeWords) {
this.allTogglesDomNodes.push(this.wholeWords.domNode);
}
if (this.regex) {
this.allTogglesDomNodes.push(this.regex.domNode);
}
for (const toggle of this.additionalToggles) {
this.allTogglesDomNodes.push(toggle.domNode);
}

// Set tabIndex: only first toggle is tabbable
for (let i = 0; i < this.allTogglesDomNodes.length; i++) {
this.allTogglesDomNodes[i].tabIndex = i === 0 ? 0 : -1;
}
}

private updateInputBoxPadding(controlsHidden = false) {
Expand Down
122 changes: 122 additions & 0 deletions src/vs/base/test/browser/ui/findinput/findInput.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import { FindInput } from '../../../../browser/ui/findinput/findInput.js';
import { Toggle, unthemedToggleStyles } from '../../../../browser/ui/toggle/toggle.js';
import { unthemedInboxStyles } from '../../../../browser/ui/inputbox/inputBox.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.js';

suite('FindInput Toggle Focus', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();

test('Toggle focus group: only first toggle should be tabbable', () => {
const container = document.createElement('div');
document.body.appendChild(container);
store.add({ dispose: () => container.remove() });

const additionalToggles = [
store.add(new Toggle({ title: 'Toggle 1', isChecked: false, ...unthemedToggleStyles })),
store.add(new Toggle({ title: 'Toggle 2', isChecked: false, ...unthemedToggleStyles }))
];

store.add(new FindInput(container, undefined, {
label: 'test',
inputBoxStyles: unthemedInboxStyles,
toggleStyles: unthemedToggleStyles,
additionalToggles
}));

// Verify that only the first toggle is tabbable
assert.strictEqual(additionalToggles[0].domNode.tabIndex, 0, 'First toggle should have tabIndex 0');
assert.strictEqual(additionalToggles[1].domNode.tabIndex, -1, 'Second toggle should have tabIndex -1');
});

test('Toggle focus group with common toggles: only first toggle should be tabbable', () => {
const container = document.createElement('div');
document.body.appendChild(container);
store.add({ dispose: () => container.remove() });

const additionalToggles = [
store.add(new Toggle({ title: 'Toggle 1', isChecked: false, ...unthemedToggleStyles })),
store.add(new Toggle({ title: 'Toggle 2', isChecked: false, ...unthemedToggleStyles }))
];

const findInput = store.add(new FindInput(container, undefined, {
label: 'test',
inputBoxStyles: unthemedInboxStyles,
toggleStyles: unthemedToggleStyles,
showCommonFindToggles: true,
additionalToggles
}));

// Get the common toggles from the findInput
const caseSensitive = (findInput as any).caseSensitive;
const wholeWords = (findInput as any).wholeWords;
const regex = (findInput as any).regex;

// Verify that only the first toggle (caseSensitive) is tabbable
assert.strictEqual(caseSensitive.domNode.tabIndex, 0, 'Case sensitive toggle should have tabIndex 0');
assert.strictEqual(wholeWords.domNode.tabIndex, -1, 'Whole words toggle should have tabIndex -1');
assert.strictEqual(regex.domNode.tabIndex, -1, 'Regex toggle should have tabIndex -1');
assert.strictEqual(additionalToggles[0].domNode.tabIndex, -1, 'Additional toggle 1 should have tabIndex -1');
assert.strictEqual(additionalToggles[1].domNode.tabIndex, -1, 'Additional toggle 2 should have tabIndex -1');
});

test('Toggle focus group: updating additional toggles maintains focus group behavior', () => {
const container = document.createElement('div');
document.body.appendChild(container);
store.add({ dispose: () => container.remove() });

const initialToggles = [
store.add(new Toggle({ title: 'Toggle 1', isChecked: false, ...unthemedToggleStyles }))
];

const findInput = store.add(new FindInput(container, undefined, {
label: 'test',
inputBoxStyles: unthemedInboxStyles,
toggleStyles: unthemedToggleStyles,
additionalToggles: initialToggles
}));

// Verify initial state
assert.strictEqual(initialToggles[0].domNode.tabIndex, 0, 'First toggle should have tabIndex 0');

// Update with new toggles
const newToggles = [
store.add(new Toggle({ title: 'New Toggle 1', isChecked: false, ...unthemedToggleStyles })),
store.add(new Toggle({ title: 'New Toggle 2', isChecked: false, ...unthemedToggleStyles }))
];
findInput.setAdditionalToggles(newToggles);

// Verify that only the first toggle is tabbable after update
assert.strictEqual(newToggles[0].domNode.tabIndex, 0, 'First new toggle should have tabIndex 0');
assert.strictEqual(newToggles[1].domNode.tabIndex, -1, 'Second new toggle should have tabIndex -1');
});

test('Toggle focus group with only common toggles and no additional toggles', () => {
const container = document.createElement('div');
document.body.appendChild(container);
store.add({ dispose: () => container.remove() });

const findInput = store.add(new FindInput(container, undefined, {
label: 'test',
inputBoxStyles: unthemedInboxStyles,
toggleStyles: unthemedToggleStyles,
showCommonFindToggles: true
// No additionalToggles provided
}));

// Get the common toggles from the findInput instance
const caseSensitive = (findInput as any).caseSensitive;
const wholeWords = (findInput as any).wholeWords;
const regex = (findInput as any).regex;

// Verify that only the first toggle is tabbable
assert.strictEqual(caseSensitive.domNode.tabIndex, 0, 'Case sensitive toggle should have tabIndex 0');
assert.strictEqual(wholeWords.domNode.tabIndex, -1, 'Whole words toggle should have tabIndex -1');
assert.strictEqual(regex.domNode.tabIndex, -1, 'Regex toggle should have tabIndex -1');
});
});
Loading