The vehicles in service as a table, one row each: the vehicle's line, where it is going, how fresh its position is, optionally when it last reported, and how late it runs, the map's text alternative, in the order the caller sorts it by.
When to use
Use VehicleTable for the vehicles in service as a table, one row each, with their line, destination, data state and delay: the map's text alternative.
| Use instead | When |
|---|---|
| DepartureRow | The list is the departures from one stop, each with how soon it leaves |
| ReportStrip | Only how many vehicles are in each data state matters, not which ones |
Props
| Name | Type | Default | Description |
|---|---|---|---|
vehicles* | readonly VehicleTableVehicle[] | (required) | The vehicles to show, one row each, in this order: each one's line, destination, data state and delay. An empty array shows the header row alone (CR-6). |
sort | VehicleTableSort | null | — | The order the caller has put vehicles in, which the table marks on that column's header with aria-sort and an arrow; it reorders no row. Left out, or null, no column is marked (CR-10). |
onSortChange | (event: MouseEvent<HTMLButtonElement>, sort: VehicleTableSort) => void | — | Called when a column's header is pressed, with the press's event and the sort it asks for: that column ascending, or the other way when sort already names it. Given, every header is a button and says its sort state; the caller sorts vehicles and passes the new sort (CR-10). |
delayColumnHidden | boolean | false | Leaves out the Delay column, for vehicles whose data carries no delay at all (CR-12) |
lastSeenColumnVisible | boolean | false | Shows the Last seen column, the time each vehicle last reported, before the Delay column (CR-11) |
lineColumnLabel | string | The locale's | Overrides "Line", the header of the line column |
destinationColumnLabel | string | The locale's | Overrides "Destination", the header of the destination column |
positionColumnLabel | string | The locale's | Overrides "Position", the header of the data state column |
delayColumnLabel | string | The locale's | Overrides "Delay", the header of the delay column |
lastSeenColumnLabel | string | The locale's | Overrides "Last seen", the header of the last-seen column |
unknownLabel | string | The locale's | Overrides "Unknown", heard in place of "—" for a vehicle with no last-seen time |
liveLabel | string | The locale's | Overrides "Live", the state's word when a vehicle's variant is 'live'; given to every VehicleDotLabel |
ageingLabel | string | The locale's | Overrides "Ageing", the state's word when a vehicle's variant is 'ageing'; given to every VehicleDotLabel |
estimatedLabel | string | The locale's | Overrides "Estimated", the state's word when a vehicle's variant is 'estimated'; given to every VehicleDotLabel |
timetableOnlyLabel | string | The locale's | Overrides "Timetable only", the state's word when a vehicle's variant is 'timetable-only'; given to every VehicleDotLabel |
noSignalLabel | string | The locale's | Overrides "No signal", the state's word when a vehicle's variant is 'no-signal'; given to every VehicleDotLabel |
minutesUnitLabel | string | The locale's | Overrides "min", the unit after each delay's figure; given to every Delay |
minutesLateLabel | string | The locale's | Overrides "min late", heard after the number when late; given to every Delay |
minutesEarlyLabel | string | The locale's | Overrides "min early", heard after the number when early; given to every Delay |
onTimeLabel | string | The locale's | Overrides "On time"; given to every Delay |
delayUnknownLabel | string | The locale's | Overrides "Delay unknown", heard in place of "—"; given to every Delay |
className | string | — | Joined after pn-VehicleTable on the root |
style | CSSProperties | — | Reaches the root |
ref | Ref<HTMLTableElement> | — | Reaches the root element |
vehicles (required)readonly VehicleTableVehicle[], default (required)- The vehicles to show, one row each, in this order: each one's line, destination, data state and delay. An empty array shows the header row alone (CR-6).
sortVehicleTableSort | null, default —- The order the caller has put
vehiclesin, which the table marks on that column's header witharia-sortand an arrow; it reorders no row. Left out, ornull, no column is marked (CR-10). onSortChange(event: MouseEvent<HTMLButtonElement>, sort: VehicleTableSort) => void, default —- Called when a column's header is pressed, with the press's event and the sort it asks for: that column ascending, or the other way when
sortalready names it. Given, every header is a button and says its sort state; the caller sortsvehiclesand passes the newsort(CR-10). delayColumnHiddenboolean, defaultfalse- Leaves out the Delay column, for vehicles whose data carries no delay at all (CR-12)
lastSeenColumnVisibleboolean, defaultfalse- Shows the Last seen column, the time each vehicle last reported, before the Delay column (CR-11)
lineColumnLabelstring, default The locale's- Overrides "Line", the header of the line column
destinationColumnLabelstring, default The locale's- Overrides "Destination", the header of the destination column
positionColumnLabelstring, default The locale's- Overrides "Position", the header of the data state column
delayColumnLabelstring, default The locale's- Overrides "Delay", the header of the delay column
lastSeenColumnLabelstring, default The locale's- Overrides "Last seen", the header of the last-seen column
unknownLabelstring, default The locale's- Overrides "Unknown", heard in place of "—" for a vehicle with no last-seen time
liveLabelstring, default The locale's- Overrides "Live", the state's word when a vehicle's
variantis'live'; given to every VehicleDotLabel ageingLabelstring, default The locale's- Overrides "Ageing", the state's word when a vehicle's
variantis'ageing'; given to every VehicleDotLabel estimatedLabelstring, default The locale's- Overrides "Estimated", the state's word when a vehicle's
variantis'estimated'; given to every VehicleDotLabel timetableOnlyLabelstring, default The locale's- Overrides "Timetable only", the state's word when a vehicle's
variantis'timetable-only'; given to every VehicleDotLabel noSignalLabelstring, default The locale's- Overrides "No signal", the state's word when a vehicle's
variantis'no-signal'; given to every VehicleDotLabel minutesUnitLabelstring, default The locale's- Overrides "min", the unit after each delay's figure; given to every Delay
minutesLateLabelstring, default The locale's- Overrides "min late", heard after the number when late; given to every Delay
minutesEarlyLabelstring, default The locale's- Overrides "min early", heard after the number when early; given to every Delay
onTimeLabelstring, default The locale's- Overrides "On time"; given to every Delay
delayUnknownLabelstring, default The locale's- Overrides "Delay unknown", heard in place of "—"; given to every Delay
classNamestring, default —- Joined after
pn-VehicleTableon the root styleCSSProperties, default —- Reaches the root
refRef<HTMLTableElement>, default —- Reaches the root element
Use it
import { VehicleTable } from '@mylder/perron/patterns'Ask your agent
search_docs({ query: "VehicleTable" })Keyboard and ARIA
No Base UI part, so nothing from Base UI here: the rows are read, not operated. They take no focus, and no key does anything of their own (CR-7). In a sortable table each header's sortButton is a native button, one tab stop each, in column order, pressed with Enter or Space as any button is (CR-10). A screen reader's table keys, which move from cell to cell, are the screen reader's.
| Key | Does | From |
|---|---|---|
Tab | In a sortable table, moves to each header's button in column order; past them, or in a table that is not sortable, passes the rows by, or reaches a caller's link in a destination, row by row in reading order | The browser |
Enter, Space | On a header's button, calls onSortChange with that column's sort (CR-10) | The browser's click |
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
root | table, from the native <table> | The caller's aria-labelledby or aria-label; none of its own | none |
headerCell | columnheader, from <th scope="col"> | Its text, the column's owned string | Its sort, from aria-sort: ascending or descending on the sorted column, and no aria-sort on the others (CR-10) |
sortButton | button, from the native <button> | Its text, the column's owned string; the sortIcon is aria-hidden | none of its own; its header's sort |
row | row, from the native <tr> | All its cells' text, "5C Husum Torv Live On time", or "5C Husum Torv Live 08:14 On time" with Last seen | none |
lineCell | rowheader, from <th scope="row"> | Its text, the badge's lineName | none |
destinationCell | rowheader, from <th scope="row"> | Its text, the vehicle's destination | none |
positionCell | cell, from the native <td> | Its text, the state's word; the dot is aria-hidden inside VehicleDotLabel | none |
lastSeenCell | cell, from the native <td> | The time, "08:14", or "Unknown" in place of "—" (CR-11) | none |
delayCell | cell, from the native <td> | Delay's words: "3 min late", "On time", "Delay unknown" | none |
The row headers are what a screen reader adds to a cell as it moves through a column: moving down the delay column, it reads "5C, Husum Torv, Delay, On time", the row headers and the column header before the cell: each delay is heard with the vehicle it belongs to. A header's sort is heard from its aria-sort, which screen readers read with the column header, "Delay, sorted descending"; the arrow beside the words is hidden, since aria-sort says the same, and a sortable column not sorted says none, which the button's role already tells apart from a header that cannot sort.
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: the line's colour is named by
lineName, read from the badge; the dot's shape by the state's word beside it, the dotaria-hidden(VehicleDotLabel's CR-2); the delay's figure and colour by Delay's words, and an unknown one's "—" by "Delay unknown" (CR-4); an unknown last-seen time's "—" by "Unknown" (CR-11); the sorted column's arrow and ink by its header'saria-sort(CR-10); each column's meaning by its header, which a screen reader reads with each cell. - Every landmark and named region has an accessible name: the table is not a landmark; it takes its name from the caller, through
aria-labeloraria-labelledbyon the root, and has none of its own (CR-5): every story names it witharia-label, andDefault'splaynames one from a heading througharia-labelledby, as Do and don't asks of a screen. - Every default label can be overridden:
lineColumnLabel,destinationColumnLabel,positionColumnLabel,delayColumnLabel,lastSeenColumnLabel,unknownLabel, the five state labels fromliveLabeltonoSignalLabel, and Delay's five,minutesUnitLabel,minutesLateLabel,minutesEarlyLabel,onTimeLabelanddelayUnknownLabel. - Every shape the type accepts renders, in every variant:
lineNameanddestinationrender a string, an element and a list alike;vehiclesrenders empty, with one vehicle and with many;delaya number,nulland left out, in everyvariant(CR-4);lastSeena time and left out (CR-11);sorteach column and direction,nulland left out, with and withoutonSortChange(CR-10); a long destination wraps inside its column (CR-8). - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; nothing animates or collapses. The dot and the delay's figure arearia-hiddeninside their components because words say them, not for animation. - Native semantics first: a native
<table>,<thead>,<tbody>,<tr>,<th scope>,<td>,<time>and, for a sortable header,<button>, whose roles no CSSdisplayoverrides (CR-1); the one ARIA of the table's own isaria-sort, which no element says. - Focus moves in reading order and returns on close: the rows take no focus and the table opens nothing; a sortable table's header buttons are reached in column order, then a caller's link in a
destinationin row order, its DOM order. - Every target is at least
size.target-minsquare: a header'ssortButtonfills its cell and is at leastsize.target-mintall and wide (CR-10); the rows have no target of their own (CR-7); a caller's link in adestinationistypography.labeltext in a row at leastsize.vehicle-rowtall, with that much room around it. - Loops stop when the system asks for less motion: none here; nothing moves. A vehicle whose data changes is new props from the screen, not an animation.
Owned strings
The line's name and the destination are the caller's data, and so is the table's accessible name (see Props); the column headers are the table's own, and the state's words and the delay's are VehicleDotLabel's and Delay's, overridable here through the props above.
The table adds five owned strings for its column headers, lineColumn "Line", destinationColumn "Destination", positionColumn "Position", lastSeenColumn "Last seen" and delayColumn "Delay", outside content design's table, and says the existing unknown, "Unknown", for a last-seen time no one gave. No key in packages/perron/src/locale/en.ts says any of them: delays, "Delays", is the heading of a line's delays and the name of a kind of disruption, plural, not a column's name. "Position" names what the five data states measure, how fresh a vehicle's reported position is, in a word a rider knows, where "Data state" is the system's own term; and the five are a table's headers rather than data-state, delay or disruption words, as DepartureRow's "Now" and StopTimeline's "Next stop" stayed out of content design's table. "Last seen" is the words Data states uses of a no-signal vehicle, which keeps moving on the timetable "from where it was last seen", for the time that state is measured from. A sorted header adds no words: its state is aria-sort's, which the screen reader says in its own language.
| Key | English | Override | Seen or heard where |
|---|---|---|---|
lineColumn | "Line" | lineColumnLabel | The line column's headerCell, seen, and heard with each cell of the column; new |
destinationColumn | "Destination" | destinationColumnLabel | The destination column's headerCell; new |
positionColumn | "Position" | positionColumnLabel | The data state column's headerCell; new |
delayColumn | "Delay" | delayColumnLabel | The delay column's headerCell; new |
lastSeenColumn | "Last seen" | lastSeenColumnLabel | The last-seen column's headerCell; new with Last seen |
unknown | "Unknown" | unknownLabel | A lastSeenCell with no lastSeen, heard in place of "—"; an existing key |
live | "Live" | liveLabel | status, seen and heard when a vehicle's variant is 'live'; an existing key |
ageing | "Ageing" | ageingLabel | status, when 'ageing'; an existing key |
estimated | "Estimated" | estimatedLabel | status, when 'estimated'; an existing key |
timetableOnly | "Timetable only" | timetableOnlyLabel | status, when 'timetable-only'; an existing key |
noSignal | "No signal" | noSignalLabel | status, when 'no-signal'; an existing key |
minutesUnit | "min" | minutesUnitLabel | delay, after the figure, as Delay's spec gives it; an existing key |
minutesLate | "min late" | minutesLateLabel | delay, as Delay's spec gives it; an existing key |
minutesEarly | "min early" | minutesEarlyLabel | delay, as Delay's spec gives it; an existing key |
onTime | "On time" | onTimeLabel | delay, as Delay's spec gives it; an existing key |
delayUnknown | "Delay unknown" | delayUnknownLabel | delay, as Delay's spec gives it; an existing key |
The five new keys join en.ts, each with a JSDoc line such as "A vehicle table: the header of the column of each vehicle's line.", and their Danish, Swedish and Norwegian join da.ts, sv.ts and nb.ts: "Last seen" is "Sidst set", "Senast sedd" and "Sist sett". unknown's JSDoc names VehicleTable among the components that say it.
Do and don't
For a caller using the pattern, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | must | Name the table from the heading above it, through aria-labelledby, such as a Section's "Vehicles on line 5C" | Leave the table without a name: a screen reader's list of tables can't tell it from another | review |
| DD-2 | must | Work out each vehicle's variant from its last report, with the app's thresholds, by default vehicle-dot.live-threshold and vehicle-dot.ageing-threshold | Pass live for a vehicle placed from the timetable: the table would claim more than the data knows | review |
| DD-3 | must | Keep each vehicle's id the same from one update to the next | Key the vehicles by their place in the array: a row would keep another vehicle's state while the list reorders | review |
| DD-4 | should | Sort the vehicles before you pass them, such as by line and then by delay, and keep the order steady between updates | Reorder the rows on every update: the rows jump under the rider's eye and a screen reader's place | review |
| DD-5 | must | Sort vehicles by the sort you pass, and pass the sort onSortChange asks for once you have sorted by it | Pass a sort the rows are not in: the arrow and aria-sort would tell the eye and the ear an order that isn't there | review |
| DD-6 | should | Hide the Delay column when nothing in the data can know a delay | Leave a column of "—" on every row: it reads as a delay feed that failed | review |
| DD-7 | must | Show Last seen where a rider asks how old a position is, and leave lastSeen out for a timetable-only vehicle | Pass the time of the timetable's call as lastSeen: the column would claim a report that never came | review |
| DD-8 | should | Hold the table in DataStatePanel while its vehicles load or when there are none | Show an empty table as the only answer to "no vehicles": a header row says nothing about why | review |
| DD-9 | must | Announce a vehicle's change of state in the screen's own polite live region, once | Put the table in a live region: every delay's change would be read | review |
| DD-10 | must | Make a vehicle openable by passing a Perron Link as its destination | Add onClick to a row: it has no focus, role or pressed look, and a keyboard can't reach it | review |
| DD-11 | should | Keep lineName the short name riders know and destination the place: "5C" to "Husum Torv" | Put the line in destination, "5C Husum Torv": the line column already reads "5C" | review |