only return visible elements as focusable (#14864)

This commit is contained in:
Alan Ren
2021-03-25 09:49:16 -07:00
committed by GitHub
parent edac96a624
commit 5db6857c49
2 changed files with 44 additions and 27 deletions

View File

@@ -44,3 +44,23 @@ export function convertSizeToNumber(size: number | string | undefined): number {
} }
return +size; return +size;
} }
const tabbableElementsQuerySelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]';
/**
* Get the focusable elements inside a HTML element
* @param container The container element inside which we should look for the focusable elements
* @returns The focusable elements
*/
export function getFocusableElements(container: HTMLElement): HTMLElement[] {
const elements = [];
container.querySelectorAll(tabbableElementsQuerySelector).forEach((element: HTMLElement) => {
const style = window.getComputedStyle(element);
// We should only return the elements that are visible. There are many ways to hide an element, for example setting the
// visibility attribute to hidden/collapse, setting the display property to none, or if one of its ancestors is invisible.
if (element.offsetWidth > 0 && element.offsetHeight > 0 && style.visibility === 'visible') {
elements.push(element);
}
});
return elements;
}

View File

@@ -2,30 +2,32 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* 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 'vs/css!./media/modal';
import 'vs/css!./media/calloutDialog'; import 'vs/css!./media/calloutDialog';
import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import 'vs/css!./media/modal';
import { Color } from 'vs/base/common/color'; import { getFocusableElements } from 'sql/base/browser/dom';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Button } from 'sql/base/browser/ui/button/button';
import { mixin } from 'vs/base/common/objects'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import * as DOM from 'vs/base/browser/dom'; import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { generateUuid } from 'vs/base/common/uuid';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { Button } from 'sql/base/browser/ui/button/button';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { localize } from 'vs/nls';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { ILogService } from 'vs/platform/log/common/log';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IThemable } from 'vs/base/common/styler';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
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 { KeyCode, KeyMod } 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';
import { IThemable } from 'vs/base/common/styler';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { localize } from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ILogService } from 'vs/platform/log/common/log';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemeService } from 'vs/platform/theme/common/themeService';
export enum MessageLevel { export enum MessageLevel {
@@ -97,8 +99,6 @@ const defaultOptions: IModalOptions = {
dialogProperties: undefined dialogProperties: undefined
}; };
const tabbableElementsQuerySelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]';
export type HideReason = 'close' | 'cancel' | 'ok'; export type HideReason = 'close' | 'cancel' | 'ok';
export abstract class Modal extends Disposable implements IThemable { export abstract class Modal extends Disposable implements IThemable {
@@ -400,7 +400,7 @@ export abstract class Modal extends Disposable implements IThemable {
* Figures out the first and last elements which the user can tab to in the dialog * Figures out the first and last elements which the user can tab to in the dialog
*/ */
public setFirstLastTabbableElement() { public setFirstLastTabbableElement() {
const tabbableElements = this._bodyContainer!.querySelectorAll(tabbableElementsQuerySelector); const tabbableElements = getFocusableElements(this._bodyContainer!);
if (tabbableElements && tabbableElements.length > 0) { if (tabbableElements && tabbableElements.length > 0) {
this._firstTabbableElement = <HTMLElement>tabbableElements[0]; this._firstTabbableElement = <HTMLElement>tabbableElements[0];
this._lastTabbableElement = <HTMLElement>tabbableElements[tabbableElements.length - 1]; this._lastTabbableElement = <HTMLElement>tabbableElements[tabbableElements.length - 1];
@@ -413,16 +413,13 @@ export abstract class Modal extends Disposable implements IThemable {
public setInitialFocusedElement() { public setInitialFocusedElement() {
// Try to find focusable element in dialog pane rather than overall container. _modalBodySection contains items in the pane for a wizard. // Try to find focusable element in dialog pane rather than overall container. _modalBodySection contains items in the pane for a wizard.
// This ensures that we are setting the focus on a useful element in the form when possible. // This ensures that we are setting the focus on a useful element in the form when possible.
const focusableElements = this._modalBodySection ? const focusableElements = getFocusableElements(this._modalBodySection ?? this._bodyContainer!);
this._modalBodySection.querySelectorAll(tabbableElementsQuerySelector) :
this._bodyContainer!.querySelectorAll(tabbableElementsQuerySelector);
if (focusableElements && focusableElements.length > 0) { if (focusableElements && focusableElements.length > 0) {
(<HTMLElement>focusableElements[0]).focus(); (<HTMLElement>focusableElements[0]).focus();
} }
} }
/** /**
* Tasks to perform before callout dialog is shown * Tasks to perform before callout dialog is shown
* Includes: positioning of dialog * Includes: positioning of dialog