Skip to content

Commit

Permalink
TreeTable | Accessibility Enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
cetincakiroglu committed Nov 20, 2023
1 parent 92d4148 commit 667ad7a
Showing 1 changed file with 201 additions and 63 deletions.
264 changes: 201 additions & 63 deletions src/app/components/treetable/treetable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,13 @@ export class TreeTableService {
</p-paginator>
<div class="p-treetable-wrapper" *ngIf="!scrollable">
<table #table [ngClass]="tableStyleClass" [ngStyle]="tableStyle">
<table role="table" #table [ngClass]="tableStyleClass" [ngStyle]="tableStyle">
<ng-container *ngTemplateOutlet="colGroupTemplate; context: { $implicit: columns }"></ng-container>
<thead class="p-treetable-thead">
<thead role="rowgroup" class="p-treetable-thead">
<ng-container *ngTemplateOutlet="headerTemplate; context: { $implicit: columns }"></ng-container>
</thead>
<tbody class="p-treetable-tbody" [pTreeTableBody]="columns" [pTreeTableBodyTemplate]="bodyTemplate"></tbody>
<tfoot class="p-treetable-tfoot">
<tbody class="p-treetable-tbody" role="rowgroup" [pTreeTableBody]="columns" [pTreeTableBodyTemplate]="bodyTemplate"></tbody>
<tfoot class="p-treetable-tfoot" role="rowgroup">
<ng-container *ngTemplateOutlet="footerTemplate; context: { $implicit: columns }"></ng-container>
</tfoot>
</table>
Expand Down Expand Up @@ -2169,7 +2169,7 @@ export class TTBody {
<div #scrollHeaderBox class="p-treetable-scrollable-header-box">
<table class="p-treetable-scrollable-header-table" [ngClass]="tt.tableStyleClass" [ngStyle]="tt.tableStyle">
<ng-container *ngTemplateOutlet="frozen ? tt.frozenColGroupTemplate || tt.colGroupTemplate : tt.colGroupTemplate; context: { $implicit: columns }"></ng-container>
<thead class="p-treetable-thead">
<thead role="rowgroup" class="p-treetable-thead">
<ng-container *ngTemplateOutlet="frozen ? tt.frozenHeaderTemplate || tt.headerTemplate : tt.headerTemplate; context: { $implicit: columns }"></ng-container>
</thead>
</table>
Expand Down Expand Up @@ -2204,9 +2204,9 @@ export class TTBody {
</ng-container>
<ng-template #buildInItems let-items let-scrollerOptions="options">
<table #scrollTable [class]="tt.tableStyleClass" [ngClass]="scrollerOptions.contentStyleClass" [ngStyle]="tt.tableStyle" [style]="scrollerOptions.contentStyle">
<table role="table" #scrollTable [class]="tt.tableStyleClass" [ngClass]="scrollerOptions.contentStyleClass" [ngStyle]="tt.tableStyle" [style]="scrollerOptions.contentStyle">
<ng-container *ngTemplateOutlet="frozen ? tt.frozenColGroupTemplate || tt.colGroupTemplate : tt.colGroupTemplate; context: { $implicit: columns }"></ng-container>
<tbody class="p-treetable-tbody" [pTreeTableBody]="columns" [pTreeTableBodyTemplate]="frozen ? tt.frozenBodyTemplate || tt.bodyTemplate : tt.bodyTemplate" [serializedNodes]="items" [frozen]="frozen"></tbody>
<tbody role="rowgroup" class="p-treetable-tbody" [pTreeTableBody]="columns" [pTreeTableBodyTemplate]="frozen ? tt.frozenBodyTemplate || tt.bodyTemplate : tt.bodyTemplate" [serializedNodes]="items" [frozen]="frozen"></tbody>
</table>
<div #scrollableAligner style="background-color:transparent" *ngIf="frozen"></div>
</ng-template>
Expand All @@ -2215,7 +2215,7 @@ export class TTBody {
<div #scrollFooterBox class="p-treetable-scrollable-footer-box">
<table class="p-treetable-scrollable-footer-table" [ngClass]="tt.tableStyleClass" [ngStyle]="tt.tableStyle">
<ng-container *ngTemplateOutlet="frozen ? tt.frozenColGroupTemplate || tt.colGroupTemplate : tt.colGroupTemplate; context: { $implicit: columns }"></ng-container>
<tfoot class="p-treetable-tfoot">
<tfoot role="rowgroup" class="p-treetable-tfoot">
<ng-container *ngTemplateOutlet="frozen ? tt.frozenFooterTemplate || tt.footerTemplate : tt.footerTemplate; context: { $implicit: columns }"></ng-container>
</tfoot>
</table>
Expand Down Expand Up @@ -2432,7 +2432,8 @@ export class TTScrollableView implements AfterViewInit, OnDestroy {
'[class.p-sortable-column]': 'isEnabled()',
'[class.p-highlight]': 'sorted',
'[attr.tabindex]': 'isEnabled() ? "0" : null',
'[attr.role]': '"columnheader"'
'[attr.role]': '"columnheader"',
'[attr.aria-sort]': 'ariaSorted'
}
})
export class TTSortableColumn implements OnInit, OnDestroy {
Expand All @@ -2444,6 +2445,12 @@ export class TTSortableColumn implements OnInit, OnDestroy {

subscription: Subscription | undefined;

get ariaSorted() {
if(this.sorted && this.tt.sortOrder < 0) return 'descending';
else if (this.sorted && this.tt.sortOrder > 0) return 'ascending';
else return 'none';
}

constructor(public tt: TreeTable) {
if (this.isEnabled()) {
this.subscription = this.tt.tableService.sortSource$.subscribe((sortMeta) => {
Expand Down Expand Up @@ -2734,7 +2741,9 @@ export class TTReorderableColumn implements AfterViewInit, OnDestroy {
selector: '[ttSelectableRow]',
host: {
class: 'p-element',
'[class.p-highlight]': 'selected'
'[class.p-highlight]': 'selected',
'[attr.data-p-highlight]': 'selected',
'[attr.aria-checked]': 'selected'
}
})
export class TTSelectableRow implements OnInit, OnDestroy {
Expand Down Expand Up @@ -2771,11 +2780,17 @@ export class TTSelectableRow implements OnInit, OnDestroy {
}

@HostListener('keydown', ['$event'])
onEnterKey(event: KeyboardEvent) {
if (event.which === 13) {
this.onClick(event);
onKeyDown(event: KeyboardEvent) {
switch(event.code){
case 'Enter':
case 'Space':
this.onEnterKey(event);
break;

default:
break;
}
}
}

@HostListener('touchend', ['$event'])
onTouchEnd(event: Event) {
Expand All @@ -2784,6 +2799,18 @@ export class TTSelectableRow implements OnInit, OnDestroy {
}
}

onEnterKey(event) {
if(this.tt.selectionMode === 'checkbox') {
this.tt.toggleNodeWithCheckbox({
originalEvent: event,
rowNode: this.rowNode
});
} else {
this.onClick(event);
}
event.preventDefault();
}

isEnabled() {
return this.ttSelectableRowDisabled !== true;
}
Expand Down Expand Up @@ -2901,7 +2928,7 @@ export class TTContextMenuRow {
template: `
<div class="p-checkbox p-component" [ngClass]="{ 'p-checkbox-focused': focused }" (click)="onClick($event)">
<div class="p-hidden-accessible">
<input type="checkbox" [checked]="checked" (focus)="onFocus()" (blur)="onBlur()" />
<input type="checkbox" [checked]="checked" (focus)="onFocus()" (blur)="onBlur()" tabindex="-1"/>
</div>
<div #box [ngClass]="{ 'p-checkbox-box': true, 'p-highlight': checked, 'p-focus': focused, 'p-indeterminate': rowNode.node.partialSelected, 'p-disabled': disabled }" role="checkbox" [attr.aria-checked]="checked">
<ng-container *ngIf="!tt.checkboxIconTemplate">
Expand Down Expand Up @@ -3068,7 +3095,7 @@ export class TTHeaderCheckbox {
@Directive({
selector: '[ttEditableColumn]',
host: {
class: 'p-element'
class: 'p-element',
}
})
export class TTEditableColumn implements AfterViewInit {
Expand Down Expand Up @@ -3281,78 +3308,187 @@ export class TreeTableCellEditor implements AfterContentInit {
selector: '[ttRow]',
host: {
class: 'p-element',
'[attr.tabindex]': '"0"'
'[attr.tabindex]': "'0'",
'[attr.aria-expanded]': 'expanded',
'[attr.aria-level]': 'level',
'[attr.data-pc-section]': 'row',
'[attr.role]': 'row'
}
})
export class TTRow {

get level() {
return this.rowNode?.['level'] + 1;
}

get expanded() {
return this.rowNode?.node['expanded'];
}

@Input('ttRow') rowNode: any;

constructor(public tt: TreeTable, public el: ElementRef, public zone: NgZone) {}

@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
switch (event.which) {
//down arrow
case 40:
let nextRow = this.el.nativeElement.nextElementSibling;
if (nextRow) {
nextRow.focus();
}
switch (event.code) {
case 'ArrowDown':
this.onArrowDownKey(event);
break;

event.preventDefault();
case 'ArrowUp':
this.onArrowUpKey(event);
break;

//down arrow
case 38:
let prevRow = this.el.nativeElement.previousElementSibling;
if (prevRow) {
prevRow.focus();
}
case 'ArrowRight':
this.onArrowRightKey(event);
break;

event.preventDefault();
case 'ArrowLeft':
this.onArrowLeftKey(event);
break;

//left arrow
case 37:
if (this.rowNode.node.expanded) {
this.tt.toggleRowIndex = DomHandler.index(this.el.nativeElement);
this.rowNode.node.expanded = false;
case 'Tab':
this.onTabKey(event);
break;

this.tt.onNodeCollapse.emit({
originalEvent: event,
node: this.rowNode.node
});
case 'Home':
this.onHomeKey(event);
break;

this.tt.updateSerializedValue();
this.tt.tableService.onUIUpdate(this.tt.value);
this.restoreFocus();
}
case 'End':
this.onEndKey(event);
break;

//right arrow
case 39:
if (!this.rowNode.node.expanded) {
this.tt.toggleRowIndex = DomHandler.index(this.el.nativeElement);
this.rowNode.node.expanded = true;
default:
break;
}
}

this.tt.onNodeExpand.emit({
originalEvent: event,
node: this.rowNode.node
});
onArrowDownKey(event: KeyboardEvent) {
let nextRow = this.el?.nativeElement?.nextElementSibling;
if (nextRow) {
this.focusRowChange(<HTMLElement>event.currentTarget, nextRow);
}

this.tt.updateSerializedValue();
this.tt.tableService.onUIUpdate(this.tt.value);
this.restoreFocus();
}
break;
event.preventDefault();
}

onArrowUpKey(event: KeyboardEvent) {
let prevRow = this.el?.nativeElement?.previousElementSibling;
if (prevRow) {
this.focusRowChange(<HTMLElement>event.currentTarget, prevRow);
}

event.preventDefault();
}

onArrowRightKey(event: KeyboardEvent) {
const currentTarget = <HTMLElement>event.currentTarget;
const isHiddenIcon = DomHandler.findSingle(currentTarget, 'button').style.visibility === 'hidden';

if (!isHiddenIcon && !this.expanded && this.rowNode.node['children']) {
this.expand(event);

currentTarget.tabIndex = -1;
}
event.preventDefault();
}

restoreFocus() {
onArrowLeftKey(event: KeyboardEvent) {
const container = this.tt.containerViewChild?.nativeElement;
const expandedRows = DomHandler.find(container, '[aria-expanded="true"]');
const lastExpandedRow = expandedRows[expandedRows.length - 1];

if (this.expanded) {
this.collapse(event);
}
if (lastExpandedRow) {
this.tt.toggleRowIndex = DomHandler.index(lastExpandedRow);
}
this.restoreFocus();
event.preventDefault();
}

onHomeKey(event: KeyboardEvent) {
const firstElement = DomHandler.findSingle(this.tt.containerViewChild?.nativeElement, `tr[aria-level="${this.level}"]`);
firstElement && DomHandler.focus(firstElement);
event.preventDefault();
}

onEndKey(event: KeyboardEvent) {
const nodes = DomHandler.find(this.tt.containerViewChild?.nativeElement, `tr[aria-level="${this.level}"]`);
const lastElement = nodes[nodes.length - 1];
DomHandler.focus(lastElement);
event.preventDefault();
}

onTabKey(event: KeyboardEvent) {
const rows = this.el.nativeElement ? [...DomHandler.find(this.el.nativeElement.parentNode, 'tr')] : undefined;

if (rows && ObjectUtils.isNotEmpty(rows)) {
const hasSelectedRow = rows.some((row) => DomHandler.getAttribute(row, 'data-p-highlight') || row.getAttribute('aria-checked') === 'true');
rows.forEach((row) => {
row.tabIndex = -1;
});

if (hasSelectedRow) {
const selectedNodes = rows.filter((node) => DomHandler.getAttribute(node, 'data-p-highlight') || node.getAttribute('aria-checked') === 'true');
selectedNodes[0].tabIndex = 0;

return;
}

rows[0].tabIndex = 0;
}
}

expand(event: Event) {
this.tt.toggleRowIndex = DomHandler.index(this.el.nativeElement);
this.rowNode.node['expanded'] = true;

this.tt.updateSerializedValue();
this.tt.tableService.onUIUpdate(this.tt.value);
this.rowNode.node['children'] ? this.restoreFocus(this.tt.toggleRowIndex + 1) : this.restoreFocus();

this.tt.onNodeExpand.emit({
originalEvent: event,
node: this.rowNode.node
});
}

collapse(event: Event) {
this.rowNode.node['expanded'] = false;

this.tt.updateSerializedValue();
this.tt.tableService.onUIUpdate(this.tt.value);

this.tt.onNodeCollapse.emit({ originalEvent: event, node: this.rowNode.node });
}

focusRowChange(firstFocusableRow, currentFocusedRow, lastVisibleDescendant?) {
firstFocusableRow.tabIndex = '-1';
currentFocusedRow.tabIndex = '0';

DomHandler.focus(currentFocusedRow);
}

restoreFocus(index?) {
this.zone.runOutsideAngular(() => {
setTimeout(() => {
let row = DomHandler.findSingle(this.tt.containerViewChild?.nativeElement, '.p-treetable-tbody').children[<number>this.tt.toggleRowIndex];
const container = this.tt.containerViewChild?.nativeElement;
const row = DomHandler.findSingle(container, '.p-treetable-tbody').children[<number>index || this.tt.toggleRowIndex];
const rows = [...DomHandler.find(container, 'tr')];

rows &&
rows.forEach((r) => {
if (!row.isSameNode(r)) {
r.tabIndex = -1;
}
});

if (row) {
row.tabIndex = 0;
row.focus();
}
}, 25);
Expand All @@ -3371,10 +3507,12 @@ export class TTRow {
pRipple
[style.visibility]="rowNode.node.leaf === false || (rowNode.node.children && rowNode.node.children.length) ? 'visible' : 'hidden'"
[style.marginLeft]="rowNode.level * 16 + 'px'"
[attr.data-pc-section]="'rowtoggler'"
[attr.data-pc-group-section]="'rowactionbutton'"
>
<ng-container *ngIf="!tt.togglerIconTemplate">
<ChevronDownIcon *ngIf="rowNode.node.expanded" />
<ChevronRightIcon *ngIf="!rowNode.node.expanded" />
<ChevronDownIcon *ngIf="rowNode.node.expanded" [attr.aria-hidden]="true"/>
<ChevronRightIcon *ngIf="!rowNode.node.expanded" [attr.aria-hidden]="true"/>
</ng-container>
<ng-template *ngTemplateOutlet="tt.togglerIconTemplate; context: { $implicit: rowNode.node.expanded }"></ng-template>
</button>
Expand Down

0 comments on commit 667ad7a

Please sign in to comment.