mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
[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:
co-authored by
MichalKinas
parent
565db01819
commit
a3e13da3a4
@@ -493,16 +493,4 @@ describe('ShareDataTableAdapter', () => {
|
||||
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[];
|
||||
selectedRow: DataRow;
|
||||
allowDropFiles: boolean;
|
||||
allowFocusOnRows = true;
|
||||
|
||||
set sortingMode(value: string) {
|
||||
let newValue = (value || 'client').toLowerCase();
|
||||
@@ -197,10 +196,6 @@ export class ShareDataTableAdapter implements DataTableAdapter {
|
||||
this.imageResolver = resolver;
|
||||
}
|
||||
|
||||
setAllowFocusOnTableRows(allow: boolean) {
|
||||
this.allowFocusOnRows = allow;
|
||||
}
|
||||
|
||||
private getFolderIcon(node: any) {
|
||||
if (this.isSmartFolder(node)) {
|
||||
return this.thumbnailService.getMimeTypeIcon('smartFolder');
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@if (data) {
|
||||
<div
|
||||
role="grid"
|
||||
*ngIf="data"
|
||||
class="adf-datatable-list"
|
||||
[class.adf-sticky-header]="isStickyHeaderEnabled()"
|
||||
[class.adf-datatable--empty]="(isEmpty() && !isHeaderVisible()) || loading"
|
||||
[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
|
||||
cdkDropList
|
||||
cdkDropListOrientation="horizontal"
|
||||
@@ -17,17 +18,22 @@
|
||||
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 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
|
||||
[indeterminate]="isSelectAllIndeterminate"
|
||||
[checked]="isSelectAllChecked"
|
||||
@@ -42,16 +48,12 @@
|
||||
{{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_ALL' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let col of getVisibleColumns();
|
||||
let columnIndex = index
|
||||
let lastColumn = last"
|
||||
>
|
||||
@for (col of getVisibleColumns(); track col.key; let lastColumn = $last; let columnIndex = $index) {
|
||||
@if (col.title || !showProvidedActions) {
|
||||
<div
|
||||
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"
|
||||
[ngClass]="{
|
||||
'adf-sortable': col.sortable,
|
||||
@@ -70,8 +72,7 @@
|
||||
(mouseenter)="hoveredHeaderColumnIndex = columnIndex"
|
||||
(mouseleave)="hoveredHeaderColumnIndex = -1"
|
||||
adf-drop-zone dropTarget="header"
|
||||
[dropColumn]="col"
|
||||
>
|
||||
[dropColumn]="col">
|
||||
|
||||
<div
|
||||
adf-resizable
|
||||
@@ -103,57 +104,69 @@
|
||||
!isDraggingHeaderColumn &&
|
||||
!isResizing && col.sortable}"
|
||||
>
|
||||
<ng-container *ngIf="!col.header">
|
||||
@if (!col.header) {
|
||||
@if (col.title) {
|
||||
<span
|
||||
*ngIf="col.title"
|
||||
title="{{col.title | translate}}"
|
||||
class="adf-datatable-cell-value"
|
||||
>
|
||||
{{col.title | translate}}
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (col.subtitle) {
|
||||
<span
|
||||
*ngIf="col.subtitle"
|
||||
title="{{col.subtitle | translate}}"
|
||||
class="adf-datatable-cell-value adf-datatable-cell-header_subtitle"
|
||||
>
|
||||
({{col.subtitle | translate}})
|
||||
</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 } }}
|
||||
</span>
|
||||
}
|
||||
|
||||
<span *ngIf="!col.title && !col.sortable && !headerFilterTemplate" [attr.title]="'ADF-DATATABLE.ACCESSIBILITY.EMPTY_HEADER' | translate"></span>
|
||||
</ng-container>
|
||||
@if (!col.title && !col.sortable && !headerFilterTemplate) {
|
||||
<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}" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<span
|
||||
[class.adf-datatable__header--sorted-asc]="isColumnSorted(col, 'asc')"
|
||||
[class.adf-datatable__header--sorted-desc]="isColumnSorted(col, 'desc')">
|
||||
</span>
|
||||
|
||||
<ng-template *ngIf="allowFiltering" [ngTemplateOutlet]="headerFilterTemplate" [ngTemplateOutletContext]="{$implicit: col}" />
|
||||
@if (allowFiltering) {
|
||||
<ng-template [ngTemplateOutlet]="headerFilterTemplate" [ngTemplateOutletContext]="{$implicit: col}" />
|
||||
}
|
||||
|
||||
@if (col.draggable) {
|
||||
<span
|
||||
*ngIf="col.draggable"
|
||||
cdkDragHandle
|
||||
[ngClass]="{ 'adf-datatable-cell-header-drag-icon': !isResizing }"
|
||||
>
|
||||
@if (hoveredHeaderColumnIndex === columnIndex && !isResizing) {
|
||||
<mat-icon
|
||||
*ngIf="hoveredHeaderColumnIndex === columnIndex && !isResizing"
|
||||
svgIcon="adf:drag_indicator"
|
||||
class="adf-datatable-cell-header-drag-icon-visible"
|
||||
[attr.data-automation-id]="'adf-datatable-cell-header-drag-icon-'+col.key"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (isResizingEnabled && col.resizable && !lastColumn) {
|
||||
<div
|
||||
*ngIf="isResizingEnabled && col.resizable && !lastColumn"
|
||||
[ngClass]="hoveredHeaderColumnIndex === columnIndex && !isResizing || resizingColumnIndex === columnIndex ? 'adf-datatable__resize-handle-visible' : 'adf-datatable__resize-handle-hidden'"
|
||||
adf-resize-handle
|
||||
tabindex="0"
|
||||
@@ -168,18 +181,19 @@
|
||||
[resizableContainer]="resizableElement">
|
||||
<div class="adf-datatable__resize-handle--divider"></div>
|
||||
</div>
|
||||
}
|
||||
<div class="adf-drop-header-cell-placeholder" *cdkDragPlaceholder></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Header actions (right) -->
|
||||
@if ((actions && actionsPosition === 'right') || (mainActionTemplate && showMainDatatableActions)) {
|
||||
<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-datatable-actions-menu-provided]="showProvidedActions"
|
||||
>
|
||||
<ng-container *ngIf="mainActionTemplate">
|
||||
@if (mainActionTemplate) {
|
||||
<button
|
||||
data-automation-id="adf-datatable-main-menu-button"
|
||||
title="{{ 'ADF-DATATABLE.CONTENT-ACTIONS.SELECT_COLUMNS' | translate }}"
|
||||
@@ -199,20 +213,21 @@
|
||||
</div>
|
||||
</mat-menu>
|
||||
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
</adf-datatable-row>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading) {
|
||||
<div
|
||||
*ngIf="!loading; else loadingRowTemplate"
|
||||
class="adf-datatable-body"
|
||||
[ngClass]="{ 'adf-blur-datatable-body': blurOnResize && (isDraggingHeaderColumn || isResizing), 'adf-datatable-body__draggable': enableDragRows && !isDraggingRow, 'adf-datatable-body__dragging': isDraggingRow }"
|
||||
cdkDropList
|
||||
[cdkDropListDisabled]="!enableDragRows"
|
||||
role="rowgroup">
|
||||
<ng-container *ngIf="!noPermission; else noPermissionsRowTemplate">
|
||||
@if (!noPermission) {
|
||||
<adf-datatable-row *ngFor="let row of data.getRows(); let idx = index"
|
||||
cdkDrag
|
||||
[cdkDragDisabled]="!enableDragRows"
|
||||
@@ -231,20 +246,32 @@
|
||||
[class.adf-datatable-row__dragging]="isDraggingRow"
|
||||
[attr.data-automation-id]="'datatable-row-' + idx"
|
||||
(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 -->
|
||||
<div *ngIf="enableDragRows"
|
||||
@if (enableDragRows) {
|
||||
<div
|
||||
role="gridcell"
|
||||
class="adf-datatable-cell adf-datatable__actions-cell adf-datatable-hover-only">
|
||||
<button mat-icon-button
|
||||
[attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.DRAG' | translate">
|
||||
<mat-icon adf-icon="drag_indicator" />
|
||||
</button>
|
||||
<mat-icon adf-icon="drag_indicator" aria-hidden="true" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 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"
|
||||
[ngClass]="getHideActionsWithoutHoverClass(actionsMenuTrigger)"
|
||||
[title]="'ADF-DATATABLE.CONTENT-ACTIONS.TOOLTIP' | translate"
|
||||
@@ -253,47 +280,54 @@
|
||||
<mat-icon adf-icon="more_vert" />
|
||||
</button>
|
||||
<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"
|
||||
[disabled]="action.disabled"
|
||||
(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>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
</div>
|
||||
}
|
||||
|
||||
<label *ngIf="multiselect"
|
||||
@if (multiselect) {
|
||||
<label
|
||||
(keydown.enter)="onEnterKeyPressed(row, $any($event))"
|
||||
(click)="onCheckboxLabelClick(row, $event)"
|
||||
[for]="'select-file-' + idx"
|
||||
class="adf-datatable-cell adf-datatable-checkbox adf-datatable-checkbox-single"
|
||||
[attr.tabindex]="null">
|
||||
[attr.tabindex]="null"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<mat-checkbox
|
||||
[id]="'select-file-' + idx"
|
||||
[disabled]="!row?.isSelectable"
|
||||
[class.adf-datatable-checkbox-selected]="row.isSelected"
|
||||
[class.adf-datatable-hover-only]="displayCheckboxesOnHover"
|
||||
[checked]="row.isSelected"
|
||||
[attr.aria-checked]="row.isSelected"
|
||||
[aria-label]="'ADF-DATATABLE.ACCESSIBILITY.SELECT_FILE' | translate"
|
||||
data-adf-datatable-row-checkbox
|
||||
(change)="onCheckboxChange(row, $event)"
|
||||
(keydown.enter)="$event.stopPropagation()"
|
||||
class="adf-checkbox-sr-only"
|
||||
>
|
||||
{{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_FILE' | translate }}
|
||||
</mat-checkbox>
|
||||
[tabIndex]="-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
|
||||
@for (col of getVisibleColumns(); track col.key; let lastColumn = $last;) {
|
||||
<div
|
||||
*ngFor="let col of getVisibleColumns(); let lastColumn = last;"
|
||||
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.data-automation-id]="getAutomationValue(row)"
|
||||
[attr.aria-selected]="row.isSelected"
|
||||
[attr.aria-label]="col.title ? (col.title | translate) : null"
|
||||
[attr.aria-hidden]='isRepresentationContent(col) ? "true" : null'
|
||||
(click)="onRowClick(row, $event)"
|
||||
[attr.tabindex]="null"
|
||||
(keydown.enter)="onEnterKeyPressed(row, $any($event))"
|
||||
@@ -302,19 +336,21 @@
|
||||
adf-drop-zone dropTarget="cell" [dropColumn]="col" [dropRow]="row"
|
||||
[ngStyle]="(col.width) && !lastColumn && {'flex': getFlexValue(col)}"
|
||||
>
|
||||
<div *ngIf="!col.template" class="adf-datatable-cell-container">
|
||||
<ng-container [ngSwitch]="data.getColumnType(row, col)">
|
||||
<div *ngSwitchCase="'image'" class="adf-cell-value">
|
||||
@if (!col.template) {
|
||||
<div class="adf-datatable-cell-container">
|
||||
@switch (data.getColumnType(row, col)) {
|
||||
@case ('image') {
|
||||
<div class="adf-cell-value">
|
||||
@if (isIconValue(row, col)) {
|
||||
<mat-icon
|
||||
[attr.aria-label]="col.srTitle? (col.srTitle | translate) : null"
|
||||
[attr.aria-hidden]="!asIconValue(row, col)"
|
||||
*ngIf="isIconValue(row, col); else no_iconvalue"
|
||||
[adf-icon]="asIconValue(row, col)"
|
||||
/>
|
||||
<ng-template #no_iconvalue>
|
||||
<mat-icon class="adf-datatable-selected"
|
||||
*ngIf="row.isSelected && !multiselect; else no_selected_row" svgIcon="selected" />
|
||||
<ng-template #no_selected_row>
|
||||
} @else {
|
||||
@if (row.isSelected && !multiselect) {
|
||||
<mat-icon class="adf-datatable-selected" svgIcon="selected" />
|
||||
} @else {
|
||||
<img class="adf-datatable-center-img-ie"
|
||||
[attr.aria-label]="(data.getValue(row, col) | fileType) === 'disable' ?
|
||||
('ADF-DATATABLE.ACCESSIBILITY.ICON_DISABLED' | translate) :
|
||||
@@ -328,11 +364,12 @@
|
||||
}"
|
||||
src="{{ data.getValue(row, col) }}"
|
||||
(error)="onImageLoadingError($event, row)">
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div *ngSwitchCase="'icon'" class="adf-cell-value">
|
||||
}
|
||||
@case ('icon') {
|
||||
<div class="adf-cell-value">
|
||||
<adf-icon-cell
|
||||
[data]="data"
|
||||
[column]="col"
|
||||
@@ -341,10 +378,10 @@
|
||||
[tooltip]="getCellTooltip(row, col)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@case ('date') {
|
||||
<div
|
||||
*ngSwitchCase="'date'"
|
||||
class="adf-cell-value adf-cell-date"
|
||||
|
||||
[attr.data-automation-id]="'date_' + (data.getValue(row, col, resolverFn) | adfLocalizedDate: 'medium') ">
|
||||
<adf-date-cell class="adf-datatable-center-date-column-ie"
|
||||
[data]="data"
|
||||
@@ -354,8 +391,9 @@
|
||||
[tooltip]="getCellTooltip(row, col)"
|
||||
[dateConfig]="col.dateConfig" />
|
||||
</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)">
|
||||
<adf-location-cell
|
||||
[data]="data"
|
||||
@@ -364,7 +402,9 @@
|
||||
[resolverFn]="resolverFn"
|
||||
[tooltip]="getCellTooltip(row, col)" />
|
||||
</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)">
|
||||
<adf-filesize-cell class="adf-datatable-center-size-column-ie"
|
||||
[data]="data"
|
||||
@@ -373,7 +413,9 @@
|
||||
[resolverFn]="resolverFn"
|
||||
[tooltip]="getCellTooltip(row, col)" />
|
||||
</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)">
|
||||
<adf-datatable-cell
|
||||
[copyContent]="col.copyContent"
|
||||
@@ -383,7 +425,9 @@
|
||||
[resolverFn]="resolverFn"
|
||||
[tooltip]="getCellTooltip(row, col)" />
|
||||
</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)">
|
||||
<adf-boolean-cell
|
||||
[data]="data"
|
||||
@@ -392,7 +436,9 @@
|
||||
[resolverFn]="resolverFn"
|
||||
[tooltip]="getCellTooltip(row, col)" />
|
||||
</div>
|
||||
<div *ngSwitchCase="'json'" class="adf-cell-value">
|
||||
}
|
||||
@case ('json') {
|
||||
<div class="adf-cell-value">
|
||||
<adf-json-cell
|
||||
[editable]="col.editable"
|
||||
[data]="data"
|
||||
@@ -400,7 +446,9 @@
|
||||
[resolverFn]="resolverFn"
|
||||
[row]="row" />
|
||||
</div>
|
||||
<div *ngSwitchCase="'amount'"
|
||||
}
|
||||
@case ('amount') {
|
||||
<div
|
||||
class="adf-cell-value"
|
||||
[attr.data-automation-id]="'amount_' + data.getValue(row, col, resolverFn)">
|
||||
<adf-amount-cell
|
||||
@@ -410,7 +458,9 @@
|
||||
[row]="row"
|
||||
[currencyConfig]="col.currencyConfig" />
|
||||
</div>
|
||||
<div *ngSwitchCase="'number'"
|
||||
}
|
||||
@case ('number') {
|
||||
<div
|
||||
class="adf-cell-value"
|
||||
[attr.data-automation-id]="'number_' + data.getValue(row, col, resolverFn)">
|
||||
<adf-number-cell
|
||||
@@ -420,29 +470,33 @@
|
||||
[row]="row"
|
||||
[decimalConfig]="col.decimalConfig" />
|
||||
</div>
|
||||
<span *ngSwitchDefault class="adf-cell-value">
|
||||
}
|
||||
@default {
|
||||
<span class="adf-cell-value">
|
||||
<!-- empty cell for unknown column type -->
|
||||
</span>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="col.template" class="adf-datatable-cell-container">
|
||||
} @else {
|
||||
<div class="adf-datatable-cell-container">
|
||||
<div class="adf-cell-value">
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="col.template"
|
||||
[ngTemplateOutletContext]="{ $implicit: { data: data, row: row, col: col }, value: data.getValue(row, col, resolverFn) }" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Row actions (right) -->
|
||||
<div *ngIf="
|
||||
!showProvidedActions &&
|
||||
((actions && actionsPosition === 'right') ||
|
||||
(mainActionTemplate && showMainDatatableActions))"
|
||||
@if (!showProvidedActions && ((actions && actionsPosition === 'right') || (mainActionTemplate && showMainDatatableActions))) {
|
||||
<div
|
||||
role="gridcell"
|
||||
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"
|
||||
[ngClass]="getHideActionsWithoutHoverClass(actionsMenuTrigger)"
|
||||
[attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.ROW_OPTION_BUTTON' | translate"
|
||||
@@ -453,47 +507,59 @@
|
||||
<mat-icon adf-icon="more_vert" />
|
||||
</button>
|
||||
<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.aria-label]="action.title | translate"
|
||||
[disabled]="action.disabled"
|
||||
(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>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</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">
|
||||
<ng-template *ngIf="noContentTemplate"
|
||||
@if (noContentTemplate) {
|
||||
<ng-template
|
||||
ngFor [ngForOf]="[data]"
|
||||
[ngForTemplate]="noContentTemplate" />
|
||||
}
|
||||
<ng-content select="adf-empty-list" />
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #noPermissionsRowTemplate>
|
||||
}
|
||||
} @else {
|
||||
<div
|
||||
role="row"
|
||||
class="adf-datatable-row adf-no-permission__row">
|
||||
<div class="adf-no-permission__cell adf-no-content-container adf-datatable-cell">
|
||||
<ng-template *ngIf="noPermissionTemplate"
|
||||
@if (noPermissionTemplate) {
|
||||
<ng-template
|
||||
ngFor [ngForOf]="[data]"
|
||||
[ngForTemplate]="noPermissionTemplate" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
</div>
|
||||
<ng-template #loadingRowTemplate>
|
||||
} @else {
|
||||
<div class="adf-datatable-row adf-datatable-data-loading">
|
||||
<div class="adf-no-content-container adf-datatable-cell">
|
||||
<ng-template *ngIf="loadingTemplate"
|
||||
@if (loadingTemplate) {
|
||||
<ng-template
|
||||
ngFor [ngForOf]="[data]"
|
||||
[ngForTemplate]="loadingTemplate" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { DataTableAdapter } from '@alfresco/adf-core';
|
||||
import { MatTooltipHarness } from '@angular/material/tooltip/testing';
|
||||
|
||||
@Component({
|
||||
@@ -1668,6 +1667,135 @@ describe('Accessibility', () => {
|
||||
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', () => {
|
||||
let column: DataColumn;
|
||||
|
||||
@@ -1700,6 +1828,32 @@ describe('Accessibility', () => {
|
||||
describe('Focus row', () => {
|
||||
let event: KeyboardEvent;
|
||||
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(() => {
|
||||
event = new KeyboardEvent('keyup', {
|
||||
@@ -1711,6 +1865,26 @@ describe('Accessibility', () => {
|
||||
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', () => {
|
||||
event = new KeyboardEvent('keyup', {
|
||||
code: 'ArrowDown',
|
||||
@@ -1718,35 +1892,23 @@ describe('Accessibility', () => {
|
||||
keyCode: 40
|
||||
} as KeyboardEventInit);
|
||||
|
||||
dataTable.ngOnChanges({
|
||||
rows: new SimpleChange(null, dataRows, false)
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
dataTable.multiselect = true;
|
||||
setRows();
|
||||
dataTable.ngAfterViewInit();
|
||||
|
||||
const rowElement = testingUtils.getAllByCSS('.adf-datatable-body .adf-datatable-row')[0];
|
||||
testingUtils.setDebugElement(rowElement);
|
||||
testingUtils.clickByCSS('.adf-datatable-cell');
|
||||
|
||||
fixture.debugElement.nativeElement.dispatchEvent(event);
|
||||
activateRow(0);
|
||||
dispatchKeyUp(event);
|
||||
|
||||
expect(document.activeElement?.getAttribute('data-automation-id')).toBe('datatable-row-1');
|
||||
});
|
||||
|
||||
it('should focus previous row on ArrowUp event', () => {
|
||||
dataTable.ngOnChanges({
|
||||
rows: new SimpleChange(null, dataRows, false)
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
dataTable.multiselect = true;
|
||||
setRows();
|
||||
dataTable.ngAfterViewInit();
|
||||
|
||||
const rowElement = testingUtils.getAllByCSS('.adf-datatable-body .adf-datatable-row')[1];
|
||||
testingUtils.setDebugElement(rowElement);
|
||||
testingUtils.clickByCSS('.adf-datatable-cell');
|
||||
|
||||
fixture.debugElement.nativeElement.dispatchEvent(event);
|
||||
activateRow(1);
|
||||
dispatchKeyUp(event);
|
||||
|
||||
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', () => {
|
||||
dataTable.showHeader = ShowHeaderMode.Always;
|
||||
dataTable.showMainDatatableActions = true;
|
||||
|
||||
@@ -853,6 +853,10 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
|
||||
return false;
|
||||
}
|
||||
|
||||
isRepresentationContent(col: DataColumn): boolean {
|
||||
return col.type === 'image' || col.type === 'icon';
|
||||
}
|
||||
|
||||
asIconValue(row: DataRow, col: DataColumn): string {
|
||||
if (this.isIconValue(row, col)) {
|
||||
const value = this.data.getValue(row, col) || '';
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Subject } from 'rxjs';
|
||||
|
||||
export interface DataTableAdapter {
|
||||
rowsChanged?: Subject<Array<DataRow>>;
|
||||
allowFocusOnRows?: boolean;
|
||||
|
||||
selectedRow: DataRow;
|
||||
getRows(): Array<DataRow>;
|
||||
@@ -34,5 +33,4 @@ export interface DataTableAdapter {
|
||||
getSorting(): DataSorting;
|
||||
setSorting(sorting: DataSorting): void;
|
||||
sort(key?: string, direction?: string): void;
|
||||
setAllowFocusOnTableRows?(allow: boolean): void;
|
||||
}
|
||||
|
||||
@@ -398,6 +398,8 @@
|
||||
"ACTIONS": "Actions",
|
||||
"SELECT_ALL": "Select all",
|
||||
"SELECT_FILE": "Select file",
|
||||
"SELECTED": "Selected",
|
||||
"ROW_SELECTION": "Press Space to toggle row selection",
|
||||
"SORT_ASCENDING": "Ascending",
|
||||
"SORT_DESCENDING": "Descending",
|
||||
"SORT_NONE": "None",
|
||||
@@ -408,6 +410,7 @@
|
||||
"ICON_DISABLED": "Disabled",
|
||||
"ROW_OPTION_BUTTON": "Actions",
|
||||
"DRAG": "Drag button",
|
||||
"DRAGGABLE": "Draggable",
|
||||
"EMPTY_HEADER": "Empty header",
|
||||
"RESIZE_COLUMN": "Resize {{ column }} column. Use arrow keys to adjust width, Shift for larger steps.",
|
||||
"COLUMN_WIDTH_CHANGED": "{{ column }} column width: {{ width }} pixels"
|
||||
|
||||
Reference in New Issue
Block a user