From 24a7cb0ca06a3389b1465ae37d6e8a83cbed77b5 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 20 Mar 2018 12:47:52 +0100 Subject: [PATCH] Keyboard access (#45589) --- .../browser/parts/quickinput/quickInput.ts | 41 ++++++++ .../browser/parts/quickinput/quickInputBox.ts | 17 +++- .../quickinput/quickInputCheckboxList.ts | 98 ++++++++++++++----- 3 files changed, 130 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index c6d52227e3b..94358b25a9c 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -20,6 +20,8 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { CancellationToken } from 'vs/base/common/cancellation'; import { QuickInputCheckboxList } from './quickInputCheckboxList'; import { QuickInputBox } from './quickInputBox'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; const $ = dom.$; @@ -56,12 +58,35 @@ export class QuickInputService extends Component implements IQuickInputService { this.container.style.display = 'none'; this.inputBox = new QuickInputBox(this.container); + this.toUnbind.push(this.inputBox); this.inputBox.style(this.themeService.getTheme()); this.inputBox.onInput(value => { this.checkboxList.filter(value); }); + this.toUnbind.push(this.inputBox.onKeyDown(event => { + switch (event.keyCode) { + case KeyCode.DownArrow: + this.checkboxList.focus('Next'); + break; + case KeyCode.UpArrow: + this.checkboxList.focus('Previous'); + break; + case KeyCode.PageDown: + this.checkboxList.focus('NextPage'); + break; + case KeyCode.PageUp: + this.checkboxList.focus('PreviousPage'); + break; + case KeyCode.Space: + if (event.ctrlKey) { + this.checkboxList.toggleCheckbox(); + } + break; + } + })); this.checkboxList = this.instantiationService.createInstance(QuickInputCheckboxList, this.container); + this.toUnbind.push(this.checkboxList); const buttonContainer = dom.append(this.container, $('.quick-input-actions')); const cancel = dom.append(buttonContainer, $('button')); @@ -79,6 +104,19 @@ export class QuickInputService extends Component implements IQuickInputService { } this.close(false); })); + this.toUnbind.push(dom.addDisposableListener(this.container, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Enter: + dom.EventHelper.stop(e, true); + this.close(true); + break; + case KeyCode.Escape: + dom.EventHelper.stop(e, true); + this.close(false); + break; + } + })); } private close(ok: boolean) { @@ -92,6 +130,9 @@ export class QuickInputService extends Component implements IQuickInputService { async pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise { this.create(); + if (this.resolve) { + this.resolve(undefined); + } this.inputBox.setPlaceholder(options.placeHolder || ''); // TODO: Progress indication. diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts index e3e99bfbb40..1a2cfb71e6e 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts @@ -11,7 +11,8 @@ import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import * as nls from 'vs/nls'; import { inputBackground, inputForeground, inputBorder } from 'vs/platform/theme/common/colorRegistry'; import { ITheme } from 'vs/platform/theme/common/themeService'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; const $ = dom.$; @@ -19,8 +20,9 @@ const DEFAULT_INPUT_ARIA_LABEL = nls.localize('quickInputBoxAriaLabel', "Type to export class QuickInputBox { - public container: HTMLElement; + private container: HTMLElement; private inputBox: InputBox; + private disposables: IDisposable[] = []; constructor( private parent: HTMLElement @@ -29,6 +31,7 @@ export class QuickInputBox { this.inputBox = new InputBox(this.container, null, { ariaLabel: DEFAULT_INPUT_ARIA_LABEL }); + this.disposables.push(this.inputBox); // ARIA const inputElement = this.inputBox.inputElement; @@ -37,6 +40,12 @@ export class QuickInputBox { inputElement.setAttribute('aria-autocomplete', 'list'); } + onKeyDown(handler: (event: StandardKeyboardEvent) => void): IDisposable { + return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + handler(new StandardKeyboardEvent(e)); + }); + } + onInput(handler: (event: string) => void): IDisposable { return this.inputBox.onDidChange(handler); } @@ -60,4 +69,8 @@ export class QuickInputBox { inputBorder: theme.getColor(inputBorder) }); } + + dispose() { + this.disposables = dispose(this.disposables); + } } diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts b/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts index d973dbf3292..a50fd1b84b5 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts @@ -15,30 +15,57 @@ import { IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; import { IMatch } from 'vs/base/common/filters'; import { matchesFuzzyOcticonAware, parseOcticons } from 'vs/base/common/octicon'; import { compareAnything } from 'vs/base/common/comparers'; +import { Emitter } from 'vs/base/common/event'; +import { assign } from 'vs/base/common/objects'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; const $ = dom.$; -export interface ISelectedElement { +interface ISelectableElement { index: number; item: object; label: string; - shouldAlwaysShow?: boolean; - hidden?: boolean; - selected?: boolean; + selected: boolean; +} + +class SelectableElement implements ISelectableElement { + index: number; + item: object; + label: string; + shouldAlwaysShow = false; + hidden = false; + private _onSelected = new Emitter(); + onSelected = this._onSelected.event; + _selected: boolean; + get selected() { + return this._selected; + } + set selected(value: boolean) { + if (value !== this._selected) { + this._selected = value; + this._onSelected.fire(value); + } + } labelHighlights?: IMatch[]; descriptionHighlights?: IMatch[]; detailHighlights?: IMatch[]; + + constructor(init: ISelectableElement) { + assign(this, init); + } } interface ISelectedElementTemplateData { element: HTMLElement; name: HTMLElement; checkbox: HTMLInputElement; - context: ISelectedElement; - toDispose: IDisposable[]; + context: SelectableElement; + toDisposeElement: IDisposable[]; + toDisposeTemplate: IDisposable[]; } -class SelectedElementRenderer implements IRenderer { +class SelectedElementRenderer implements IRenderer { static readonly ID = 'selectedelement'; @@ -52,8 +79,11 @@ class SelectedElementRenderer implements IRenderer$('input'); data.checkbox.type = 'checkbox'; - data.toDispose = []; - data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => data.context.selected = !data.context.selected)); + data.toDisposeElement = []; + data.toDisposeTemplate = []; + data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { + data.context.selected = data.checkbox.checked; + })); dom.append(data.element, data.checkbox); @@ -62,34 +92,37 @@ class SelectedElementRenderer implements IRenderer data.checkbox.checked = selected)); } - disposeTemplate(templateData: ISelectedElementTemplateData): void { - dispose(templateData.toDispose); + disposeTemplate(data: ISelectedElementTemplateData): void { + dispose(data.toDisposeTemplate); } } -class SelectedElementDelegate implements IDelegate { +class SelectedElementDelegate implements IDelegate { - getHeight(element: ISelectedElement): number { + getHeight(element: SelectableElement): number { return 22; } - getTemplateId(element: ISelectedElement): string { + getTemplateId(element: SelectableElement): string { return SelectedElementRenderer.ID; } } export class QuickInputCheckboxList { - container: HTMLElement; - private list: WorkbenchList; - private elements: ISelectedElement[] = []; + private container: HTMLElement; + private list: WorkbenchList; + private elements: SelectableElement[] = []; + private disposables: IDisposable[] = []; constructor( private parent: HTMLElement, @@ -100,11 +133,18 @@ export class QuickInputCheckboxList { this.list = this.instantiationService.createInstance(WorkbenchList, this.container, delegate, [new SelectedElementRenderer()], { identityProvider: element => element.label, multipleSelectionSupport: false - }) as WorkbenchList; + }) as WorkbenchList; + this.disposables.push(this.list); + this.disposables.push(this.list.onKeyDown(e => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Space) { + this.toggleCheckbox(); + } + })); } setElements(elements: IPickOpenEntry[]): void { - this.elements = elements.map((item, index) => ({ + this.elements = elements.map((item, index) => new SelectableElement({ index, item, label: item.label, @@ -118,9 +158,8 @@ export class QuickInputCheckboxList { .map(e => e.item); } - setFocus(): void { - this.list.focusFirst(); - this.list.domFocus(); + focus(what: 'Next' | 'Previous' | 'NextPage' | 'PreviousPage'): void { + this.list['focus' + what](); } layout(): void { @@ -176,9 +215,20 @@ export class QuickInputCheckboxList { this.list.focusFirst(); } } + + toggleCheckbox() { + const elements = this.list.getFocusedElements(); + for (const element of elements) { + element.selected = !element.selected; + } + } + + dispose() { + this.disposables = dispose(this.disposables); + } } -function compareEntries(elementA: ISelectedElement, elementB: ISelectedElement, lookFor: string): number { +function compareEntries(elementA: SelectableElement, elementB: SelectableElement, lookFor: string): number { const labelHighlightsA = elementA.labelHighlights || []; const labelHighlightsB = elementB.labelHighlights || []; -- GitLab