Skip to content

Commit

Permalink
va-table: update va-table-inner to properly scope table header row to…
Browse files Browse the repository at this point in the history
… columns (#1370)

* Added logic to scope VA-table-inner columns correctly.
* Added e2e tests to verify correct TH scopes.
* Refactored TH scope logic for better readability.
  • Loading branch information
1Copenut authored Oct 24, 2024
1 parent 714e11c commit 1d88710
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,25 @@ describe('va-table', () => {
await page.setContent(makeTable());
const caption = await page.find('va-table-inner >>> caption');
expect(caption.innerHTML).toEqual('this is a caption');
})
});

it('renders a table with the proper number of rows and columns', async () => {
const page = await newE2EPage();
await page.setContent(makeTable());

const table = await page.find('va-table-inner');
expect(table.getAttribute('rows')).toEqual("2");
expect(table.getAttribute('cols',)).toEqual("3");
expect(table.getAttribute('rows')).toEqual('2');
expect(table.getAttribute('cols')).toEqual('3');
});

it('sets the proper scope attributes for rows and columns', async () => {
const page = await newE2EPage();
await page.setContent(makeTable());

const columnHeader = await page.find('va-table-inner >>> thead >>> th');
const rowHeader = await page.find('va-table-inner >>> tbody >>> th');
expect(columnHeader.getAttribute('scope')).toEqual('col');
expect(rowHeader.getAttribute('scope')).toEqual('row');
});
});

Expand Down Expand Up @@ -225,13 +235,18 @@ describe('sorted va-table ', () => {
delectus explicabo
</span>
</va-table-row>
</va-table>`
};
</va-table>`;
}

// due to content in slots being hard to access in tests,
// due to content in slots being hard to access in tests,
// the va-table-rows are given ids (which don't change during sort),
// then after each sort we check that the ids are in the correct order for the sort
async function checkSorts(page: E2EPage, index: number, asc: string, desc: string) {
async function checkSorts(
page: E2EPage,
index: number,
asc: string,
desc: string,
) {
// we need to update these variables after each sort
let table = null;
let rows = null;
Expand Down Expand Up @@ -376,4 +391,4 @@ describe('sort utilities', () => {
const func3 = _getCompareFunc('October 31, 1899', '');
expect(func3.name).toEqual('datesSort');
});
})
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Component, Element, Prop, h, Event, EventEmitter, State } from '@stencil/core';
import {
Component,
Element,
Prop,
h,
Event,
EventEmitter,
State,
} from '@stencil/core';
import classnames from 'classnames';

/**
Expand Down Expand Up @@ -28,8 +36,8 @@ export class VaTableInner {
@Prop() tableTitle: string;

/*
* The number of rows in the table
*/
* The number of rows in the table
*/
@Prop() rows?: number;

/**
Expand Down Expand Up @@ -62,11 +70,11 @@ export class VaTableInner {
*/
@State() sortindex?: number = null;

/**
/**
* Fires when the component is closed by clicking on the close icon. This fires only
* when closeable is true.
*/
@Event({
@Event({
composed: true,
bubbles: true,
})
Expand All @@ -85,7 +93,7 @@ export class VaTableInner {
const th = target.closest('th');
const sortdir = th.dataset.sortdir;
const index = th.dataset.rowindex;
this.sortTable.emit({ index, sortdir });
this.sortTable.emit({ index, sortdir });
}

handleKeyDown(e: KeyboardEvent) {
Expand All @@ -100,21 +108,22 @@ export class VaTableInner {
if (this.sortable && row === 0) {
// we just performed a sort on this column
if (this.sortindex !== null && this.sortindex === index) {
const icon_name = this.sortdir == 'descending' ? 'arrow_upward' : 'arrow_downward';
icon = <va-icon icon={icon_name} size={3} />
// we did not just perform a sort on this column
const icon_name =
this.sortdir == 'descending' ? 'arrow_upward' : 'arrow_downward';
icon = <va-icon icon={icon_name} size={3} />;
// we did not just perform a sort on this column
} else {
icon = <va-icon icon="sort_arrow" size={3} />
icon = <va-icon icon="sort_arrow" size={3} />;
}
return (
<button
tabIndex={0}
onClick={(e) => this.fireSort(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
onClick={e => this.fireSort(e)}
onKeyDown={e => this.handleKeyDown(e)}
>
{icon}
</button>
)
);
} else {
return null;
}
Expand All @@ -124,32 +133,38 @@ export class VaTableInner {
* Generate the markup for a table row where row is the zero-indexed row number
*/
makeRow(row: number): HTMLTableRowElement {
// Ensure first row <th> are col scoped for screen reader usability
let scopeDimension = (row === 0 && 'col') || 'row';

return (
<tr>
{Array.from({ length: this.cols }).map((_, i) => {
const slotName = `va-table-slot-${row * this.cols + i}`;
const slot = <slot name={slotName}></slot>;
const header = this.el.querySelector(`[slot="va-table-slot-${i}"]`).innerHTML;
const header = this.el.querySelector(
`[slot="va-table-slot-${i}"]`,
).innerHTML;
const dataSortActive = row > 0 && this.sortindex === i ? true : false;
return (i === 0 || row === 0)
?
return i === 0 || row === 0 ? (
<th
scope="row"
scope={scopeDimension}
data-sortable
data-sort-active={dataSortActive}
data-label={header}
data-rowindex={i}
data-sortdir={i === this.sortindex ? this.sortdir : 'ascending'}
>
{slot}{this.getSortIcon(i, row)}
{slot}
{this.getSortIcon(i, row)}
</th>
:
) : (
<td data-label={header} data-sort-active={dataSortActive}>
{slot}
</td>
);
})}
</tr>
)
);
}

/**
Expand All @@ -158,7 +173,7 @@ export class VaTableInner {
getBodyRows(): HTMLTableRowElement[] {
const rows = [];
for (let i = 1; i < this.rows; i++) {
rows.push(this.makeRow(i))
rows.push(this.makeRow(i));
}
return rows;
}
Expand All @@ -177,7 +192,12 @@ export class VaTableInner {

// only runs if sortable is true
// update the aria-label for a th after a sort
updateThAriaLabel(th: HTMLTableCellElement, thSorted: boolean, currentSortDirection: string, content: string) {
updateThAriaLabel(
th: HTMLTableCellElement,
thSorted: boolean,
currentSortDirection: string,
content: string,
) {
let thSortInfo: string;
if (thSorted) {
thSortInfo = `currently sorted ${currentSortDirection}`;
Expand All @@ -191,7 +211,12 @@ export class VaTableInner {

// only runs if sortable is true
// update the title info on span clicked to sort and focus it
updateSpan(th: HTMLTableCellElement, thSorted: boolean, nextSortDirection: string, content: string) {
updateSpan(
th: HTMLTableCellElement,
thSorted: boolean,
nextSortDirection: string,
content: string,
) {
const button = th.querySelector('button');
let spanSortInfo = `Click to sort by ${content} in ${nextSortDirection} order`;
button.setAttribute('title', spanSortInfo);
Expand All @@ -204,26 +229,42 @@ export class VaTableInner {

// only runs if sortable is true
// if a sort has occurred update the sr text to reflect this
updateSRtext(thSorted: boolean, content: string, currentSortDirection: string) {
updateSRtext(
thSorted: boolean,
content: string,
currentSortDirection: string,
) {
if (thSorted) {
const tableInfo = !!this.tableTitle ? `The table named "${this.tableTitle}"` : 'This table';
this.el.shadowRoot.querySelector('table + div').innerHTML = `${tableInfo} is now sorted by ${content} in ${currentSortDirection} order`;
const tableInfo = !!this.tableTitle
? `The table named "${this.tableTitle}"`
: 'This table';
this.el.shadowRoot.querySelector(
'table + div',
).innerHTML = `${tableInfo} is now sorted by ${content} in ${currentSortDirection} order`;
}
}

// only runs if sortable is true
// for a given th, get the current and the next sort directions
getSortDirections(): {currentSortDirection: string, nextSortDirection: string} {
getSortDirections(): {
currentSortDirection: string;
nextSortDirection: string;
} {
const nextSortDirection = !!this.sortdir ? this.sortdir : 'ascending';
const currentSortDirection = nextSortDirection === 'ascending' ? 'descending' : 'ascending';
return { currentSortDirection, nextSortDirection }
const currentSortDirection =
nextSortDirection === 'ascending' ? 'descending' : 'ascending';
return { currentSortDirection, nextSortDirection };
}

// for a sortable table set the state to take into account previous sort
componentWillLoad() {
if (this.sortable) {
this.sortindex = this.el.dataset.sortindex ? +this.el.dataset.sortindex : this.sortindex;
this.sortdir = this.el.dataset.sortdir ? this.el.dataset.sortdir : this.sortdir;
this.sortindex = this.el.dataset.sortindex
? +this.el.dataset.sortindex
: this.sortindex;
this.sortdir = this.el.dataset.sortdir
? this.el.dataset.sortdir
: this.sortdir;
}
}

Expand All @@ -237,27 +278,30 @@ export class VaTableInner {
if (this.sortable) {
const slots = this.el.shadowRoot.querySelectorAll('slot');
// loop through slots inside the table header ths
Array.from(slots).slice(0, this.cols).forEach((slot, i) => {
const th = slot.closest('th');
Array.from(slots)
.slice(0, this.cols)
.forEach((slot, i) => {
const th = slot.closest('th');

// was this th just sorted
const thSorted = this.sortindex !== null && this.sortindex === i;
// was this th just sorted
const thSorted = this.sortindex !== null && this.sortindex === i;

// text content of this th
const content = this.getSortColumnText(slot);
// text content of this th
const content = this.getSortColumnText(slot);

// get the sort directions of this th
const { currentSortDirection, nextSortDirection } = this.getSortDirections();
// get the sort directions of this th
const { currentSortDirection, nextSortDirection } =
this.getSortDirections();

// update aria-label of th to reflect sort
this.updateThAriaLabel(th, thSorted, currentSortDirection, content);
// update aria-label of th to reflect sort
this.updateThAriaLabel(th, thSorted, currentSortDirection, content);

// update title info of span inside th to reflect sort
this.updateSpan(th, thSorted, nextSortDirection, content);
// update title info of span inside th to reflect sort
this.updateSpan(th, thSorted, nextSortDirection, content);

// update sr text to reflect sort
this.updateSRtext(thSorted, content, currentSortDirection);
});
// update sr text to reflect sort
this.updateSRtext(thSorted, content, currentSortDirection);
});
}
}

Expand All @@ -271,14 +315,12 @@ export class VaTableInner {
return (
<div>
<table class={classes}>
{ tableTitle && <caption>{tableTitle}</caption> }
<thead>{ this.makeRow(0) }</thead>
<tbody id="va-table-body">
{ this.getBodyRows() }
</tbody>
{tableTitle && <caption>{tableTitle}</caption>}
<thead>{this.makeRow(0)}</thead>
<tbody id="va-table-body">{this.getBodyRows()}</tbody>
</table>
<div class="usa-sr-only" aria-live="polite"></div>
</div>
)
);
}
}

0 comments on commit 1d88710

Please sign in to comment.