authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 22:26:54-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 22:37:36-07:00
log99c4702676053808b8621aec20d27b8f0815e627
treeafc5282ac9ed56fd803e38c941d6128678bc6d83
parentaff2de64c7f950b0e204e4c3e7e094e644847f4d
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: preserve browser accessibility identities and disabled states

Carry 64-bit AccessKit node IDs as decimal strings across the JavaScript bridge so clicks return to the original node. Use explicit true/false values for aria-disabled. Assisted-by: gpt-6.1-sol

2 files changed, 10 insertions(+), 11 deletions(-)

crates/snowbound/src/web.rs+9-10
...@@ -428,7 +428,7 @@ fn mirrored(update: &accesskit::TreeUpdate) -> js_sys::Object {...@@ -428,7 +428,7 @@ fn mirrored(update: &accesskit::TreeUpdate) -> js_sys::Object {
428 let children: js_sys::Array = node428 let children: js_sys::Array = node
429 .children()429 .children()
430 .iter()430 .iter()
431 .map(|child| JsValue::from_f64(child.0 as f64))431 .map(|child| JsValue::from_str(&child.0.to_string()))
432 .collect();432 .collect();
433 let bounds = node.bounds().map_or(JsValue::NULL, |rect| {433 let bounds = node.bounds().map_or(JsValue::NULL, |rect| {
434 js_sys::Array::of4(434 js_sys::Array::of4(
...@@ -441,7 +441,7 @@ fn mirrored(update: &accesskit::TreeUpdate) -> js_sys::Object {...@@ -441,7 +441,7 @@ fn mirrored(update: &accesskit::TreeUpdate) -> js_sys::Object {
441 });441 });
442 let text = |text: Option<&str>| text.map_or(JsValue::NULL, JsValue::from_str);442 let text = |text: Option<&str>| text.map_or(JsValue::NULL, JsValue::from_str);
443 [443 [
444 JsValue::from_f64(id.0 as f64),444 JsValue::from_str(&id.0.to_string()),
445 JsValue::from_str(&format!("{:?}", node.role())),445 JsValue::from_str(&format!("{:?}", node.role())),
446 text(node.label()),446 text(node.label()),
447 text(node.value()),447 text(node.value()),
...@@ -462,13 +462,12 @@ fn mirrored(update: &accesskit::TreeUpdate) -> js_sys::Object {...@@ -462,13 +462,12 @@ fn mirrored(update: &accesskit::TreeUpdate) -> js_sys::Object {
462 let _ = js_sys::Reflect::set(&mirrored, &key.into(), value);462 let _ = js_sys::Reflect::set(&mirrored, &key.into(), value);
463 };463 };
464 set("tree", &tree(update.tree_id));464 set("tree", &tree(update.tree_id));
465 set("focus", &JsValue::from_f64(update.focus.0 as f64));465 set("focus", &JsValue::from_str(&update.focus.0.to_string()));
466 set(466 set(
467 "root",467 "root",
468 &update468 &update.tree.as_ref().map_or(JsValue::NULL, |info| {
469 .tree469 JsValue::from_str(&info.root.0.to_string())
470 .as_ref()470 }),
471 .map_or(JsValue::NULL, |info| JsValue::from_f64(info.root.0 as f64)),
472 );471 );
473 set("nodes", &nodes);472 set("nodes", &nodes);
474 mirrored473 mirrored
...@@ -486,8 +485,8 @@ pub fn accessibility(on: bool) {...@@ -486,8 +485,8 @@ pub fn accessibility(on: bool) {
486485
487/// Assistive technology acted on node `node` of tree `tree`: 0 clicks it, 1 focuses it.486/// Assistive technology acted on node `node` of tree `tree`: 0 clicks it, 1 focuses it.
488#[wasm_bindgen]487#[wasm_bindgen]
489pub fn access(tree: String, node: f64, action: u8) {488pub fn access(tree: String, node: String, action: u8) {
490 let Ok(tree) = tree.parse() else {489 let (Ok(tree), Ok(node)) = (tree.parse(), node.parse()) else {
491 return;490 return;
492 };491 };
493 let request = accesskit::ActionRequest {492 let request = accesskit::ActionRequest {
...@@ -496,7 +495,7 @@ pub fn access(tree: String, node: f64, action: u8) {...@@ -496,7 +495,7 @@ pub fn access(tree: String, node: f64, action: u8) {
496 _ => accesskit::Action::Click,495 _ => accesskit::Action::Click,
497 },496 },
498 target_tree: accesskit::TreeId(tree),497 target_tree: accesskit::TreeId(tree),
499 target_node: accesskit::NodeId(node as u64),498 target_node: accesskit::NodeId(node),
500 data: None,499 data: None,
501 };500 };
502 send(UserEvent::Then(Box::new(move |state| {501 send(UserEvent::Then(Box::new(move |state| {
crates/snowbound/web/glue.js+1-1
...@@ -564,7 +564,7 @@ export function mirrorTree({ tree, focus, root, nodes }) {...@@ -564,7 +564,7 @@ export function mirrorTree({ tree, focus, root, nodes }) {
564 const text = role === "Label" || role === "TextRun" || role === "StaticText";564 const text = role === "Label" || role === "TextRun" || role === "StaticText";
565 if (name && !text) node.setAttribute("aria-label", name);565 if (name && !text) node.setAttribute("aria-label", name);
566 else node.removeAttribute("aria-label");566 else node.removeAttribute("aria-label");
567 node.toggleAttribute("aria-disabled", disabled);567 node.setAttribute("aria-disabled", String(disabled));
568 if (toggled === null) node.removeAttribute("aria-checked");568 if (toggled === null) node.removeAttribute("aria-checked");
569 else node.setAttribute("aria-checked", String(toggled));569 else node.setAttribute("aria-checked", String(toggled));
570 const kids = children.map(element);570 const kids = children.map(element);