feat(kanban): prove accessible local reorder

req: client_local/013\nreq: accessibility/002\nreq: accessibility/003\nreq: accessibility/004\nreq: accessibility/006\nreq: milestone/001
This commit is contained in:
slhx agent
2026-07-13 14:29:21 +02:00
parent dd5ea0827f
commit 7eb65d77c8
11 changed files with 407 additions and 48 deletions
+37 -11
View File
@@ -256,7 +256,7 @@
const wire = await handler(
1,
event.type,
"value" in el ? String(el.value) : undefined,
dragKeys.get(root) || el.getAttribute("data-card-id") || ("value" in el ? String(el.value) : undefined),
"checked" in el ? Boolean(el.checked) : undefined,
event.key || undefined,
stateVersion,
@@ -265,6 +265,14 @@
if (!(wire instanceof Uint8Array)) throw new Error(`client-local hemx handler ${name} returned an invalid effect batch`);
if (!active()) return;
applyBatch(wire, root);
if (name === "reorder_card") {
const key = dragKeys.get(root) || el.getAttribute("data-card-id");
const moved = key ? firstElement(root, (node) => node.getAttribute("data-key") === key) : null;
const focus = moved && (firstElement(moved, (node) => node.tagName === "BUTTON") || moved);
if (focus && typeof focus.focus === "function") focus.focus();
if (matchMedia("(prefers-reduced-motion: reduce)").matches) root.setAttribute("data-hemx-reduced-motion", "");
else root.removeAttribute("data-hemx-reduced-motion");
}
} catch (error) {
if (!active()) return;
const fallback = el.hasAttribute("data-hemx-client-fallback");
@@ -677,14 +685,23 @@
}
function defaultEvent(el) {
if (el.getAttribute("data-hemx-on")) return el.getAttribute("data-hemx-on");
if (el.getAttribute("data-hemx-on")) return el.getAttribute("data-hemx-on").trim().split(/\s+/)[0];
if (el.tagName === "FORM") return "submit";
return "click";
}
function handlesEvent(el, name) {
const declared = el.getAttribute("data-hemx-on");
return declared ? declared.trim().split(/\s+/).includes(name) : defaultEvent(el) === name;
}
function bindRoot(root) {
["click", "submit", "input", "change", "dragstart", "dragover", "drop"].forEach((name) => {
["click", "submit", "input", "change", "keydown", "dragstart", "dragover", "drop"].forEach((name) => {
root.addEventListener(name, (event) => {
if (name === "keydown") {
const direct = closestInRoot(event.target, root, (el) => el.hasAttribute("data-hemx-client"));
if (direct && !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
}
if (name === "dragstart") {
const item = closestInRoot(event.target, root, (el) => el.hasAttribute("data-key"));
if (item) {
@@ -709,16 +726,17 @@
}
if (name === "click") {
const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(HID));
if (direct && defaultEvent(direct) === "click") {
const active = direct || closestInRoot(event.target, root, (el) => el.hasAttribute("data-hemx-client"));
if (active && handlesEvent(active, "click")) {
event.preventDefault();
if (direct.hasAttribute("data-hemx-client")) {
runClient(direct, event).catch((error) => emit(root, "hemx:client-error", {
handler: direct.getAttribute("data-hemx-client"),
if (active.hasAttribute("data-hemx-client")) {
runClient(active, event).catch((error) => emit(root, "hemx:client-error", {
handler: active.getAttribute("data-hemx-client"),
message: String(error),
fallback: false,
}));
} else {
schedule(direct, name);
schedule(active, name);
}
return;
}
@@ -737,13 +755,21 @@
}
}
let el = closestInRoot(event.target, root, (node) =>
node.hasAttribute(HID) ||
node.hasAttribute(HID) || node.hasAttribute("data-hemx-client") ||
(node.tagName === "FORM" && (pageFormHistoryMode(event.submitter || node, node) || boostRoot(node)))
);
if (name === "submit" && !el && event.target && event.target.tagName === "FORM" && (formHandleId(event.target) || pageFormHistoryMode(event.submitter || event.target, event.target))) el = event.target;
if (!el || defaultEvent(el) !== name) return;
if (!el || !handlesEvent(el, name)) return;
event.preventDefault();
schedule(el, name, event.submitter || el);
if (el.hasAttribute("data-hemx-client")) {
runClient(el, event).catch((error) => emit(root, "hemx:client-error", {
handler: el.getAttribute("data-hemx-client"),
message: String(error),
fallback: false,
}));
} else {
schedule(el, name, event.submitter || el);
}
});
});
bindPolling(root);