|
|
|
|
@ -134,56 +134,52 @@ export function DataTable(props: {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
// Horizontal-scroll boundary: on narrow screens a wide table scrolls inside this box
|
|
|
|
|
// instead of pushing the page body wider than the viewport (see .wf-table-wrap CSS).
|
|
|
|
|
<div className="wf-table-wrap">
|
|
|
|
|
<table className="wf">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
{props.columns.map((c) => {
|
|
|
|
|
const sortable = canSort(c)
|
|
|
|
|
const arrow = sort?.key === c.key ? (sort.dir === 'asc' ? ' ▲' : ' ▼') : ''
|
|
|
|
|
return (
|
|
|
|
|
<th key={c.key} className={c.numeric === true ? 'num' : undefined}
|
|
|
|
|
aria-sort={sort?.key === c.key ? (sort.dir === 'asc' ? 'ascending' : 'descending') : undefined}
|
|
|
|
|
onClick={sortable ? () => toggle(c.key) : undefined}
|
|
|
|
|
style={sortable ? { cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' } : undefined}
|
|
|
|
|
title={sortable ? 'Sort' : undefined}>
|
|
|
|
|
{c.label}<span style={{ opacity: 0.7 }}>{arrow}</span>
|
|
|
|
|
</th>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{ordered.map(({ row, i }) => {
|
|
|
|
|
const tone = props.rowTone?.(i)
|
|
|
|
|
const clickable = props.onRowClick !== undefined
|
|
|
|
|
<table className="wf">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
{props.columns.map((c) => {
|
|
|
|
|
const sortable = canSort(c)
|
|
|
|
|
const arrow = sort?.key === c.key ? (sort.dir === 'asc' ? ' ▲' : ' ▼') : ''
|
|
|
|
|
return (
|
|
|
|
|
<tr
|
|
|
|
|
key={i}
|
|
|
|
|
className={tone !== undefined ? `tone-${tone}` : undefined}
|
|
|
|
|
// Row click is keyboard-operable (WCAG 2.1.1): role=button + tabindex
|
|
|
|
|
// make it a focusable control, and Enter/Space fire the same handler.
|
|
|
|
|
onClick={clickable ? () => props.onRowClick?.(row, i) : undefined}
|
|
|
|
|
onKeyDown={clickable
|
|
|
|
|
? (e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); props.onRowClick?.(row, i) }
|
|
|
|
|
}
|
|
|
|
|
: undefined}
|
|
|
|
|
role={clickable ? 'button' : undefined}
|
|
|
|
|
tabIndex={clickable ? 0 : undefined}
|
|
|
|
|
style={clickable ? { cursor: 'pointer' } : undefined}
|
|
|
|
|
>
|
|
|
|
|
{props.columns.map((c) => (
|
|
|
|
|
<td key={c.key} className={cellClass(c)}>{row[c.key]}</td>
|
|
|
|
|
))}
|
|
|
|
|
</tr>
|
|
|
|
|
<th key={c.key} className={c.numeric === true ? 'num' : undefined}
|
|
|
|
|
aria-sort={sort?.key === c.key ? (sort.dir === 'asc' ? 'ascending' : 'descending') : undefined}
|
|
|
|
|
onClick={sortable ? () => toggle(c.key) : undefined}
|
|
|
|
|
style={sortable ? { cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' } : undefined}
|
|
|
|
|
title={sortable ? 'Sort' : undefined}>
|
|
|
|
|
{c.label}<span style={{ opacity: 0.7 }}>{arrow}</span>
|
|
|
|
|
</th>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{ordered.map(({ row, i }) => {
|
|
|
|
|
const tone = props.rowTone?.(i)
|
|
|
|
|
const clickable = props.onRowClick !== undefined
|
|
|
|
|
return (
|
|
|
|
|
<tr
|
|
|
|
|
key={i}
|
|
|
|
|
className={tone !== undefined ? `tone-${tone}` : undefined}
|
|
|
|
|
// Row click is keyboard-operable (WCAG 2.1.1): role=button + tabindex
|
|
|
|
|
// make it a focusable control, and Enter/Space fire the same handler.
|
|
|
|
|
onClick={clickable ? () => props.onRowClick?.(row, i) : undefined}
|
|
|
|
|
onKeyDown={clickable
|
|
|
|
|
? (e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); props.onRowClick?.(row, i) }
|
|
|
|
|
}
|
|
|
|
|
: undefined}
|
|
|
|
|
role={clickable ? 'button' : undefined}
|
|
|
|
|
tabIndex={clickable ? 0 : undefined}
|
|
|
|
|
style={clickable ? { cursor: 'pointer' } : undefined}
|
|
|
|
|
>
|
|
|
|
|
{props.columns.map((c) => (
|
|
|
|
|
<td key={c.key} className={cellClass(c)}>{row[c.key]}</td>
|
|
|
|
|
))}
|
|
|
|
|
</tr>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|