diff --git a/src/sql/base/browser/dom.ts b/src/sql/base/browser/dom.ts index c2483cfc0e..dd627c1eff 100644 --- a/src/sql/base/browser/dom.ts +++ b/src/sql/base/browser/dom.ts @@ -3,7 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import * as types from 'vs/base/common/types'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; export function isHidden(element: HTMLElement): boolean { return element.style.display === 'none'; @@ -64,3 +68,33 @@ export function getFocusableElements(container: HTMLElement): HTMLElement[] { }); return elements; } + +/** + * Trap the keyboard navigation (Tab/Shift+Tab) inside the specified container + * @param container The container element to trap the keyboard focus in + * @returns The object to be disposed when the trap should be removed. + */ +export function trapKeyboardNavigation(container: HTMLElement): IDisposable { + return addDisposableListener(container, EventType.KEY_DOWN, (e) => { + const focusableElements = getFocusableElements(container); + if (focusableElements.length === 0) { + return; + } + const firstFocusable = focusableElements[0]; + const lastFocusable = focusableElements[focusableElements.length - 1]; + const event = new StandardKeyboardEvent(e); + let elementToFocus = undefined; + if (event.equals(KeyMod.Shift | KeyCode.Tab) && firstFocusable === document.activeElement) { + // Backward navigation + elementToFocus = lastFocusable; + } else if (event.equals(KeyCode.Tab) && lastFocusable === document.activeElement) { + // Forward navigation + elementToFocus = firstFocusable; + } + + if (elementToFocus) { + e.preventDefault(); + elementToFocus.focus(); + } + }); +} diff --git a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts index 4da9550e56..adb9cf3f6f 100644 --- a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts @@ -13,6 +13,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; +import { trapKeyboardNavigation } from 'sql/base/browser/dom'; export type HeaderFilterCommands = 'sort-asc' | 'sort-desc'; @@ -307,6 +308,9 @@ export class HeaderFilter { jQuery(':checkbox', $filter).bind('click', (e) => { this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, jQuery(e.target)); }); + + // No need to add this to disposable store, it will be disposed when the menu is closed. + trapKeyboardNavigation(this.$menu[0]); } public style(styles: ITableFilterStyle): void { diff --git a/src/sql/workbench/browser/modal/modal.ts b/src/sql/workbench/browser/modal/modal.ts index e7526beee9..09a7489e08 100644 --- a/src/sql/workbench/browser/modal/modal.ts +++ b/src/sql/workbench/browser/modal/modal.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/calloutDialog'; import 'vs/css!./media/modal'; -import { getFocusableElements } from 'sql/base/browser/dom'; +import { getFocusableElements, trapKeyboardNavigation } from 'sql/base/browser/dom'; import { Button } from 'sql/base/browser/ui/button/button'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; @@ -13,7 +13,7 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Color } from 'vs/base/common/color'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { mixin } from 'vs/base/common/objects'; @@ -120,8 +120,6 @@ export abstract class Modal extends Disposable implements IThemable { private _messageDetailText?: string; private _spinnerElement?: HTMLElement; - private _firstTabbableElement?: HTMLElement; // The first element in the dialog the user could tab to - private _lastTabbableElement?: HTMLElement; // The last element in the dialog the user could tab to private _focusedElementBeforeOpen?: HTMLElement; private _dialogForeground?: Color; @@ -347,22 +345,6 @@ export abstract class Modal extends Disposable implements IThemable { this.hide('ok'); } - private handleBackwardTab(e: KeyboardEvent) { - this.setFirstLastTabbableElement(); // called every time to get the current elements - if (this._firstTabbableElement && this._lastTabbableElement && document.activeElement === this._firstTabbableElement) { - e.preventDefault(); - this._lastTabbableElement.focus(); - } - } - - private handleForwardTab(e: KeyboardEvent) { - this.setFirstLastTabbableElement(); // called everytime to get the current elements - if (this._firstTabbableElement && this._lastTabbableElement && document.activeElement === this._lastTabbableElement) { - e.preventDefault(); - this._firstTabbableElement.focus(); - } - } - private getTextForClipboard(): string { const eol = this.textResourcePropertiesService.getEOL(URI.from({ scheme: Schemas.untitled })); return this._messageDetailText === '' ? this._messageSummaryText! : `${this._messageSummaryText}${eol}========================${eol}${this._messageDetailText}`; @@ -396,17 +378,6 @@ export abstract class Modal extends Disposable implements IThemable { return this._messageDetailText !== '' || this._messageSummary!.scrollWidth > this._messageSummary!.offsetWidth; } - /** - * Figures out the first and last elements which the user can tab to in the dialog - */ - public setFirstLastTabbableElement() { - const tabbableElements = getFocusableElements(this._bodyContainer!); - if (tabbableElements && tabbableElements.length > 0) { - this._firstTabbableElement = tabbableElements[0]; - this._lastTabbableElement = tabbableElements[tabbableElements.length - 1]; - } - } - /** * Set focusable elements in the modal dialog */ @@ -485,13 +456,10 @@ export abstract class Modal extends Disposable implements IThemable { } else if (event.equals(KeyCode.Escape)) { DOM.EventHelper.stop(e, true); this.onClose(event); - } else if (event.equals(KeyMod.Shift | KeyCode.Tab)) { - this.handleBackwardTab(e); - } else if (event.equals(KeyCode.Tab)) { - this.handleForwardTab(e); } } })); + this.disposableStore.add(trapKeyboardNavigation(this._modalDialog!)); this.disposableStore.add(DOM.addDisposableListener(window, DOM.EventType.RESIZE, (e: Event) => { this.layout(DOM.getTotalHeight(this._modalBodySection!)); }));