From afd09e0d153478157dfe875b06c60be3fbad2662 Mon Sep 17 00:00:00 2001 From: Mathias Myrland Date: Sat, 25 Jul 2026 17:22:51 +0200 Subject: [PATCH] Fix a Firefox crash from a WebKit-only scrollbar selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deployed site rendered a blank page in Firefox on every route. `slim_scrollbar` is applied to the app root, and it used `[&::-webkit-scrollbar-thumb:hover]:`. Firefox tolerates the unknown pseudo-element on its own but rejects it with a pseudo-class appended, so `insertRule` throws — and dominator panics rather than skipping the rule (dom.rs:1691), killing the app mid-construction. Firefox reported hasHeader=false, hasMain=false, empty body, 12 stylesheets instead of 21. This is a regression from moving the scrollbar styling out of styles.rs: a raw stylesheet drops an unparseable rule silently, so the same selector was harmless there. Making a selector into a class makes an unsupported one fatal. The pseudo-elements are not needed anyway — Firefox has supported scrollbar-width/scrollbar-color since 64 and Chrome since 121 — so the mixin now uses the standard properties alone. Adds browser tests pinning every selector shape the example applies. Since CI runs both Firefox and Chrome, an engine that rejects any of them now fails the build instead of the page. Bisected with a disposable probe first: the bare pseudo-element, the track and the thumb all insert fine; only the thumb-plus-:hover form throws. Verified in real Firefox via geckodriver, before and after, across five routes. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 13 ++++++ crates/dwui/tests/styling.rs | 83 ++++++++++++++++++++++++++++++++++++ examples/webpage/src/fx.rs | 23 +++++----- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9001c1e..8336218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,19 @@ hold a `&String` in a `String`, so only generator-based selectors worked. Aliasi now works for a plain utility and for a `dwkeyframes!` animation alike, and an aliased animation still registers its keyframes. +### Watch out: an unsupported selector is fatal + +A selector the browser cannot parse is not ignored. `insertRule` throws, and +dominator panics with "selectors are incorrect" rather than skipping the rule +(`dominator/src/dom.rs:1691`), which takes the whole app down at load. + +This matters more now that variants can express selectors a stylesheet used to +hold, because a raw stylesheet drops an unparseable rule silently. Vendor +pseudo-elements are the trap: Firefox tolerates `[&::-webkit-scrollbar]:` on its +own but rejects `[&::-webkit-scrollbar-thumb:hover]:`, so a WebKit-only scrollbar +style that works in Chrome blanks the page in Firefox. Prefer standard properties +(`scrollbar-width`, `scrollbar-color`) and test in every engine you support. + ### New utilities `delay-0`…`delay-1000`, `underline` / `overline` / `line-through` / diff --git a/crates/dwui/tests/styling.rs b/crates/dwui/tests/styling.rs index 847cf50..8bb2a3e 100644 --- a/crates/dwui/tests/styling.rs +++ b/crates/dwui/tests/styling.rs @@ -551,3 +551,86 @@ async fn dwgenerate_can_alias_a_plain_class_and_an_animation() { ); assert_eq!(count_keyframes("dwuitest-slide-probe"), 1); } + +// --------------------------------------------------------------------------- +// Selector forms +// --------------------------------------------------------------------------- + +// A selector the engine cannot parse is not ignored: `insertRule` throws and +// dominator panics ("selectors are incorrect"), taking the whole app down at +// load. A raw stylesheet would have dropped the rule silently, so moving a +// selector into `dwclass!` makes an unsupported one fatal. +// +// These pin the shapes the example webpage actually applies. CI runs both +// Firefox and Chrome, so an engine that rejects any of them fails the build +// rather than the page. + +#[wasm_bindgen_test] +async fn pointer_spotlight_selectors_insert() { + let tc = TestContainer::new(); + + dominator::append_dom( + &tc.dom_element(), + html!("div", { + .dwclass!("relative isolate [transition:transform 400ms cubic-bezier(0.16, 1, 0.3, 1), border-color 300ms ease]") + .dwclass!("[&::before]:absolute [&::before]:inset-0 [&::before]:[z-index:-1] \ + [&::before]:[border-radius:inherit] [&::before]:opacity-0 \ + [&::before]:[transition:opacity 320ms ease] [&.hot::before]:opacity-100 \ + [&::before]:[background:radial-gradient(22rem circle at var(--sx, 50%) var(--sy, 50%), rgba(213, 182, 95, 0.13), transparent 62%)]") + .dwclass!("[&::after]:absolute [&::after]:inset-0 [&::after]:[z-index:-1] \ + [&::after]:[border-radius:inherit] [&::after]:[padding:1px] \ + [&::after]:opacity-0 [&::after]:[transition:opacity 320ms ease] [&.hot::after]:opacity-100 \ + [&::after]:[-webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)] \ + [&::after]:[-webkit-mask-composite:xor] \ + [&::after]:[mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)] \ + [&::after]:[mask-composite:exclude]") + }), + ); + wait_frame().await; +} + +#[wasm_bindgen_test] +async fn child_and_state_variant_selectors_insert() { + let tc = TestContainer::new(); + + dominator::append_dom( + &tc.dom_element(), + html!("div", { + // The scroll-reveal cascade: parent-state plus :nth-child stagger. + .dwclass!("[& > *]:opacity-0 [& > *]:[transform:translateY(26px)] \ + [& > *]:[transition:opacity 650ms cubic-bezier(0.16, 1, 0.3, 1), transform 650ms cubic-bezier(0.16, 1, 0.3, 1)]") + .dwclass!("[& > *:nth-child(2)]:delay-75 [& > *:nth-child(5)]:[transition-delay:280ms]") + .dwclass!("[&.reveal-in > *]:opacity-100") + // Hover the parent, animate the child — the marquee pause. + .dwclass!("[&:hover > *]:[animation-play-state:paused]") + }), + ); + wait_frame().await; +} + +#[wasm_bindgen_test] +async fn scrollbar_styling_uses_standard_properties_only() { + // `[&::-webkit-scrollbar-thumb:hover]:` panics in Firefox: the engine + // tolerates the unknown pseudo-element on its own but rejects it with a + // pseudo-class appended. The standard properties cover both engines. + let tc = TestContainer::new(); + + dominator::append_dom( + &tc.dom_element(), + html!("div", { + .attr("id", "probe-scrollbar") + .dwclass!("[scrollbar-width:thin] [scrollbar-color:#26262C transparent]") + }), + ); + wait_frame().await; + + let doc = web_sys::window().unwrap().document().unwrap(); + let el = doc.get_element_by_id("probe-scrollbar").unwrap(); + let style = web_sys::window() + .unwrap() + .get_computed_style(&el) + .unwrap() + .unwrap(); + + assert_eq!(style.get_property_value("scrollbar-width").unwrap(), "thin"); +} diff --git a/examples/webpage/src/fx.rs b/examples/webpage/src/fx.rs index efc462e..4dbaef8 100644 --- a/examples/webpage/src/fx.rs +++ b/examples/webpage/src/fx.rs @@ -474,21 +474,20 @@ pub fn glass(builder: DomBuilder) -> DomBuilder { /// Thin scrollbars that match the surface they sit on. /// -/// WebKit exposes these as pseudo-elements, which is exactly what the bracketed -/// variant syntax selects. Firefox uses the standard `scrollbar-*` properties, -/// set alongside. +/// The standard `scrollbar-width` / `scrollbar-color` properties only — no +/// `::-webkit-scrollbar` pseudo-elements. Two reasons: +/// +/// 1. They are no longer needed. Firefox has supported the standard properties +/// since 64 and Chrome since 121. +/// 2. `::-webkit-scrollbar-thumb:hover` **crashes Firefox**. A selector that the +/// browser cannot parse makes `insertRule` throw, and dominator panics rather +/// than skipping it (`dom.rs:1691`), so the whole app dies on load. A raw +/// stylesheet would have ignored the rule silently — moving these selectors +/// into `dwclass!` is what made an unsupported selector fatal. pub fn slim_scrollbar(builder: DomBuilder) -> DomBuilder { dwclass!( builder, - "[scrollbar-width:thin] [scrollbar-color:#26262C transparent] \ - [&::-webkit-scrollbar]:[width:10px] [&::-webkit-scrollbar]:[height:10px] \ - [&::-webkit-scrollbar-track]:[background:transparent] \ - [&::-webkit-scrollbar-thumb]:[background:#26262C] \ - [&::-webkit-scrollbar-thumb]:[border-radius:8px] \ - [&::-webkit-scrollbar-thumb]:[border:3px solid transparent] \ - [&::-webkit-scrollbar-thumb]:[background-clip:content-box] \ - [&::-webkit-scrollbar-thumb:hover]:[background:#3A3A44] \ - [&::-webkit-scrollbar-thumb:hover]:[background-clip:content-box]" + "[scrollbar-width:thin] [scrollbar-color:#26262C transparent]" ) }