From: Bill Erickson <berickxx@gmail.com> Date: Tue, 5 Mar 2019 22:07:21 +0000 (-0500) Subject: LP1816480 Angular grid ARIA improvements X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=67e85c40d41c46fd123eed51729f966d9336549f;p=evergreen%2Fjoelewis.git LP1816480 Angular grid ARIA improvements Various navigation and "role" improvements to the Angular grid: * Apply "grid", "row", "columnheader", and "gridcell" role attributes. * Page-Down goes to next page * Page-Up goes to previous page * Shift-UpArrow extends selection one row up (spanning pages). * Shift-DownArrow extends selection one row down (spanning pages). * Shift-Arrow controls support reverse navigation for back-tracking to de-select certain rows. ** E.g. shift-up 3 rows then shift-down 1 will leave 2 rows selected. * Control-A now selects all rows in the page. ** For consistency with the select-all checkbox, only rows in the current page are selected. ** Note we could add an option to extend the selection to all rows, but it would require pre-fetching all of the data, simimar to how grid printing pre-fetches. Signed-off-by: Bill Erickson <berickxx@gmail.com> Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu> --- diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html index 8d495aa84d..a9f35aaaa5 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html @@ -2,7 +2,7 @@ tabindex=1 so the grid body can capture keyboard events. --> <div class="eg-grid-body" tabindex="1" (keydown)="onGridKeyDown($event)"> - <div class="eg-grid-row eg-grid-body-row {{context.rowClassCallback(row)}}" + <div role="row" class="eg-grid-row eg-grid-body-row {{context.rowClassCallback(row)}}" [ngClass]="{'selected': context.rowSelector.contains(context.getRowIndex(row))}" *ngFor="let row of context.dataSource.getPageOfRows(context.pager); let idx = index"> @@ -27,7 +27,8 @@ </ng-container> </ng-container> </div> - <div class="eg-grid-cell eg-grid-body-cell" [ngStyle]="{flex:col.flex}" + <div role="gridcell" class="eg-grid-cell eg-grid-body-cell" + [ngStyle]="{flex:col.flex}" [ngClass]="{'eg-grid-cell-overflow': context.overflowCells}" (dblclick)="onRowDblClick(row)" (click)="onRowClick($event, row, idx)" diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts index 15aa2b7038..9c9b19041c 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts @@ -20,23 +20,45 @@ export class GridBodyComponent implements OnInit { onGridKeyDown(evt: KeyboardEvent) { switch (evt.key) { case 'ArrowUp': - this.context.selectPreviousRow(); + if (evt.shiftKey) { + // Extend selection up one row + this.context.selectMultiRowsPrevious(); + } else { + this.context.selectPreviousRow(); + } evt.stopPropagation(); break; case 'ArrowDown': - this.context.selectNextRow(); + if (evt.shiftKey) { + // Extend selection down one row + this.context.selectMultiRowsNext(); + } else { + this.context.selectNextRow(); + } evt.stopPropagation(); break; case 'ArrowLeft': + case 'PageUp': this.context.toPrevPage() .then(ok => this.context.selectFirstRow(), err => {}); evt.stopPropagation(); break; case 'ArrowRight': + case 'PageDown': this.context.toNextPage() .then(ok => this.context.selectFirstRow(), err => {}); evt.stopPropagation(); break; + case 'a': + // control-a means select all visible rows. + // For consistency, select all rows in the current page only. + if (evt.ctrlKey) { + this.context.rowSelector.clear(); + this.context.selectRowsInPage(); + evt.preventDefault(); + } + break; + case 'Enter': if (this.context.lastSelectedIndex) { this.grid.onRowActivate.emit( diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html index 98a6fbf365..96811a32aa 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html @@ -1,26 +1,31 @@ -<div class="eg-grid-row eg-grid-header-row"> +<div row="row" class="eg-grid-row eg-grid-header-row"> <ng-container *ngIf="!context.disableSelect"> - <div class="eg-grid-cell eg-grid-header-cell eg-grid-checkbox-cell eg-grid-cell-skinny"> - <input type='checkbox' (click)="handleBatchSelect($event)"> + <div role="columnheader" + class="eg-grid-cell eg-grid-header-cell eg-grid-checkbox-cell eg-grid-cell-skinny"> + <input type='checkbox' (click)="handleBatchSelect($event)" + [(ngModel)]="batchRowCheckbox"> </div> </ng-container> - <div class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny"> + <div role="columnheader" + class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny"> <span i18n="number|Row Number Header">#</span> </div> - <div *ngIf="context.rowFlairIsEnabled" + <div *ngIf="context.rowFlairIsEnabled" + role="columnheader" class="eg-grid-cell eg-grid-header-cell eg-grid-flair-cell"> <span class="material-icons">notifications</span> </div> - <div *ngFor="let col of context.columnSet.displayColumns()" - draggable="true" + <div role="columnheader" + *ngFor="let col of context.columnSet.displayColumns()" + draggable="true" (dragstart)="dragColumn = col" (drop)="onColumnDrop(col)" (dragover)="onColumnDragEnter($event, col)" (dragleave)="onColumnDragLeave($event, col)" [ngClass]="{'dragover' : col.isDragTarget}" class="eg-grid-cell eg-grid-header-cell" [ngStyle]="{flex:col.flex}"> - <a class="sortable label-with-material-icon" *ngIf="col.isSortable" + <a class="sortable label-with-material-icon" *ngIf="col.isSortable" (click)="sortOneColumn(col)"> <span class="eg-grid-header-cell-sort-label">{{col.label}}</span> <span class="material-icons eg-grid-header-cell-sort-arrow" diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts index 0010a45f38..591fc66c2b 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts @@ -13,9 +13,15 @@ export class GridHeaderComponent implements OnInit { dragColumn: GridColumn; + batchRowCheckbox: boolean; + constructor() {} - ngOnInit() {} + ngOnInit() { + this.context.selectRowsInPageEmitter.subscribe( + () => this.batchRowCheckbox = true + ); + } onColumnDragEnter($event: any, col: any) { if (this.dragColumn && this.dragColumn.name !== col.name) { @@ -71,9 +77,7 @@ export class GridHeaderComponent implements OnInit { } selectAll() { - const rows = this.context.dataSource.getPageOfRows(this.context.pager); - const indexes = rows.map(r => this.context.getRowIndex(r)); - this.context.rowSelector.select(indexes); + this.context.selectRowsInPage(); } allRowsAreSelected(): boolean { diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html index 5dd307f4f9..af223fefdf 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html @@ -19,7 +19,7 @@ <label class="form-check-label"> <input class="form-check-input" type="checkbox" [(ngModel)]="cb.isChecked" - (click)="cb.onChange.emit($event.target.checked)"/> + (click)="cb.onChange($event.target.checked)"/> {{cb.label}} </label> </ng-container> @@ -29,6 +29,9 @@ <!-- push everything else to the right --> <div class="flex-1"></div> + <div class="font-sm font-italic d-flex flex-column-reverse mr-2"> + {{gridContext.rowSelector.selected().length}} selected + </div> <div ngbDropdown class="mr-1" placement="bottom-right"> <button ngbDropdownToggle [disabled]="!gridContext.toolbarActions.length" class="btn btn-outline-dark no-dropdown-caret"> diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html index a98e17afaf..77ea0e6fb1 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html @@ -1,8 +1,8 @@ -<div class="eg-grid"> +<div class="eg-grid" role="grid"> <eg-grid-toolbar - [gridContext]="context" + [gridContext]="context" [gridPrinter]="gridPrinter" [colWidthConfig]="colWidthConfig"> </eg-grid-toolbar> @@ -11,7 +11,7 @@ <eg-grid-column-width #colWidthConfig [gridContext]="context"> </eg-grid-column-width> - + <eg-grid-print #gridPrinter [gridContext]="context"> </eg-grid-print> diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts index 7c30438ccc..fbc020895d 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts @@ -442,6 +442,10 @@ export class GridContext { overflowCells: boolean; showLinkSelectors: boolean; + // Allow calling code to know when the select-all-rows-in-page + // action has occurred. + selectRowsInPageEmitter: EventEmitter<void>; + // Services injected by our grid component idl: IdlService; org: OrgService; @@ -467,6 +471,7 @@ export class GridContext { } init() { + this.selectRowsInPageEmitter = new EventEmitter<void>(); this.columnSet = new GridColumnSet(this.idl, this.idlClass); this.columnSet.isSortable = this.isSortable === true; this.columnSet.isMultiSortable = this.isMultiSortable === true; @@ -731,6 +736,12 @@ export class GridContext { this.lastSelectedIndex = index; } + selectMultipleRows(indexes: any[]) { + this.rowSelector.clear(); + this.rowSelector.select(indexes); + this.lastSelectedIndex = indexes[indexes.length - 1]; + } + // selects or deselects an item, without affecting the others. // returns true if the item is selected; false if de-selected. toggleSelectOneRow(index: any) { @@ -770,12 +781,84 @@ export class GridContext { } } + // shift-up-arrow + // Select the previous row in addition to any currently selected row. + // However, if the previous row is already selected, assume the user + // has reversed direction and now wants to de-select the last selected row. + selectMultiRowsPrevious() { + if (!this.lastSelectedIndex) { return; } + const pos = this.getRowPosition(this.lastSelectedIndex); + const selectedIndexes = this.rowSelector.selected(); + + const promise = // load the previous page of data if needed + (pos === this.pager.offset) ? this.toPrevPage() : Promise.resolve(); + + promise.then( + ok => { + const row = this.dataSource.data[pos - 1]; + const newIndex = this.getRowIndex(row); + if (selectedIndexes.filter(i => i === newIndex).length > 0) { + // Prev row is already selected. User is reversing direction. + this.rowSelector.deselect(this.lastSelectedIndex); + this.lastSelectedIndex = newIndex; + } else { + this.selectMultipleRows(selectedIndexes.concat(newIndex)); + } + }, + err => {} + ); + } + + // shift-down-arrow + // Select the next row in addition to any currently selected row. + // However, if the next row is already selected, assume the user + // has reversed direction and wants to de-select the last selected row. + selectMultiRowsNext() { + if (!this.lastSelectedIndex) { return; } + const pos = this.getRowPosition(this.lastSelectedIndex); + const selectedIndexes = this.rowSelector.selected(); + + const promise = // load the next page of data if needed + (pos === (this.pager.offset + this.pager.limit - 1)) ? + this.toNextPage() : Promise.resolve(); + + promise.then( + ok => { + const row = this.dataSource.data[pos + 1]; + const newIndex = this.getRowIndex(row); + if (selectedIndexes.filter(i => i === newIndex).length > 0) { + // Next row is already selected. User is reversing direction. + this.rowSelector.deselect(this.lastSelectedIndex); + this.lastSelectedIndex = newIndex; + } else { + this.selectMultipleRows(selectedIndexes.concat(newIndex)); + } + }, + err => {} + ); + } + + getFirstRowInPage(): any { + return this.dataSource.data[this.pager.offset]; + } + + getLastRowInPage(): any { + return this.dataSource.data[this.pager.offset + this.pager.limit - 1]; + } + selectFirstRow() { - this.selectRowByPos(this.pager.offset); + this.selectOneRow(this.getRowIndex(this.getFirstRowInPage())); } selectLastRow() { - this.selectRowByPos(this.pager.offset + this.pager.limit - 1); + this.selectOneRow(this.getRowIndex(this.getLastRowInPage())); + } + + selectRowsInPage() { + const rows = this.dataSource.getPageOfRows(this.pager); + const indexes = rows.map(r => this.getRowIndex(r)); + this.rowSelector.select(indexes); + this.selectRowsInPageEmitter.emit(); } toPrevPage(): Promise<any> {