Fix tab navigation within modal dialog (#8326)

* Fix tab navigation within modal dialog

* Add import

* Fix spelling

* Change to just add/remove items from DOM as necessary
This commit is contained in:
Charles Gagnon
2019-11-19 12:43:55 -08:00
committed by GitHub
parent 5b50696a1b
commit 8ca0082ec4

View File

@@ -69,13 +69,17 @@ const defaultOptions: IModalOptions = {
hasSpinner: false
};
const tabbableElementsQuerySelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]';
export abstract class Modal extends Disposable implements IThemable {
protected _useDefaultMessageBoxLocation: boolean = true;
protected _messageElement: HTMLElement;
protected _modalOptions: IModalOptions;
private _detailsButtonContainer: HTMLElement;
private _messageIcon: HTMLElement;
private _messageSeverity: HTMLElement;
private _messageSummary: HTMLElement;
private _messageBody: HTMLElement;
private _messageDetail: HTMLElement;
private _toggleMessageDetailButton: Button;
private _copyMessageButton: Button;
@@ -94,6 +98,7 @@ export abstract class Modal extends Disposable implements IThemable {
private _dialogBodyBackground?: Color;
private _modalDialog: HTMLElement;
private _modalContent: HTMLElement;
private _modalHeaderSection: HTMLElement;
private _modalBodySection: HTMLElement;
private _modalFooterSection: HTMLElement;
@@ -173,10 +178,10 @@ export abstract class Modal extends Disposable implements IThemable {
const top = this.layoutService.getTitleBarOffset();
this._bodyContainer.style.top = `${top}px`;
this._modalDialog = DOM.append(this._bodyContainer, DOM.$('.modal-dialog'));
const modalContent = DOM.append(this._modalDialog, DOM.$('.modal-content'));
this._modalContent = DOM.append(this._modalDialog, DOM.$('.modal-content'));
if (!isUndefinedOrNull(this._title)) {
this._modalHeaderSection = DOM.append(modalContent, DOM.$('.modal-header'));
this._modalHeaderSection = DOM.append(this._modalContent, DOM.$('.modal-header'));
if (this._modalOptions.hasBackButton) {
const container = DOM.append(this._modalHeaderSection, DOM.$('.modal-go-back'));
this._backButton = new Button(container);
@@ -197,8 +202,8 @@ export abstract class Modal extends Disposable implements IThemable {
const headerContainer = DOM.append(this._messageElement, DOM.$('.dialog-message-header'));
this._messageIcon = DOM.append(headerContainer, DOM.$('.dialog-message-icon'));
this._messageSeverity = DOM.append(headerContainer, DOM.$('.dialog-message-severity'));
const detailsButtonContainer = DOM.append(headerContainer, DOM.$('.dialog-message-button'));
this._toggleMessageDetailButton = new Button(detailsButtonContainer);
this._detailsButtonContainer = DOM.append(headerContainer, DOM.$('.dialog-message-button'));
this._toggleMessageDetailButton = new Button(this._detailsButtonContainer);
this._toggleMessageDetailButton.icon = 'message-details-icon';
this._toggleMessageDetailButton.label = SHOW_DETAILS_TEXT;
this._register(this._toggleMessageDetailButton.onDidClick(() => this.toggleMessageDetail()));
@@ -217,27 +222,21 @@ export abstract class Modal extends Disposable implements IThemable {
this._register(attachButtonStyler(this._copyMessageButton, this._themeService));
this._register(attachButtonStyler(this._closeMessageButton, this._themeService));
const messageBody = DOM.append(this._messageElement, DOM.$('.dialog-message-body'));
this._messageSummary = DOM.append(messageBody, DOM.$('.dialog-message-summary'));
this._messageBody = DOM.append(this._messageElement, DOM.$('.dialog-message-body'));
this._messageSummary = DOM.append(this._messageBody, DOM.$('.dialog-message-summary'));
this._register(DOM.addDisposableListener(this._messageSummary, DOM.EventType.CLICK, () => this.toggleMessageDetail()));
this._messageDetail = DOM.append(messageBody, DOM.$('.dialog-message-detail'));
DOM.hide(this._messageDetail);
DOM.hide(this._messageElement);
if (this._useDefaultMessageBoxLocation) {
DOM.append(modalContent, (this._messageElement));
}
this._messageDetail = DOM.$('.dialog-message-detail');
}
const modalBodyClass = (this._modalOptions.isAngular === false ? 'modal-body' : 'modal-body-and-footer');
this._modalBodySection = DOM.append(modalContent, DOM.$(`.${modalBodyClass}`));
this._modalBodySection = DOM.append(this._modalContent, DOM.$(`.${modalBodyClass}`));
this.renderBody(this._modalBodySection);
// This modal footer section refers to the footer of of the dialog
if (!this._modalOptions.isAngular) {
this._modalFooterSection = DOM.append(modalContent, DOM.$('.modal-footer'));
this._modalFooterSection = DOM.append(this._modalContent, DOM.$('.modal-footer'));
if (this._modalOptions.hasSpinner) {
this._spinnerElement = DOM.append(this._modalFooterSection, DOM.$('.codicon.in-progress'));
DOM.hide(this._spinnerElement);
@@ -292,9 +291,9 @@ export abstract class Modal extends Disposable implements IThemable {
this._messageSummary.style.cursor = this.shouldShowExpandMessageButton ? 'pointer' : 'default';
DOM.removeClass(this._messageSummary, MESSAGE_EXPANDED_MODE_CLASS);
if (this.shouldShowExpandMessageButton) {
DOM.show(this._toggleMessageDetailButton.element);
DOM.append(this._detailsButtonContainer, this._toggleMessageDetailButton.element);
} else {
DOM.hide(this._toggleMessageDetailButton.element);
DOM.removeNode(this._toggleMessageDetailButton.element);
}
}
@@ -305,9 +304,9 @@ export abstract class Modal extends Disposable implements IThemable {
if (this._messageDetailText) {
if (isExpanded) {
DOM.hide(this._messageDetail);
DOM.removeNode(this._messageDetail);
} else {
DOM.show(this._messageDetail);
DOM.append(this._messageBody, this._messageDetail);
}
}
}
@@ -320,7 +319,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
*/
public setFirstLastTabbableElement() {
let tabbableElements = this._bodyContainer.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
const tabbableElements = this._bodyContainer.querySelectorAll(tabbableElementsQuerySelector);
if (tabbableElements && tabbableElements.length > 0) {
this._firstTabbableElement = <HTMLElement>tabbableElements[0];
this._lastTabbableElement = <HTMLElement>tabbableElements[tabbableElements.length - 1];
@@ -333,9 +332,9 @@ export abstract class Modal extends Disposable implements IThemable {
public setInitialFocusedElement() {
// 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.
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"]');
const focusableElements = this._modalBodySection ?
this._modalBodySection.querySelectorAll('input') :
this._bodyContainer.querySelectorAll(tabbableElementsQuerySelector);
this._focusedElementBeforeOpen = <HTMLElement>document.activeElement;
@@ -481,11 +480,15 @@ export abstract class Modal extends Disposable implements IThemable {
this._messageSummary.title = message!;
this._messageDetail.innerText = description;
}
DOM.hide(this._messageDetail);
DOM.removeNode(this._messageDetail);
if (this._messageSummaryText) {
DOM.show(this._messageElement);
if (this._useDefaultMessageBoxLocation) {
DOM.prepend(this._modalContent, (this._messageElement));
}
} else {
DOM.hide(this._messageElement);
// Set the focus manually otherwise it'll escape the dialog to something behind it
this.setInitialFocusedElement();
DOM.removeNode(this._messageElement);
}
this.updateExpandMessageState();
}