Skip to main content

max / quasi

Apply the current/selected split to table rows The 2026-08-08 decision renamed Row::selected to Row::current because one word meant two things: the app's pointer and the user's tick. Cells kept the old word for the pointer meaning, so a table row emitted class table-row-selected and aria-current from the same field, saying one thing in CSS and the other to a screen reader. A table row and a list row are the same fact in two arrangements. No tick added to go with it. Row grew one because goingson's contact cards have a bulk checkbox; no table asks for one, and a member added because its sibling has it is a member with no consumer to say what it means.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-09 02:56 UTC
Signed with PGP, not checked
Commit: 624148de7047c554de734850993deea6d9489e88
Parent: 6b6b0ac
3 files changed, +42 insertions, -5 deletions
@@ -755,7 +755,19 @@
755 755 /// The route that opens this row.
756 756 pub activate: Option<Action>,
757 757 /// Whether this is the row currently being shown elsewhere.
758 - pub selected: bool,
758 + ///
759 + /// The same fact [`Row::current`] carries, under the same name. It was
760 + /// `selected` until 2026-08-08, which is the word that decision retired:
761 + /// the app's pointer and the user's tick are two things, and one word for
762 + /// both is how every renderer ends up guessing which was meant. `Row` was
763 + /// renamed and this was missed, so it kept the ambiguous word while
764 + /// emitting `aria-current` from it.
765 + ///
766 + /// There is deliberately no tick here to go with it. `Row` grew one because
767 + /// goingson's contact cards have a bulk checkbox; no table asks for one, and
768 + /// a member added because its sibling has it is a member with no consumer to
769 + /// tell us what it should mean.
770 + pub current: bool,
759 771 }
760 772
761 773 impl Cells {
@@ -764,7 +776,7 @@
764 776 Self {
765 777 values: values.into_iter().map(Into::into).collect(),
766 778 activate: None,
767 - selected: false,
779 + current: false,
768 780 }
769 781 }
770 782
@@ -312,13 +312,16 @@
312 312 out: &mut String,
313 313 ) {
314 314 let mut classes = vec!["table-row"];
315 - if cells.selected {
316 - classes.push("table-row-selected");
315 + if cells.current {
316 + // `table-row-current`, matching `row-current` on a list row. It was
317 + // `table-row-selected` while the field was, so the class said one thing
318 + // and the `aria-current` two lines down said the other.
319 + classes.push("table-row-current");
317 320 }
318 321
319 322 out.push_str("<div role=\"row\"");
320 323 class_attr(&classes, opts, out);
321 - if cells.selected {
324 + if cells.current {
322 325 out.push_str(" aria-current=\"true\"");
323 326 }
324 327 if let Some(action) = &cells.activate {
@@ -368,6 +368,28 @@
368 368 assert!(!html.contains("style="));
369 369 }
370 370
371 + #[test]
372 + fn a_table_row_says_current_the_same_way_a_list_row_does() {
373 + // The 2026-08-08 rename applied where it was missed. `Cells` kept
374 + // `selected` while meaning the app's pointer, so the class said one thing
375 + // and the `aria-current` beside it said the other. A table row and a list
376 + // row are the same fact in two arrangements and a stylesheet should not
377 + // have to know which it is reading.
378 + let html = fragment(&Node::Table {
379 + columns: vec![Column::new("Name").width(layout::Width::Fill)],
380 + rows: vec![Cells {
381 + current: true,
382 + ..Cells::new(["kick.wav"])
383 + }],
384 + });
385 + assert!(html.contains("aria-current=\"true\""));
386 + assert!(html.contains("table-row-current"));
387 + assert!(!html.contains("table-row-selected"));
388 +
389 + // And no tick, because a table has none to describe.
390 + assert!(!html.contains("type=\"checkbox\""));
391 + }
392 +
371 393 #[test]
372 394 fn a_cell_value_is_text_and_cannot_become_markup() {
373 395 let html = fragment(&Node::Table {