[ACS-10280]: Keyboard Navigation: Search: Unnecessary stops in the results table using the Tab key (#11736)

* [ACS-10280]: move selection handling to row

* [ACS-10280]: clean up

* [ACS-10280]: rollback title

* [ACS-10280]: migrate to conditional aria-hidden

* [ACS-10280]: revert to fix hxp

* [ACS-10280]: sonar fix

* [ACS-10280] Use new Angular control flow syntax

* [ACS-10280] Unit test fix

---------

Co-authored-by: MichalKinas <michal.kinas@hyland.com>
This commit is contained in:
Anton Ramanovich
2026-04-01 10:17:00 +02:00
committed by GitHub
co-authored by MichalKinas
parent 565db01819
commit a3e13da3a4
7 changed files with 737 additions and 562 deletions
@@ -493,16 +493,4 @@ describe('ShareDataTableAdapter', () => {
expect(adapter.getRowByNodeId('fake-node-id-2')).toEqual(fakeShareDataRows[1]); expect(adapter.getRowByNodeId('fake-node-id-2')).toEqual(fakeShareDataRows[1]);
}); });
}); });
it('should initialize with allowFocusOnRows as true by default', () => {
const adapter = new ShareDataTableAdapter(thumbnailService, contentService, null);
expect(adapter.allowFocusOnRows).toBe(true);
});
it('should set allowFocusOnRows value', () => {
const adapter = new ShareDataTableAdapter(thumbnailService, contentService, null);
expect(adapter.allowFocusOnRows).toBe(true);
adapter.setAllowFocusOnTableRows(false);
expect(adapter.allowFocusOnRows).toBe(false);
});
}); });
@@ -38,7 +38,6 @@ export class ShareDataTableAdapter implements DataTableAdapter {
permissionsStyle: PermissionStyleModel[]; permissionsStyle: PermissionStyleModel[];
selectedRow: DataRow; selectedRow: DataRow;
allowDropFiles: boolean; allowDropFiles: boolean;
allowFocusOnRows = true;
set sortingMode(value: string) { set sortingMode(value: string) {
let newValue = (value || 'client').toLowerCase(); let newValue = (value || 'client').toLowerCase();
@@ -197,10 +196,6 @@ export class ShareDataTableAdapter implements DataTableAdapter {
this.imageResolver = resolver; this.imageResolver = resolver;
} }
setAllowFocusOnTableRows(allow: boolean) {
this.allowFocusOnRows = allow;
}
private getFolderIcon(node: any) { private getFolderIcon(node: any) {
if (this.isSmartFolder(node)) { if (this.isSmartFolder(node)) {
return this.thumbnailService.getMimeTypeIcon('smartFolder'); return this.thumbnailService.getMimeTypeIcon('smartFolder');
@@ -1,12 +1,13 @@
@if (data) {
<div <div
role="grid" role="grid"
*ngIf="data"
class="adf-datatable-list" class="adf-datatable-list"
[class.adf-sticky-header]="isStickyHeaderEnabled()" [class.adf-sticky-header]="isStickyHeaderEnabled()"
[class.adf-datatable--empty]="(isEmpty() && !isHeaderVisible()) || loading" [class.adf-datatable--empty]="(isEmpty() && !isHeaderVisible()) || loading"
[class.adf-datatable--empty--header-visible]="isEmpty() && isHeaderVisible()" [class.adf-datatable--empty--header-visible]="isEmpty() && isHeaderVisible()"
> >
<div *ngIf="isHeaderVisible()" class="adf-datatable-header" role="rowgroup" [ngClass]="{ 'adf-sr-only': !isHeaderVisible() }"> @if (isHeaderVisible()) {
<div class="adf-datatable-header" role="rowgroup" [ngClass]="{ 'adf-sr-only': !isHeaderVisible() }">
<adf-datatable-row <adf-datatable-row
cdkDropList cdkDropList
cdkDropListOrientation="horizontal" cdkDropListOrientation="horizontal"
@@ -17,17 +18,22 @@
<!-- Drag --> <!-- Drag -->
<div *ngIf="enableDragRows" class="adf-datatable-cell-header adf-drag-column"> @if (enableDragRows) {
<div class="adf-datatable-cell-header adf-drag-column">
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.DRAG' | translate }}</span> <span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.DRAG' | translate }}</span>
</div> </div>
}
<!-- Actions (left) --> <!-- Actions (left) -->
<div *ngIf="actions && actionsPosition === 'left'" class="adf-actions-column adf-datatable-cell-header"> @if (actions && actionsPosition === 'left') {
<div class="adf-actions-column adf-datatable-cell-header">
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span> <span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
</div> </div>
}
<!-- Columns --> <!-- Columns -->
<div *ngIf="multiselect" class="adf-datatable-cell-header adf-datatable-checkbox"> @if (multiselect) {
<div class="adf-datatable-cell-header adf-datatable-checkbox">
<mat-checkbox <mat-checkbox
[indeterminate]="isSelectAllIndeterminate" [indeterminate]="isSelectAllIndeterminate"
[checked]="isSelectAllChecked" [checked]="isSelectAllChecked"
@@ -42,16 +48,12 @@
{{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_ALL' | translate }} {{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_ALL' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
}
<ng-container @for (col of getVisibleColumns(); track col.key; let lastColumn = $last; let columnIndex = $index) {
*ngFor=" @if (col.title || !showProvidedActions) {
let col of getVisibleColumns();
let columnIndex = index
let lastColumn = last"
>
<div <div
class="adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}} adf-datatable-cell-header adf-datatable-cell-data" class="adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}} adf-datatable-cell-header adf-datatable-cell-data"
*ngIf="col.title || !showProvidedActions"
[attr.data-automation-id]="'auto_id_' + col.key" [attr.data-automation-id]="'auto_id_' + col.key"
[ngClass]="{ [ngClass]="{
'adf-sortable': col.sortable, 'adf-sortable': col.sortable,
@@ -70,8 +72,7 @@
(mouseenter)="hoveredHeaderColumnIndex = columnIndex" (mouseenter)="hoveredHeaderColumnIndex = columnIndex"
(mouseleave)="hoveredHeaderColumnIndex = -1" (mouseleave)="hoveredHeaderColumnIndex = -1"
adf-drop-zone dropTarget="header" adf-drop-zone dropTarget="header"
[dropColumn]="col" [dropColumn]="col">
>
<div <div
adf-resizable adf-resizable
@@ -103,57 +104,69 @@
!isDraggingHeaderColumn && !isDraggingHeaderColumn &&
!isResizing && col.sortable}" !isResizing && col.sortable}"
> >
<ng-container *ngIf="!col.header"> @if (!col.header) {
@if (col.title) {
<span <span
*ngIf="col.title"
title="{{col.title | translate}}" title="{{col.title | translate}}"
class="adf-datatable-cell-value" class="adf-datatable-cell-value"
> >
{{col.title | translate}} {{col.title | translate}}
</span> </span>
}
@if (col.subtitle) {
<span <span
*ngIf="col.subtitle"
title="{{col.subtitle | translate}}" title="{{col.subtitle | translate}}"
class="adf-datatable-cell-value adf-datatable-cell-header_subtitle" class="adf-datatable-cell-value adf-datatable-cell-header_subtitle"
> >
({{col.subtitle | translate}}) ({{col.subtitle | translate}})
</span> </span>
}
<span *ngIf="col.title && col.sortable && isDraggingHeaderColumn" class="adf-sr-only" aria-live="polite"> @if (col.title && col.sortable && isDraggingHeaderColumn) {
<span class="adf-sr-only" aria-live="polite">
{{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }} {{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }}
</span> </span>
}
<span *ngIf="!col.title && !col.sortable && !headerFilterTemplate" [attr.title]="'ADF-DATATABLE.ACCESSIBILITY.EMPTY_HEADER' | translate"></span> @if (!col.title && !col.sortable && !headerFilterTemplate) {
</ng-container> <span [attr.title]="'ADF-DATATABLE.ACCESSIBILITY.EMPTY_HEADER' | translate"></span>
}
}
<div *ngIf="col.header" class="adf-datatable-cell-value"> @if (col.header) {
<div class="adf-datatable-cell-value">
<ng-template [ngTemplateOutlet]="col.header" [ngTemplateOutletContext]="{$implicit: col}" /> <ng-template [ngTemplateOutlet]="col.header" [ngTemplateOutletContext]="{$implicit: col}" />
</div> </div>
}
<span <span
[class.adf-datatable__header--sorted-asc]="isColumnSorted(col, 'asc')" [class.adf-datatable__header--sorted-asc]="isColumnSorted(col, 'asc')"
[class.adf-datatable__header--sorted-desc]="isColumnSorted(col, 'desc')"> [class.adf-datatable__header--sorted-desc]="isColumnSorted(col, 'desc')">
</span> </span>
<ng-template *ngIf="allowFiltering" [ngTemplateOutlet]="headerFilterTemplate" [ngTemplateOutletContext]="{$implicit: col}" /> @if (allowFiltering) {
<ng-template [ngTemplateOutlet]="headerFilterTemplate" [ngTemplateOutletContext]="{$implicit: col}" />
}
@if (col.draggable) {
<span <span
*ngIf="col.draggable"
cdkDragHandle cdkDragHandle
[ngClass]="{ 'adf-datatable-cell-header-drag-icon': !isResizing }" [ngClass]="{ 'adf-datatable-cell-header-drag-icon': !isResizing }"
> >
@if (hoveredHeaderColumnIndex === columnIndex && !isResizing) {
<mat-icon <mat-icon
*ngIf="hoveredHeaderColumnIndex === columnIndex && !isResizing"
svgIcon="adf:drag_indicator" svgIcon="adf:drag_indicator"
class="adf-datatable-cell-header-drag-icon-visible" class="adf-datatable-cell-header-drag-icon-visible"
[attr.data-automation-id]="'adf-datatable-cell-header-drag-icon-'+col.key" [attr.data-automation-id]="'adf-datatable-cell-header-drag-icon-'+col.key"
aria-hidden="true" aria-hidden="true"
/> />
}
</span> </span>
}
</div> </div>
@if (isResizingEnabled && col.resizable && !lastColumn) {
<div <div
*ngIf="isResizingEnabled && col.resizable && !lastColumn"
[ngClass]="hoveredHeaderColumnIndex === columnIndex && !isResizing || resizingColumnIndex === columnIndex ? 'adf-datatable__resize-handle-visible' : 'adf-datatable__resize-handle-hidden'" [ngClass]="hoveredHeaderColumnIndex === columnIndex && !isResizing || resizingColumnIndex === columnIndex ? 'adf-datatable__resize-handle-visible' : 'adf-datatable__resize-handle-hidden'"
adf-resize-handle adf-resize-handle
tabindex="0" tabindex="0"
@@ -168,18 +181,19 @@
[resizableContainer]="resizableElement"> [resizableContainer]="resizableElement">
<div class="adf-datatable__resize-handle--divider"></div> <div class="adf-datatable__resize-handle--divider"></div>
</div> </div>
}
<div class="adf-drop-header-cell-placeholder" *cdkDragPlaceholder></div> <div class="adf-drop-header-cell-placeholder" *cdkDragPlaceholder></div>
</div> </div>
</ng-container> }
}
<!-- Header actions (right) --> <!-- Header actions (right) -->
@if ((actions && actionsPosition === 'right') || (mainActionTemplate && showMainDatatableActions)) {
<div <div
*ngIf="(actions && actionsPosition === 'right') ||
(mainActionTemplate && showMainDatatableActions)"
class="adf-actions-column adf-datatable-actions-menu adf-datatable-cell-header adf-datatable__actions-cell" class="adf-actions-column adf-datatable-actions-menu adf-datatable-cell-header adf-datatable__actions-cell"
[class.adf-datatable-actions-menu-provided]="showProvidedActions" [class.adf-datatable-actions-menu-provided]="showProvidedActions"
> >
<ng-container *ngIf="mainActionTemplate"> @if (mainActionTemplate) {
<button <button
data-automation-id="adf-datatable-main-menu-button" data-automation-id="adf-datatable-main-menu-button"
title="{{ 'ADF-DATATABLE.CONTENT-ACTIONS.SELECT_COLUMNS' | translate }}" title="{{ 'ADF-DATATABLE.CONTENT-ACTIONS.SELECT_COLUMNS' | translate }}"
@@ -199,20 +213,21 @@
</div> </div>
</mat-menu> </mat-menu>
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span> <span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
</ng-container> }
</div> </div>
}
</adf-datatable-row> </adf-datatable-row>
</div> </div>
}
@if (!loading) {
<div <div
*ngIf="!loading; else loadingRowTemplate"
class="adf-datatable-body" class="adf-datatable-body"
[ngClass]="{ 'adf-blur-datatable-body': blurOnResize && (isDraggingHeaderColumn || isResizing), 'adf-datatable-body__draggable': enableDragRows && !isDraggingRow, 'adf-datatable-body__dragging': isDraggingRow }" [ngClass]="{ 'adf-blur-datatable-body': blurOnResize && (isDraggingHeaderColumn || isResizing), 'adf-datatable-body__draggable': enableDragRows && !isDraggingRow, 'adf-datatable-body__dragging': isDraggingRow }"
cdkDropList cdkDropList
[cdkDropListDisabled]="!enableDragRows" [cdkDropListDisabled]="!enableDragRows"
role="rowgroup"> role="rowgroup">
<ng-container *ngIf="!noPermission; else noPermissionsRowTemplate"> @if (!noPermission) {
<adf-datatable-row *ngFor="let row of data.getRows(); let idx = index" <adf-datatable-row *ngFor="let row of data.getRows(); let idx = index"
cdkDrag cdkDrag
[cdkDragDisabled]="!enableDragRows" [cdkDragDisabled]="!enableDragRows"
@@ -231,20 +246,32 @@
[class.adf-datatable-row__dragging]="isDraggingRow" [class.adf-datatable-row__dragging]="isDraggingRow"
[attr.data-automation-id]="'datatable-row-' + idx" [attr.data-automation-id]="'datatable-row-' + idx"
(contextmenu)="markRowAsContextMenuSource(row)" (contextmenu)="markRowAsContextMenuSource(row)"
[disabled]="!(data?.allowFocusOnRows ?? true)" [disabled]="!(enableDragRows || multiselect)"
[attr.aria-description]="
(multiselect ? ('ADF-DATATABLE.ACCESSIBILITY.ROW_SELECTION' | translate) : '') +
(multiselect && enableDragRows ? '. ' : '') +
(enableDragRows ? ('ADF-DATATABLE.ACCESSIBILITY.DRAGGABLE' | translate) : '')
"
> >
<!-- Live region for selection state -->
@if (multiselect) {
<span class="adf-sr-only" aria-live="off">
{{ row.isSelected ? ('ADF-DATATABLE.ACCESSIBILITY.SELECTED' | translate) : '' }}
</span>
}
<!-- Drag button --> <!-- Drag button -->
<div *ngIf="enableDragRows" @if (enableDragRows) {
<div
role="gridcell" role="gridcell"
class="adf-datatable-cell adf-datatable__actions-cell adf-datatable-hover-only"> class="adf-datatable-cell adf-datatable__actions-cell adf-datatable-hover-only">
<button mat-icon-button <mat-icon adf-icon="drag_indicator" aria-hidden="true" />
[attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.DRAG' | translate">
<mat-icon adf-icon="drag_indicator" />
</button>
</div> </div>
}
<!-- Actions (left) --> <!-- Actions (left) -->
<div *ngIf="actions && actionsPosition === 'left'" role="gridcell" class="adf-datatable-cell"> @if (actions && actionsPosition === 'left') {
<div role="gridcell" class="adf-datatable-cell">
<button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger" <button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger"
[ngClass]="getHideActionsWithoutHoverClass(actionsMenuTrigger)" [ngClass]="getHideActionsWithoutHoverClass(actionsMenuTrigger)"
[title]="'ADF-DATATABLE.CONTENT-ACTIONS.TOOLTIP' | translate" [title]="'ADF-DATATABLE.CONTENT-ACTIONS.TOOLTIP' | translate"
@@ -253,47 +280,54 @@
<mat-icon adf-icon="more_vert" /> <mat-icon adf-icon="more_vert" />
</button> </button>
<mat-menu #menu="matMenu"> <mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let action of getRowActions(row)" @for (action of getRowActions(row); track action.title) {
<button mat-menu-item
[attr.data-automation-id]="action.title" [attr.data-automation-id]="action.title"
[disabled]="action.disabled" [disabled]="action.disabled"
(click)="onExecuteRowAction(row, action)"> (click)="onExecuteRowAction(row, action)">
<mat-icon *ngIf="action.icon" [adf-icon]="action.icon" /> @if (action.icon) {
<mat-icon [adf-icon]="action.icon" />
}
<span>{{ action.title | translate }}</span> <span>{{ action.title | translate }}</span>
</button> </button>
}
</mat-menu> </mat-menu>
</div> </div>
}
<label *ngIf="multiselect" @if (multiselect) {
<label
(keydown.enter)="onEnterKeyPressed(row, $any($event))" (keydown.enter)="onEnterKeyPressed(row, $any($event))"
(click)="onCheckboxLabelClick(row, $event)" (click)="onCheckboxLabelClick(row, $event)"
[for]="'select-file-' + idx" [for]="'select-file-' + idx"
class="adf-datatable-cell adf-datatable-checkbox adf-datatable-checkbox-single" class="adf-datatable-cell adf-datatable-checkbox adf-datatable-checkbox-single"
[attr.tabindex]="null"> [attr.tabindex]="null"
aria-hidden="true"
>
<mat-checkbox <mat-checkbox
[id]="'select-file-' + idx" [id]="'select-file-' + idx"
[disabled]="!row?.isSelectable" [disabled]="!row?.isSelectable"
[class.adf-datatable-checkbox-selected]="row.isSelected" [class.adf-datatable-checkbox-selected]="row.isSelected"
[class.adf-datatable-hover-only]="displayCheckboxesOnHover" [class.adf-datatable-hover-only]="displayCheckboxesOnHover"
[checked]="row.isSelected" [checked]="row.isSelected"
[attr.aria-checked]="row.isSelected"
[aria-label]="'ADF-DATATABLE.ACCESSIBILITY.SELECT_FILE' | translate"
data-adf-datatable-row-checkbox data-adf-datatable-row-checkbox
(change)="onCheckboxChange(row, $event)" (change)="onCheckboxChange(row, $event)"
(keydown.enter)="$event.stopPropagation()" (keydown.enter)="$event.stopPropagation()"
class="adf-checkbox-sr-only" class="adf-checkbox-sr-only"
> [tabIndex]="-1"
{{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_FILE' | translate }} aria-hidden="true"
</mat-checkbox> />
</label> </label>
}
@for (col of getVisibleColumns(); track col.key; let lastColumn = $last;) {
<div <div
*ngFor="let col of getVisibleColumns(); let lastColumn = last;"
role="gridcell" role="gridcell"
class="adf-datatable-cell adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}} adf-datatable-cell-data" class="adf-datatable-cell adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}} adf-datatable-cell-data adf-datatable-cell--{{getAutomationValue(row)}}"
[attr.title]="col.title | translate" [attr.title]="col.title | translate"
[attr.data-automation-id]="getAutomationValue(row)" [attr.data-automation-id]="getAutomationValue(row)"
[attr.aria-selected]="row.isSelected"
[attr.aria-label]="col.title ? (col.title | translate) : null" [attr.aria-label]="col.title ? (col.title | translate) : null"
[attr.aria-hidden]='isRepresentationContent(col) ? "true" : null'
(click)="onRowClick(row, $event)" (click)="onRowClick(row, $event)"
[attr.tabindex]="null" [attr.tabindex]="null"
(keydown.enter)="onEnterKeyPressed(row, $any($event))" (keydown.enter)="onEnterKeyPressed(row, $any($event))"
@@ -302,19 +336,21 @@
adf-drop-zone dropTarget="cell" [dropColumn]="col" [dropRow]="row" adf-drop-zone dropTarget="cell" [dropColumn]="col" [dropRow]="row"
[ngStyle]="(col.width) && !lastColumn && {'flex': getFlexValue(col)}" [ngStyle]="(col.width) && !lastColumn && {'flex': getFlexValue(col)}"
> >
<div *ngIf="!col.template" class="adf-datatable-cell-container"> @if (!col.template) {
<ng-container [ngSwitch]="data.getColumnType(row, col)"> <div class="adf-datatable-cell-container">
<div *ngSwitchCase="'image'" class="adf-cell-value"> @switch (data.getColumnType(row, col)) {
@case ('image') {
<div class="adf-cell-value">
@if (isIconValue(row, col)) {
<mat-icon <mat-icon
[attr.aria-label]="col.srTitle? (col.srTitle | translate) : null" [attr.aria-label]="col.srTitle? (col.srTitle | translate) : null"
[attr.aria-hidden]="!asIconValue(row, col)" [attr.aria-hidden]="!asIconValue(row, col)"
*ngIf="isIconValue(row, col); else no_iconvalue"
[adf-icon]="asIconValue(row, col)" [adf-icon]="asIconValue(row, col)"
/> />
<ng-template #no_iconvalue> } @else {
<mat-icon class="adf-datatable-selected" @if (row.isSelected && !multiselect) {
*ngIf="row.isSelected && !multiselect; else no_selected_row" svgIcon="selected" /> <mat-icon class="adf-datatable-selected" svgIcon="selected" />
<ng-template #no_selected_row> } @else {
<img class="adf-datatable-center-img-ie" <img class="adf-datatable-center-img-ie"
[attr.aria-label]="(data.getValue(row, col) | fileType) === 'disable' ? [attr.aria-label]="(data.getValue(row, col) | fileType) === 'disable' ?
('ADF-DATATABLE.ACCESSIBILITY.ICON_DISABLED' | translate) : ('ADF-DATATABLE.ACCESSIBILITY.ICON_DISABLED' | translate) :
@@ -328,11 +364,12 @@
}" }"
src="{{ data.getValue(row, col) }}" src="{{ data.getValue(row, col) }}"
(error)="onImageLoadingError($event, row)"> (error)="onImageLoadingError($event, row)">
</ng-template> }
</ng-template> }
</div> </div>
}
<div *ngSwitchCase="'icon'" class="adf-cell-value"> @case ('icon') {
<div class="adf-cell-value">
<adf-icon-cell <adf-icon-cell
[data]="data" [data]="data"
[column]="col" [column]="col"
@@ -341,10 +378,10 @@
[tooltip]="getCellTooltip(row, col)" [tooltip]="getCellTooltip(row, col)"
/> />
</div> </div>
}
@case ('date') {
<div <div
*ngSwitchCase="'date'"
class="adf-cell-value adf-cell-date" class="adf-cell-value adf-cell-date"
[attr.data-automation-id]="'date_' + (data.getValue(row, col, resolverFn) | adfLocalizedDate: 'medium') "> [attr.data-automation-id]="'date_' + (data.getValue(row, col, resolverFn) | adfLocalizedDate: 'medium') ">
<adf-date-cell class="adf-datatable-center-date-column-ie" <adf-date-cell class="adf-datatable-center-date-column-ie"
[data]="data" [data]="data"
@@ -354,8 +391,9 @@
[tooltip]="getCellTooltip(row, col)" [tooltip]="getCellTooltip(row, col)"
[dateConfig]="col.dateConfig" /> [dateConfig]="col.dateConfig" />
</div> </div>
}
<div *ngSwitchCase="'location'" class="adf-cell-value" @case ('location') {
<div class="adf-cell-value"
[attr.data-automation-id]="'location' + data.getValue(row, col, resolverFn)"> [attr.data-automation-id]="'location' + data.getValue(row, col, resolverFn)">
<adf-location-cell <adf-location-cell
[data]="data" [data]="data"
@@ -364,7 +402,9 @@
[resolverFn]="resolverFn" [resolverFn]="resolverFn"
[tooltip]="getCellTooltip(row, col)" /> [tooltip]="getCellTooltip(row, col)" />
</div> </div>
<div *ngSwitchCase="'fileSize'" class="adf-cell-value" }
@case ('fileSize') {
<div class="adf-cell-value"
[attr.data-automation-id]="'fileSize_' + data.getValue(row, col, resolverFn)"> [attr.data-automation-id]="'fileSize_' + data.getValue(row, col, resolverFn)">
<adf-filesize-cell class="adf-datatable-center-size-column-ie" <adf-filesize-cell class="adf-datatable-center-size-column-ie"
[data]="data" [data]="data"
@@ -373,7 +413,9 @@
[resolverFn]="resolverFn" [resolverFn]="resolverFn"
[tooltip]="getCellTooltip(row, col)" /> [tooltip]="getCellTooltip(row, col)" />
</div> </div>
<div *ngSwitchCase="'text'" class="adf-cell-value" }
@case ('text') {
<div class="adf-cell-value"
[attr.data-automation-id]="'text_' + data.getValue(row, col, resolverFn)"> [attr.data-automation-id]="'text_' + data.getValue(row, col, resolverFn)">
<adf-datatable-cell <adf-datatable-cell
[copyContent]="col.copyContent" [copyContent]="col.copyContent"
@@ -383,7 +425,9 @@
[resolverFn]="resolverFn" [resolverFn]="resolverFn"
[tooltip]="getCellTooltip(row, col)" /> [tooltip]="getCellTooltip(row, col)" />
</div> </div>
<div *ngSwitchCase="'boolean'" class="adf-cell-value" }
@case ('boolean') {
<div class="adf-cell-value"
[attr.data-automation-id]="'boolean_' + data.getValue(row, col, resolverFn)"> [attr.data-automation-id]="'boolean_' + data.getValue(row, col, resolverFn)">
<adf-boolean-cell <adf-boolean-cell
[data]="data" [data]="data"
@@ -392,7 +436,9 @@
[resolverFn]="resolverFn" [resolverFn]="resolverFn"
[tooltip]="getCellTooltip(row, col)" /> [tooltip]="getCellTooltip(row, col)" />
</div> </div>
<div *ngSwitchCase="'json'" class="adf-cell-value"> }
@case ('json') {
<div class="adf-cell-value">
<adf-json-cell <adf-json-cell
[editable]="col.editable" [editable]="col.editable"
[data]="data" [data]="data"
@@ -400,7 +446,9 @@
[resolverFn]="resolverFn" [resolverFn]="resolverFn"
[row]="row" /> [row]="row" />
</div> </div>
<div *ngSwitchCase="'amount'" }
@case ('amount') {
<div
class="adf-cell-value" class="adf-cell-value"
[attr.data-automation-id]="'amount_' + data.getValue(row, col, resolverFn)"> [attr.data-automation-id]="'amount_' + data.getValue(row, col, resolverFn)">
<adf-amount-cell <adf-amount-cell
@@ -410,7 +458,9 @@
[row]="row" [row]="row"
[currencyConfig]="col.currencyConfig" /> [currencyConfig]="col.currencyConfig" />
</div> </div>
<div *ngSwitchCase="'number'" }
@case ('number') {
<div
class="adf-cell-value" class="adf-cell-value"
[attr.data-automation-id]="'number_' + data.getValue(row, col, resolverFn)"> [attr.data-automation-id]="'number_' + data.getValue(row, col, resolverFn)">
<adf-number-cell <adf-number-cell
@@ -420,29 +470,33 @@
[row]="row" [row]="row"
[decimalConfig]="col.decimalConfig" /> [decimalConfig]="col.decimalConfig" />
</div> </div>
<span *ngSwitchDefault class="adf-cell-value"> }
@default {
<span class="adf-cell-value">
<!-- empty cell for unknown column type --> <!-- empty cell for unknown column type -->
</span> </span>
</ng-container> }
}
</div> </div>
<div *ngIf="col.template" class="adf-datatable-cell-container"> } @else {
<div class="adf-datatable-cell-container">
<div class="adf-cell-value"> <div class="adf-cell-value">
<ng-container <ng-container
[ngTemplateOutlet]="col.template" [ngTemplateOutlet]="col.template"
[ngTemplateOutletContext]="{ $implicit: { data: data, row: row, col: col }, value: data.getValue(row, col, resolverFn) }" /> [ngTemplateOutletContext]="{ $implicit: { data: data, row: row, col: col }, value: data.getValue(row, col, resolverFn) }" />
</div> </div>
</div> </div>
}
</div> </div>
}
<!-- Row actions (right) --> <!-- Row actions (right) -->
<div *ngIf=" @if (!showProvidedActions && ((actions && actionsPosition === 'right') || (mainActionTemplate && showMainDatatableActions))) {
!showProvidedActions && <div
((actions && actionsPosition === 'right') ||
(mainActionTemplate && showMainDatatableActions))"
role="gridcell" role="gridcell"
class="adf-datatable-cell adf-datatable__actions-cell adf-datatable-center-actions-column-ie adf-datatable-actions-menu"> class="adf-datatable-cell adf-datatable__actions-cell adf-datatable-center-actions-column-ie adf-datatable-actions-menu">
<ng-container *ngIf="(actions && actionsPosition === 'right')"> @if (actions && actionsPosition === 'right') {
<button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger" <button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger"
[ngClass]="getHideActionsWithoutHoverClass(actionsMenuTrigger)" [ngClass]="getHideActionsWithoutHoverClass(actionsMenuTrigger)"
[attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.ROW_OPTION_BUTTON' | translate" [attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.ROW_OPTION_BUTTON' | translate"
@@ -453,47 +507,59 @@
<mat-icon adf-icon="more_vert" /> <mat-icon adf-icon="more_vert" />
</button> </button>
<mat-menu #menu="matMenu"> <mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let action of getRowActions(row)" @for (action of getRowActions(row); track action.title) {
<button mat-menu-item
[attr.data-automation-id]="action.title" [attr.data-automation-id]="action.title"
[attr.aria-label]="action.title | translate" [attr.aria-label]="action.title | translate"
[disabled]="action.disabled" [disabled]="action.disabled"
(click)="onExecuteRowAction(row, action)"> (click)="onExecuteRowAction(row, action)">
<mat-icon *ngIf="action.icon" [adf-icon]="action.icon" /> @if (action.icon) {
<mat-icon [adf-icon]="action.icon" />
}
<span>{{ action.title | translate }}</span> <span>{{ action.title | translate }}</span>
</button> </button>
}
</mat-menu> </mat-menu>
</ng-container> }
</div> </div>
}
</adf-datatable-row> </adf-datatable-row>
<div *ngIf="isEmpty()" role="row" class="adf-datatable-row"> @if (isEmpty()) {
<div role="row" class="adf-datatable-row">
<div class="adf-no-content-container adf-datatable-cell" role="gridcell"> <div class="adf-no-content-container adf-datatable-cell" role="gridcell">
<ng-template *ngIf="noContentTemplate" @if (noContentTemplate) {
<ng-template
ngFor [ngForOf]="[data]" ngFor [ngForOf]="[data]"
[ngForTemplate]="noContentTemplate" /> [ngForTemplate]="noContentTemplate" />
}
<ng-content select="adf-empty-list" /> <ng-content select="adf-empty-list" />
</div> </div>
</div> </div>
</ng-container> }
} @else {
<ng-template #noPermissionsRowTemplate>
<div <div
role="row" role="row"
class="adf-datatable-row adf-no-permission__row"> class="adf-datatable-row adf-no-permission__row">
<div class="adf-no-permission__cell adf-no-content-container adf-datatable-cell"> <div class="adf-no-permission__cell adf-no-content-container adf-datatable-cell">
<ng-template *ngIf="noPermissionTemplate" @if (noPermissionTemplate) {
<ng-template
ngFor [ngForOf]="[data]" ngFor [ngForOf]="[data]"
[ngForTemplate]="noPermissionTemplate" /> [ngForTemplate]="noPermissionTemplate" />
}
</div> </div>
</div> </div>
</ng-template> }
</div> </div>
<ng-template #loadingRowTemplate> } @else {
<div class="adf-datatable-row adf-datatable-data-loading"> <div class="adf-datatable-row adf-datatable-data-loading">
<div class="adf-no-content-container adf-datatable-cell"> <div class="adf-no-content-container adf-datatable-cell">
<ng-template *ngIf="loadingTemplate" @if (loadingTemplate) {
<ng-template
ngFor [ngForOf]="[data]" ngFor [ngForOf]="[data]"
[ngForTemplate]="loadingTemplate" /> [ngForTemplate]="loadingTemplate" />
}
</div> </div>
</div> </div>
</ng-template> }
</div> </div>
}
@@ -36,7 +36,6 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y'; import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { DataTableAdapter } from '@alfresco/adf-core';
import { MatTooltipHarness } from '@angular/material/tooltip/testing'; import { MatTooltipHarness } from '@angular/material/tooltip/testing';
@Component({ @Component({
@@ -1668,6 +1667,135 @@ describe('Accessibility', () => {
expect(datatableBodyCellAttributes['role']).toEqual('gridcell'); expect(datatableBodyCellAttributes['role']).toEqual('gridcell');
}); });
describe('isRepresentationContent', () => {
const setupTable = (columns: ObjectDataColumn[]) => {
dataTable.data = new ObjectDataTableAdapter([{ photo: '', status: '', name: 'test', created: '' }], columns);
dataTable.ngOnChanges({});
fixture.detectChanges();
};
it('should render image column cell with aria-hidden', () => {
setupTable([new ObjectDataColumn({ key: 'photo', type: 'image' })]);
const cell = testingUtils.getByCSS('.adf-datatable-body .adf-datatable-cell--image');
expect(cell.attributes['aria-hidden']).toBe('true');
});
it('should render icon column cell with aria-hidden', () => {
setupTable([new ObjectDataColumn({ key: 'status', type: 'icon' })]);
const cell = testingUtils.getByCSS('.adf-datatable-body .adf-datatable-cell--icon');
expect(cell.attributes['aria-hidden']).toBe('true');
});
it('should render text column cell without aria-hidden', () => {
setupTable([new ObjectDataColumn({ key: 'name', type: 'text' })]);
const cell = testingUtils.getByCSS('.adf-datatable-body .adf-datatable-cell--text');
expect(cell.attributes['aria-hidden']).toBeUndefined();
});
it('should render date column cell without aria-hidden', () => {
setupTable([new ObjectDataColumn({ key: 'created', type: 'date' })]);
const cell = testingUtils.getByCSS('.adf-datatable-body .adf-datatable-cell--date');
expect(cell.attributes['aria-hidden']).toBeUndefined();
});
it('should correctly set aria-hidden when mixing representation and non-representation columns', () => {
setupTable([new ObjectDataColumn({ key: 'photo', type: 'image' }), new ObjectDataColumn({ key: 'name', type: 'text' })]);
const cells = testingUtils.getAllByCSS('.adf-datatable-body .adf-datatable-cell-data');
expect(cells[0].attributes['aria-hidden']).toBe('true');
expect(cells[1].attributes['aria-hidden']).toBeUndefined();
});
});
describe('row aria-description', () => {
const rowSelector = '.adf-datatable-body .adf-datatable-row';
it('should have aria-description on row when multiselect is enabled', () => {
const dataRows = [{ name: 'test1' }, { name: 'test2' }];
dataTable.multiselect = true;
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]);
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
const rows = testingUtils.getAllByCSS(rowSelector);
expect(rows.length).toBeGreaterThan(0);
expect(rows[0].attributes['aria-description']).toBeTruthy();
});
it('should have aria-description on row when enableDragRows is true', () => {
const dataRows = [{ name: 'test1' }, { name: 'test2' }];
dataTable.enableDragRows = true;
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]);
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
const rows = testingUtils.getAllByCSS(rowSelector);
expect(rows.length).toBeGreaterThan(0);
expect(rows[0].attributes['aria-description']).toBeTruthy();
});
it('should not have aria-description on row when neither multiselect nor enableDragRows is enabled', () => {
const dataRows = [{ name: 'test1' }, { name: 'test2' }];
dataTable.multiselect = false;
dataTable.enableDragRows = false;
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]);
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
const rows = testingUtils.getAllByCSS(rowSelector);
expect(rows.length).toBeGreaterThan(0);
expect(rows[0].attributes['aria-description']).toBeFalsy();
});
});
describe('drag button and multiselect checkbox accessibility', () => {
it('should have aria-hidden on drag button', () => {
const dataRows = [{ name: 'test1' }];
dataTable.enableDragRows = true;
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]);
dataTable.showHeader = ShowHeaderMode.Never;
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
const dragButton = testingUtils.getByCSS('.adf-datatable__actions-cell [aria-hidden="true"]');
expect(dragButton).toBeTruthy();
expect(dragButton.attributes['aria-hidden']).toBe('true');
});
it('should have aria-hidden and negative tabindex on multiselect checkbox', () => {
const dataRows = [{ name: 'test1' }];
dataTable.multiselect = true;
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]);
dataTable.showHeader = ShowHeaderMode.Never;
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
const checkboxContainer = testingUtils.getByCSS('.adf-checkbox-sr-only');
expect(checkboxContainer).toBeTruthy();
expect(checkboxContainer.attributes['aria-hidden']).toBe('true');
expect(checkboxContainer.nativeElement.tabIndex).toBe(-1);
});
});
describe('aria-sort', () => { describe('aria-sort', () => {
let column: DataColumn; let column: DataColumn;
@@ -1700,6 +1828,32 @@ describe('Accessibility', () => {
describe('Focus row', () => { describe('Focus row', () => {
let event: KeyboardEvent; let event: KeyboardEvent;
let dataRows: { name: string }[]; let dataRows: { name: string }[];
const rowSelector = '.adf-datatable-body .adf-datatable-row';
const setRows = (): void => {
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
};
const getBodyRows = (): DebugElement[] => testingUtils.getAllByCSS(rowSelector);
const expectRowsTabindex = (expected: string | null): void => {
const rowElements = getBodyRows();
expect(rowElements.length).toBeGreaterThan(0);
expect(rowElements.every((row) => row.nativeElement.getAttribute('tabindex') === expected)).toBeTrue();
};
const activateRow = (rowIndex: number): void => {
const rowElement = getBodyRows()[rowIndex];
testingUtils.setDebugElement(rowElement);
testingUtils.clickByCSS('.adf-datatable-cell-data');
};
const dispatchKeyUp = (keyboardEvent: KeyboardEvent): void => {
fixture.debugElement.nativeElement.dispatchEvent(keyboardEvent);
};
beforeEach(() => { beforeEach(() => {
event = new KeyboardEvent('keyup', { event = new KeyboardEvent('keyup', {
@@ -1711,6 +1865,26 @@ describe('Accessibility', () => {
dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]); dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })]);
}); });
it('should set tabindex to null (disabled === true) on datatable-body rows when neither multiselect nor enableDragRows is enabled', () => {
setRows();
expectRowsTabindex(null);
});
it('should set tabindex to 0 (disabled === false) on datatable-body rows when multiselect is enabled', () => {
dataTable.multiselect = true;
setRows();
expectRowsTabindex('0');
});
it('should set tabindex to 0 (disabled === false) on datatable-body rows when enableDragRows is enabled', () => {
dataTable.enableDragRows = true;
setRows();
expectRowsTabindex('0');
});
it('should focus next row on ArrowDown event', () => { it('should focus next row on ArrowDown event', () => {
event = new KeyboardEvent('keyup', { event = new KeyboardEvent('keyup', {
code: 'ArrowDown', code: 'ArrowDown',
@@ -1718,35 +1892,23 @@ describe('Accessibility', () => {
keyCode: 40 keyCode: 40
} as KeyboardEventInit); } as KeyboardEventInit);
dataTable.ngOnChanges({ dataTable.multiselect = true;
rows: new SimpleChange(null, dataRows, false) setRows();
});
fixture.detectChanges();
dataTable.ngAfterViewInit(); dataTable.ngAfterViewInit();
const rowElement = testingUtils.getAllByCSS('.adf-datatable-body .adf-datatable-row')[0]; activateRow(0);
testingUtils.setDebugElement(rowElement); dispatchKeyUp(event);
testingUtils.clickByCSS('.adf-datatable-cell');
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-1'); expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-1');
}); });
it('should focus previous row on ArrowUp event', () => { it('should focus previous row on ArrowUp event', () => {
dataTable.ngOnChanges({ dataTable.multiselect = true;
rows: new SimpleChange(null, dataRows, false) setRows();
});
fixture.detectChanges();
dataTable.ngAfterViewInit(); dataTable.ngAfterViewInit();
const rowElement = testingUtils.getAllByCSS('.adf-datatable-body .adf-datatable-row')[1]; activateRow(1);
testingUtils.setDebugElement(rowElement); dispatchKeyUp(event);
testingUtils.clickByCSS('.adf-datatable-cell');
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-0'); expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-0');
}); });
@@ -1818,47 +1980,6 @@ describe('Accessibility', () => {
}); });
}); });
describe('ShareDatatable adapter allowFocusOnRows', () => {
class ShareAdapterMock extends ObjectDataTableAdapter {
public allowFocusOnRows = true;
constructor(data: any[], schema: DataColumn[]) {
super(data, schema);
}
setAllowFocusOnTableRows(allow: boolean) {
this.allowFocusOnRows = allow;
}
}
const testAllowFocusOnRows = (expectedTabindex: string | null, adapter: DataTableAdapter) => {
const fakeDataRows = [new FakeDataRow(), new FakeDataRow()];
adapter.setRows(fakeDataRows);
dataTable.data = adapter;
fixture.detectChanges();
const rowElements = testingUtils.getAllByCSS('.adf-datatable-body adf-datatable-row');
expect(rowElements.length).toBeGreaterThan(0);
expect(rowElements.every((row) => row.nativeElement.getAttribute('tabindex') === expectedTabindex)).toBeTrue();
};
it('should set tabindex to null (disabled === true) on datatable-body rows when allowFocusOnRows is set to false in ShareDatatable adapter', () => {
const adapter = new ShareAdapterMock([], []);
adapter.setAllowFocusOnTableRows(false);
testAllowFocusOnRows(null, adapter);
});
it('should set tabindex to 0 (disabled === false) on datatable-body rows when allowFocusOnRows is not set explicitly in ShareDatatable adapter and falls back to default value ', () => {
const adapter = new ShareAdapterMock([], []);
adapter.setAllowFocusOnTableRows(true);
testAllowFocusOnRows('0', adapter);
});
it('should set tabindex to 0 (disabled === false) by default on datatable-body rows when allowFocusOnRows is not defined in Datatable adapter (fallback case)', () => {
const adapter = new ObjectDataTableAdapter([], []);
testAllowFocusOnRows('0', adapter);
});
});
it('should create focus trap on main menu open', () => { it('should create focus trap on main menu open', () => {
dataTable.showHeader = ShowHeaderMode.Always; dataTable.showHeader = ShowHeaderMode.Always;
dataTable.showMainDatatableActions = true; dataTable.showMainDatatableActions = true;
@@ -853,6 +853,10 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
return false; return false;
} }
isRepresentationContent(col: DataColumn): boolean {
return col.type === 'image' || col.type === 'icon';
}
asIconValue(row: DataRow, col: DataColumn): string { asIconValue(row: DataRow, col: DataColumn): string {
if (this.isIconValue(row, col)) { if (this.isIconValue(row, col)) {
const value = this.data.getValue(row, col) || ''; const value = this.data.getValue(row, col) || '';
@@ -22,7 +22,6 @@ import { Subject } from 'rxjs';
export interface DataTableAdapter { export interface DataTableAdapter {
rowsChanged?: Subject<Array<DataRow>>; rowsChanged?: Subject<Array<DataRow>>;
allowFocusOnRows?: boolean;
selectedRow: DataRow; selectedRow: DataRow;
getRows(): Array<DataRow>; getRows(): Array<DataRow>;
@@ -34,5 +33,4 @@ export interface DataTableAdapter {
getSorting(): DataSorting; getSorting(): DataSorting;
setSorting(sorting: DataSorting): void; setSorting(sorting: DataSorting): void;
sort(key?: string, direction?: string): void; sort(key?: string, direction?: string): void;
setAllowFocusOnTableRows?(allow: boolean): void;
} }
+3
View File
@@ -398,6 +398,8 @@
"ACTIONS": "Actions", "ACTIONS": "Actions",
"SELECT_ALL": "Select all", "SELECT_ALL": "Select all",
"SELECT_FILE": "Select file", "SELECT_FILE": "Select file",
"SELECTED": "Selected",
"ROW_SELECTION": "Press Space to toggle row selection",
"SORT_ASCENDING": "Ascending", "SORT_ASCENDING": "Ascending",
"SORT_DESCENDING": "Descending", "SORT_DESCENDING": "Descending",
"SORT_NONE": "None", "SORT_NONE": "None",
@@ -408,6 +410,7 @@
"ICON_DISABLED": "Disabled", "ICON_DISABLED": "Disabled",
"ROW_OPTION_BUTTON": "Actions", "ROW_OPTION_BUTTON": "Actions",
"DRAG": "Drag button", "DRAG": "Drag button",
"DRAGGABLE": "Draggable",
"EMPTY_HEADER": "Empty header", "EMPTY_HEADER": "Empty header",
"RESIZE_COLUMN": "Resize {{ column }} column. Use arrow keys to adjust width, Shift for larger steps.", "RESIZE_COLUMN": "Resize {{ column }} column. Use arrow keys to adjust width, Shift for larger steps.",
"COLUMN_WIDTH_CHANGED": "{{ column }} column width: {{ width }} pixels" "COLUMN_WIDTH_CHANGED": "{{ column }} column width: {{ width }} pixels"