Tab trap for modal (closes #5930) (#8043)

* Tab trap for modal (closes #5930)

* Addressing PR comments

* Fixed formatting.
This commit is contained in:
Shafiq Ur Rahman
2019-10-30 11:38:11 -07:00
committed by GitHub
parent 82e5221024
commit f8858a3511
2 changed files with 28 additions and 26 deletions

View File

@@ -84,9 +84,8 @@ export abstract class Modal extends Disposable implements IThemable {
private _messageDetailText: string; private _messageDetailText: string;
private _spinnerElement: HTMLElement; private _spinnerElement: HTMLElement;
private _focusableElements: NodeListOf<Element>; private _firstTabbableElement: HTMLElement; // The first element in the dialog the user could tab to
private _firstFocusableElement: HTMLElement; private _lastTabbableElement: HTMLElement; // The last element in the dialog the user could tab to
private _lastFocusableElement: HTMLElement;
private _focusedElementBeforeOpen: HTMLElement; private _focusedElementBeforeOpen: HTMLElement;
private _dialogForeground?: Color; private _dialogForeground?: Color;
@@ -269,16 +268,18 @@ export abstract class Modal extends Disposable implements IThemable {
} }
private handleBackwardTab(e: KeyboardEvent) { private handleBackwardTab(e: KeyboardEvent) {
if (this._firstFocusableElement && this._lastFocusableElement && document.activeElement === this._firstFocusableElement) { this.setFirstLastTabbableElement(); // called every time to get the current elements
if (this._firstTabbableElement && this._lastTabbableElement && document.activeElement === this._firstTabbableElement) {
e.preventDefault(); e.preventDefault();
this._lastFocusableElement.focus(); this._lastTabbableElement.focus();
} }
} }
private handleForwardTab(e: KeyboardEvent) { private handleForwardTab(e: KeyboardEvent) {
if (this._firstFocusableElement && this._lastFocusableElement && document.activeElement === this._lastFocusableElement) { this.setFirstLastTabbableElement(); // called everytime to get the current elements
if (this._firstTabbableElement && this._lastTabbableElement && document.activeElement === this._lastTabbableElement) {
e.preventDefault(); e.preventDefault();
this._firstFocusableElement.focus(); this._firstTabbableElement.focus();
} }
} }
@@ -316,29 +317,30 @@ export abstract class Modal extends Disposable implements IThemable {
} }
/** /**
* Set focusable elements in the modal dialog * Figures out the first and last elements which the user can tab to in the dialog
*/ */
public setFocusableElements() { public setFirstLastTabbableElement() {
// try to find focusable element in dialog pane rather than overall container let tabbableElements = this._bodyContainer.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
this._focusableElements = this._modalBodySection ? if (tabbableElements && tabbableElements.length > 0) {
this._modalBodySection.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]') : this._firstTabbableElement = <HTMLElement>tabbableElements[0];
this._bodyContainer.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'); this._lastTabbableElement = <HTMLElement>tabbableElements[tabbableElements.length - 1];
if (this._focusableElements && this._focusableElements.length > 0) {
this._firstFocusableElement = <HTMLElement>this._focusableElements[0];
this._lastFocusableElement = <HTMLElement>this._focusableElements[this._focusableElements.length - 1];
} }
this._focusedElementBeforeOpen = <HTMLElement>document.activeElement;
this.focus();
} }
/** /**
* Focuses the modal * Set focusable elements in the modal dialog
* Default behavior: focus the first focusable element
*/ */
protected focus() { public setInitialFocusedElement() {
if (this._firstFocusableElement) { // Try to find focusable element in dialog pane rather than overall container. _modalBodySection contains items in the pane for a wizard.
this._firstFocusableElement.focus(); // This ensures that we are setting the focus on a useful element in the form when possible.
let focusableElements = this._modalBodySection ?
this._modalBodySection.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]') :
this._bodyContainer.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
this._focusedElementBeforeOpen = <HTMLElement>document.activeElement;
if (focusableElements && focusableElements.length > 0) {
(<HTMLElement>focusableElements[0]).focus();
} }
} }
@@ -348,7 +350,7 @@ export abstract class Modal extends Disposable implements IThemable {
protected show() { protected show() {
this._modalShowingContext.get()!.push(this._staticKey); this._modalShowingContext.get()!.push(this._staticKey);
DOM.append(this.layoutService.container, this._bodyContainer); DOM.append(this.layoutService.container, this._bodyContainer);
this.setFocusableElements(); this.setInitialFocusedElement();
this._keydownListener = DOM.addDisposableListener(document, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { this._keydownListener = DOM.addDisposableListener(document, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let context = this._modalShowingContext.get()!; let context = this._modalShowingContext.get()!;

View File

@@ -108,7 +108,7 @@ export class BackupUiService implements IBackupUiService {
public onShowBackupDialog() { public onShowBackupDialog() {
let backupDialog = this._backupDialogs[this._currentProvider]; let backupDialog = this._backupDialogs[this._currentProvider];
if (backupDialog) { if (backupDialog) {
backupDialog.setFocusableElements(); backupDialog.setInitialFocusedElement();
} }
} }