Skip to content

Commit

Permalink
add aria attributes, change left cells from td to th, add test on col…
Browse files Browse the repository at this point in the history
…umn sort
  • Loading branch information
severo committed Jan 13, 2025
1 parent da40bff commit 3da9a90
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 23 deletions.
18 changes: 9 additions & 9 deletions src/HighTable.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
cursor: default;
width: 0;
}
.table tbody tr:first-child td {
.table tbody tr:first-child td, .table tbody tr:first-child th {
border-top: 1px solid transparent;
}

Expand Down Expand Up @@ -89,7 +89,7 @@
}

/* row numbers */
.table td:first-child {
.table th:first-child {
background-color: #eaeaeb;
border-right: 1px solid #ddd;
color: #888;
Expand All @@ -104,23 +104,23 @@
width: 32px;
cursor: pointer;
}
.table td:first-child span {
.table th:first-child span {
display: inline;
}
.table td:first-child input {
.table th:first-child input {
display: none;
}
.selectable td:first-child:hover span, .selectable tr.selected td:first-child span {
.selectable th:first-child:hover span, .selectable tr.selected th:first-child span {
display: none;
}
.selectable td:first-child:hover input, .selectable tr.selected td:first-child input {
.selectable th:first-child:hover input, .selectable tr.selected th:first-child input {
display: inline;
cursor: pointer;
}
.selectable tr.selected {
background-color: #fbf7bf;
}
.selectable tr.selected td:first-child {
.selectable tr.selected th:first-child {
background-color: #f1edbb;
}

Expand Down Expand Up @@ -175,7 +175,7 @@
}

/* pending table state */
.table th::before {
.table thead th::before {
content: '';
position: absolute;
top: 0;
Expand All @@ -185,7 +185,7 @@
background-color: #706fb1;
z-index: 100;
}
.pending .table th::before {
.pending .table thead th::before {
animation: shimmer 2s infinite linear;
}
@keyframes shimmer {
Expand Down
26 changes: 15 additions & 11 deletions src/HighTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,9 @@ export default function HighTable({
<div className='table-scroll' ref={scrollRef}>
<div style={{ height: `${scrollHeight}px` }}>
<table
aria-colcount={data.header.length}
aria-rowcount={data.numRows}
aria-readonly={true}
aria-colcount={data.header.length + 1 /* don't forget the selection column */}
aria-rowcount={data.numRows + 1 /* don't forget the header row */}
className={`table${data.sortable ? ' sortable' : ''}`}
ref={tableRef}
role='grid'
Expand All @@ -375,41 +376,44 @@ export default function HighTable({
<tbody>
{prePadding.map((_, prePaddingIndex) => {
const tableIndex = startIndex - prePadding.length + prePaddingIndex
return <tr key={tableIndex}>
<td style={cornerStyle}>
return <tr key={tableIndex} aria-rowindex={tableIndex + 2 /* 1-based + the header row */} >
<th scope="row" style={cornerStyle}>
{
/// TODO(SL): if the data is sorted, this sequence of row labels is incorrect and might include duplicate
/// labels with respect to the next slice of rows. Better to hide this number if the data is sorted?
rowLabel(tableIndex)
}
</td>
</th>
</tr>
})}
{rows.map((row, sliceIndex) => {
const { tableIndex, dataIndex } = getRowIndexes(sliceIndex)
return <tr key={tableIndex} title={rowError(row, dataIndex)} className={isSelected({ selection, index: tableIndex }) ? 'selected' : ''}>
<td style={cornerStyle} onClick={event => onRowNumberClick({ useAnchor: event.shiftKey, tableIndex })}>
return <tr key={tableIndex} aria-rowindex={tableIndex + 2 /* 1-based + the header row */} title={rowError(row, dataIndex)}
className={isSelected({ selection, index: tableIndex }) ? 'selected' : ''}
aria-selected={isSelected({ selection, index: tableIndex })}
>
<th scope="row" style={cornerStyle} onClick={event => onRowNumberClick({ useAnchor: event.shiftKey, tableIndex })}>
<span>{
/// TODO(SL): we might want to show two columns: one for the tableIndex (for selection) and one for the dataIndex (to refer to the original data ids)
rowLabel(dataIndex)
}</span>
<input type='checkbox' checked={isSelected({ selection, index: tableIndex })} />
</td>
</th>
{data.header.map((col, colIndex) =>
Cell(row[col], colIndex, dataIndex)
)}
</tr>
})}
{postPadding.map((_, postPaddingIndex) => {
const tableIndex = startIndex + rows.length + postPaddingIndex
return <tr key={tableIndex}>
<td style={cornerStyle}>
return <tr key={tableIndex} aria-rowindex={tableIndex + 2 /* 1-based + the header row */} >
<th scope="row" style={cornerStyle} >
{
/// TODO(SL): if the data is sorted, this sequence of row labels is incorrect and might include duplicate
/// labels with respect to the previous slice of rows. Better to hide this number if the data is sorted?
rowLabel(tableIndex)
}
</td>
</th>
</tr>
})}
</tbody>
Expand Down
3 changes: 2 additions & 1 deletion src/TableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,11 @@ export default function TableHeader({
const memoizedStyles = useMemo(() => columnWidths.map(cellStyle), [columnWidths])

return <thead>
<tr>
<tr aria-rowindex={1}>
<th><span /></th>
{header.map((columnHeader, columnIndex) =>
<th
scope="col"
aria-sort={orderBy === columnHeader ? 'ascending' : undefined}
className={orderBy === columnHeader ? 'orderby' : undefined}
key={columnIndex}
Expand Down
51 changes: 49 additions & 2 deletions test/HighTable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { fireEvent, render, waitFor } from '@testing-library/react'
import React, { act } from 'react'
import { fireEvent, render, waitFor, within } from '@testing-library/react'
import { act } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import HighTable from '../src/HighTable.js'
import { sortableDataFrame } from '../src/dataframe.js'

describe('HighTable', () => {
const mockData = {
Expand Down Expand Up @@ -89,3 +90,49 @@ describe('HighTable', () => {
expect(container.querySelector('div.pending')).toBeNull()
})
})


describe('When sorted, HighTable', () => {
const data = {
header: ['ID', 'Count'],
numRows: 1000,
rows: (start: number, end: number) => Promise.resolve(
Array.from({ length: end - start }, (_, index) => ({
ID: 'row ' + (index + start),
Count: 1000 - start - index,
}))
),
}

function checkRowContents(row: HTMLElement, rowNumber: string, ID: string, Count: string) {
const selectionCell = within(row).getByRole('rowheader')
expect(selectionCell).toBeDefined()
expect(selectionCell.textContent).toBe(rowNumber)

const columns = within(row).getAllByRole('cell')
expect(columns).toHaveLength(2)
expect(columns[0].textContent).toBe(ID)
expect(columns[1].textContent).toBe(Count)
}

it('shows the rows in the right order', async () => {
const { findByRole, getByRole, findAllByRole } = render(<HighTable data={sortableDataFrame(data)} />)

expect(getByRole('columnheader', { name: 'ID' })).toBeDefined()
await findByRole('cell', { name: 'row 1' })

const table = getByRole('grid') // not table! because the table is interactive. See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/grid_role
// first rowgroup is for the thead second is for tbody
const tbody = within(table).getAllByRole('rowgroup')[1]
let rows = within(tbody).getAllByRole('row')
checkRowContents(rows[0], '1', 'row 0', '1,000')

// Click on the Count header to sort by Count
const countHeader = getByRole('columnheader', { name: 'Count' })
fireEvent.click(countHeader)
await findAllByRole('cell', { name: 'row 999' })

rows = within(within(getByRole('grid')).getAllByRole('rowgroup')[1]).getAllByRole('row')
checkRowContents(rows[0], '1', 'row 999', '1')
})
})

0 comments on commit 3da9a90

Please sign in to comment.