mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-03 01:25:38 -05:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
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}/>`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// use data for first time rendering
|
||||
// note: make sure Init is called before using this._grid
|
||||
if (propertyValue.checked) {
|
||||
this._selectedCheckBoxLookup[row] = true;
|
||||
}
|
||||
else {
|
||||
delete this._selectedCheckBoxLookup[row];
|
||||
}
|
||||
return this.getCheckboxHtml(propertyValue.checked, this._options.title, propertyValue.enabled);
|
||||
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));
|
||||
}
|
||||
|
||||
private isCustomActionRequested(): boolean {
|
||||
return (this._options.actionOnCheck === ActionOnCheck.customAction);
|
||||
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();
|
||||
}
|
||||
|
||||
private getCheckboxHtml(checked: boolean, title: string, enabled: boolean = true): string {
|
||||
return strings.format(checkboxTemplate, checked ? 'checked' : '', title, enabled ? '' : 'disabled');
|
||||
private handleKeyDown(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs<T>): void {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (args.cell !== this.index) {
|
||||
return;
|
||||
}
|
||||
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
|
||||
this.toggleCellCheckbox(args.row);
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user