diff --git a/src/sql/workbench/browser/modal/modal.ts b/src/sql/workbench/browser/modal/modal.ts index a7b2c6cfdd..961068dc25 100644 --- a/src/sql/workbench/browser/modal/modal.ts +++ b/src/sql/workbench/browser/modal/modal.ts @@ -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 = tabbableElements[0]; this._lastTabbableElement = 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 = 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(); }