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