[ADF-2990] Datatable - row navigation (#5198)

* datatable row component

* add to module

* implement datatable row component

* use FocusKeyManager for arrows navigation

* tests

* prevent screen reader to announce undefined value

* prevent from bubbling up

* fix unit test

* fix row locator

* fix locator reference

* fix row reference locators

* fix locator xpath
This commit is contained in:
Cilibiu Bogdan
2019-10-30 09:39:43 +02:00
committed by Denys Vuika
parent a150e74366
commit af6bd0892c
17 changed files with 320 additions and 45 deletions

View File

@@ -36,8 +36,8 @@ export class DataTablePage {
dataTable: DataTableComponentPage; dataTable: DataTableComponentPage;
multiSelect: ElementFinder = element(by.css(`div[data-automation-id='multiselect'] label > div[class='mat-checkbox-inner-container']`)); multiSelect: ElementFinder = element(by.css(`div[data-automation-id='multiselect'] label > div[class='mat-checkbox-inner-container']`));
reset: ElementFinder = element(by.xpath(`//span[contains(text(),'Reset to default')]/..`)); reset: ElementFinder = element(by.xpath(`//span[contains(text(),'Reset to default')]/..`));
allSelectedRows: ElementArrayFinder = element.all(by.css(`div[class*='is-selected']`)); allSelectedRows: ElementArrayFinder = element.all(by.css(`adf-datatable-row[class*='is-selected']`));
selectedRowNumber: ElementFinder = element(by.css(`div[class*='is-selected'] div[data-automation-id*='text_']`)); selectedRowNumber: ElementFinder = element(by.css(`adf-datatable-row[class*='is-selected'] div[data-automation-id*='text_']`));
selectAll: ElementFinder = element(by.css(`div[class*='header'] label`)); selectAll: ElementFinder = element(by.css(`div[class*='header'] label`));
addRowElement: ElementFinder = element(by.xpath(`//span[contains(text(),'Add row')]/..`)); addRowElement: ElementFinder = element(by.xpath(`//span[contains(text(),'Add row')]/..`));
replaceRowsElement: ElementFinder = element(by.xpath(`//span[contains(text(),'Replace rows')]/..`)); replaceRowsElement: ElementFinder = element(by.xpath(`//span[contains(text(),'Replace rows')]/..`));
@@ -85,7 +85,7 @@ export class DataTablePage {
async checkRowIsNotSelected(rowNumber: string): Promise<void> { async checkRowIsNotSelected(rowNumber: string): Promise<void> {
const isRowSelected = this.dataTable.getCellElementByValue(this.columns.id, rowNumber) const isRowSelected = this.dataTable.getCellElementByValue(this.columns.id, rowNumber)
.element(by.xpath(`ancestor::div[contains(@class, 'adf-datatable-row custom-row-style ng-star-inserted is-selected')]`)); .element(by.xpath(`ancestor::adf-datatable-row[contains(@class, 'adf-datatable-row custom-row-style ng-star-inserted is-selected')]`));
await BrowserVisibility.waitUntilElementIsNotVisible(isRowSelected); await BrowserVisibility.waitUntilElementIsNotVisible(isRowSelected);
} }
@@ -112,7 +112,7 @@ export class DataTablePage {
async clickCheckbox(rowNumber: string): Promise<void> { async clickCheckbox(rowNumber: string): Promise<void> {
await BrowserActions.closeMenuAndDialogs(); await BrowserActions.closeMenuAndDialogs();
const checkbox = this.dataTable.getCellElementByValue(this.columns.id, rowNumber) const checkbox = this.dataTable.getCellElementByValue(this.columns.id, rowNumber)
.element(by.xpath(`ancestor::div[contains(@class, 'adf-datatable-row')]//mat-checkbox/label`)); .element(by.xpath(`ancestor::adf-datatable-row[contains(@class, 'adf-datatable-row')]//mat-checkbox/label`));
await BrowserActions.click(checkbox); await BrowserActions.click(checkbox);
} }
@@ -134,7 +134,7 @@ export class DataTablePage {
} }
getRowCheckbox(rowNumber: string): ElementFinder { getRowCheckbox(rowNumber: string): ElementFinder {
return this.dataTable.getCellElementByValue(this.columns.id, rowNumber).element(by.xpath(`ancestor::div/div/mat-checkbox[contains(@class, 'mat-checkbox-checked')]`)); return this.dataTable.getCellElementByValue(this.columns.id, rowNumber).element(by.xpath(`ancestor::adf-datatable-row/div/mat-checkbox[contains(@class, 'mat-checkbox-checked')]`));
} }
async getCopyContentTooltip(): Promise<string> { async getCopyContentTooltip(): Promise<string> {

View File

@@ -32,9 +32,9 @@ export class ProcessFiltersPage {
accordionMenu: ElementFinder = element(by.css('.app-processes-menu mat-accordion')); accordionMenu: ElementFinder = element(by.css('.app-processes-menu mat-accordion'));
buttonWindow: ElementFinder = element(by.css('div > button[data-automation-id="btn-start-process"] > div')); buttonWindow: ElementFinder = element(by.css('div > button[data-automation-id="btn-start-process"] > div'));
noContentMessage: ElementFinder = element.all(by.css('div[class="adf-empty-content__title"]')).first(); noContentMessage: ElementFinder = element.all(by.css('div[class="adf-empty-content__title"]')).first();
rows: Locator = by.css('adf-process-instance-list div[class="adf-datatable-body"] div[class*="adf-datatable-row"]'); rows: Locator = by.css('adf-process-instance-list div[class="adf-datatable-body"] adf-datatable-row[class*="adf-datatable-row"]');
tableBody: ElementFinder = element.all(by.css('adf-datatable div[class="adf-datatable-body"]')).first(); tableBody: ElementFinder = element.all(by.css('adf-datatable div[class="adf-datatable-body"]')).first();
nameColumn: Locator = by.css('div[class*="adf-datatable-body"] div[class*="adf-datatable-row"] div[title="Name"] span'); nameColumn: Locator = by.css('div[class*="adf-datatable-body"] adf-datatable-row[class*="adf-datatable-row"] div[title="Name"] span');
processIcon: Locator = by.xpath('ancestor::div[@class="mat-list-item-content"]/mat-icon'); processIcon: Locator = by.xpath('ancestor::div[@class="mat-list-item-content"]/mat-icon');
async startProcess(): Promise<StartProcessPage> { async startProcess(): Promise<StartProcessPage> {

View File

@@ -263,7 +263,7 @@ export class TaskDetailsPage {
} }
async removeInvolvedUser(user): Promise<void> { async removeInvolvedUser(user): Promise<void> {
const row = this.getRowsUser(user).element(by.xpath('ancestor::div[contains(@class, "adf-datatable-row")]')); const row = this.getRowsUser(user).element(by.xpath('ancestor::adf-datatable-row[contains(@class, "adf-datatable-row")]'));
await BrowserActions.click(row.element(by.css('button[data-automation-id="action_menu_0"]'))); await BrowserActions.click(row.element(by.css('button[data-automation-id="action_menu_0"]')));
await BrowserVisibility.waitUntilElementIsVisible(this.removeInvolvedPeople); await BrowserVisibility.waitUntilElementIsVisible(this.removeInvolvedPeople);
await BrowserActions.click(this.removeInvolvedPeople); await BrowserActions.click(this.removeInvolvedPeople);

View File

@@ -32,7 +32,7 @@ export class TasksPage {
rowByRowName = by.xpath('ancestor::mat-chip'); rowByRowName = by.xpath('ancestor::mat-chip');
checklistContainer = by.css('div[class*="checklist-menu"]'); checklistContainer = by.css('div[class*="checklist-menu"]');
taskTitle = 'h2[class="adf-activiti-task-details__header"] span'; taskTitle = 'h2[class="adf-activiti-task-details__header"] span';
rows = by.css('div[class*="adf-datatable-body"] div[class*="adf-datatable-row"] div[class*="adf-datatable-cell"]'); rows = by.css('div[class*="adf-datatable-body"] adf-datatable-row[class*="adf-datatable-row"] div[class*="adf-datatable-cell"]');
completeButtonNoForm: ElementFinder = element(by.id('adf-no-form-complete-button')); completeButtonNoForm: ElementFinder = element(by.id('adf-no-form-complete-button'));
checklistDialog: ElementFinder = element(by.id('checklist-dialog')); checklistDialog: ElementFinder = element(by.id('checklist-dialog'));
checklistNoMessage: ElementFinder = element(by.id('checklist-none-message')); checklistNoMessage: ElementFinder = element(by.id('checklist-none-message'));

View File

@@ -22,7 +22,7 @@ import { element, by, ElementFinder, Locator } from 'protractor';
export class TrashcanPage { export class TrashcanPage {
contentList: DocumentListPage = new DocumentListPage(element(by.css('adf-document-list'))); contentList: DocumentListPage = new DocumentListPage(element(by.css('adf-document-list')));
rows: Locator = by.css('adf-document-list div[class*="adf-datatable-body"] div[class*="adf-datatable-row"]'); rows: Locator = by.css('adf-document-list div[class*="adf-datatable-body"] adf-datatable-row[class*="adf-datatable-row"]');
tableBody: ElementFinder = element.all(by.css('adf-document-list div[class="adf-datatable-body"]')).first(); tableBody: ElementFinder = element.all(by.css('adf-document-list div[class="adf-datatable-body"]')).first();
pagination: ElementFinder = element(by.css('adf-pagination')); pagination: ElementFinder = element(by.css('adf-pagination'));
emptyTrashcan: ElementFinder = element(by.css('adf-empty-content')); emptyTrashcan: ElementFinder = element(by.css('adf-empty-content'));

View File

@@ -73,9 +73,7 @@ describe('NodeLock Directive', () => {
fixture.detectChanges(); fixture.detectChanges();
element = fixture.debugElement.query(By.directive(NodeLockDirective)); element = fixture.debugElement.query(By.directive(NodeLockDirective));
element.triggerEventHandler('click', { element.nativeElement.dispatchEvent(new MouseEvent('click'));
preventDefault: () => {}
});
expect(contentNodeDialogService.openLockNodeDialog).toHaveBeenCalledWith(fakeNode); expect(contentNodeDialogService.openLockNodeDialog).toHaveBeenCalledWith(fakeNode);
}); });

View File

@@ -33,7 +33,7 @@ export class NodeLockDirective implements AfterViewInit {
@HostListener('click', [ '$event' ]) @HostListener('click', [ '$event' ])
onClick(event) { onClick(event) {
event.preventDefault(); event.stopPropagation();
this.contentNodeDialogService.openLockNodeDialog(this.node); this.contentNodeDialogService.openLockNodeDialog(this.node);
} }

View File

@@ -0,0 +1,99 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DataTableRowComponent } from './datatable-row.component';
import { DataRow } from '../../data/data-row.model';
import { TestBed, ComponentFixture } from '@angular/core/testing';
describe('DataTableRowComponent', () => {
let fixture: ComponentFixture<DataTableRowComponent>;
let component: DataTableRowComponent;
const row: DataRow = {
isSelected: false,
hasValue: jasmine.createSpy('hasValue'),
getValue: () => {}
};
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DataTableRowComponent]
});
fixture = TestBed.createComponent(DataTableRowComponent);
component = fixture.componentInstance;
});
it('should add select class when row is selected', () => {
row.isSelected = true;
component.row = row;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.classList.contains('adf-is-selected')).toBe(true);
});
it('should not have select class when row is not selected', () => {
row.isSelected = false;
component.row = row;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.classList.contains('adf-is-selected'))
.not.toBe(true);
});
it('should set aria selected to true when row is selected', () => {
row.isSelected = true;
component.row = row;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('aria-selected')).toBe('true');
});
it('should set aria selected to false when row is not selected', () => {
row.isSelected = false;
component.row = row;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('aria-selected')).toBe('false');
});
it('should set aria label', () => {
spyOn(row, 'getValue').and.returnValue('some-name');
component.row = row;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.getAttribute('aria-label')).toBe('some-name');
});
it('should focus element', () => {
expect(document.activeElement.classList.contains('adf-datatable-row')).toBe(false);
component.focus();
expect(document.activeElement.classList.contains('adf-datatable-row')).toBe(true);
});
it('should emit keyboard space event', () => {
spyOn(component.select, 'emit');
const event = new KeyboardEvent('keydown', {
key: ' ',
code: 'Space'
});
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(component.select.emit).toHaveBeenCalledWith(event);
});
});

View File

@@ -0,0 +1,75 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Component,
ViewEncapsulation,
ElementRef,
Input,
HostBinding,
HostListener,
Output,
EventEmitter
} from '@angular/core';
import { FocusableOption } from '@angular/cdk/a11y';
import { DataRow } from '../../data/data-row.model';
@Component({
selector: 'adf-datatable-row',
template: `<ng-content></ng-content>`,
encapsulation: ViewEncapsulation.None,
host: {
class: 'adf-datatable-row',
tabindex: '0',
role: 'row'
}
})
export class DataTableRowComponent implements FocusableOption {
@Input() row: DataRow;
@Output()
select: EventEmitter<any> = new EventEmitter<any>();
@HostBinding('class.adf-is-selected')
get isSelected(): boolean {
return this.row.isSelected;
}
@HostBinding('attr.aria-selected')
get isAriaSelected(): boolean {
return this.row.isSelected;
}
@HostBinding('attr.aria-label')
get ariaLabel(): boolean {
return this.row.getValue('name') || '';
}
@HostListener('keydown.space', ['$event'])
onKeyDown(event: KeyboardEvent) {
if ((event.target as Element).tagName === this.element.nativeElement.tagName) {
event.preventDefault();
this.select.emit(event);
}
}
constructor(private element: ElementRef) {}
focus() {
this.element.nativeElement.focus();
}
}

View File

@@ -51,14 +51,15 @@
<div class="adf-datatable-body" role="rowgroup"> <div class="adf-datatable-body" role="rowgroup">
<ng-container *ngIf="!loading && !noPermission"> <ng-container *ngIf="!loading && !noPermission">
<div *ngFor="let row of data.getRows(); let idx = index" <adf-datatable-row *ngFor="let row of data.getRows(); let idx = index"
class="adf-datatable-row" [row]="row"
role="row" (select)="onEnterKeyPressed(row, $event)"
[class.adf-is-selected]="row.isSelected" (keyup)="onRowKeyUp(row, $event)"
[adf-upload]="allowDropFiles && rowAllowsDrop(row)" [adf-upload-data]="row" [adf-upload]="allowDropFiles && rowAllowsDrop(row)"
[ngStyle]="rowStyle" [adf-upload-data]="row"
[ngClass]="getRowStyle(row)" [ngStyle]="rowStyle"
(keyup)="onRowKeyUp(row, $event)"> [ngClass]="getRowStyle(row)"
[attr.data-automation-id]="'datatable-row-' + idx">
<!-- Actions (left) --> <!-- Actions (left) -->
<div *ngIf="actions && actionsPosition === 'left'" role="gridcell" class="adf-datatable-cell"> <div *ngIf="actions && actionsPosition === 'left'" role="gridcell" class="adf-datatable-cell">
<button mat-icon-button [matMenuTriggerFor]="menu" <button mat-icon-button [matMenuTriggerFor]="menu"
@@ -90,7 +91,7 @@
</div> </div>
<div *ngFor="let col of data.getColumns()" <div *ngFor="let col of data.getColumns()"
role="gridcell" role="gridcell"
class=" adf-datatable-cell adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}}" class="adf-datatable-cell adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}}"
[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 ? true : false" [attr.aria-selected]="row.isSelected ? true : false"
@@ -191,7 +192,7 @@
</ng-container> </ng-container>
</div> </div>
<div *ngIf="col.template" class="adf-datatable-cell-container"> <div *ngIf="col.template" class="adf-datatable-cell-container">
<div class="adf-cell-value" tabindex="0"> <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) }">
@@ -222,8 +223,7 @@
</button> </button>
</mat-menu> </mat-menu>
</div> </div>
</adf-datatable-row>
</div>
<div *ngIf="isEmpty()" <div *ngIf="isEmpty()"
role="row" role="row"
[class.adf-datatable-row]="display === 'list'" [class.adf-datatable-row]="display === 'list'"

View File

@@ -12,7 +12,7 @@
$data-table-cell-text-color: mat-color($foreground, text) !default; $data-table-cell-text-color: mat-color($foreground, text) !default;
$data-table-cell-link-color: mat-color($foreground, text) !default; $data-table-cell-link-color: mat-color($foreground, text) !default;
$data-table-cell-link-hover-color: mat-color($alfresco-ecm-blue, 500) !default; $data-table-cell-link-hover-color: mat-color($alfresco-ecm-blue, 500) !default;
$data-table-cell-outline: 1px solid mat-color($alfresco-ecm-blue, A200) !default; $data-table-outline: 1px solid mat-color($alfresco-ecm-blue, A200) !default;
$data-table-divider-color: mat-color($foreground, text, 0.07) !default; $data-table-divider-color: mat-color($foreground, text, 0.07) !default;
$data-table-hover-color: mat-color($background, 'hover') !default; $data-table-hover-color: mat-color($background, 'hover') !default;
$data-table-selection-color: mat-color($background, 'selected-button') !default; $data-table-selection-color: mat-color($background, 'selected-button') !default;
@@ -238,6 +238,11 @@
background-color: $data-table-hover-color; background-color: $data-table-hover-color;
} }
&:focus {
outline-offset: -1px;
outline: $data-table-outline;
}
&.adf-is-selected, &.adf-is-selected:hover { &.adf-is-selected, &.adf-is-selected:hover {
background-color: $data-table-selection-color; background-color: $data-table-selection-color;
} }
@@ -416,7 +421,7 @@
&:focus { &:focus {
outline-offset: -1px; outline-offset: -1px;
outline: $data-table-cell-outline; outline: $data-table-outline;
} }
} }

View File

@@ -373,6 +373,7 @@ describe('DataTable', () => {
const rows = dataTable.data.getRows(); const rows = dataTable.data.getRows();
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.rowClick.subscribe(() => { dataTable.rowClick.subscribe(() => {
expect(rows[0].isSelected).toBeFalsy(); expect(rows[0].isSelected).toBeFalsy();
@@ -395,6 +396,7 @@ describe('DataTable', () => {
const rows = dataTable.data.getRows(); const rows = dataTable.data.getRows();
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.rowClick.subscribe(() => { dataTable.rowClick.subscribe(() => {
expect(rows[0].isSelected).toBeFalsy(); expect(rows[0].isSelected).toBeFalsy();
@@ -462,6 +464,7 @@ describe('DataTable', () => {
); );
const rows = dataTable.data.getRows(); const rows = dataTable.data.getRows();
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.rowClick.subscribe(() => { dataTable.rowClick.subscribe(() => {
expect(rows[0].isSelected).toBeTruthy(); expect(rows[0].isSelected).toBeTruthy();
@@ -481,6 +484,7 @@ describe('DataTable', () => {
rows[0].isSelected = true; rows[0].isSelected = true;
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.rowClick.subscribe(() => { dataTable.rowClick.subscribe(() => {
expect(rows[0].isSelected).toBeFalsy(); expect(rows[0].isSelected).toBeFalsy();
done(); done();
@@ -509,6 +513,7 @@ describe('DataTable', () => {
}); });
dataTable.selection.push(rows[0]); dataTable.selection.push(rows[0]);
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.rowClick.subscribe(() => { dataTable.rowClick.subscribe(() => {
expect(rows[0].isSelected).toBeTruthy(); expect(rows[0].isSelected).toBeTruthy();
expect(rows[1].isSelected).toBeTruthy(); expect(rows[1].isSelected).toBeTruthy();
@@ -584,6 +589,7 @@ describe('DataTable', () => {
it('should emit row click event', (done) => { it('should emit row click event', (done) => {
const row = <DataRow> {}; const row = <DataRow> {};
dataTable.data = new ObjectDataTableAdapter([], []);
dataTable.rowClick.subscribe((e) => { dataTable.rowClick.subscribe((e) => {
expect(e.value).toBe(row); expect(e.value).toBe(row);
@@ -591,13 +597,16 @@ describe('DataTable', () => {
}); });
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.onRowClick(row, null); dataTable.onRowClick(row, null);
}); });
it('should emit double click if there are two single click in 250ms', (done) => { it('should emit double click if there are two single click in 250ms', (done) => {
const row = <DataRow> {}; const row = <DataRow> {};
dataTable.data = new ObjectDataTableAdapter([], []);
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.rowDblClick.subscribe(() => { dataTable.rowDblClick.subscribe(() => {
done(); done();
@@ -614,7 +623,9 @@ describe('DataTable', () => {
it('should emit double click if there are more than two single click in 250ms', (done) => { it('should emit double click if there are more than two single click in 250ms', (done) => {
const row = <DataRow> {}; const row = <DataRow> {};
dataTable.data = new ObjectDataTableAdapter([], []);
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.rowDblClick.subscribe(() => { dataTable.rowDblClick.subscribe(() => {
done(); done();
@@ -635,7 +646,9 @@ describe('DataTable', () => {
const row = <DataRow> {}; const row = <DataRow> {};
let clickCount = 0; let clickCount = 0;
dataTable.data = new ObjectDataTableAdapter([], []);
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.rowClick.subscribe(() => { dataTable.rowClick.subscribe(() => {
clickCount += 1; clickCount += 1;
@@ -653,6 +666,7 @@ describe('DataTable', () => {
it('should emit row-click dom event', (done) => { it('should emit row-click dom event', (done) => {
const row = <DataRow> {}; const row = <DataRow> {};
dataTable.data = new ObjectDataTableAdapter([], []);
fixture.nativeElement.addEventListener('row-click', (e) => { fixture.nativeElement.addEventListener('row-click', (e) => {
expect(e.detail.value).toBe(row); expect(e.detail.value).toBe(row);
@@ -660,17 +674,20 @@ describe('DataTable', () => {
}); });
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.onRowClick(row, null); dataTable.onRowClick(row, null);
}); });
it('should emit row-dblclick dom event', (done) => { it('should emit row-dblclick dom event', (done) => {
const row = <DataRow> {}; const row = <DataRow> {};
dataTable.data = new ObjectDataTableAdapter([], []);
fixture.nativeElement.addEventListener('row-dblclick', (e) => { fixture.nativeElement.addEventListener('row-dblclick', (e) => {
expect(e.detail.value).toBe(row); expect(e.detail.value).toBe(row);
done(); done();
}); });
dataTable.ngOnChanges({}); dataTable.ngOnChanges({});
fixture.detectChanges();
dataTable.onRowClick(row, null); dataTable.onRowClick(row, null);
dataTable.onRowClick(row, null); dataTable.onRowClick(row, null);
}); });
@@ -1177,4 +1194,64 @@ describe('Accesibility', () => {
expect(dataTable.getAriaSort(column)).toBe('ADF-DATATABLE.ACCESSIBILITY.SORT_DESCENDING'); expect(dataTable.getAriaSort(column)).toBe('ADF-DATATABLE.ACCESSIBILITY.SORT_DESCENDING');
}); });
}); });
it('should focus next row on ArrowDown event', () => {
const event = new KeyboardEvent('keyup', {
code: 'ArrowDown',
key: 'ArrowDown',
keyCode: 40
} as KeyboardEventInit );
const dataRows =
[ { name: 'test1'}, { name: 'test2' } ];
dataTable.data = new ObjectDataTableAdapter([],
[new ObjectDataColumn({ key: 'name' })]
);
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
dataTable.ngAfterViewInit();
const rowElement = document.querySelectorAll('.adf-datatable-body .adf-datatable-row')[0];
const rowCellElement = rowElement.querySelector('.adf-datatable-cell');
rowCellElement.dispatchEvent(new MouseEvent('click'));
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-1');
});
it('should focus previous row on ArrowUp event', () => {
const event = new KeyboardEvent('keyup', {
code: 'ArrowDown',
key: 'ArrowDown',
keyCode: 38
} as KeyboardEventInit );
const dataRows =
[ { name: 'test1'}, { name: 'test2' } ];
dataTable.data = new ObjectDataTableAdapter([],
[new ObjectDataColumn({ key: 'name' })]
);
dataTable.ngOnChanges({
rows: new SimpleChange(null, dataRows, false)
});
fixture.detectChanges();
dataTable.ngAfterViewInit();
const rowElement = document.querySelectorAll('.adf-datatable-body .adf-datatable-row')[1];
const rowCellElement = rowElement.querySelector('.adf-datatable-cell');
rowCellElement.dispatchEvent(new MouseEvent('click'));
fixture.debugElement.nativeElement.dispatchEvent(event);
expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-0');
});
}); });

View File

@@ -16,9 +16,11 @@
*/ */
import { import {
ViewChildren, QueryList, HostListener,
AfterContentInit, Component, ContentChild, DoCheck, ElementRef, EventEmitter, Input, AfterContentInit, Component, ContentChild, DoCheck, ElementRef, EventEmitter, Input,
IterableDiffers, OnChanges, Output, SimpleChange, SimpleChanges, TemplateRef, ViewEncapsulation, OnDestroy IterableDiffers, OnChanges, Output, SimpleChange, SimpleChanges, TemplateRef, ViewEncapsulation, OnDestroy
} from '@angular/core'; } from '@angular/core';
import { FocusKeyManager } from '@angular/cdk/a11y';
import { MatCheckboxChange } from '@angular/material'; import { MatCheckboxChange } from '@angular/material';
import { Subscription, Observable, Observer } from 'rxjs'; import { Subscription, Observable, Observer } from 'rxjs';
import { DataColumnListComponent } from '../../../data-column/data-column-list.component'; import { DataColumnListComponent } from '../../../data-column/data-column-list.component';
@@ -27,6 +29,7 @@ import { DataRowEvent } from '../../data/data-row-event.model';
import { DataRow } from '../../data/data-row.model'; import { DataRow } from '../../data/data-row.model';
import { DataSorting } from '../../data/data-sorting.model'; import { DataSorting } from '../../data/data-sorting.model';
import { DataTableAdapter } from '../../data/datatable-adapter'; import { DataTableAdapter } from '../../data/datatable-adapter';
import { DataTableRowComponent } from './datatable-row.component';
import { ObjectDataRow } from '../../data/object-datarow.model'; import { ObjectDataRow } from '../../data/object-datarow.model';
import { ObjectDataTableAdapter } from '../../data/object-datatable-adapter'; import { ObjectDataTableAdapter } from '../../data/object-datatable-adapter';
@@ -48,6 +51,9 @@ export enum DisplayMode {
}) })
export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck, OnDestroy { export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck, OnDestroy {
@ViewChildren(DataTableRowComponent)
rowsList: QueryList<DataTableRowComponent>;
@ContentChild(DataColumnListComponent) @ContentChild(DataColumnListComponent)
columnList: DataColumnListComponent; columnList: DataColumnListComponent;
@@ -178,6 +184,7 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
/** This array of fake rows fix the flex layout for the gallery view */ /** This array of fake rows fix the flex layout for the gallery view */
fakeRows = []; fakeRows = [];
private keyManager: FocusKeyManager<DataTableRowComponent>;
private clickObserver: Observer<DataRowEvent>; private clickObserver: Observer<DataRowEvent>;
private click$: Observable<DataRowEvent>; private click$: Observable<DataRowEvent>;
@@ -189,6 +196,11 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
private multiClickStreamSub: Subscription; private multiClickStreamSub: Subscription;
private dataRowsChanged: Subscription; private dataRowsChanged: Subscription;
@HostListener('keyup', ['$event'])
onKeydown(event: KeyboardEvent): void {
this.keyManager.onKeydown(event);
}
constructor(private elementRef: ElementRef, constructor(private elementRef: ElementRef,
differs: IterableDiffers) { differs: IterableDiffers) {
if (differs) { if (differs) {
@@ -210,6 +222,10 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
this.setTableSchema(); this.setTableSchema();
} }
ngAfterViewInit() {
this.keyManager = new FocusKeyManager(this.rowsList).withWrap();
}
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
this.initAndSubscribeClickStream(); this.initAndSubscribeClickStream();
if (this.isPropertyChanged(changes['data'])) { if (this.isPropertyChanged(changes['data'])) {
@@ -390,6 +406,7 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
} }
if (row) { if (row) {
this.keyManager.setActiveItem(this.data.getRows().indexOf(row));
const dataRowEvent = new DataRowEvent(row, mouseEvent, this); const dataRowEvent = new DataRowEvent(row, mouseEvent, this);
this.clickObserver.next(dataRowEvent); this.clickObserver.next(dataRowEvent);
} }

View File

@@ -22,11 +22,11 @@ import { AlfrescoApiService } from '../../../services/alfresco-api.service';
@Component({ @Component({
selector: 'adf-filesize-cell', selector: 'adf-filesize-cell',
template: ` template: `
<ng-container> <ng-container *ngIf="(value$ | async | adfFileSize) as fileSize">
<span <span
[title]="tooltip" [title]="tooltip"
[attr.aria-label]="value$ | async | adfFileSize" [attr.aria-label]="fileSize"
>{{ value$ | async | adfFileSize }}</span >{{ fileSize }}</span
> >
</ng-container> </ng-container>
`, `,

View File

@@ -26,6 +26,7 @@ import { PipeModule } from '../pipes/pipe.module';
import { DirectiveModule } from '../directives/directive.module'; import { DirectiveModule } from '../directives/directive.module';
import { DataTableCellComponent } from './components/datatable/datatable-cell.component'; import { DataTableCellComponent } from './components/datatable/datatable-cell.component';
import { DataTableRowComponent } from './components/datatable/datatable-row.component';
import { DataTableComponent } from './components/datatable/datatable.component'; import { DataTableComponent } from './components/datatable/datatable.component';
import { DateCellComponent } from './components/datatable/date-cell.component'; import { DateCellComponent } from './components/datatable/date-cell.component';
import { EmptyListBodyDirective, import { EmptyListBodyDirective,
@@ -62,6 +63,7 @@ import { DropZoneDirective } from './components/datatable/drop-zone.directive';
EmptyListBodyDirective, EmptyListBodyDirective,
EmptyListFooterDirective, EmptyListFooterDirective,
DataTableCellComponent, DataTableCellComponent,
DataTableRowComponent,
DateCellComponent, DateCellComponent,
FileSizeCellComponent, FileSizeCellComponent,
LocationCellComponent, LocationCellComponent,
@@ -81,6 +83,7 @@ import { DropZoneDirective } from './components/datatable/drop-zone.directive';
EmptyListBodyDirective, EmptyListBodyDirective,
EmptyListFooterDirective, EmptyListFooterDirective,
DataTableCellComponent, DataTableCellComponent,
DataTableRowComponent,
DateCellComponent, DateCellComponent,
FileSizeCellComponent, FileSizeCellComponent,
LocationCellComponent, LocationCellComponent,

View File

@@ -30,6 +30,7 @@ export * from './components/datatable/data-cell.event';
export * from './components/datatable/data-row-action.event'; export * from './components/datatable/data-row-action.event';
export * from './components/datatable/drop-zone.directive'; export * from './components/datatable/drop-zone.directive';
export * from './components/datatable/datatable-cell.component'; export * from './components/datatable/datatable-cell.component';
export * from './components/datatable/datatable-row.component';
export * from './components/datatable/datatable.component'; export * from './components/datatable/datatable.component';
export * from './components/datatable/date-cell.component'; export * from './components/datatable/date-cell.component';
export * from './components/datatable/empty-list.component'; export * from './components/datatable/empty-list.component';

View File

@@ -25,7 +25,7 @@ export class DataTableComponentPage {
list: ElementArrayFinder; list: ElementArrayFinder;
contents: ElementArrayFinder; contents: ElementArrayFinder;
tableBody: ElementFinder; tableBody: ElementFinder;
rows: Locator = by.css(`adf-datatable div[class*='adf-datatable-body'] div[class*='adf-datatable-row']`); rows: Locator = by.css(`adf-datatable div[class*='adf-datatable-body'] adf-datatable-row[class*='adf-datatable-row']`);
allColumns: ElementArrayFinder; allColumns: ElementArrayFinder;
selectedRowNumber: ElementFinder; selectedRowNumber: ElementFinder;
allSelectedRows: ElementArrayFinder; allSelectedRows: ElementArrayFinder;
@@ -34,12 +34,12 @@ export class DataTableComponentPage {
constructor(rootElement: ElementFinder = element.all(by.css('adf-datatable')).first()) { constructor(rootElement: ElementFinder = element.all(by.css('adf-datatable')).first()) {
this.rootElement = rootElement; this.rootElement = rootElement;
this.list = this.rootElement.all(by.css(`div[class*='adf-datatable-body'] div[class*='adf-datatable-row']`)); this.list = this.rootElement.all(by.css(`div[class*='adf-datatable-body'] adf-datatable-row[class*='adf-datatable-row']`));
this.contents = this.rootElement.all(by.css('div[class="adf-datatable-body"] span')); this.contents = this.rootElement.all(by.css('div[class="adf-datatable-body"] span'));
this.tableBody = this.rootElement.all(by.css(`div[class='adf-datatable-body']`)).first(); this.tableBody = this.rootElement.all(by.css(`div[class='adf-datatable-body']`)).first();
this.allColumns = this.rootElement.all(by.css('div[data-automation-id*="auto_id_entry."]')); this.allColumns = this.rootElement.all(by.css('div[data-automation-id*="auto_id_entry."]'));
this.selectedRowNumber = this.rootElement.element(by.css(`div[class*='is-selected'] div[data-automation-id*='text_']`)); this.selectedRowNumber = this.rootElement.element(by.css(`adf-datatable-row[class*='is-selected'] div[data-automation-id*='text_']`));
this.allSelectedRows = this.rootElement.all(by.css(`div[class*='is-selected']`)); this.allSelectedRows = this.rootElement.all(by.css(`adf-datatable-row[class*='is-selected']`));
this.selectAll = this.rootElement.element(by.css(`div[class*='adf-datatable-header'] mat-checkbox`)); this.selectAll = this.rootElement.element(by.css(`div[class*='adf-datatable-header'] mat-checkbox`));
this.copyColumnTooltip = this.rootElement.element(by.css(`adf-copy-content-tooltip span`)); this.copyColumnTooltip = this.rootElement.element(by.css(`adf-copy-content-tooltip span`));
} }
@@ -97,12 +97,12 @@ export class DataTableComponentPage {
} }
async checkRowIsSelected(columnName, columnValue): Promise<void> { async checkRowIsSelected(columnName, columnValue): Promise<void> {
const selectedRow = this.getCellElementByValue(columnName, columnValue).element(by.xpath(`ancestor::div[contains(@class, 'is-selected')]`)); const selectedRow = this.getCellElementByValue(columnName, columnValue).element(by.xpath(`ancestor::adf-datatable-row[contains(@class, 'is-selected')]`));
await BrowserVisibility.waitUntilElementIsVisible(selectedRow); await BrowserVisibility.waitUntilElementIsVisible(selectedRow);
} }
async checkRowIsNotSelected(columnName, columnValue): Promise<void> { async checkRowIsNotSelected(columnName, columnValue): Promise<void> {
const selectedRow = this.getCellElementByValue(columnName, columnValue).element(by.xpath(`ancestor::div[contains(@class, 'is-selected')]`)); const selectedRow = this.getCellElementByValue(columnName, columnValue).element(by.xpath(`ancestor::adf-datatable-row[contains(@class, 'is-selected')]`));
await BrowserVisibility.waitUntilElementIsNotVisible(selectedRow); await BrowserVisibility.waitUntilElementIsNotVisible(selectedRow);
} }
@@ -164,7 +164,7 @@ export class DataTableComponentPage {
} }
async getAllRowsColumnValues(column: string) { async getAllRowsColumnValues(column: string) {
const columnLocator = by.css("adf-datatable div[class*='adf-datatable-body'] div[class*='adf-datatable-row'] div[title='" + column + "'] span"); const columnLocator = by.css("adf-datatable div[class*='adf-datatable-body'] adf-datatable-row[class*='adf-datatable-row'] div[title='" + column + "'] span");
await BrowserVisibility.waitUntilElementIsPresent(element.all(columnLocator).first()); await BrowserVisibility.waitUntilElementIsPresent(element.all(columnLocator).first());
return await element.all(columnLocator) return await element.all(columnLocator)
.filter(async (el) => await el.isPresent()) .filter(async (el) => await el.isPresent())
@@ -232,11 +232,11 @@ export class DataTableComponentPage {
} }
getRow(columnName: string, columnValue: string): ElementFinder { getRow(columnName: string, columnValue: string): ElementFinder {
return this.rootElement.all(by.xpath(`//div[@title="${columnName}"]//div[@data-automation-id="text_${columnValue}"]//ancestor::div[contains(@class, 'adf-datatable-row')]`)).first(); return this.rootElement.all(by.xpath(`//div[@title="${columnName}"]//div[@data-automation-id="text_${columnValue}"]//ancestor::adf-datatable-row[contains(@class, 'adf-datatable-row')]`)).first();
} }
getRowByIndex(index: number): ElementFinder { getRowByIndex(index: number): ElementFinder {
return this.rootElement.element(by.xpath(`//div[contains(@class,'adf-datatable-body')]//div[contains(@class,'adf-datatable-row')][${index}]`)); return this.rootElement.element(by.xpath(`//div[contains(@class,'adf-datatable-body')]//adf-datatable-row[contains(@class,'adf-datatable-row')][${index}]`));
} }
async contentInPosition(position: number): Promise<string> { async contentInPosition(position: number): Promise<string> {
@@ -282,21 +282,21 @@ export class DataTableComponentPage {
} }
async checkRowByContentIsSelected(folderName): Promise<void> { async checkRowByContentIsSelected(folderName): Promise<void> {
const selectedRow = this.getCellByContent(folderName).element(by.xpath(`ancestor::div[contains(@class, 'is-selected')]`)); const selectedRow = this.getCellByContent(folderName).element(by.xpath(`ancestor::adf-datatable-row[contains(@class, 'is-selected')]`));
await BrowserVisibility.waitUntilElementIsVisible(selectedRow); await BrowserVisibility.waitUntilElementIsVisible(selectedRow);
} }
async checkRowByContentIsNotSelected(folderName): Promise<void> { async checkRowByContentIsNotSelected(folderName): Promise<void> {
const selectedRow = this.getCellByContent(folderName).element(by.xpath(`ancestor::div[contains(@class, 'is-selected')]`)); const selectedRow = this.getCellByContent(folderName).element(by.xpath(`ancestor::adf-datatable-row[contains(@class, 'is-selected')]`));
await BrowserVisibility.waitUntilElementIsNotVisible(selectedRow); await BrowserVisibility.waitUntilElementIsNotVisible(selectedRow);
} }
getCellByContent(content) { getCellByContent(content) {
return this.rootElement.all(by.cssContainingText(`div[class*='adf-datatable-row'] div[class*='adf-datatable-cell']`, content)).first(); return this.rootElement.all(by.cssContainingText(`adf-datatable-row[class*='adf-datatable-row'] div[class*='adf-datatable-cell']`, content)).first();
} }
async checkCellByHighlightContent(content) { async checkCellByHighlightContent(content) {
const cell = this.rootElement.element(by.cssContainingText(`div[class*='adf-datatable-row'] div[class*='adf-name-location-cell-name'] span.adf-highlight`, content)); const cell = this.rootElement.element(by.cssContainingText(`adf-datatable-row[class*='adf-datatable-row'] div[class*='adf-name-location-cell-name'] span.adf-highlight`, content));
await BrowserVisibility.waitUntilElementIsVisible(cell); await BrowserVisibility.waitUntilElementIsVisible(cell);
} }
@@ -306,7 +306,7 @@ export class DataTableComponentPage {
} }
async clickRowByContentCheckbox(name: string): Promise<void> { async clickRowByContentCheckbox(name: string): Promise<void> {
const resultElement = this.rootElement.all(by.css(`div[data-automation-id='${name}']`)).first().element(by.xpath(`ancestor::div/div/mat-checkbox`)); const resultElement = this.rootElement.all(by.css(`div[data-automation-id='${name}']`)).first().element(by.xpath(`ancestor::adf-datatable-row/div/mat-checkbox`));
await browser.actions().mouseMove(resultElement); await browser.actions().mouseMove(resultElement);
await BrowserActions.click(resultElement); await BrowserActions.click(resultElement);
} }