Skip to content

Commit e8487b2

Browse files
Make technology report table columns sortable (#1233)
* Make technology report table columns sortable on click - Wrap sortable column headers (Tech name + data columns) in a <button> - Toggle asc/desc when clicking the active column, default to desc otherwise - Add aria-sort attribute for accessibility - Show ↕/↑/↓ sort indicators via CSS ::after pseudo-element - Support alphabetical sort for the Technology name column * Apply suggestion from @tunetheweb --------- Co-authored-by: Barry Pollard <barrypollard@google.com> Co-authored-by: Barry Pollard <barry_pollard@hotmail.com>
1 parent e18e811 commit e8487b2

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

src/js/techreport/tableLinked.js

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class TableLinked {
1515

1616
this.updateContent();
1717
this.updateSelectionText(DataUtils.getTechsFromURL());
18+
this.initSortHeaders();
1819

1920
const rowCount = document.getElementById('rowsPerPage');
2021
rowCount?.addEventListener('change', (e) => this.updateRowsPerPage(e));
@@ -61,9 +62,16 @@ class TableLinked {
6162
const sortEndpoint = component.dataset.sortEndpoint;
6263
const sortMetric = component.dataset.sortMetric;
6364
const sortKey = component.dataset.sortKey;
65+
const sortOrder = component.dataset.sortOrder || 'desc';
6466
const client = component.dataset.client;
6567

66-
if(sortMetric) {
68+
if(sortMetric === 'technology') {
69+
this.dataArray = this.dataArray.sort((techA, techB) => {
70+
const aName = techA[0]?.technology || '';
71+
const bName = techB[0]?.technology || '';
72+
return sortOrder === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
73+
});
74+
} else if(sortMetric) {
6775
this.dataArray = this.dataArray.sort((techA, techB) => {
6876
// Sort techs by date to get the latest
6977
const aSortedDate = techA.sort((a, b) => new Date(b.date) - new Date(a.date));
@@ -78,6 +86,9 @@ class TableLinked {
7886
const aValue = aMetric?.[client]?.[sortKey];
7987
const bValue = bMetric?.[client]?.[sortKey];
8088

89+
if (sortOrder === 'asc') {
90+
return aValue - bValue > 0 ? 1 : -1;
91+
}
8192
return bValue - aValue > 0 ? 1 : -1;
8293
});
8394
}
@@ -326,6 +337,91 @@ class TableLinked {
326337
const rowsAnnouncement = document.getElementById('rows-announcement');
327338
rowsAnnouncement.innerText = `Showing ${this.rows} rows.`;
328339
}
340+
341+
initSortHeaders() {
342+
const component = document.getElementById(`table-${this.id}`);
343+
const headers = component.querySelectorAll('thead th');
344+
345+
headers.forEach(th => {
346+
const endpoint = th.dataset.endpoint;
347+
const metric = th.dataset.metric;
348+
const key = th.dataset.key;
349+
350+
const isSortable = (key === 'technology') || (endpoint && metric);
351+
if (!isSortable) return;
352+
353+
const btn = document.createElement('button');
354+
btn.className = 'sort-btn';
355+
btn.setAttribute('type', 'button');
356+
while (th.firstChild) {
357+
btn.appendChild(th.firstChild);
358+
}
359+
th.appendChild(btn);
360+
btn.addEventListener('click', () => this.onSortClick(th));
361+
});
362+
363+
this.updateSortIndicators();
364+
}
365+
366+
onSortClick(th) {
367+
const component = document.getElementById(`table-${this.id}`);
368+
const endpoint = th.dataset.endpoint || '';
369+
const subcategory = th.dataset.subcategory || '';
370+
const metric = th.dataset.metric || '';
371+
const key = th.dataset.key;
372+
373+
const isTechColumn = key === 'technology';
374+
const currentOrder = component.dataset.sortOrder || 'desc';
375+
376+
const isCurrentCol = isTechColumn
377+
? component.dataset.sortMetric === 'technology'
378+
: (component.dataset.sortEndpoint === endpoint &&
379+
component.dataset.sortMetric === subcategory &&
380+
component.dataset.sortKey === metric);
381+
382+
const newOrder = isCurrentCol && currentOrder === 'desc' ? 'asc' : 'desc';
383+
384+
if (isTechColumn) {
385+
component.dataset.sortEndpoint = '';
386+
component.dataset.sortMetric = 'technology';
387+
component.dataset.sortKey = 'technology';
388+
} else {
389+
component.dataset.sortEndpoint = endpoint;
390+
component.dataset.sortMetric = subcategory;
391+
component.dataset.sortKey = metric;
392+
}
393+
component.dataset.sortOrder = newOrder;
394+
395+
this.updateContent();
396+
this.updateSortIndicators();
397+
}
398+
399+
updateSortIndicators() {
400+
const component = document.getElementById(`table-${this.id}`);
401+
const headers = component.querySelectorAll('thead th');
402+
const sortMetric = component.dataset.sortMetric;
403+
const sortEndpoint = component.dataset.sortEndpoint || '';
404+
const sortKey = component.dataset.sortKey || '';
405+
const sortOrder = component.dataset.sortOrder || 'desc';
406+
407+
headers.forEach(th => {
408+
const endpoint = th.dataset.endpoint || '';
409+
const subcategory = th.dataset.subcategory || '';
410+
const metric = th.dataset.metric || '';
411+
const key = th.dataset.key;
412+
413+
const isTechColumn = key === 'technology';
414+
const isActive = isTechColumn
415+
? sortMetric === 'technology'
416+
: (endpoint && endpoint === sortEndpoint && subcategory === sortMetric && metric === sortKey);
417+
418+
if (isActive) {
419+
th.setAttribute('aria-sort', sortOrder === 'asc' ? 'ascending' : 'descending');
420+
} else {
421+
th.removeAttribute('aria-sort');
422+
}
423+
});
424+
}
329425
}
330426

331427
export default TableLinked;

static/css/techreport/techreport.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,32 @@ select {
12491249
border-bottom: 1px solid var(--color-text);
12501250
}
12511251

1252+
.table-ui thead th .sort-btn {
1253+
background: none;
1254+
border: none;
1255+
padding: 0;
1256+
font: inherit;
1257+
cursor: pointer;
1258+
color: inherit;
1259+
text-align: left;
1260+
}
1261+
1262+
.table-ui thead th .sort-btn::after {
1263+
content: ' ↕';
1264+
opacity: 0.4;
1265+
font-size: 0.75em;
1266+
}
1267+
1268+
.table-ui thead th[aria-sort="ascending"] .sort-btn::after {
1269+
content: ' ↑';
1270+
opacity: 1;
1271+
}
1272+
1273+
.table-ui thead th[aria-sort="descending"] .sort-btn::after {
1274+
content: ' ↓';
1275+
opacity: 1;
1276+
}
1277+
12521278
.table-ui :is(td, th) {
12531279
min-width: 5rem;
12541280
text-align: left;

templates/techreport/components/table_linked.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
data-key="{{ column.key }}"
2525
data-metric="{{ column.metric }}"
2626
data-endpoint="{{ column.endpoint }}"
27+
data-subcategory="{{ column.subcategory or '' }}"
2728
class="{{ column.className }}"
29+
scope="col"
2830
>
2931
{% if column.hiddenName %}
3032
<span class="sr-only">{{ column.name }}</span>

0 commit comments

Comments
 (0)