mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
trap keyboard navigation (#15134)
This commit is contained in:
@@ -3,7 +3,11 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* 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 * 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 {
|
export function isHidden(element: HTMLElement): boolean {
|
||||||
return element.style.display === 'none';
|
return element.style.display === 'none';
|
||||||
@@ -64,3 +68,33 @@ export function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
|||||||
});
|
});
|
||||||
return elements;
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { withNullAsUndefined } from 'vs/base/common/types';
|
|||||||
import { IDisposableDataProvider } from 'sql/base/common/dataProvider';
|
import { IDisposableDataProvider } from 'sql/base/common/dataProvider';
|
||||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||||
import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||||
|
import { trapKeyboardNavigation } from 'sql/base/browser/dom';
|
||||||
|
|
||||||
export type HeaderFilterCommands = 'sort-asc' | 'sort-desc';
|
export type HeaderFilterCommands = 'sort-asc' | 'sort-desc';
|
||||||
|
|
||||||
@@ -307,6 +308,9 @@ export class HeaderFilter<T extends Slick.SlickData> {
|
|||||||
jQuery(':checkbox', $filter).bind('click', (e) => {
|
jQuery(':checkbox', $filter).bind('click', (e) => {
|
||||||
this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, jQuery(e.target));
|
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 {
|
public style(styles: ITableFilterStyle): void {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import 'vs/css!./media/calloutDialog';
|
import 'vs/css!./media/calloutDialog';
|
||||||
import 'vs/css!./media/modal';
|
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 { Button } from 'sql/base/browser/ui/button/button';
|
||||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
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 { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||||
import { Color } from 'vs/base/common/color';
|
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 { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||||
import { Schemas } from 'vs/base/common/network';
|
import { Schemas } from 'vs/base/common/network';
|
||||||
import { mixin } from 'vs/base/common/objects';
|
import { mixin } from 'vs/base/common/objects';
|
||||||
@@ -120,8 +120,6 @@ export abstract class Modal extends Disposable implements IThemable {
|
|||||||
private _messageDetailText?: string;
|
private _messageDetailText?: string;
|
||||||
|
|
||||||
private _spinnerElement?: HTMLElement;
|
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 _focusedElementBeforeOpen?: HTMLElement;
|
||||||
|
|
||||||
private _dialogForeground?: Color;
|
private _dialogForeground?: Color;
|
||||||
@@ -347,22 +345,6 @@ export abstract class Modal extends Disposable implements IThemable {
|
|||||||
this.hide('ok');
|
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 {
|
private getTextForClipboard(): string {
|
||||||
const eol = this.textResourcePropertiesService.getEOL(URI.from({ scheme: Schemas.untitled }));
|
const eol = this.textResourcePropertiesService.getEOL(URI.from({ scheme: Schemas.untitled }));
|
||||||
return this._messageDetailText === '' ? this._messageSummaryText! : `${this._messageSummaryText}${eol}========================${eol}${this._messageDetailText}`;
|
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;
|
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 = <HTMLElement>tabbableElements[0];
|
|
||||||
this._lastTabbableElement = <HTMLElement>tabbableElements[tabbableElements.length - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set focusable elements in the modal dialog
|
* 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)) {
|
} else if (event.equals(KeyCode.Escape)) {
|
||||||
DOM.EventHelper.stop(e, true);
|
DOM.EventHelper.stop(e, true);
|
||||||
this.onClose(event);
|
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.disposableStore.add(DOM.addDisposableListener(window, DOM.EventType.RESIZE, (e: Event) => {
|
||||||
this.layout(DOM.getTotalHeight(this._modalBodySection!));
|
this.layout(DOM.getTotalHeight(this._modalBodySection!));
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user