diff --git a/src/sql/base/browser/ui/panel/panel.ts b/src/sql/base/browser/ui/panel/panel.ts index 8e95060afd..380e911207 100644 --- a/src/sql/base/browser/ui/panel/panel.ts +++ b/src/sql/base/browser/ui/panel/panel.ts @@ -33,7 +33,6 @@ export interface IPanelOptions { export interface IPanelView { render(container: HTMLElement): void; layout(dimension: DOM.Dimension): void; - focus(): void; remove?(): void; onShow?(): void; onHide?(): void; @@ -184,15 +183,6 @@ export class TabbedPanel extends Disposable { let currentIndex = this._tabOrder.findIndex(x => x === tab.tab.identifier); this.focusNextTab(currentIndex - 1); } - if (event.equals(KeyCode.Tab)) { - e.preventDefault(); - if (this._shownTabId) { - const shownTab = this._tabMap.get(this._shownTabId); - if (shownTab) { - shownTab.tab.view.focus(); - } - } - } })); const insertBefore = !isUndefinedOrNull(index) ? this.tabList.children.item(index) : undefined; @@ -401,15 +391,6 @@ export class TabbedPanel extends Disposable { } } - public focus(): void { - if (this._shownTabId) { - const tab = this._tabMap.get(this._shownTabId); - if (tab) { - tab.tab.view.focus(); - } - } - } - public set collapsed(val: boolean) { if (val === this._collapsed) { return; diff --git a/src/sql/base/browser/ui/table/table.ts b/src/sql/base/browser/ui/table/table.ts index 8141881979..a78ccaf7a4 100644 --- a/src/sql/base/browser/ui/table/table.ts +++ b/src/sql/base/browser/ui/table/table.ts @@ -80,6 +80,23 @@ export class Table extends Widget implements IDisposa this._container = document.createElement('div'); this._container.className = 'monaco-table'; this._register(DOM.addDisposableListener(this._container, DOM.EventType.FOCUS, () => { + if (!this.grid.getActiveCell() && this._data.getLength() > 0) { + // When the table receives focus and there are currently no active cell, the focus should go to the first focusable cell. + let cellToFocus = undefined; + for (let col = 0; col < this.columns.length; col++) { + // some cells are not keyboard focusable (e.g. row number column), we need to find the first focusable cell. + if (this.grid.canCellBeActive(0, col)) { + cellToFocus = { + row: 0, + cell: col + }; + break; + } + } + if (cellToFocus) { + this.grid.setActiveCell(cellToFocus.row, cellToFocus.cell); + } + } clearTimeout(this._classChangeTimeout); this._classChangeTimeout = setTimeout(() => { this._container.classList.add('focused'); @@ -121,6 +138,14 @@ export class Table extends Widget implements IDisposa this.mapMouseEvent(this._grid.onHeaderClick, this._onHeaderClick); this.mapMouseEvent(this._grid.onDblClick, this._onDoubleClick); this._grid.onColumnsResized.subscribe(() => this._onColumnResize.fire()); + this._grid.onRendered.subscribe(() => { + if (!this._grid.getActiveCell()) { + this._container.tabIndex = this._data.getLength() > 0 ? 0 : -1; + } + }); + this._grid.onActiveCellChanged.subscribe((e, data) => { + this._container.tabIndex = -1; + }); } public rerenderGrid() { diff --git a/src/sql/platform/dashboard/browser/interfaces.ts b/src/sql/platform/dashboard/browser/interfaces.ts index 286a6b5b16..795bf9c844 100644 --- a/src/sql/platform/dashboard/browser/interfaces.ts +++ b/src/sql/platform/dashboard/browser/interfaces.ts @@ -21,7 +21,8 @@ export enum ComponentEventType { onComponentCreated, onCellAction, onEnterKeyPressed, - onInput + onInput, + onComponentLoaded } /** diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 670ed4949f..1bc810c755 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -244,7 +244,8 @@ export enum ComponentEventType { onComponentCreated, onCellAction, onEnterKeyPressed, - onInput + onInput, + onComponentLoaded } export interface IComponentEventArgs { diff --git a/src/sql/workbench/browser/modal/modal.ts b/src/sql/workbench/browser/modal/modal.ts index 989d05a54e..a7fae9aa1c 100644 --- a/src/sql/workbench/browser/modal/modal.ts +++ b/src/sql/workbench/browser/modal/modal.ts @@ -382,12 +382,9 @@ export abstract class Modal extends Disposable implements IThemable { * Set focusable elements in the modal dialog */ 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. - const focusableElements = getFocusableElements(this._modalBodySection ?? this._bodyContainer!); - - if (focusableElements && focusableElements.length > 0) { - (focusableElements[0]).focus(); + const focusableElements = getFocusableElements(this._modalDialog!); + if (focusableElements?.length > 0) { + focusableElements[0].focus(); } } diff --git a/src/sql/workbench/browser/modelComponents/componentBase.ts b/src/sql/workbench/browser/modelComponents/componentBase.ts index d111de056a..47850341ec 100644 --- a/src/sql/workbench/browser/modelComponents/componentBase.ts +++ b/src/sql/workbench/browser/modelComponents/componentBase.ts @@ -56,6 +56,10 @@ export abstract class ComponentBase this.modelStore.validate(this)); } + this.fireEvent({ + eventType: ComponentEventType.onComponentLoaded, + args: undefined + }); } abstract ngAfterViewInit(): void; diff --git a/src/sql/workbench/contrib/charts/browser/chartView.ts b/src/sql/workbench/contrib/charts/browser/chartView.ts index 9df619f846..74a640a40f 100644 --- a/src/sql/workbench/contrib/charts/browser/chartView.ts +++ b/src/sql/workbench/contrib/charts/browser/chartView.ts @@ -206,9 +206,6 @@ export class ChartView extends Disposable implements IPanelView { } } - focus(): void { - } - public set queryRunner(runner: QueryRunner) { this._queryRunner = runner; this.fetchData(); diff --git a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts index dfd4bb89a9..1410d85dce 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts @@ -362,8 +362,7 @@ export class ProfilerEditor extends EditorPane { title: nls.localize('text', "Text"), view: { layout: dim => this._editor.layout(dim), - render: parent => parent.appendChild(editorContainer), - focus: () => this._editor.focus() + render: parent => parent.appendChild(editorContainer) }, tabSelectedHandler: expandPanel }); @@ -417,8 +416,7 @@ export class ProfilerEditor extends EditorPane { title: nls.localize('details', "Details"), view: { layout: dim => this._detailTable.layout(dim), - render: parent => parent.appendChild(detailTableContainer), - focus: () => this._detailTable.focus() + render: parent => parent.appendChild(detailTableContainer) }, tabSelectedHandler: expandPanel }); diff --git a/src/sql/workbench/contrib/query/browser/modelViewTab/queryModelViewTab.ts b/src/sql/workbench/contrib/query/browser/modelViewTab/queryModelViewTab.ts index d6f3f28adf..85254345e8 100644 --- a/src/sql/workbench/contrib/query/browser/modelViewTab/queryModelViewTab.ts +++ b/src/sql/workbench/contrib/query/browser/modelViewTab/queryModelViewTab.ts @@ -63,9 +63,6 @@ export class QueryModelViewTabView implements IPanelView { public layout(dimension: Dimension): void { } - public focus(): void { - } - public get componentId(): string { return this.state.componentId; } diff --git a/src/sql/workbench/contrib/query/browser/queryResultsView.ts b/src/sql/workbench/contrib/query/browser/queryResultsView.ts index 20e52f0868..fc07160d0b 100644 --- a/src/sql/workbench/contrib/query/browser/queryResultsView.ts +++ b/src/sql/workbench/contrib/query/browser/queryResultsView.ts @@ -44,10 +44,6 @@ class MessagesView extends Disposable implements IPanelView { this.messagePanel.layout(dimension); } - focus(): void { - this.messagePanel.focus(); - } - public clear() { this.messagePanel.clear(); } @@ -83,10 +79,6 @@ class ResultsView extends Disposable implements IPanelView { this.gridPanel.layout(dimension); } - focus(): void { - this.gridPanel.focus(); - } - public clear() { this.gridPanel.clear(); } diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts index b159aff296..19e8ea5ad0 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.ts @@ -58,10 +58,6 @@ export class QueryPlanView implements IPanelView { this.container.style.height = dimension.height + 'px'; } - public focus() { - this.container.focus(); - } - public clear() { if (this.qp) { this.qp.xml = undefined; diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts index 090ba50c6b..bafe30d684 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts @@ -47,13 +47,6 @@ export class QueryPlanEditor extends EditorPane { this.view.render(parent); } - /** - * Sets focus on this editor. Specifically, it sets the focus on the hosted text editor. - */ - public override focus(): void { - this.view.focus(); - } - /** * Updates the internal variable keeping track of the editor's size, and re-calculates the sash position. * To be called when the container of this editor changes size. diff --git a/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts b/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts index e402b20f0d..151f05843b 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts @@ -77,10 +77,6 @@ export class TopOperationsView extends Disposable implements IPanelView { this.table.layout(dimension); } - public focus(): void { - this.table.focus(); - } - public clear() { this.dataView.clear(); } diff --git a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts index 00fea65678..7b9d32495a 100644 --- a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts +++ b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts @@ -281,10 +281,6 @@ export class ConnectionBrowserView extends Disposable implements IPanelView { this.treeContainer.style.height = `${treeHeight}px`; this.tree.layout(treeHeight, dimension.width); } - - focus(): void { - this.filterInput.focus(); - } } export interface ITreeItemFromProvider { diff --git a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts index fd98f18be8..9d66ee2d1a 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts @@ -191,9 +191,6 @@ export class ConnectionDialogWidget extends Modal { }, layout: (dimension: DOM.Dimension) => { this._recentConnectionTree.layout(dimension.height - DOM.getTotalHeight(this._recentConnectionActionBarContainer)); - }, - focus: () => { - this._actionbar.focus(); } } }); diff --git a/src/sql/workbench/services/dialog/browser/dialogContainer.component.ts b/src/sql/workbench/services/dialog/browser/dialogContainer.component.ts index ef51928df9..7474218444 100644 --- a/src/sql/workbench/services/dialog/browser/dialogContainer.component.ts +++ b/src/sql/workbench/services/dialog/browser/dialogContainer.component.ts @@ -10,6 +10,7 @@ import { DialogPane } from 'sql/workbench/services/dialog/browser/dialogPane'; import { Event, Emitter } from 'vs/base/common/event'; import { IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams'; import { ComponentEventType } from 'sql/platform/dashboard/browser/interfaces'; +import { getFocusableElements } from 'sql/base/browser/dom'; export interface LayoutRequestParams { modelViewId?: string; @@ -20,6 +21,7 @@ export interface DialogComponentParams extends IBootstrapParams { validityChangedCallback: (valid: boolean) => void; onLayoutRequested: Event; dialogPane: DialogPane; + setInitialFocus: boolean; } @Component({ @@ -62,12 +64,20 @@ export class DialogContainer implements AfterViewInit { } ngAfterViewInit(): void { + const element = this._el.nativeElement; this._modelViewContent.onEvent(event => { if (event.isRootComponent && event.eventType === ComponentEventType.validityChanged) { this._params.validityChangedCallback(event.args); } + + // Set focus to the first focusable elements when the content is loaded and the focus is not currently inside the content area + if (this._params.setInitialFocus && event.isRootComponent && event.eventType === ComponentEventType.onComponentLoaded && !element.contains(document.activeElement)) { + const focusableElements = getFocusableElements(element); + if (focusableElements?.length > 0) { + focusableElements[0].focus(); + } + } }); - let element = this._el.nativeElement; element.style.height = '100%'; element.style.width = '100%'; } diff --git a/src/sql/workbench/services/dialog/browser/dialogPane.ts b/src/sql/workbench/services/dialog/browser/dialogPane.ts index 60d6a720fa..1e6c067618 100644 --- a/src/sql/workbench/services/dialog/browser/dialogPane.ts +++ b/src/sql/workbench/services/dialog/browser/dialogPane.ts @@ -22,6 +22,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemable } from 'vs/base/common/styler'; import { attachTabbedPanelStyler } from 'sql/workbench/common/styler'; import { localize } from 'vs/nls'; +import { getFocusableElements } from 'sql/base/browser/dom'; export class DialogPane extends Disposable implements IThemable { private _tabbedPanel: TabbedPanel | undefined; @@ -44,6 +45,7 @@ export class DialogPane extends Disposable implements IThemable { private _themeService: IThemeService, public displayPageTitle: boolean, public description?: string, + private setInitialFocus: boolean = true ) { super(); } @@ -56,7 +58,7 @@ export class DialogPane extends Disposable implements IThemable { this._body = DOM.append(container, DOM.$('div.dialogModal-pane')); if (typeof this._content === 'string' || this._content.length < 2) { let modelViewId = typeof this._content === 'string' ? this._content : this._content[0].content; - this.initializeModelViewContainer(this._body, modelViewId); + this.initializeModelViewContainer(this._body, modelViewId, undefined, this.setInitialFocus); } else { this._tabbedPanel = new TabbedPanel(this._body); attachTabbedPanelStyler(this._tabbedPanel, this._themeService); @@ -67,7 +69,8 @@ export class DialogPane extends Disposable implements IThemable { let tabContainer = document.createElement('div'); tabContainer.style.display = 'none'; this._body.appendChild(tabContainer); - this.initializeModelViewContainer(tabContainer, tab.content, tab); + // Only set initial focus when the tab is active one. + this.initializeModelViewContainer(tabContainer, tab.content, tab, this.setInitialFocus && tabIndex === this._selectedTabIndex); this._tabbedPanel!.onTabChange(e => { tabContainer.style.height = (this.getTabDimension().height - this._tabbedPanel!.headersize) + 'px'; this._onLayoutChange.fire({ modelViewId: tab.content }); @@ -83,8 +86,7 @@ export class DialogPane extends Disposable implements IThemable { container.appendChild(tabContainer); tabContainer.style.display = 'block'; }, - layout: (dimension) => { this.getTabDimension(); }, - focus: () => { this.focus(); } + layout: (dimension) => { this.getTabDimension(); } } }); }); @@ -113,7 +115,7 @@ export class DialogPane extends Disposable implements IThemable { /** * Bootstrap angular for the dialog's model view controller with the given model view ID */ - private initializeModelViewContainer(bodyContainer: HTMLElement, modelViewId: string, tab?: DialogTab) { + private initializeModelViewContainer(bodyContainer: HTMLElement, modelViewId: string, tab?: DialogTab, setInitialFocus: boolean = true) { this._instantiationService.invokeFunction(bootstrapAngular, DialogModule, bodyContainer, @@ -127,7 +129,8 @@ export class DialogPane extends Disposable implements IThemable { } }, onLayoutRequested: this._onLayoutChange.event, - dialogPane: this + dialogPane: this, + setInitialFocus: setInitialFocus } as DialogComponentParams, undefined, (moduleRef: NgModuleRef<{}>) => { @@ -147,8 +150,10 @@ export class DialogPane extends Disposable implements IThemable { } private focus(): void { - let focusedElement = this._body.querySelector('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])'); - focusedElement ? focusedElement.focus() : this._body.focus(); + const focusableElements = getFocusableElements(this._body); + if (focusableElements?.length > 0) { + focusableElements[0].focus(); + } } /** diff --git a/src/sql/workbench/services/dialog/browser/wizardModal.ts b/src/sql/workbench/services/dialog/browser/wizardModal.ts index d5c3b85fd7..391f7b66e5 100644 --- a/src/sql/workbench/services/dialog/browser/wizardModal.ts +++ b/src/sql/workbench/services/dialog/browser/wizardModal.ts @@ -133,8 +133,8 @@ export class WizardModal extends Modal { this._mpContainer = append(this._body, $('div.dialog-message-and-page-container')); this._pageContainer = append(this._mpContainer, $('div.dialogModal-page-container')); - this._wizard.pages.forEach(page => { - this.registerPage(page); + this._wizard.pages.forEach((page, index) => { + this.registerPage(page, index === 0); // only do auto-focus for the first page. }); this._wizard.onPageAdded(page => { this.registerPage(page); @@ -167,8 +167,8 @@ export class WizardModal extends Modal { }); } - private registerPage(page: WizardPage): void { - let dialogPane = new DialogPane(page.title, page.content, valid => page.notifyValidityChanged(valid), this._instantiationService, this._themeService, this._wizard.displayPageTitles, page.description); + private registerPage(page: WizardPage, setInitialFocus: boolean = false): void { + let dialogPane = new DialogPane(page.title, page.content, valid => page.notifyValidityChanged(valid), this._instantiationService, this._themeService, this._wizard.displayPageTitles, page.description, setInitialFocus); dialogPane.createBody(this._pageContainer); this._dialogPanes.set(page, dialogPane); page.onUpdate(() => this.setButtonsForPage(this._wizard.currentPage)); diff --git a/src/sql/workbench/services/restore/browser/restoreDialog.ts b/src/sql/workbench/services/restore/browser/restoreDialog.ts index c64cb7f485..dc97dc90e1 100644 --- a/src/sql/workbench/services/restore/browser/restoreDialog.ts +++ b/src/sql/workbench/services/restore/browser/restoreDialog.ts @@ -334,8 +334,7 @@ export class RestoreDialog extends Modal { render: c => { DOM.append(c, generalTab); }, - layout: () => { }, - focus: () => this._restoreFromSelectBox ? this._restoreFromSelectBox.focus() : generalTab.focus() + layout: () => { } } }); @@ -346,8 +345,7 @@ export class RestoreDialog extends Modal { layout: () => { }, render: c => { c.appendChild(fileContentElement); - }, - focus: () => this._optionsMap[this._relocateDatabaseFilesOption] ? this._optionsMap[this._relocateDatabaseFilesOption].focus() : fileContentElement.focus() + } } }); @@ -358,8 +356,7 @@ export class RestoreDialog extends Modal { layout: () => { }, render: c => { c.appendChild(optionsContentElement); - }, - focus: () => this._optionsMap[this._withReplaceDatabaseOption] ? this._optionsMap[this._withReplaceDatabaseOption].focus() : optionsContentElement.focus() + } } });