table markup and IDL-based classes for eg-grid
authorStephanie Leary <stephanie.leary@equinoxoli.org>
Wed, 8 Mar 2023 19:24:48 +0000 (19:24 +0000)
committerStephanie Leary <stephanie.leary@equinoxoli.org>
Mon, 22 May 2023 15:27:25 +0000 (15:27 +0000)
Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org>
27 files changed:
Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html
Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html
Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid-filter-control.component.html
Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.component.css
Open-ILS/src/eg2/src/app/share/grid/grid.component.html
Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts
Open-ILS/src/eg2/src/app/staff/acq/provider/provider-contacts.component.html
Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.html
Open-ILS/src/eg2/src/app/staff/acq/provider/provider-results.component.html
Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html
Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html
Open-ILS/src/eg2/src/app/staff/admin/acq/currency/currencies.component.html
Open-ILS/src/eg2/src/app/staff/admin/acq/edi_attr_set/edi-attr-sets.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-setting-history-dialog.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/org-unit-settings/org-unit-settings.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/standing-penalty.component.ts
Open-ILS/src/eg2/src/app/staff/booking/create-reservation.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts
Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.html
Open-ILS/src/eg2/tsconfig.json

index 14ce003..137b2be 100644 (file)
@@ -1,8 +1,7 @@
 
-<span *ngIf="!column.cellTemplate"
-  [ngbTooltip]="column.disableTooltip ? null : context.getRowColumnValue(row, column)"
+<div *ngIf="!column.cellTemplate"
+  [ngbTooltip]="column.enableTooltip ? context.getRowColumnValue(row, column) : null"
   placement="top-left"
-  class="{{context.cellClassCallback(row, column)}}"
   triggers="mouseenter:mouseleave">
   <ng-container *ngIf="column.datatype === 'bool'">
     <eg-bool [value]="context.getRowColumnValue(row, column)"
   <ng-container *ngIf="column.datatype !== 'bool'">
     {{context.getRowColumnValue(row, column)}}
   </ng-container>
-</span>
-<span *ngIf="column.cellTemplate" 
-  class="{{context.cellClassCallback(row, column)}}"
-  [ngbTooltip]="column.disableTooltip ? null : column.cellTemplate"
+</div>
+<div *ngIf="column.cellTemplate" 
+  [ngbTooltip]="column.enableTooltip ? column.cellTemplate : null"
   placement="top-left"
   #tooltip="ngbTooltip" 
   (mouseenter)="tooltip.open(column.getCellContext(row))"
@@ -23,5 +21,5 @@
   <ng-container #templateContainer
     *ngTemplateOutlet="column.cellTemplate; context: column.getCellContext(row)">
   </ng-container> 
-</span>
+</div>
 
index 55c7fef..a9a62a7 100644 (file)
@@ -1,19 +1,16 @@
-<!-- uses dropdown menu CSS for easy stying, but it's not a dropdown -->
-<ng-template #contextMenu let-gridContext="gridContext">
-  <eg-grid-toolbar-actions-menu [gridContext]="gridContext" [viaContextMenu]="true">
-  </eg-grid-toolbar-actions-menu>
-</ng-template>
+
 
 <!--
   tabindex=1 so the grid body can capture keyboard events.
 -->
-<div class="eg-grid-body" tabindex="1" (keydown)="onGridKeyDown($event)">
-  <div role="row" class="eg-grid-row eg-grid-body-row {{context.rowClassCallback(row)}}"
+
+  <tr scope="row" 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">
 
     <ng-container *ngIf="!context.disableSelect">
-      <div class="eg-grid-cell eg-grid-checkbox-cell eg-grid-cell-skinny">
+      <td role="cell" class="eg-grid-cell eg-grid-checkbox-cell">
+        <div class="eg-grid-cell-contents">
         <input type='checkbox' 
           [ngModel]="context.rowSelector.indexes[context.getRowIndex(row)]"
           (ngModelChange)="context.rowSelector.toggle(context.getRowIndex(row))"
           [ngbPopover]="contextMenu"
           placement="right"
           triggers="manual">
-      </div>
+        </div>
+      </td>
     </ng-container>
-    <div class="eg-grid-cell eg-grid-number-cell eg-grid-cell-skinny-2">
-      {{context.pager.rowNumber(idx)}}
-    </div>
-    <div *ngIf="context.rowFlairIsEnabled" class="eg-grid-cell eg-grid-flair-cell">
+    <td role="cell" class="eg-grid-cell eg-grid-number-cell numeric">
+      <div class="eg-grid-cell-contents">{{context.pager.rowNumber(idx)}}</div>
+    </td>
+    <td role="cell" *ngIf="context.rowFlairIsEnabled" class="eg-grid-cell eg-grid-flair-cell">
       <!-- using *ngIf allows us to assign the flair callback to a value,
             obviating the need for multiple calls of the same function -->
       <ng-container *ngIf="context.rowFlairCallback(row); let flair">
         <ng-container *ngIf="flair.icon">
+          <div class="eg-grid-cell-contents">
           <!-- tooltip is disabled when no title is set -->
           <span class="material-icons"
             ngbTooltip="{{flair.title || ''}}" triggers="mouseenter:mouseleave">
             {{flair.icon}}
           </span>
+          </div>
         </ng-container>
       </ng-container>
-    </div>
+    </td>
     <!-- contextMenu applied to cells instead of rows so the position
          of the popover is close to the mouse.  As of writing, no way
-         to position the popover at the mouse -->
-    <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)"
-      #rowContextMenu="ngbPopover"
-      popoverTitle="Actions for Selected Rows" i18n-popoverTitle
-      (contextmenu)="onRowContextClick($event, row, rowContextMenu)"
-      [ngbPopover]="contextMenu"
-      placement="bottom"
-      triggers="manual"
-      *ngFor="let col of context.columnSet.displayColumns()">
+         to position the popover at the mouse. -->
+    <!-- aria-describedby is removed to prevent the entire context menu 
+        from being read after the cell contents -->
+    <td role="cell" *ngFor="let col of context.columnSet.displayColumns()"
+      class="eg-grid-cell eg-grid-body-cell {{context.setClassNames(row, col)}}">
+        <div class="eg-grid-cell-contents" (dblclick)="onRowDblClick(row)"
+          (click)="onRowClick($event, row, idx)"
+          #rowContextMenu="ngbPopover"
+          popoverTitle="Actions for Selected Rows" i18n-popoverTitle
+          (contextmenu)="onRowContextClick($event, row, rowContextMenu)"
+          [ngbPopover]="contextMenu" [attr.aria-describedby]="null"
+          placement="bottom"
+          triggers="manual">  
+          <eg-grid-body-cell [context]="context" [row]="row" [column]="col">
+          </eg-grid-body-cell>
+        </div>
+      </td>
 
-      <eg-grid-body-cell [context]="context" [row]="row" [column]="col">
-      </eg-grid-body-cell>
-    </div>
-  </div>
-</div>
+      <!-- This is set to visually-hidden so its presence in a <tr> doesn't disrupt table columns -->
+      <ng-template class="visually-hidden" #contextMenu let-gridContext="gridContext">
+        <eg-grid-toolbar-actions-menu [gridContext]="gridContext" [viaContextMenu]="true">
+        </eg-grid-toolbar-actions-menu>
+      </ng-template>
+  </tr>
 
index 2c21527..ece5f9e 100644 (file)
@@ -4,7 +4,7 @@ import {GridComponent} from './grid.component';
 import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
 
 @Component({
-  selector: 'eg-grid-body',
+  selector: 'tbody.eg-grid-body',
   templateUrl: './grid-body.component.html'
 })
 
index 62fa2c7..66889c0 100644 (file)
@@ -46,7 +46,7 @@ export class GridColumnComponent implements OnInit {
     @Input() cellContext: any;
     @Input() cellTemplate: TemplateRef<any>;
 
-    @Input() disableTooltip: boolean;
+    @Input() enableTooltip: boolean;
     @Input() asyncSupportsEmptyTermClick: boolean;
 
     // Required columns are those that must be present in any auto-generated
@@ -77,7 +77,7 @@ export class GridColumnComponent implements OnInit {
         col.isIndex = this.index === true;
         col.cellTemplate = this.cellTemplate;
         col.cellContext = this.cellContext;
-        col.disableTooltip = this.disableTooltip;
+        col.enableTooltip = this.enableTooltip;
         col.isSortable = this.sortable;
         col.isFilterable = this.filterable;
         col.filterOperator = this.initialFilterOperator;
index 25d8e0a..3dc99ff 100644 (file)
@@ -1,12 +1,13 @@
 
 <!-- drop-down toggle link -->
 <ng-template #dropdownToggle>
-  <span i18n>Filter</span>
   <ng-container *ngIf="!col.isFiltered">
-    <span class="material-icons mat-icon-in-button">filter_list</span>
+    <span id="filter_toggle_{{col.name}}" i18n hidden>Filter by {{col.headerLabel}}</span>
+    <span class="material-icons mat-icon-in-button" attr.aria-hidden="true" title="Filter by {{col.headerLabel}}" i18n-title>filter_alt</span>
   </ng-container>
   <ng-container *ngIf="col.isFiltered">
-    <span class="material-icons mat-icon-in-button">create</span>
+    <span id="filter_toggle_{{col.name}}" i18n hidden>Edit {{col.headerLabel}} filter</span>
+    <span class="material-icons mat-icon-in-button" attr.aria-hidden="true" title="Edit {{col.headerLabel}} filter" i18n-title>create</span>
   </ng-container>
 </ng-template>
 
   </select>
 </ng-template>
 
-<div *ngIf="col.isFilterable" class="eg-grid-filter-control">
-  <div [ngSwitch]="col.datatype">
-    <div *ngSwitchCase="'link'">
-      <div class="input-group">
-        <div ngbDropdown container="body" class="d-inline-block p-1" [autoClose]="false" placement="bottom-left" 
+
+  <ng-container *ngIf="col.isFilterable" [ngSwitch]="col.datatype">
+    <div *ngSwitchCase="'link'" class="eg-grid-filter-control">
+      
+        <div ngbDropdown container="body"  [autoClose]="false" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
           </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'bool'">
-      <div class="input-group">
-        <div ngbDropdown container="body" class="d-inline-block p-1" autoClose="outside" placement="bottom-left" 
+    <div *ngSwitchCase="'bool'" class="eg-grid-filter-control">
+      
+        <div ngbDropdown container="body"  autoClose="outside" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
           </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'text'">
-      <div class="input-group">
-        <div ngbDropdown container="body" class="d-inline-block p-1" autoClose="outside" placement="bottom-left" 
+    <div *ngSwitchCase="'text'" class="eg-grid-filter-control">
+      
+        <div ngbDropdown container="body"  autoClose="outside" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
           </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'int'">
-      <div class="input-group">
-        <div ngbDropdown container="body" class="d-inline-block p-1" autoClose="outside" placement="bottom-left" 
+    <div *ngSwitchCase="'int'" class="eg-grid-filter-control">
+      
+        <div ngbDropdown container="body"  autoClose="outside" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
           </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'id'">
-      <div class="input-group">
-        <div ngbDropdown container="body" class="d-inline-block p-1" autoClose="outside" placement="bottom-left" 
+    <div *ngSwitchCase="'id'" class="eg-grid-filter-control">
+      
+        <div ngbDropdown container="body"  autoClose="outside" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
           </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'float'">
-      <div class="input-group">
-        <div ngbDropdown container="body" class="d-inline-block p-1" autoClose="outside" placement="bottom-left" 
+    <div *ngSwitchCase="'float'" class="eg-grid-filter-control">
+      
+        <div ngbDropdown container="body"  autoClose="outside" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
           </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'money'">
-      <div class="input-group">
-        <div ngbDropdown container="body" class="d-inline-block p-1" autoClose="outside" placement="bottom-left" 
+    <div *ngSwitchCase="'money'" class="eg-grid-filter-control">
+      
+        <div ngbDropdown container="body"  autoClose="outside" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
          </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'timestamp'">
-      <div class="input-group">
+    <div *ngSwitchCase="'timestamp'" class="eg-grid-filter-control">
+      
         <!-- [autoClose]="false" because editing the date widgets, which open
              their open popups, registers to the dropdown as clicking 
              outside the dropdown -->
-        <div ngbDropdown container="body" class="d-inline-block p-1" [autoClose]="false" placement="bottom-left" 
+        <div ngbDropdown container="body"  [autoClose]="false" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
           </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'org_unit'">
-      <div class="input-group">
-        <div ngbDropdown container="body" class="d-inline-block p-1" [autoClose]="false" placement="bottom-left" 
+    <div *ngSwitchCase="'org_unit'" class="eg-grid-filter-control">
+      
+        <div ngbDropdown container="body"  [autoClose]="false" placement="bottom-left" 
           [ngClass]="{'border rounded border-secondary eg-grid-col-is-filtered' : col.isFiltered}">
-          <a ngbDropdownToggle class="no-dropdown-caret text-dark" href="javascript:;">
+          <button ngbDropdownToggle class="btn no-dropdown-caret ps-1" attr.aria-labelledby="filter_toggle_{{col.name}}">
             <ng-container *ngTemplateOutlet="dropdownToggle"></ng-container>
-          </a>
+          </button>
           <div ngbDropdownMenu class="eg-grid-filter-menu">
             <div class="dropdown-item">
               <div>
             </div>
           </div>
         </div>
-      </div>
+      
     </div>
-    <div *ngSwitchCase="'interval'">
+    <div *ngSwitchCase="'interval'" class="eg-grid-filter-control">
       <!-- this is a short-term fix to prevent *ngSwitchDefault from displaying -->
     </div>
     <div *ngSwitchDefault>I don't know how to filter {{col.name}} - {{col.datatype}}</div>
-  </div>
+  </ng-container>
   <!--
   <span *ngIf="col.datatype !== 'org_unit'" class="eg-grid-filter-operator"><ng-container i18n>Operator:</ng-container>
     <span [ngSwitch]="col.filterOperator">
     </span>
   </span>
   -->
-</div>
+
index a3d0a6b..c603367 100644 (file)
@@ -1,53 +1,43 @@
+<!-- note that colgroups are set up in grid.component.html -->
 
-<div row="row" class="eg-grid-row eg-grid-header-row">
+<tr row="row" role="row" class="eg-grid-row eg-grid-header-row">
   <ng-container *ngIf="!context.disableSelect">
-    <div role="columnheader"
-      class="eg-grid-cell eg-grid-header-cell eg-grid-checkbox-cell eg-grid-cell-skinny">
+    <th scope="col" role="columnheader" 
+      class="eg-grid-cell eg-grid-header-cell eg-grid-checkbox-cell">
       <input type='checkbox' (click)="handleBatchSelect($event)"
         i18n-aria-label aria-label="All rows"
         [(ngModel)]="batchRowCheckbox">
-    </div>
+    </th>
   </ng-container>
-  <div role="columnheader"
-    class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny">
+  <th scope="col" role="columnheader" class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell numeric">
     <span i18n="number|Row Number Header">#</span>
-  </div>
-  <div *ngIf="context.rowFlairIsEnabled"
-    role="columnheader"
+  </th>
+  <th *ngIf="context.rowFlairIsEnabled" scope="col" 
     class="eg-grid-cell eg-grid-header-cell eg-grid-flair-cell">
     <span class="material-icons">notifications</span>
-  </div>
-  <div role="columnheader"
-    *ngFor="let col of context.columnSet.displayColumns()"
+  </th>
+  <th *ngFor="let col of context.columnSet.displayColumns()"
+    scope="col" role="columnheader"
     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"
+    [attr.aria-sort]="ariaSortDirection(col)"
+    class="eg-grid-cell eg-grid-header-cell {{context.setClassNames(row, col)}}">
+    <button class="sortable btn d-inline p-0" *ngIf="col.isSortable"
       (click)="sortOneColumn(col)">
-      <span class="eg-grid-header-cell-sort-label">{{col.headerLabel}}</span>
+      <span class="eg-grid-header-cell-sort-label" title="Sort by {{col.headerLabel}}" i18n-title>{{col.headerLabel}}</span>
       <span class="material-icons eg-grid-header-cell-sort-arrow"
         *ngIf="isColumnSorting(col, 'ASC')">arrow_upwards</span>
       <span class="material-icons eg-grid-header-cell-sort-arrow"
         *ngIf="isColumnSorting(col, 'DESC')">arrow_downwards</span>
-    </a>
+    </button>
     <span *ngIf="!col.isSortable">{{col.headerLabel}}</span>
-  </div>
-</div>
-<div *ngIf="context.isFilterable"
-  class="eg-grid-row eg-grid-filter-controls-row">
-  <ng-container *ngIf="!context.disableSelect">
-    <div class="eg-grid-cell eg-grid-header-cell eg-grid-cell-skinny"></div>
-  </ng-container>
-  <div class="eg-grid-cell eg-grid-header-cell eg-grid-cell-skinny"></div>
-  <div *ngIf="context.rowFlairIsEnabled" 
-    class="eg-grid-cell eg-grid-header-cell"></div>
 
-  <div *ngFor="let col of context.columnSet.displayColumns()" 
-    class="eg-grid-cell eg-grid-filter-control-cell" [ngStyle]="{flex:col.flex}">
-    <eg-grid-filter-control [context]="context" [col]="col"></eg-grid-filter-control>
-  </div>
-</div>
+    <ng-container *ngIf="context.isFilterable">
+      <eg-grid-filter-control [context]="context" [col]="col"></eg-grid-filter-control>
+    </ng-container>
+  </th>
+</tr>
\ No newline at end of file
index cc53b26..db85186 100644 (file)
@@ -4,7 +4,7 @@ import {GridContext, GridColumn, GridRowSelector,
 import {GridFilterControlComponent} from './grid-filter-control.component';
 
 @Component({
-  selector: 'eg-grid-header',
+  selector: '.eg-grid-header',
   templateUrl: './grid-header.component.html'
 })
 
@@ -71,6 +71,18 @@ export class GridHeaderComponent implements OnInit, AfterViewInit {
         return sort && sort.dir === dir;
     }
 
+    // Returns sorting direction in ARIA's required format
+    ariaSortDirection(col: GridColumn): string {
+        const sort = this.context.dataSource.sort.filter(c => c.name === col.name)[0];
+        
+        if (sort && sort.dir === 'ASC')
+            return 'ascending';
+        if (sort && sort.dir === 'DESC')
+            return 'descending';
+        
+        return null;
+    }
+
     handleBatchSelect($event) {
         if ($event.target.checked) {
             if (this.context.rowSelector.isEmpty() || !this.allRowsAreSelected()) {
index 0cd8bf3..efd2ac9 100644 (file)
@@ -1,14 +1,44 @@
+@media (max-width: 960px) {
+  .eg-grid-wrapper {
+    overflow-x: auto;
+  }
+}
+
+.eg-grid-wrapper {
+  width: fit-content;
+  overflow-x: auto;
+}
+
+.eg-grid-wrapper:focus-visible {
+  outline: 2px solid #0A58CA;
+  outline-offset: 5px;
+  width: auto;
+}
+
+/* Undo Bootstrap table width */
+table.table.eg-grid {
+  width: revert;
+}
 
 .eg-grid {
-    width: 100%;
-    color: rgba(0,0,0,.87); 
+  caption-side: top;
+  color: rgba(0,0,0,.87);
+  table-layout: auto;
+  white-space: normal; 
+}
+
+.eg-grid > :not(:first-child) {
+  border-top: revert;
+}
+
+.eg-grid caption {
+  color: #000;
 }
     
 .eg-grid-row {
-    display: flex;
-    border-bottom: 1px solid rgba(0,0,0,.12);
-    padding-left: 10px;
-    padding-right: 10px;
+  border-bottom: 1px solid rgba(0,0,0,.12);
+  padding-left: 10px;
+  padding-right: 10px;
 }
 
 .eg-grid-header-row {
 }
 
 .eg-grid-body {
-    outline: none; /* for keyboard events */
-}
-
-.eg-grid-body-row {
+  outline: none; /* for keyboard events */
 }
 
 .eg-grid-body-row.selected, 
   border-color: #b8daff;
 }
 
-.eg-grid-cell {
-    flex: 2; /* applied per column */
-    padding: 6px;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    overflow: hidden;
-}
-
 /* allow tooltips to be wider than the default 200px */
 .eg-grid-cell .tooltip-inner {
   max-width: 400px; 
  * inconsistent grid column widths
  */
 .eg-grid-cell-overflow {
-    white-space: normal;
+  white-space: normal;
 }
 
-.eg-grid-body-cell {
+.eg-grid-header-cell {
+  font-weight: 600;
+  white-space: normal;
 }
 
-.eg-grid-body-cell a[href] {
-    text-decoration: underline !important;
+.eg-grid-header-cell .btn.sortable {
+  color: #255a88;
+  font-weight: 600;
+  text-align: inherit;
 }
 
-.eg-grid-header-cell {
-  font-weight: bold;
-  white-space: normal;
+.eg-grid-header-cell[draggable=true] {
+  cursor: grab;
 }
 
 .eg-grid-header-cell.dragover {
-    background-color: #cce5ff;
-    border-color: #b8daff;
-}
-
-.eg-grid-header-cell-sort-label {
-  cursor: pointer;
-  text-decoration: underline;
+  background-color: #cce5ff;
+  border-color: #b8daff;
 }
 
 .eg-grid-header-cell-sort-arrow {
   font-size: 14px;
 }
 
+.col-resize {
+  display: block;
+  background: transparent;
+  border: 0;
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 5px;
+  height: 100%;
+  cursor: col-resize;
+}
+
+.col-resize:hover,
+.col-resize:active,
+.col-resize:focus,
+.resizing {
+  background-color: rgba(0,0,225,0.05);
+  border-right: 2px solid blue;
+  cursor: col-resize;
+  outline: 2px solid transparent;
+}
+
 .eg-grid-toolbar {
   display: flex;
 }
   padding-bottom: 11px;
 }
 
-.eg-grid-cell-skinny {
-  width: 2em;
-  text-align: center;
-  flex: none;
-}
-
-.eg-grid-cell-skinny-2 {
-  width: 2.6em;
-  flex: none;
-}
-
-.eg-grid-flair-cell {
-  /* mat icons currently 22px, unclear why it needs this much space */
-  width: 34px; 
-  text-align: center;
-  flex: none;
-}
-
-/* depends on width of .eg-grid-cell-skinny */
-.eg-grid-column-width-header {
-  width: 4.6em;
-  text-align: center;
-  flex: none;
-  display: inline-flex;
-  vertical-align: middle;
-  align-items: center;
-}
-
 .eg-grid-column-width-config .eg-grid-cell {
-    border-left: 2px dashed grey;
+  border-left: 2px dashed grey;
 }
 
 .eg-grid-column-width-icon {
   cursor: pointer;
   font-size: 18px;
-  color: #007bff;
+  color: #0A58CA;
 }
 
 .eg-grid-column-config-dialog {
   box-shadow: none;
 }
 
-.eg-grid-filter-control-cell {
-  white-space: normal;
+.eg-grid-filter-control,
+.eg-grid-filter-control .dropdown {
+  display: inline;
 }
+
+.eg-grid-filter-control .material-icons {
+  font-size: 18px;
+}
+
+.eg-grid-filter-control .dropdown-toggle.btn {
+  border-width: 0;
+  color: #255a88; /* match link color */
+  line-height: 1;
+  padding: 0;
+}
+
 .eg-grid-col-is-filtered {
-    background: lightblue;
+  background: lightblue;
 }
 .eg-grid-filter-menu {
   min-width: 17rem;
 .popover {
     max-width: initial;
 }
+
+/* Popovers' container needs to be inside <td> and should be the same size */
+.eg-grid-cell-contents {
+  height: 100%;
+  width: 100%;
+}
+
+/* Firefox td > div height fix */
+@-moz-document url-prefix() {
+  .eg-grid tr, 
+  .eg-grid th, 
+  .eg-grid td {
+    height: 100%;
+  }
+}
+
+/* GRID PADDING */
+/* Sets up basic padding for table headers and cells. See the widths and
+alignment section below for some overrides for specific data types and formats,
+such as numeric cells that should be right-aligned.
+
+Padding should be set on .eg-grid-cell-contents rather than its parent <td>.
+This div exists to house the popovers, and should take up the full unpadded
+height and width of the cell.
+/**/
+
+/* override Bootstrap's table settings */
+.table > :not(caption) > * > * {
+  padding: revert;
+}
+
+.eg-grid th,
+.eg-grid td .eg-grid-cell-contents {
+  padding: .2rem .5rem;
+}
+
+.eg-grid.compact th,
+.eg-grid.compact td .eg-grid-cell-contents {
+  line-height: 1.4;
+  padding: .1rem .5rem .1rem .1rem;
+}
+
+.eg-grid.wide th,
+.eg-grid.wide td .eg-grid-cell-contents {
+  padding: .5rem;
+}
+
+/* GRID COLUMN WIDTH AND ALIGNMENT */
+/* 
+Sets up column widths and text alignment for eg-grid tables according to
+datatype, column name, and IDL class. Use more specific combinations in
+components' CSS to override the default width.
+/**/
+
+.eg-grid th {
+  vertical-align: bottom;
+}
+
+.eg-grid td {
+  vertical-align: top;
+  white-space: normal;
+  word-break: break-all;
+}
+
+.eg-grid-col-utilities col {
+  min-width: 4ch;
+}
+
+.eg-grid-checkbox-cell,
+.eg-grid-flair-cell {
+  text-align: center;
+}
+
+.eg-grid .numeric {
+  font-variant-numeric: tabular-nums lining-nums;
+  text-align: right;
+}
+
+.compact td.numeric .eg-grid-cell-contents {
+  padding: .1rem .25rem .1rem .4rem;
+}
+
+/* These contain numbers we might want to compare vertically, but should not be aligned right */
+.eg-grid .alphanumeric,
+.eg-grid-type-timestamp {
+  font-variant-numeric: tabular-nums lining-nums;
+}
+
+.eg-grid td.eg-grid-number-cell {
+  white-space: nowrap;
+}
+
+.eg-grid-col-name {
+  width: 30ch;
+  hyphens: none !important;
+  /* use with <wbr> before periods: */
+  /*
+  overflow-wrap: break-word;
+  word-wrap: break-word;
+  -ms-word-break: break-all;
+  word-break: break-all;
+  word-break: break-word;
+  /**/
+}
+
+.eg-grid-col-barcode {
+  width: 16ch;
+}
+
+.eg-grid-col-description,
+.eg-grid-col-label {
+  width: 20ch;
+}
+
+/* <td> only, not the headers */
+td.eg-grid-col-datatype {
+  font-family: monospace;
+  font-size: .85rem;
+}
+
+td.eg-grid-col-url,
+td.eg-grid-col-email {
+  hyphens: none !important;
+  overflow-wrap: anywhere;
+  word-break: break-all;
+}
\ No newline at end of file
index ad8b6e4..c705464 100644 (file)
@@ -1,41 +1,51 @@
-
-<div class="eg-grid" role="grid">
+<div id="{{gridDomId}}" class="eg-grid-wrapper" role="region" aria-labelledby="eg-grid-caption" tabindex="0">
+  
+  <eg-grid-print #gridPrinter [gridContext]="context">
+  </eg-grid-print>
 
   <eg-grid-toolbar #toolbar
     [gridContext]="context"
     [gridPrinter]="gridPrinter"
-    [colWidthConfig]="colWidthConfig"
     [disableSaveSettings]="!persistKey || ('disabled' === persistKey)">
   </eg-grid-toolbar>
 
-  <div #egGridStickyHeader [ngClass]="{'eg-grid-sticky-header' : context.stickyGridHeader}">
-    <eg-grid-header [context]="context"></eg-grid-header>
-  </div>
+<!-- uses dropdown menu CSS for easy stying, but it's not a dropdown -->
+<ng-template #contextMenu let-gridContext="gridContext">
+  <eg-grid-toolbar-actions-menu [gridContext]="gridContext" [viaContextMenu]="true">
+  </eg-grid-toolbar-actions-menu>
+</ng-template>
 
-  <eg-grid-column-width #colWidthConfig [gridContext]="context">
-  </eg-grid-column-width>
-
-  <eg-grid-print #gridPrinter [gridContext]="context">
-  </eg-grid-print>
-
-  <ng-container *ngIf="dataSource.data.length === 0">
-    <div class="row">
+<table #egGrid class="eg-grid table" role="table" [ngClass]="context.persistKey.replaceAll('.', '_')">
+    <caption id="eg-grid-caption">
+    <ng-container *ngIf="dataSource.data.length === 0">
       <ng-container *ngIf="dataSource.requestingData">
-        <div class="col-lg-6 offset-lg-3 text-center mt-3">
+        <span>
           <eg-progress-inline></eg-progress-inline>
-        </div>
+        </span>
       </ng-container>
       <ng-container *ngIf="!dataSource.requestingData">
-        <div class="col-lg-12 text-center alert alert-danger fst-italic fw-bold" *ngIf="dataSource.retrievalError">
-          <span i18n>Error Retrieving Results</span>
-        </div>
-        <div class="col-lg-12 text-center alert fst-italic fw-bold" *ngIf="!dataSource.retrievalError">
-          <span i18n>Nothing to Display</span>
-        </div>
+        <span i18n *ngIf="dataSource.retrievalError">
+          Error Retrieving Results
+        </span>
+        <span i18n *ngIf="!dataSource.retrievalError">
+          Nothing to Display
+        </span>
       </ng-container>
-    </div>
-  </ng-container>
-
-  <eg-grid-body [context]="context"></eg-grid-body>
-</div>
-
+    </ng-container>
+    </caption>
+
+    <colgroup class="eg-grid-col-utilities">
+      <col *ngIf="!context.disableSelect" class="eg-grid-col eg-grid-checkbox-col">
+      <col class="eg-grid-col eg-grid-number-col">
+      <col *ngIf="context.rowFlairIsEnabled" class="eg-grid-col eg-grid-flair-col">
+    </colgroup>
+    <colgroup class="eg-grid-col-data">
+      <col *ngFor="let col of context.columnSet.displayColumns()" 
+        class="eg-grid-col {{context.setClassNames(row, col)}}">
+    </colgroup>
+    
+    <thead class="eg-grid-header" #egGridStickyHeader [context]="context" [ngClass]="{'eg-grid-sticky-header' : context.stickyGridHeader}" role="rowgroup">  </thead>
+    <tbody class="eg-grid-body" [context]="context" role="rowgroup"></tbody>
+</table>
+
+</div>
\ No newline at end of file
index eab8442..0d8fcc1 100644 (file)
@@ -128,6 +128,8 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
     @Input() reloadOnColumnChange = false;
 
     context: GridContext;
+    private static id: number = 0;
+    gridDomId: string = 'eg-grid-';
 
     // These events are emitted from our grid-body component.
     // They are defined here for ease of access to the caller.
@@ -155,9 +157,11 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
     ngOnInit() {
 
         if (!this.dataSource) {
-            throw new Error('<eg-grid/> requires a [dataSource]');
+            throw new Error('[egGrid] requires a [dataSource]');
         }
 
+        this.gridDomId += GridComponent.id++;
+
         this.context.idlClass = this.idlClass;
         this.context.dataSource = this.dataSource;
         this.context.persistKey = this.persistKey;
@@ -200,27 +204,12 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
         if (this.pageSize) {
             this.context.pager.limit = this.pageSize;
         }
-
+        
         // TS doesn't seem to like: let foo = bar || () => '';
         this.context.rowClassCallback =
             this.rowClassCallback || function () { return ''; };
         this.context.cellClassCallback =
-            this.cellClassCallback ||
-            function (row: any, col: GridColumn) {
-                if (col.datatype === 'money') {
-                    // get raw value
-                    let val;
-                    if (col.path) {
-                        val = this.nestedItemFieldValue(row, col);
-                    } else if (col.name in row) {
-                        val = this.getObjectFieldValue(row, col.name);
-                    }
-                    if (Number(val) < 0) {
-                        return 'negative-money-amount';
-                    }
-                }
-                return '';
-            };
+            this.cellClassCallback || function () { return ''; };
 
         this.context.rowSelector.selectionChange.subscribe(
             keys => this.rowSelectionChange.emit(keys)
index 6aeddab..04d19c4 100644 (file)
@@ -40,7 +40,7 @@ export class GridColumn {
     isFilterable: boolean;
     isFiltered: boolean;
     isMultiSortable: boolean;
-    disableTooltip: boolean;
+    enableTooltip: boolean;
     asyncSupportsEmptyTermClick: boolean;
     comparator: (valueA: any, valueB: any) => number;
     required = false;
@@ -91,7 +91,7 @@ export class GridColumn {
         col.isIndex = this.isIndex;
         col.cellTemplate = this.cellTemplate;
         col.cellContext = this.cellContext;
-        col.disableTooltip = this.disableTooltip;
+        col.enableTooltip = this.enableTooltip;
         col.isSortable = this.isSortable;
         col.isFilterable = this.isFilterable;
         col.isMultiSortable = this.isMultiSortable;
@@ -325,9 +325,8 @@ export class GridColumnSet {
 
     reset() {
         this.columns.forEach(col => {
-            col.flex = 2;
             col.sort = 0;
-            col.align = 'left';
+            col.align = '';
             col.visible = this.stockVisible.includes(col.name);
         });
     }
@@ -350,7 +349,7 @@ export class GridColumnSet {
 
         if (!col.name) { col.name = col.path; }
         if (!col.flex) { col.flex = 2; }
-        if (!col.align) { col.align = 'left'; }
+        if (!col.align) { col.align = ''; }
         if (!col.label) { col.label = col.name; }
         if (!col.datatype) { col.datatype = 'text'; }
         if (!col.isAuto) { col.headerLabel = col.label; }
@@ -461,8 +460,7 @@ export class GridColumnSet {
         // scrunch the data down to just the needed info.
         return this.displayColumns().map(col => {
             const c: GridColumnPersistConf = {name : col.name};
-            if (col.align !== 'left') { c.align = col.align; }
-            if (col.flex !== 2) { c.flex = Number(col.flex); }
+            if (col.align !== '') { c.align = col.align; }
             if (Number(col.sort)) { c.sort = Number(col.sort); }
             return c;
         });
@@ -503,7 +501,7 @@ export class GridColumnSet {
 
             col.visible = true;
             if (colConf.align) { col.align = colConf.align; }
-            if (colConf.flex)  { col.flex = Number(colConf.flex); }
+            if (colConf.flex) { col.flex = colConf.flex; }
             if (colConf.sort)  { col.sort = Number(colConf.sort); }
 
             // Add to new columns array, avoid dupes.
@@ -1353,6 +1351,78 @@ export class GridContext {
     columnHasTextGenerator(col: GridColumn): boolean {
         return this.cellTextGenerator && col.name in this.cellTextGenerator;
     }
+
+    setClassNames(row: any, col: GridColumn): string {
+        /* set initial classes from specific grids' callbacks */
+        const classes = [this.cellClassCallback(row, col)];
+        /* preserve alignmnet, if set */
+        switch (col.align) {
+            case 'left':
+                classes.push('text-start');
+                break;
+            case 'right':
+                classes.push('text-end');
+                break;
+            case 'center':
+                classes.push('text-center');
+                break;
+            default:
+                break;
+        }
+
+        /* Base classes */
+        if (col.datatype)
+            classes.push('eg-grid-type-' + col.datatype);
+        if (col.name)
+            classes.push('eg-grid-col-' + col.name);
+        if (col.idlClass)
+            classes.push('eg-grid-class-' + col.idlClass);
+        if (col.path)
+            classes.push('eg-grid-path-' + col.path);
+        if (this.overflowCells)
+            classes.push('eg-grid-cell-overflow');
+        
+        /* Name-based formats */
+        if (col.name.endsWith('count'))
+            classes.push('numeric');
+
+        switch (col.name) {
+            case 'callnumber':
+            case 'barcode':
+                classes.push('alphanumeric');
+                break;
+            default:
+                break;
+        }
+
+        /* Type-based formats */
+        switch (col.datatype) {
+            case 'money':
+                // get raw value
+                let val;
+                if (col.path) {
+                    val = this.nestedItemFieldValue(row, col);
+                } else if (col.name in row) {
+                    val = this.getObjectFieldValue(row, col.name);
+                }
+                if (Number(val) < 0) {
+                    classes.push('negative-money-amount');
+                }
+                // don't break
+            case 'id':
+            case 'int':
+            case 'float':
+            case 'money':
+            case 'number':
+                classes.push('numeric');
+                break;
+            default:
+                break;
+        }
+
+        // smush into string and replace dots in name and path
+        return classes.join(' ').replaceAll('.', '');
+    }
 }
 
 
index 694ca44..e76733d 100644 (file)
@@ -47,8 +47,8 @@
   </eg-grid-toolbar-action>
 
 
-  <eg-grid-column path="email" [cellTemplate]="emailTmpl" [disableTooltip]="true"></eg-grid-column>
-  <eg-grid-column path="phone" [cellTemplate]="phoneTmpl" [disableTooltip]="true"></eg-grid-column>
+  <eg-grid-column path="email" [cellTemplate]="emailTmpl"></eg-grid-column>
+  <eg-grid-column path="phone" [cellTemplate]="phoneTmpl"></eg-grid-column>
   <eg-grid-column [filterable]="false" [sortable]="false" i18n-label label="Is Primary?" path="_is_primary" datatype="bool"></eg-grid-column>
 </eg-grid>
 
index 1cf4d8f..0f78802 100644 (file)
@@ -31,7 +31,7 @@
   <eg-grid-column path="creator" [hidden]="true"></eg-grid-column>
   <eg-grid-column path="editor" [hidden]="true"></eg-grid-column>
   <eg-grid-column path="owner" [hidden]="true"></eg-grid-column>
-  <eg-grid-column [asyncSupportsEmptyTermClick]="true" i18n-label label="Status" path="state" [disableTooltip]="true"></eg-grid-column>
+  <eg-grid-column [asyncSupportsEmptyTermClick]="true" i18n-label label="Status" path="state"></eg-grid-column>
   <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="cancel_reason"></eg-grid-column>
   <eg-grid-column path="prepayment_required" [sortable]="false"></eg-grid-column>
 
index acd696f..45c8875 100644 (file)
@@ -20,6 +20,6 @@
   <eg-grid-toolbar-action label="Retrieve Provider" i18n-label (onClick)="retrieveRow($event)"></eg-grid-toolbar-action>
   <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="currency_type" [hidden]="true"></eg-grid-column>
   <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="default_claim_policy" [hidden]="true"></eg-grid-column>
-  <eg-grid-column path="contacts" [cellTemplate]="contactTmpl" [filterable]="false" [sortable]="false" [hidden]="true" [disableTooltip]="true"></eg-grid-column>
+  <eg-grid-column path="contacts" [cellTemplate]="contactTmpl" [filterable]="false" [sortable]="false" [hidden]="true"></eg-grid-column>
 </eg-grid>
 
index b0c8dd1..cd248f0 100644 (file)
     (onClick)="exportSingleAttributeList($event)" [disableOnRows]="noSelectedRows">
   </eg-grid-toolbar-action>
 
-  <eg-grid-column path="id" [cellTemplate]="idTmpl" [disableTooltip]="true"></eg-grid-column>
+  <eg-grid-column path="id" [cellTemplate]="idTmpl"></eg-grid-column>
   <eg-grid-column i18n-label label="Title" path="title" [cellTemplate]="liAttrTmpl"></eg-grid-column>
   <eg-grid-column i18n-label label="Author" path="author" [cellTemplate]="liAttrTmpl"></eg-grid-column>
   <eg-grid-column path="provider" [cellTemplate]="providerTmpl"></eg-grid-column>
-  <eg-grid-column i18n-label label="Links" path="_links" [cellTemplate]="liLinksTmpl" [disableTooltip]="true" [filterable]="false" [sortable]="false"></eg-grid-column>
+  <eg-grid-column i18n-label label="Links" path="_links" [cellTemplate]="liLinksTmpl" [filterable]="false" [sortable]="false"></eg-grid-column>
   <eg-grid-column i18n-label label="Item Count" path="item_count"></eg-grid-column>
   <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="claim_policy" [sortable]="false"></eg-grid-column>
-  <eg-grid-column [asyncSupportsEmptyTermClick]="true" i18n-label label="Status" path="state" [disableTooltip]="true"></eg-grid-column>
-  <eg-grid-column path="estimated_unit_price" [disableTooltip]="true"></eg-grid-column>
-  <eg-grid-column i18n-label label="PO ID" path="purchase_order.id" [disableTooltip]="true"></eg-grid-column>
-  <eg-grid-column i18n-label label="PO Name" path="purchase_order" [cellTemplate]="poTmpl" [disableTooltip]="true" [hidden]="true"></eg-grid-column>
-  <eg-grid-column path="picklist" [cellTemplate]="plTmpl" [disableTooltip]="true" [hidden]="true"></eg-grid-column>
+  <eg-grid-column [asyncSupportsEmptyTermClick]="true" i18n-label label="Status" path="state"></eg-grid-column>
+  <eg-grid-column path="estimated_unit_price"></eg-grid-column>
+  <eg-grid-column i18n-label label="PO ID" path="purchase_order.id"></eg-grid-column>
+  <eg-grid-column i18n-label label="PO Name" path="purchase_order" [cellTemplate]="poTmpl" [hidden]="true"></eg-grid-column>
+  <eg-grid-column path="picklist" [cellTemplate]="plTmpl" [hidden]="true"></eg-grid-column>
   <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="cancel_reason" [hidden]="true"></eg-grid-column>
   <eg-grid-column path="create_time" [datePlusTime]="true"></eg-grid-column>
   <eg-grid-column path="edit_time" [datePlusTime]="true"></eg-grid-column>
index 14f78de..226127c 100644 (file)
@@ -38,7 +38,7 @@
   <eg-grid-column path="creator" [hidden]="true"></eg-grid-column>
   <eg-grid-column path="editor" [hidden]="true"></eg-grid-column>
   <eg-grid-column path="owner" [hidden]="true"></eg-grid-column>
-  <eg-grid-column [asyncSupportsEmptyTermClick]="true" i18n-label label="Status" path="state" [disableTooltip]="true"></eg-grid-column>
+  <eg-grid-column [asyncSupportsEmptyTermClick]="true" i18n-label label="Status" path="state"></eg-grid-column>
   <eg-grid-column [asyncSupportsEmptyTermClick]="true" path="cancel_reason"></eg-grid-column>
   <eg-grid-column path="prepayment_required" [sortable]="false"></eg-grid-column>
 
index 539e637..af4b9ad 100644 (file)
@@ -68,7 +68,7 @@
     <button class="btn btn-outline-dark" (click)="openExchangeRatesDialog(currency.code())" i18n>Manage Exchange Rates</button>
   </ng-template>
   <eg-grid-column i18n-label label="Exchange Rates" name="exchange_rates"
-    [sortable]="false" [filterable]="false" [cellTemplate]="exchangeRatesTmpl" [disableTooltip]="true"></eg-grid-column>
+    [sortable]="false" [filterable]="false" [cellTemplate]="exchangeRatesTmpl"></eg-grid-column>
 
 </eg-grid>
 
index b0e2681..a6f12e2 100644 (file)
@@ -74,7 +74,7 @@
     <button class="btn btn-outline-dark" (click)="openEdiAttrSetProvidersDialog(attrSet.id())" [disabled]="attrSet.num_providers < 1" i18n>View Providers</button>
   </ng-template>
   <eg-grid-column i18n-label label="View Providers" name="view_providers"
-    [sortable]="false" [filterable]="false" [cellTemplate]="ediAttrSetProvidersTmpl" [disableTooltip]="true"></eg-grid-column>
+    [sortable]="false" [filterable]="false" [cellTemplate]="ediAttrSetProvidersTmpl"></eg-grid-column>
   <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
 
 
index 9c9b9da..2189f78 100644 (file)
@@ -21,7 +21,7 @@
             <eg-grid-column path="original_value_str" label="Original Value" i18n-label></eg-grid-column>
             <eg-grid-column path="new_value_str" label="New Value" i18n-label></eg-grid-column>
             <eg-grid-column i18n-label label="Revert?" name="revert" 
-                [disableTooltip]="true" [cellTemplate]="revertTemplate"></eg-grid-column>
+                [cellTemplate]="revertTemplate"></eg-grid-column>
         </eg-grid>
       </div>
   </div>
index 51c83df..37ce5df 100644 (file)
@@ -55,9 +55,9 @@
     persistKey="admin.actor.org_unit_settings">
 
     <eg-grid-column i18n-label label="Edit" name="edit" [flex]="1" [sortable]="false" [multiSortable]="false"
-      [disableTooltip]="true" [cellTemplate]="editCellTemplate"></eg-grid-column>
+      [cellTemplate]="editCellTemplate"></eg-grid-column>
     <eg-grid-column i18n-label label="History" name="history" [flex]="1" [sortable]="false" [multiSortable]="false"
-      [disableTooltip]="true" [cellTemplate]="historyCellTemplate"></eg-grid-column>
+      [cellTemplate]="historyCellTemplate"></eg-grid-column>
     <eg-grid-column path="name" label="Name" i18n-label [hidden]="true" [flex]="3"></eg-grid-column>
     <eg-grid-column path="grp" label="Group" i18n-label [flex]="1"></eg-grid-column>
     <eg-grid-column path="label" label="Setting" [index]="true" i18n-label [flex]="4"></eg-grid-column>
index 3bd1d75..87f567d 100644 (file)
@@ -74,7 +74,7 @@ export class StandingPenaltyComponent implements OnInit {
 
         this.cspRowFlairCallback = (row: any): GridRowFlairEntry => {
             const flair = {icon: null, title: null};
-            if (row.id() < 100) {
+            if (row && (row.id() < 100)) {
                 flair.icon = 'not_interested';
                 flair.title = this.cspFlairTooltip.text;
             }
@@ -111,7 +111,7 @@ export class StandingPenaltyComponent implements OnInit {
     }
 
     cspGridCellClassCallback = (row: any, col: GridColumn): string => {
-        if (col.name === 'id' && row.a[0] < 100) {
+        if (row && col.name === 'id' && row.a[0] < 100) {
             return 'text-danger';
         }
         return '';
index f6d896d..fe51e42 100644 (file)
     <eg-grid-toolbar-button label="Create Reservation" i18n-label (onClick)="openTheDialog($event)"></eg-grid-toolbar-button>
     <eg-grid-toolbar-action label="Create Reservation" i18n-label (onClick)="openTheDialog($event)"></eg-grid-toolbar-action>
     <eg-grid-column path="time" [index]="true" name="Time" i18n-name [cellTemplate]="timeTemplate" ></eg-grid-column>
-    <eg-grid-column *ngFor="let resource of resources" path="{{resource.barcode()}}" [cellTemplate]="reservationsTemplate" [name]="resource.barcode()" [disableTooltip]="true"></eg-grid-column>
+    <eg-grid-column *ngFor="let resource of resources" path="{{resource.barcode()}}" [cellTemplate]="reservationsTemplate" [name]="resource.barcode()"></eg-grid-column>
   </eg-grid>
 </ng-container>
 <div class="text-sm-center" *ngIf="this.resourceType.value && !resources.length" i18n>
index 166d71d..0a9ed36 100644 (file)
@@ -4,17 +4,17 @@
   {{copy.call_number_suffix_label}}
   <div>
     <a routerLink="/staff/cat/volcopy/holdings/callnumber/{{copy.call_number}}"
-      class="" target="_blank"i18n>Edit</a> 
+      target="_blank" i18n attr.aria-describedby="copy-callnumber-{{copy.call_number}}">Edit</a> 
   </div>
 </ng-template>
 
 <ng-template #barcodeTemplate let-copy="row" let-context="userContext">
   <div>{{copy.barcode}}</div>
   <div>
-  <a class="ps-1" target="_blank" 
+  <a class="ps-1" target="_blank" attr.
     href="/eg/staff/cat/item/{{copy.id}}" i18n>View</a>
     <ng-container *ngIf="context.editable(copy)">
-      | <a class="ps-1" target="_blank"
+      | <a target="_blank" attr.aria-describedby="copy-barcode-{{copy.id}}"
         routerLink="/staff/cat/volcopy/attrs/item/{{copy.id}}" i18n>Edit</a>
     </ng-container>
   </div>
     <div class="border-bottom">
       <a routerLink="/staff/catalog/hold/C"
         [queryParams]="{target: copy.id}"
-        queryParamsHandling="merge" i18n>
+        queryParamsHandling="merge" i18n
+        attr.aria-describedby="copy-barcode-{{copy.id}}">
         Item Hold
       </a>
     </div>
     <div>
       <a routerLink="/staff/catalog/hold/V"
         [queryParams]="{target: copy.call_number}"
-        queryParamsHandling="merge" i18n>
+        queryParamsHandling="merge" i18n
+        attr.aria-describedby="copy-callnumber-{{copy.call_number}}">
         Call Number Hold
       </a>
     </div>
@@ -42,7 +44,9 @@
 
 <ng-template #courseTemplate let-copy="row">
   <div *ngFor="let course of copy._courses">
-    <a routerLink="/staff/admin/local/asset/course_list/{{course.id()}}">
+    <div class="visually-hidden" id="course-name-{{course.id()}}">{{course.name()}}</div>
+    <a routerLink="/staff/admin/local/asset/course_list/{{course.id()}}"
+    attr.aria-describedby="course-name-{{course.id()}}">
       {{course.course_number()}}
     </a>
   </div>
index 1f87ce9..eacb6f7 100644 (file)
     </eg-grid-column>
     <eg-grid-column name="owner_label" [flex]="4"
       [cellTemplate]="locationTemplate" [cellContext]="gridTemplateContext" 
-      label="Location/Barcode" [disableTooltip]="true" i18n-label>
+      label="Location / Barcode" i18n-label>
     </eg-grid-column>
     <eg-grid-column path="callNumCount" datatype="number" label="Call Numbers" i18n-label>
     </eg-grid-column>
index 3f8b11d..a116ef0 100644 (file)
@@ -12,7 +12,7 @@ import {AuthService} from '@eg/core/auth.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
 import {PatronService} from '@eg/staff/share/patron/patron.service';
 import {PatronContextService} from './patron.service';
-import {GridDataSource, GridColumn, GridCellTextGenerator, GridRowFlairEntry} from '@eg/share/grid/grid';
+import {GridContext, GridDataSource, GridColumn, GridCellTextGenerator, GridRowFlairEntry} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {Pager} from '@eg/share/util/pager';
 import {CircService, CircDisplayInfo} from '@eg/staff/share/circ/circ.service';
index 00007ff..89af317 100644 (file)
@@ -65,7 +65,7 @@
     <eg-grid-column path="id" [hidden]=true [index]="true" i18n-label label="Run ID" [filterable]="false" [sortable]="false"></eg-grid-column>
     <eg-grid-column path="template_name" i18n-label label="Report"></eg-grid-column>
     <eg-grid-column path="complete_time" i18n-label label="Finish Time" datatype="timestamp" [datePlusTime]="true"></eg-grid-column>
-    <eg-grid-column path="_output"  [cellTemplate]="outputTmpl" i18n-label label="Output" [sortable]="false" [filterable]="false" [disableTooltip]="true"></eg-grid-column>
+    <eg-grid-column path="_output"  [cellTemplate]="outputTmpl" i18n-label label="Output" [sortable]="false" [filterable]="false"></eg-grid-column>
     <eg-grid-column path="error_text"[hidden]=true i18n-label label="Error Text"></eg-grid-column>
 
   </eg-grid>
index 381f990..0941ff1 100644 (file)
@@ -18,7 +18,7 @@
       "node_modules/@types"
     ],
     "lib": [
-      "es2018",
+      "ESNext",
       "dom"
     ]
   }