Skip to main content

max / makeover-webview

Keep a hidden row action focusable, and off the touch hit area visibility: hidden takes an element out of the focus order and out of the accessibility tree, so the focus-within escape emitted beside it could never fire from an action itself: tabbing could not reach one to trigger it. Only some other focusable child of the row could, which is not what the comment claimed. Transparent instead, which keeps the element in both. goingson had already reached the opacity form independently, on its own comment "always in the DOM for keyboard and screen readers", so this is the third correction the adoption has sent back with goingson being right. pointer-events rides along because opacity leaves the hit area behind, and a renderer with no hover never reveals the actions: without it the row carries an invisible tappable control. Keyboard focus is unaffected by it. Patch, not minor: no consumer has adopted .row-actions yet, so the emitted change reaches nobody's rendered UI until they do.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 02:06 UTC
Signed with PGP, not checked
Commit: 37c4a6906e59e8b163f926ab447fca7c0a6295c2
Parent: 661de6e
2 files changed, +35 insertions, -9 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.5.0"
3 + version = "0.5.1"
4 4 edition = "2024"
5 5 description = "The webview renderer for makeover-layout. Emits CSS, and is the one renderer that needs no palette: var() is the late binding, so resolution stays with the browser."
6 6 license = "MIT"
M src/lib.rs +34 -8
@@ -383,10 +383,25 @@
383 383 // Hidden rather than absent: the row must not change height when
384 384 // the pointer arrives. `focus-within` carries the keyboard, which
385 385 // hover on its own would lock out.
386 - let _ = writeln!(css, ".{c} {{\n visibility: hidden;\n}}");
386 + //
387 + // Transparent rather than `visibility: hidden`, which was the first
388 + // form and defeated the very escape above: a `visibility: hidden`
389 + // element is out of the focus order and out of the accessibility
390 + // tree, so tabbing could never reach an action and could never
391 + // trigger the row's `focus-within`. goingson had reached the same
392 + // opacity form independently, on its own comment "always in the DOM
393 + // for keyboard and screen readers".
394 + //
395 + // `pointer-events` rides along because opacity leaves the hit area
396 + // behind: without it a renderer with no hover carries an invisible
397 + // tappable control. Keyboard focus is unaffected by it.
387 398 let _ = writeln!(
388 399 css,
389 - ".{row}:hover .{c},\n.{row}:focus-within .{c} {{\n visibility: visible;\n}}"
400 + ".{c} {{\n opacity: 0;\n pointer-events: none;\n}}"
401 + );
402 + let _ = writeln!(
403 + css,
404 + ".{row}:hover .{c},\n.{row}:focus-within .{c} {{\n opacity: 1;\n pointer-events: auto;\n}}"
390 405 );
391 406 }
392 407 }
@@ -672,7 +687,7 @@
672 687 #[test]
673 688 fn row_actions_are_revealed_without_moving_the_row() {
674 689 let css = row_rules(&Emit::default());
675 - assert!(css.contains(".row-actions {\n visibility: hidden;"));
690 + assert!(css.contains(".row-actions {\n opacity: 0;"));
676 691 // Not display:none, which would reflow the row under the pointer.
677 692 assert!(!css.contains("display: none"));
678 693 // Hover alone would lock the keyboard out.
@@ -680,6 +695,18 @@
680 695 assert!(RowPart::Actions.revealed_on_hover());
681 696 }
682 697
698 + #[test]
699 + fn a_hidden_row_action_is_still_focusable_and_not_tappable() {
700 + let css = row_rules(&Emit::default());
701 + // `visibility: hidden` takes the actions out of the focus order, so the
702 + // `focus-within` reveal above could never fire from an action itself.
703 + assert!(!css.contains("visibility:"));
704 + // Opacity leaves the hit area behind; the row must not carry an
705 + // invisible tappable control where there is no hover to reveal it.
706 + assert!(css.contains(".row-actions {\n opacity: 0;\n pointer-events: none;\n}"));
707 + assert!(css.contains("opacity: 1;\n pointer-events: auto;"));
708 + }
709 +
683 710 #[test]
684 711 fn the_three_text_parts_take_their_intents_and_actions_inherits() {
685 712 let css = row_rules(&Emit::default());
@@ -826,12 +853,11 @@
826 853 if value.contains("var(--") {
827 854 continue;
828 855 }
829 - // Everything left has to be a keyword or a caller-supplied length,
830 - // never a colour.
856 + // Everything left has to be a keyword, a number or a
857 + // caller-supplied length, never a colour.
831 858 assert!(
832 - value.contains("hidden")
833 - || value.contains("visible")
834 - || value.contains("inset"),
859 + value.contains("inset")
860 + || matches!(value.trim_end_matches(';'), "0" | "1" | "none" | "auto"),
835 861 "unrecognised literal value: {line}"
836 862 );
837 863 }