Fixing Slick Grid accessibility bugs. (#20514)

* some alignment fixes

* Redesigning slickgrid checkbox plugin

* Reverting original plugin to main state

* Fixing css

* Reverting file

* Removing extra whitespace

* Adding title

* Removing older plugin

* vbumping slick grid

* Pushing other vbumps

* Adding more targeted css selectors
This commit is contained in:
Aasim Khan
2022-09-22 14:25:19 -07:00
committed by GitHub
parent f0b04faba4
commit 37636012da
10 changed files with 180 additions and 234 deletions

View File

@@ -101,7 +101,7 @@
"rxjs": "5.4.0",
"sanitize-html": "1.19.1",
"semver-umd": "^5.5.7",
"slickgrid": "github:Microsoft/SlickGrid.ADS#2.3.36",
"slickgrid": "github:Microsoft/SlickGrid.ADS#2.3.37",
"spdlog": "^0.13.0",
"sudo-prompt": "9.2.1",
"tas-client-umd": "0.1.4",

View File

@@ -40,7 +40,7 @@
"sanitize-html": "1.19.1",
"semver-umd": "^5.5.7",
"spdlog": "^0.13.0",
"slickgrid": "github:Microsoft/SlickGrid.ADS#2.3.36",
"slickgrid": "github:Microsoft/SlickGrid.ADS#2.3.37",
"turndown": "^7.0.0",
"turndown-plugin-gfm": "^1.0.2",
"tas-client-umd": "0.1.4",

View File

@@ -30,7 +30,7 @@
"rxjs": "5.4.0",
"sanitize-html": "1.19.1",
"semver-umd": "^5.5.7",
"slickgrid": "github:Microsoft/SlickGrid.ADS#2.3.36",
"slickgrid": "github:Microsoft/SlickGrid.ADS#2.3.37",
"turndown": "^7.0.0",
"turndown-plugin-gfm": "^1.0.2",
"tas-client-umd": "0.1.4",

View File

@@ -416,9 +416,9 @@ semver-umd@^5.5.7:
resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.7.tgz#966beb5e96c7da6fbf09c3da14c2872d6836c528"
integrity sha512-XgjPNlD0J6aIc8xoTN6GQGwWc2Xg0kq8NzrqMVuKG/4Arl6ab1F8+Am5Y/XKKCR+FceFr2yN/Uv5ZJBhRyRqKg==
"slickgrid@github:Microsoft/SlickGrid.ADS#2.3.36":
version "2.3.36"
resolved "https://codeload.github.com/Microsoft/SlickGrid.ADS/tar.gz/08f1ca3f6eb2cd1ea7f56e2af4b69600d48e1932"
"slickgrid@github:Microsoft/SlickGrid.ADS#2.3.37":
version "2.3.37"
resolved "https://codeload.github.com/Microsoft/SlickGrid.ADS/tar.gz/1de979b3cf66cee46846e5e0d2edbc938c8d6563"
source-map@^0.6.1:
version "0.6.1"

View File

@@ -739,9 +739,9 @@ semver@^5.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
"slickgrid@github:Microsoft/SlickGrid.ADS#2.3.36":
version "2.3.36"
resolved "https://codeload.github.com/Microsoft/SlickGrid.ADS/tar.gz/08f1ca3f6eb2cd1ea7f56e2af4b69600d48e1932"
"slickgrid@github:Microsoft/SlickGrid.ADS#2.3.37":
version "2.3.37"
resolved "https://codeload.github.com/Microsoft/SlickGrid.ADS/tar.gz/1de979b3cf66cee46846e5e0d2edbc938c8d6563"
smart-buffer@^4.1.0:
version "4.1.0"

View File

@@ -1,14 +1,15 @@
// Adopted and converted to typescript from https://github.com/6pac/SlickGrid/blob/master/plugins/slick.checkboxselectcolumn.js
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mixin } from 'vs/base/common/objects';
import * as nls from 'vs/nls';
import { ICheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
import { Emitter, Event as vsEvent } from 'vs/base/common/event';
import * as strings from 'vs/base/common/strings';
import { ICheckboxStyles } from 'sql/base/browser/ui/checkbox/checkbox';
import { mixin } from 'sql/base/common/objects';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Emitter, Event as vsEvent } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { range } from 'vs/base/common/arrays';
import * as dict from 'vs/base/common/collections';
import 'vs/css!./media/checkboxSelectColumn.plugin';
import * as nls from 'vs/nls';
export interface ICheckboxSelectColumnOptions extends Slick.PluginOptions, ICheckboxStyles {
columnId?: string;
@@ -21,18 +22,18 @@ export interface ICheckboxSelectColumnOptions extends Slick.PluginOptions, IChec
actionOnCheck?: ActionOnCheck;
}
// Actions expected on checkbox click
export enum ActionOnCheck {
selectRow = 0,
customAction = 1
}
export interface ICheckboxCellActionEventArgs {
checked: boolean;
row: number;
column: number;
}
// Actions expected on checkbox click
export enum ActionOnCheck {
selectRow = 0,
customAction = 1
}
interface ICheckboxColumnValue {
enabled: boolean;
checked: boolean;
@@ -42,205 +43,33 @@ const HeaderCheckboxTitle: string = nls.localize('selectDeselectAll', "Select/De
const defaultOptions: ICheckboxSelectColumnOptions = {
columnId: '_checkbox_selector',
cssClass: undefined,
headerCssClass: undefined,
cssClass: 'slick-plugin-checkbox-select-column',
headerCssClass: 'slick-plugin-checkbox-select-column',
toolTip: undefined,
width: 30
};
const checkboxTemplate = `<div style="display: flex; align-items: center; flex-direction: column">
<input type="checkbox" {0} title="{1}" aria-label="{1}" {2} />
</div>`;
export class CheckboxSelectColumn<T extends Slick.SlickData> implements Slick.Plugin<T> {
private _options: ICheckboxSelectColumnOptions;
private _grid!: Slick.Grid<T>;
private _handler = new Slick.EventHandler();
private _selectedRowsLookup: dict.INumberDictionary<boolean> = {};
private _selectedCheckBoxLookup: { [key: string]: boolean } = {};
private _useState = false;
private _options: ICheckboxSelectColumnOptions;
public index: number;
private _headerCheckbox: HTMLInputElement;
private _onChange = new Emitter<ICheckboxCellActionEventArgs>();
public readonly onChange: vsEvent<ICheckboxCellActionEventArgs> = this._onChange.event;
public index: number;
constructor(options?: ICheckboxSelectColumnOptions, columnIndex?: number) {
this._options = mixin(options, defaultOptions, false);
this._options.headerCssClass = (this._options.headerCssClass === undefined) ? this._options.headerCssClass + ' ' + defaultOptions.headerCssClass : defaultOptions.headerCssClass;
this._options.cssClass = (this._options.cssClass === undefined) ? this._options.cssClass + ' ' + defaultOptions.cssClass : defaultOptions.cssClass;
this.index = columnIndex ? columnIndex : 0;
}
public init(grid: Slick.Grid<T>): void {
this._grid = grid;
this._handler
.subscribe(this._grid.onSelectedRowsChanged, (e: Event, args: Slick.OnSelectedRowsChangedEventArgs<T>) => this.handleSelectedRowsChanged(e, args))
.subscribe(this._grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs<T>) => this.handleClick(e as MouseEvent, args))
.subscribe(this._grid.onHeaderClick, (e: DOMEvent, args: Slick.OnHeaderClickEventArgs<T>) => this.handleHeaderClick(e as MouseEvent, args))
.subscribe(this._grid.onKeyDown, (e: DOMEvent, args: Slick.OnKeyDownEventArgs<T>) => this.handleKeyDown(e as KeyboardEvent, args));
}
public destroy(): void {
this._handler.unsubscribeAll();
}
private handleSelectedRowsChanged(e: Event, args: Slick.OnSelectedRowsChangedEventArgs<T>): void {
if (this.isCustomActionRequested()) {
// do not assume anything for column based on row selection
// we can emit event here later if required.
return;
}
const selectedRows = this._grid.getSelectedRows();
let lookup: dict.INumberDictionary<boolean> = {}, row: number, i: number;
for (i = 0; i < selectedRows.length; i++) {
row = selectedRows[i];
lookup[row] = true;
if (lookup[row] !== this._selectedRowsLookup[row]) {
this._grid.invalidateRow(row);
delete this._selectedRowsLookup[row];
}
}
dict.forEach(this._selectedRowsLookup, (e) => this._grid.invalidateRow(Number(e.key)));
this._selectedRowsLookup = lookup;
this._grid.render();
if (!this._options.title) {
// when no title is specified, show the select all/deselect all checkbox
const headerCheckboxChecked = selectedRows.length > 0 && selectedRows.length === this._grid.getDataLength();
this._grid.updateColumnHeader(this._options.columnId!, this.getCheckboxHtml(headerCheckboxChecked, HeaderCheckboxTitle, true), this._options.toolTip);
}
}
private handleKeyDown(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs<T>): void {
if (this._grid.getColumns()[args.cell] && this._grid.getColumns()[args.cell].id !== this._options.columnId
|| !(this.getCheckboxPropertyValue(args.row).enabled)
) {
return;
}
const event = new StandardKeyboardEvent(e);
let handled = false;
if (event.equals(KeyCode.Space)) {
// if editing, try to commit
if (!this._grid.getEditorLock().isActive() || this._grid.getEditorLock().commitCurrentEdit()) {
if (this.isCustomActionRequested()) {
this.toggleCheckBox(args.row, args.cell, true);
}
else {
this.toggleRowSelection(args.row);
}
}
handled = true;
} else if (event.equals(KeyCode.Enter)) {
if (this.isCustomActionRequested()) {
this.toggleCheckBox(args.row, args.cell, true);
}
else {
this.toggleRowSelection(args.row);
}
handled = true;
}
if (handled) {
e.preventDefault();
e.stopPropagation();
}
}
private handleClick(e: Event, args: Slick.OnClickEventArgs<T>): void {
// clicking on a row select checkbox
if (this._grid.getColumns()[args.cell] && this._grid.getColumns()[args.cell].id === this._options.columnId && jQuery(e.target!).is('input[type="checkbox"]')) {
// if editing, try to commit
if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
if (this.isCustomActionRequested()) {
this.toggleCheckBox(args.row, args.cell, false);
}
else {
this.toggleRowSelection(args.row);
}
e.stopPropagation();
e.stopImmediatePropagation();
}
}
private toggleRowSelection(row: number): void {
if (this._selectedRowsLookup[row]) {
this._grid.setSelectedRows(this._grid.getSelectedRows().filter(n => n !== row));
} else {
this._grid.setSelectedRows(this._grid.getSelectedRows().concat(row));
}
}
private toggleCheckBox(row: number, col: number, reRender: boolean): void {
this._useState = true;
if (this._selectedCheckBoxLookup[row]) {
delete this._selectedCheckBoxLookup[row];
this._onChange.fire({ checked: false, row: row, column: col });
} else {
this._selectedCheckBoxLookup[row] = true;
this._onChange.fire({ checked: true, row: row, column: col });
}
if (reRender) {
// ensure that grid reflects the change
this._grid.invalidateRow(row);
this._grid.render();
}
//Ensure that the focus stays on current selected checkbox cell
this._grid.setActiveCell(row, col);
if (this._grid.getActiveCellNode()) {
this._grid.getActiveCellNode().focus();
}
// set selected row to the row of this checkbox
this._grid.setSelectedRows([row]);
}
// This call is to handle reactive changes in check box UI
// This DOES NOT fire UI change Events
reactiveCheckboxCheck(row: number, value: boolean) {
value ? this._selectedCheckBoxLookup[row] = true : delete this._selectedCheckBoxLookup[row];
// update row to call formatter
this._grid.updateRow(row);
// ensure that grid reflects the change
this._grid.scrollRowIntoView(row);
}
private handleHeaderClick(e: Event, args: Slick.OnHeaderClickEventArgs<T>): void {
if (this.isCustomActionRequested()) {
// do not assume action for column based on header click.
// we can emit event here later if required.
return;
}
if (!this._options.title && args.column.id === this._options.columnId && jQuery(e.target!).is('input[type="checkbox"]')) {
// if editing, try to commit
if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
const headerCheckboxChecked = jQuery(e.target!).is(':checked');
this._grid.setSelectedRows(headerCheckboxChecked ? range(this._grid.getDataLength()) : []);
this._grid.updateColumnHeader(this._options.columnId!,
this.getCheckboxHtml(headerCheckboxChecked, this._options.toolTip),
this._options.toolTip);
e.preventDefault();
e.stopPropagation();
}
}
public get definition(): Slick.Column<T> {
return {
id: this._options.columnId,
name: this._options.title || strings.format(checkboxTemplate, '', ''),
name: this._options.title || `<input type="checkbox" tabIndex="0" title=${HeaderCheckboxTitle}/>`,
toolTip: this._options.toolTip,
field: 'sel',
width: this._options.width,
@@ -253,43 +82,127 @@ export class CheckboxSelectColumn<T extends Slick.SlickData> implements Slick.Pl
}
private checkboxSelectionFormatter(row: number, cell: number, value: any, columnDef: Slick.Column<T>, dataContext: T): string {
if (this.isCustomActionRequested()) {
return this.checkboxTemplateCustom(row);
const state = this.getCheckboxPropertyValue(row);
const checked = state.checked ? 'checked' : '';
const enable = state.enabled ? '' : 'disabled';
return `<input type="checkbox" style="pointer-events: none;" tabIndex="-1" ${checked} ${enable}/>`;
}
// If checkbox is a row selector, we don't have requirement to enable/disable it, so always leave it enabled
return this.getCheckboxHtml(this._selectedRowsLookup[row], this._options.title, true);
public init(grid: Slick.Grid<T>): void {
this._grid = grid;
this._handler
.subscribe(this._grid.onClick, (e: Event, args: Slick.OnClickEventArgs<T>) => this.handleClick(e, args))
.subscribe(this._grid.onKeyDown, (e: DOMEvent, args: Slick.OnKeyDownEventArgs<T>) => this.handleKeyDown(e as KeyboardEvent, args))
.subscribe(this._grid.onHeaderClick, (e: Event, args: Slick.OnHeaderClickEventArgs<T>) => this.handleHeaderClick(e, args))
.subscribe(this._grid.onHeaderCellRendered, (e: Event, args: Slick.OnHeaderCellRenderedEventArgs<T>) => this.handleHeaderCellRendered(e, args));
}
checkboxTemplateCustom(row: number): string {
const propertyValue = this.getCheckboxPropertyValue(row);
// use state after toggles
if (this._useState) {
return this.getCheckboxHtml(this._selectedCheckBoxLookup[row], this._options.title, propertyValue.enabled);
private handleClick(e: DOMEvent, args: Slick.OnClickEventArgs<T>): void {
if (args.cell !== this.index) {
return;
}
this.toggleCellCheckbox(args.row);
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
}
// use data for first time rendering
// note: make sure Init is called before using this._grid
if (propertyValue.checked) {
this._selectedCheckBoxLookup[row] = true;
private handleKeyDown(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs<T>): void {
const event = new StandardKeyboardEvent(e);
if (args.cell !== this.index) {
return;
}
else {
delete this._selectedCheckBoxLookup[row];
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
this.toggleCellCheckbox(args.row);
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
}
return this.getCheckboxHtml(propertyValue.checked, this._options.title, propertyValue.enabled);
}
private isCustomActionRequested(): boolean {
return (this._options.actionOnCheck === ActionOnCheck.customAction);
private toggleCellCheckbox(row: number): void {
const currentValue = this.getCheckboxPropertyValue(row);
this.setCheckboxPropertyValue(row, !currentValue.checked);
this._grid.invalidateRow(row);
this._grid.render();
this._grid.setActiveCell(row, this.index);
this.checkSelectAll();
if(this._options.actionOnCheck === ActionOnCheck.selectRow){
this.updateSelectedRows();
} else {
this._onChange.fire({ checked: false, row: row, column: this.index });
}
}
private getCheckboxHtml(checked: boolean, title: string, enabled: boolean = true): string {
return strings.format(checkboxTemplate, checked ? 'checked' : '', title, enabled ? '' : 'disabled');
private updateSelectedRows(): void {
const checkedRows = [];
const rows = this._grid.getDataLength();
for (let i = 0; i < rows; i++) {
if (this.getCheckboxPropertyValue(i).checked) {
checkedRows.push(i);
}
}
this._grid.setSelectedRows(checkedRows);
}
private handleHeaderClick(e: Event, args?: Slick.OnHeaderClickEventArgs<T>): void {
this.onHeaderCheckboxStateChange();
e.preventDefault();
e.stopPropagation();
}
private handleHeaderKeyDown(e: KeyboardEvent): void {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
this._headerCheckbox.checked = !this._headerCheckbox.checked;
this.onHeaderCheckboxStateChange();
this._headerCheckbox.focus();
event.preventDefault();
event.stopPropagation();
}
}
public onHeaderCheckboxStateChange(): void {
const rows = this._grid.getDataLength();
for (let i = 0; i < rows; i++) {
this.setCheckboxPropertyValue(i, this._headerCheckbox.checked);
}
this._grid.updateColumnHeader(this._options.columnId!, `<input type="checkbox" tabIndex="0" ${this._headerCheckbox.checked ? 'checked' : ''} title=${HeaderCheckboxTitle}/>`, this._options.toolTip);
if(this._options.actionOnCheck === ActionOnCheck.selectRow){
this.updateSelectedRows();
}
this._grid.invalidateAllRows();
this._grid.render();
}
private handleHeaderCellRendered(e: Event, args: Slick.OnHeaderCellRenderedEventArgs<T>): void {
if (args.column.id === this._options.columnId) {
this._headerCheckbox = <HTMLInputElement>args.node.firstChild.firstChild;
this._headerCheckbox.onkeydown = (e) => this.handleHeaderKeyDown(e);
}
}
private checkSelectAll(): void {
const rows = this._grid.getDataLength();
let checked = true;
for (let i = 0; i < rows; i++) {
if (!this.getCheckboxPropertyValue(i).checked) {
checked = false;
break;
}
}
this._headerCheckbox.checked = checked;
}
public destroy(): void {
this._handler.unsubscribeAll();
}
private getCheckboxPropertyValue(row: number): ICheckboxColumnValue {
const dataItem = this._grid?.getDataItem(row);
const propertyValue = (dataItem && this._options.title) ? dataItem[this._options.title] : undefined;
const propertyValue = dataItem[this._options.title];
let checkboxEnabled: boolean = true;
let checkboxChecked: boolean = false;
if (typeof propertyValue === 'boolean') {
@@ -305,4 +218,17 @@ export class CheckboxSelectColumn<T extends Slick.SlickData> implements Slick.Pl
enabled: checkboxEnabled
};
}
private setCheckboxPropertyValue(row: number, value: boolean): void {
const dataItem = this._grid?.getDataItem(row);
const propertyValue = dataItem[this._options.title];
if (typeof propertyValue === 'boolean') {
(<any>dataItem)[this._options.title] = value;
} else {
(<any>dataItem)[this._options.title] = {
checked: value,
enabled: propertyValue.enabled
};
}
}
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.slick-header-columns .slick-header-column.slick-plugin-checkbox-select-column.ui-state-default,
.slick-plugin-checkbox-select-column {
text-align: center;
border-bottom: 0px;
border-right: 0px;
}
.slick-plugin-checkbox-select-column > input,
.slick-column-name > input[type='checkbox']{
margin: 0px;
vertical-align: middle;
}

View File

@@ -174,6 +174,8 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
if (this._autoscroll) {
this._grid.scrollRowIntoView(this._data.getLength() - 1, false);
}
this.ariaRowCount = this.grid.getDataLength();
this.ariaColumnCount = this.grid.getColumns().length;
}
set columns(columns: Slick.Column<T>[]) {
@@ -194,6 +196,7 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
this._data = new TableDataView<T>(data);
}
this._grid.setData(this._data, true);
this.updateRowCount();
}
getData(): IDisposableDataProvider<T> {

View File

@@ -277,7 +277,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
this._table = new Table<Slick.SlickData>(this._inputContainer.nativeElement, { dataProvider: this._tableData, columns: this._tableColumns }, options);
this._table.setData(this._tableData);
this._table.setSelectionModel(new RowSelectionModel({ selectActiveRow: true }));
this._table.setSelectionModel(new RowSelectionModel({ selectActiveRow: false }));
this._register(this._table);
this._register(attachTableStyler(this._table, this.themeService));

View File

@@ -9483,9 +9483,9 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
"slickgrid@github:Microsoft/SlickGrid.ADS#2.3.36":
version "2.3.36"
resolved "https://codeload.github.com/Microsoft/SlickGrid.ADS/tar.gz/08f1ca3f6eb2cd1ea7f56e2af4b69600d48e1932"
"slickgrid@github:Microsoft/SlickGrid.ADS#2.3.37":
version "2.3.37"
resolved "https://codeload.github.com/Microsoft/SlickGrid.ADS/tar.gz/1de979b3cf66cee46846e5e0d2edbc938c8d6563"
smart-buffer@^4.1.0:
version "4.1.0"