diff --git a/src/sql/base/browser/ui/table/formatters.ts b/src/sql/base/browser/ui/table/formatters.ts index 7c78fa8453..feff12f6c7 100644 --- a/src/sql/base/browser/ui/table/formatters.ts +++ b/src/sql/base/browser/ui/table/formatters.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { escape } from 'sql/base/common/strings'; +import { localize } from 'vs/nls'; export class DBCellValue { displayValue: string; @@ -77,6 +78,19 @@ export function slickGridDataItemColumnValueExtractor(value: any, columnDef: any }; } +/** + * Alternate function to provide slick grid cell with ariaLabel and plain text + * In this case, for no display value ariaLabel will be set to specific string "no data available" for accessibily support for screen readers + * Set 'no data' lable only if cell is present and has no value (so that checkbox and other custom plugins do not get 'no data' label) + */ +export function slickGridDataItemColumnValueWithNoData(value: any, columnDef: any): { text: string; ariaLabel: string; } { + let displayValue = value[columnDef.field]; + return { + text: displayValue, + ariaLabel: displayValue ? escape(displayValue) : ((displayValue !== undefined) ? localize("tableCell.NoDataAvailable", "no data available") : displayValue) + }; +} + /** The following code is a rewrite over the both formatter function using dom builder * rather than string manipulation, which is a safer and easier method of achieving the same goal. * However, when electron is in "Run as node" mode, dom creation acts differently than normal and therefore diff --git a/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts index f713cb2d10..c3e76a1c19 100644 --- a/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts @@ -99,11 +99,11 @@ export class CheckboxSelectColumn implements Slick.Pl if (!this._options.title) { if (selectedRows.length && selectedRows.length === this._grid.getDataLength()) { this._grid.updateColumnHeader(this._options.columnId!, - strings.format(checkboxTemplate, 'checked', this._options.title), + strings.format(checkboxTemplate, 'checked', this.getAriaLabel(true)), this._options.toolTip); } else { this._grid.updateColumnHeader(this._options.columnId!, - strings.format(checkboxTemplate, '',this._options.title), + strings.format(checkboxTemplate, '', this.getAriaLabel(false)), this._options.toolTip); } } @@ -210,12 +210,12 @@ export class CheckboxSelectColumn implements Slick.Pl const rows = range(this._grid.getDataLength()); this._grid.setSelectedRows(rows); this._grid.updateColumnHeader(this._options.columnId!, - strings.format(checkboxTemplate, 'checked', this._options.title), + strings.format(checkboxTemplate, 'checked', this.getAriaLabel(true)), this._options.toolTip); } else { this._grid.setSelectedRows([]); this._grid.updateColumnHeader(this._options.columnId!, - strings.format(checkboxTemplate, '', this._options.title), this._options.toolTip); + strings.format(checkboxTemplate, '', this.getAriaLabel(false)), this._options.toolTip); e.stopPropagation(); e.stopImmediatePropagation(); } @@ -243,16 +243,16 @@ export class CheckboxSelectColumn implements Slick.Pl } return this._selectedRowsLookup[row] - ? strings.format(checkboxTemplate, 'checked', this._options.title) - : strings.format(checkboxTemplate, '', this._options.title); + ? strings.format(checkboxTemplate, 'checked', this.getAriaLabel(true)) + : strings.format(checkboxTemplate, '', this.getAriaLabel(false)); } checkboxTemplateCustom(row: number): string { // use state after toggles if (this._useState) { return this._selectedCheckBoxLookup[row] - ? strings.format(checkboxTemplate, 'checked', this._options.title) - : strings.format(checkboxTemplate, '', this._options.title); + ? strings.format(checkboxTemplate, 'checked', this.getAriaLabel(true)) + : strings.format(checkboxTemplate, '', this.getAriaLabel(false)); } // use data for first time rendering @@ -260,15 +260,20 @@ export class CheckboxSelectColumn implements Slick.Pl let rowVal = (this._grid) ? this._grid.getDataItem(row) : null; if (rowVal && this._options.title && rowVal[this._options.title] === true) { this._selectedCheckBoxLookup[row] = true; - return strings.format(checkboxTemplate, 'checked', this._options.title); + return strings.format(checkboxTemplate, 'checked', this.getAriaLabel(true)); } else { delete this._selectedCheckBoxLookup[row]; - return strings.format(checkboxTemplate, '', this._options.title); + return strings.format(checkboxTemplate, '', this.getAriaLabel(false)); } } private isCustomActionRequested(): boolean { return (this._options.actionOnCheck === ActionOnCheck.customAction); } + + private getAriaLabel(checked: boolean): string { + return checked ? `"${this._options.title} ${nls.localize("tableCheckboxCell.Checked", "checkbox checked")}"` : + `"${this._options.title} ${nls.localize("tableCheckboxCell.unChecked", "checkbox unchecked")}"`; + } } diff --git a/src/sql/base/browser/ui/table/table.ts b/src/sql/base/browser/ui/table/table.ts index 8df1169b02..27dee900cd 100644 --- a/src/sql/base/browser/ui/table/table.ts +++ b/src/sql/base/browser/ui/table/table.ts @@ -322,6 +322,7 @@ export class Table extends Widget implements IDisposa if (styles.listFocusOutline) { content.push(`.monaco-table.${this.idPrefix}.focused .slick-row .selected { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }`); + content.push(`.monaco-table.${this.idPrefix}.focused .slick-row .selected.active { outline: 2px solid ${styles.listFocusOutline}; outline-offset: -1px; }`); } if (styles.listInactiveFocusOutline) { diff --git a/src/sql/platform/dialog/browser/dialogPane.ts b/src/sql/platform/dialog/browser/dialogPane.ts index f0cbee8cee..caf9d6b395 100644 --- a/src/sql/platform/dialog/browser/dialogPane.ts +++ b/src/sql/platform/dialog/browser/dialogPane.ts @@ -79,7 +79,7 @@ export class DialogPane extends Disposable implements IThemable { tabContainer.style.display = 'block'; }, layout: (dimension) => { this.getTabDimension(); }, - focus: () => { } + focus: () => { this.focus(); } } }); }); diff --git a/src/sql/workbench/browser/modal/modal.ts b/src/sql/workbench/browser/modal/modal.ts index 8d7f7633b9..b4ce5f7a28 100644 --- a/src/sql/workbench/browser/modal/modal.ts +++ b/src/sql/workbench/browser/modal/modal.ts @@ -319,13 +319,27 @@ export abstract class Modal extends Disposable implements IThemable { * Set focusable elements in the modal dialog */ public setFocusableElements() { - this._focusableElements = this._bodyContainer.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'); + // try to find focusable element in dialog pane rather than overall container + this._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"]'); if (this._focusableElements && this._focusableElements.length > 0) { this._firstFocusableElement = this._focusableElements[0]; this._lastFocusableElement = this._focusableElements[this._focusableElements.length - 1]; } this._focusedElementBeforeOpen = document.activeElement; + this.focus(); + } + + /** + * Focuses the modal + * Default behavior: focus the first focusable element + */ + protected focus() { + if (this._firstFocusableElement) { + this._firstFocusableElement.focus(); + } } /** diff --git a/src/sql/workbench/browser/modelComponents/table.component.ts b/src/sql/workbench/browser/modelComponents/table.component.ts index 0ab006d6bc..f4796178cf 100644 --- a/src/sql/workbench/browser/modelComponents/table.component.ts +++ b/src/sql/workbench/browser/modelComponents/table.component.ts @@ -25,6 +25,7 @@ import { CheckboxSelectColumn, ICheckboxCellActionEventArgs } from 'sql/base/bro import { Emitter, Event as vsEvent } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { slickGridDataItemColumnValueWithNoData, textFormatter } from 'sql/base/browser/ui/table/formatters'; @Component({ selector: 'modelview-table', @@ -72,13 +73,15 @@ export default class TableComponent extends ComponentBase implements IComponent, width: col.width, cssClass: col.cssClass, headerCssClass: col.headerCssClass, - toolTip: col.toolTip + toolTip: col.toolTip, + formatter: textFormatter, }); } else { mycolumns.push(>{ name: col, id: col, - field: col + field: col, + formatter: textFormatter }); } index++; @@ -95,8 +98,6 @@ export default class TableComponent extends ComponentBase implements IComponent, } } - - public static transformData(rows: string[][], columns: any[]): { [key: string]: string }[] { if (rows && columns) { return rows.map(row => { @@ -122,7 +123,8 @@ export default class TableComponent extends ComponentBase implements IComponent, syncColumnCellResize: true, enableColumnReorder: false, enableCellNavigation: true, - forceFitColumns: true // default to true during init, actual value will be updated when setProperties() is called + forceFitColumns: true, // default to true during init, actual value will be updated when setProperties() is called + dataItemColumnValueExtractor: slickGridDataItemColumnValueWithNoData // must change formatter if you are changing explicit column value extractor }; this._table = new Table(this._inputContainer.nativeElement, { dataProvider: this._tableData, columns: this._tableColumns }, options);