Skip to content

Commit

Permalink
Keyboard access (#45589)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Mar 26, 2018
1 parent 7cd8795 commit 24a7cb0
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 26 deletions.
41 changes: 41 additions & 0 deletions src/vs/workbench/browser/parts/quickinput/quickInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.$;

Expand Down Expand Up @@ -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'));
Expand All @@ -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) {
Expand All @@ -92,6 +130,9 @@ export class QuickInputService extends Component implements IQuickInputService {

async pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions, token?: CancellationToken): TPromise<T[]> {
this.create();
if (this.resolve) {
this.resolve(undefined);
}

this.inputBox.setPlaceholder(options.placeHolder || '');
// TODO: Progress indication.
Expand Down
17 changes: 15 additions & 2 deletions src/vs/workbench/browser/parts/quickinput/quickInputBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ 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.$;

const DEFAULT_INPUT_ARIA_LABEL = nls.localize('quickInputBoxAriaLabel', "Type to narrow down results.");

export class QuickInputBox {

public container: HTMLElement;
private container: HTMLElement;
private inputBox: InputBox;
private disposables: IDisposable[] = [];

constructor(
private parent: HTMLElement
Expand All @@ -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;
Expand All @@ -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);
}
Expand All @@ -60,4 +69,8 @@ export class QuickInputBox {
inputBorder: theme.getColor(inputBorder)
});
}

dispose() {
this.disposables = dispose(this.disposables);
}
}
98 changes: 74 additions & 24 deletions src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>();
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<ISelectedElement, ISelectedElementTemplateData> {
class SelectedElementRenderer implements IRenderer<SelectableElement, ISelectedElementTemplateData> {

static readonly ID = 'selectedelement';

Expand All @@ -52,8 +79,11 @@ class SelectedElementRenderer implements IRenderer<ISelectedElement, ISelectedEl

data.checkbox = <HTMLInputElement>$('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);

Expand All @@ -62,34 +92,37 @@ class SelectedElementRenderer implements IRenderer<ISelectedElement, ISelectedEl
return data;
}

renderElement(element: ISelectedElement, index: number, data: ISelectedElementTemplateData): void {
renderElement(element: SelectableElement, index: number, data: ISelectedElementTemplateData): void {
dispose(data.toDisposeElement);
data.context = element;
data.name.textContent = element.label;
data.element.title = data.name.textContent;
data.checkbox.checked = element.selected;
data.toDisposeElement.push(element.onSelected(selected => data.checkbox.checked = selected));
}

disposeTemplate(templateData: ISelectedElementTemplateData): void {
dispose(templateData.toDispose);
disposeTemplate(data: ISelectedElementTemplateData): void {
dispose(data.toDisposeTemplate);
}
}

class SelectedElementDelegate implements IDelegate<ISelectedElement> {
class SelectedElementDelegate implements IDelegate<SelectableElement> {

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<ISelectedElement>;
private elements: ISelectedElement[] = [];
private container: HTMLElement;
private list: WorkbenchList<SelectableElement>;
private elements: SelectableElement[] = [];
private disposables: IDisposable[] = [];

constructor(
private parent: HTMLElement,
Expand All @@ -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<ISelectedElement>;
}) as WorkbenchList<SelectableElement>;
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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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 || [];
Expand Down

0 comments on commit 24a7cb0

Please sign in to comment.