Skip to content

Commit 1d88710

Browse files
authored
va-table: update va-table-inner to properly scope table header row to 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.
1 parent 714e11c commit 1d88710

File tree

2 files changed

+117
-60
lines changed

2 files changed

+117
-60
lines changed

packages/web-components/src/components/va-table/va-table-inner/test/va-table-inner.e2e.ts

+23-8
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,25 @@ describe('va-table', () => {
7676
await page.setContent(makeTable());
7777
const caption = await page.find('va-table-inner >>> caption');
7878
expect(caption.innerHTML).toEqual('this is a caption');
79-
})
79+
});
8080

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

8585
const table = await page.find('va-table-inner');
86-
expect(table.getAttribute('rows')).toEqual("2");
87-
expect(table.getAttribute('cols',)).toEqual("3");
86+
expect(table.getAttribute('rows')).toEqual('2');
87+
expect(table.getAttribute('cols')).toEqual('3');
88+
});
89+
90+
it('sets the proper scope attributes for rows and columns', async () => {
91+
const page = await newE2EPage();
92+
await page.setContent(makeTable());
93+
94+
const columnHeader = await page.find('va-table-inner >>> thead >>> th');
95+
const rowHeader = await page.find('va-table-inner >>> tbody >>> th');
96+
expect(columnHeader.getAttribute('scope')).toEqual('col');
97+
expect(rowHeader.getAttribute('scope')).toEqual('row');
8898
});
8999
});
90100

@@ -225,13 +235,18 @@ describe('sorted va-table ', () => {
225235
delectus explicabo
226236
</span>
227237
</va-table-row>
228-
</va-table>`
229-
};
238+
</va-table>`;
239+
}
230240

231-
// due to content in slots being hard to access in tests,
241+
// due to content in slots being hard to access in tests,
232242
// the va-table-rows are given ids (which don't change during sort),
233243
// then after each sort we check that the ids are in the correct order for the sort
234-
async function checkSorts(page: E2EPage, index: number, asc: string, desc: string) {
244+
async function checkSorts(
245+
page: E2EPage,
246+
index: number,
247+
asc: string,
248+
desc: string,
249+
) {
235250
// we need to update these variables after each sort
236251
let table = null;
237252
let rows = null;
@@ -376,4 +391,4 @@ describe('sort utilities', () => {
376391
const func3 = _getCompareFunc('October 31, 1899', '');
377392
expect(func3.name).toEqual('datesSort');
378393
});
379-
})
394+
});

packages/web-components/src/components/va-table/va-table-inner/va-table-inner.tsx

+94-52
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Component, Element, Prop, h, Event, EventEmitter, State } from '@stencil/core';
1+
import {
2+
Component,
3+
Element,
4+
Prop,
5+
h,
6+
Event,
7+
EventEmitter,
8+
State,
9+
} from '@stencil/core';
210
import classnames from 'classnames';
311

412
/**
@@ -28,8 +36,8 @@ export class VaTableInner {
2836
@Prop() tableTitle: string;
2937

3038
/*
31-
* The number of rows in the table
32-
*/
39+
* The number of rows in the table
40+
*/
3341
@Prop() rows?: number;
3442

3543
/**
@@ -62,11 +70,11 @@ export class VaTableInner {
6270
*/
6371
@State() sortindex?: number = null;
6472

65-
/**
73+
/**
6674
* Fires when the component is closed by clicking on the close icon. This fires only
6775
* when closeable is true.
6876
*/
69-
@Event({
77+
@Event({
7078
composed: true,
7179
bubbles: true,
7280
})
@@ -85,7 +93,7 @@ export class VaTableInner {
8593
const th = target.closest('th');
8694
const sortdir = th.dataset.sortdir;
8795
const index = th.dataset.rowindex;
88-
this.sortTable.emit({ index, sortdir });
96+
this.sortTable.emit({ index, sortdir });
8997
}
9098

9199
handleKeyDown(e: KeyboardEvent) {
@@ -100,21 +108,22 @@ export class VaTableInner {
100108
if (this.sortable && row === 0) {
101109
// we just performed a sort on this column
102110
if (this.sortindex !== null && this.sortindex === index) {
103-
const icon_name = this.sortdir == 'descending' ? 'arrow_upward' : 'arrow_downward';
104-
icon = <va-icon icon={icon_name} size={3} />
105-
// we did not just perform a sort on this column
111+
const icon_name =
112+
this.sortdir == 'descending' ? 'arrow_upward' : 'arrow_downward';
113+
icon = <va-icon icon={icon_name} size={3} />;
114+
// we did not just perform a sort on this column
106115
} else {
107-
icon = <va-icon icon="sort_arrow" size={3} />
116+
icon = <va-icon icon="sort_arrow" size={3} />;
108117
}
109118
return (
110119
<button
111120
tabIndex={0}
112-
onClick={(e) => this.fireSort(e)}
113-
onKeyDown={(e) => this.handleKeyDown(e)}
121+
onClick={e => this.fireSort(e)}
122+
onKeyDown={e => this.handleKeyDown(e)}
114123
>
115124
{icon}
116125
</button>
117-
)
126+
);
118127
} else {
119128
return null;
120129
}
@@ -124,32 +133,38 @@ export class VaTableInner {
124133
* Generate the markup for a table row where row is the zero-indexed row number
125134
*/
126135
makeRow(row: number): HTMLTableRowElement {
136+
// Ensure first row <th> are col scoped for screen reader usability
137+
let scopeDimension = (row === 0 && 'col') || 'row';
138+
127139
return (
128140
<tr>
129141
{Array.from({ length: this.cols }).map((_, i) => {
130142
const slotName = `va-table-slot-${row * this.cols + i}`;
131143
const slot = <slot name={slotName}></slot>;
132-
const header = this.el.querySelector(`[slot="va-table-slot-${i}"]`).innerHTML;
144+
const header = this.el.querySelector(
145+
`[slot="va-table-slot-${i}"]`,
146+
).innerHTML;
133147
const dataSortActive = row > 0 && this.sortindex === i ? true : false;
134-
return (i === 0 || row === 0)
135-
?
148+
return i === 0 || row === 0 ? (
136149
<th
137-
scope="row"
150+
scope={scopeDimension}
138151
data-sortable
139152
data-sort-active={dataSortActive}
140153
data-label={header}
141154
data-rowindex={i}
142155
data-sortdir={i === this.sortindex ? this.sortdir : 'ascending'}
143156
>
144-
{slot}{this.getSortIcon(i, row)}
157+
{slot}
158+
{this.getSortIcon(i, row)}
145159
</th>
146-
:
160+
) : (
147161
<td data-label={header} data-sort-active={dataSortActive}>
148162
{slot}
149163
</td>
164+
);
150165
})}
151166
</tr>
152-
)
167+
);
153168
}
154169

155170
/**
@@ -158,7 +173,7 @@ export class VaTableInner {
158173
getBodyRows(): HTMLTableRowElement[] {
159174
const rows = [];
160175
for (let i = 1; i < this.rows; i++) {
161-
rows.push(this.makeRow(i))
176+
rows.push(this.makeRow(i));
162177
}
163178
return rows;
164179
}
@@ -177,7 +192,12 @@ export class VaTableInner {
177192

178193
// only runs if sortable is true
179194
// update the aria-label for a th after a sort
180-
updateThAriaLabel(th: HTMLTableCellElement, thSorted: boolean, currentSortDirection: string, content: string) {
195+
updateThAriaLabel(
196+
th: HTMLTableCellElement,
197+
thSorted: boolean,
198+
currentSortDirection: string,
199+
content: string,
200+
) {
181201
let thSortInfo: string;
182202
if (thSorted) {
183203
thSortInfo = `currently sorted ${currentSortDirection}`;
@@ -191,7 +211,12 @@ export class VaTableInner {
191211

192212
// only runs if sortable is true
193213
// update the title info on span clicked to sort and focus it
194-
updateSpan(th: HTMLTableCellElement, thSorted: boolean, nextSortDirection: string, content: string) {
214+
updateSpan(
215+
th: HTMLTableCellElement,
216+
thSorted: boolean,
217+
nextSortDirection: string,
218+
content: string,
219+
) {
195220
const button = th.querySelector('button');
196221
let spanSortInfo = `Click to sort by ${content} in ${nextSortDirection} order`;
197222
button.setAttribute('title', spanSortInfo);
@@ -204,26 +229,42 @@ export class VaTableInner {
204229

205230
// only runs if sortable is true
206231
// if a sort has occurred update the sr text to reflect this
207-
updateSRtext(thSorted: boolean, content: string, currentSortDirection: string) {
232+
updateSRtext(
233+
thSorted: boolean,
234+
content: string,
235+
currentSortDirection: string,
236+
) {
208237
if (thSorted) {
209-
const tableInfo = !!this.tableTitle ? `The table named "${this.tableTitle}"` : 'This table';
210-
this.el.shadowRoot.querySelector('table + div').innerHTML = `${tableInfo} is now sorted by ${content} in ${currentSortDirection} order`;
238+
const tableInfo = !!this.tableTitle
239+
? `The table named "${this.tableTitle}"`
240+
: 'This table';
241+
this.el.shadowRoot.querySelector(
242+
'table + div',
243+
).innerHTML = `${tableInfo} is now sorted by ${content} in ${currentSortDirection} order`;
211244
}
212245
}
213246

214247
// only runs if sortable is true
215248
// for a given th, get the current and the next sort directions
216-
getSortDirections(): {currentSortDirection: string, nextSortDirection: string} {
249+
getSortDirections(): {
250+
currentSortDirection: string;
251+
nextSortDirection: string;
252+
} {
217253
const nextSortDirection = !!this.sortdir ? this.sortdir : 'ascending';
218-
const currentSortDirection = nextSortDirection === 'ascending' ? 'descending' : 'ascending';
219-
return { currentSortDirection, nextSortDirection }
254+
const currentSortDirection =
255+
nextSortDirection === 'ascending' ? 'descending' : 'ascending';
256+
return { currentSortDirection, nextSortDirection };
220257
}
221258

222259
// for a sortable table set the state to take into account previous sort
223260
componentWillLoad() {
224261
if (this.sortable) {
225-
this.sortindex = this.el.dataset.sortindex ? +this.el.dataset.sortindex : this.sortindex;
226-
this.sortdir = this.el.dataset.sortdir ? this.el.dataset.sortdir : this.sortdir;
262+
this.sortindex = this.el.dataset.sortindex
263+
? +this.el.dataset.sortindex
264+
: this.sortindex;
265+
this.sortdir = this.el.dataset.sortdir
266+
? this.el.dataset.sortdir
267+
: this.sortdir;
227268
}
228269
}
229270

@@ -237,27 +278,30 @@ export class VaTableInner {
237278
if (this.sortable) {
238279
const slots = this.el.shadowRoot.querySelectorAll('slot');
239280
// loop through slots inside the table header ths
240-
Array.from(slots).slice(0, this.cols).forEach((slot, i) => {
241-
const th = slot.closest('th');
281+
Array.from(slots)
282+
.slice(0, this.cols)
283+
.forEach((slot, i) => {
284+
const th = slot.closest('th');
242285

243-
// was this th just sorted
244-
const thSorted = this.sortindex !== null && this.sortindex === i;
286+
// was this th just sorted
287+
const thSorted = this.sortindex !== null && this.sortindex === i;
245288

246-
// text content of this th
247-
const content = this.getSortColumnText(slot);
289+
// text content of this th
290+
const content = this.getSortColumnText(slot);
248291

249-
// get the sort directions of this th
250-
const { currentSortDirection, nextSortDirection } = this.getSortDirections();
292+
// get the sort directions of this th
293+
const { currentSortDirection, nextSortDirection } =
294+
this.getSortDirections();
251295

252-
// update aria-label of th to reflect sort
253-
this.updateThAriaLabel(th, thSorted, currentSortDirection, content);
296+
// update aria-label of th to reflect sort
297+
this.updateThAriaLabel(th, thSorted, currentSortDirection, content);
254298

255-
// update title info of span inside th to reflect sort
256-
this.updateSpan(th, thSorted, nextSortDirection, content);
299+
// update title info of span inside th to reflect sort
300+
this.updateSpan(th, thSorted, nextSortDirection, content);
257301

258-
// update sr text to reflect sort
259-
this.updateSRtext(thSorted, content, currentSortDirection);
260-
});
302+
// update sr text to reflect sort
303+
this.updateSRtext(thSorted, content, currentSortDirection);
304+
});
261305
}
262306
}
263307

@@ -271,14 +315,12 @@ export class VaTableInner {
271315
return (
272316
<div>
273317
<table class={classes}>
274-
{ tableTitle && <caption>{tableTitle}</caption> }
275-
<thead>{ this.makeRow(0) }</thead>
276-
<tbody id="va-table-body">
277-
{ this.getBodyRows() }
278-
</tbody>
318+
{tableTitle && <caption>{tableTitle}</caption>}
319+
<thead>{this.makeRow(0)}</thead>
320+
<tbody id="va-table-body">{this.getBodyRows()}</tbody>
279321
</table>
280322
<div class="usa-sr-only" aria-live="polite"></div>
281323
</div>
282-
)
324+
);
283325
}
284326
}

0 commit comments

Comments
 (0)