Use compact accessible SVG actions for selected blocks
This commit is contained in:
1 parent
eb1875f0bc
commit
640dadaa75
3 files changed
+46
-7
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/block-editor",
|
"name": "@wrnexus/block-editor",
|
||||||
"version": "0.1.9",
|
"version": "0.1.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Hydrated accessible editor runtime for WRNexus block documents.",
|
"description": "Hydrated accessible editor runtime for WRNexus block documents.",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -21,16 +21,33 @@ export function showBuilderBlockActions(canvas: Document, id: string, label: str
|
|||||||
toolbar.dataset.builderBlockActions = "";
|
toolbar.dataset.builderBlockActions = "";
|
||||||
toolbar.setAttribute("role", "toolbar");
|
toolbar.setAttribute("role", "toolbar");
|
||||||
toolbar.setAttribute("aria-label", label + " actions");
|
toolbar.setAttribute("aria-label", label + " actions");
|
||||||
toolbar.style.cssText = "position:absolute;z-index:100;display:flex;flex-wrap:wrap;gap:4px;align-items:center;padding:6px;box-sizing:border-box;max-width:100%;background:#111827;color:white;font:12px Arial;box-shadow:0 2px 8px #0003";
|
toolbar.style.cssText = "position:absolute;z-index:100;display:flex;gap:2px;align-items:center;padding:3px;box-sizing:border-box;max-width:100%;border-radius:6px;background:#7c3aed;color:white;font:11px Arial;box-shadow:0 3px 12px #0003";
|
||||||
const name = canvas.createElement("strong");
|
const name = canvas.createElement("strong");
|
||||||
name.textContent = label;
|
name.textContent = label.split(" — ")[0];
|
||||||
name.style.cssText = "margin-inline-end:auto";
|
name.title = label;
|
||||||
|
name.style.cssText = "padding:0 6px;max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
|
||||||
toolbar.append(name);
|
toolbar.append(name);
|
||||||
for (const [name, action] of Object.entries(actions)) {
|
for (const [name, action] of Object.entries(actions)) {
|
||||||
const button = canvas.createElement("button");
|
const button = canvas.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.textContent = name;
|
button.setAttribute("aria-label", name);
|
||||||
button.style.cssText = "border:1px solid #64748b;border-radius:4px;background:#1f2937;color:white;padding:5px 8px;cursor:pointer";
|
button.title = name;
|
||||||
|
const paths: Record<string, string> = {
|
||||||
|
Edit: "m4 16 12-12 4 4-12 12H4z M13 7l4 4",
|
||||||
|
"Move up": "M12 20V4 M5 11l7-7 7 7",
|
||||||
|
"Move down": "M12 4v16 M5 13l7 7 7-7",
|
||||||
|
Duplicate: "M8 8h12v12H8z M16 4H4v12",
|
||||||
|
Delete: "M4 6h16 M9 6V3h6v3 M6 6l1 15h10l1-15 M10 10v7 M14 10v7",
|
||||||
|
};
|
||||||
|
if (paths[name]) {
|
||||||
|
const icon = canvas.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
for (const [key, value] of Object.entries({ width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.7", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" })) icon.setAttribute(key, value);
|
||||||
|
const path = canvas.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
path.setAttribute("d", paths[name]);
|
||||||
|
icon.append(path);
|
||||||
|
button.append(icon);
|
||||||
|
} else button.textContent = name;
|
||||||
|
button.style.cssText = "border:0;border-radius:3px;background:transparent;color:white;min-width:28px;height:28px;display:grid;place-items:center;cursor:pointer";
|
||||||
button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); action(); });
|
button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); action(); });
|
||||||
toolbar.append(button);
|
toolbar.append(button);
|
||||||
}
|
}
|
||||||
@@ -38,7 +55,7 @@ export function showBuilderBlockActions(canvas: Document, id: string, label: str
|
|||||||
canvas.body.prepend(toolbar);
|
canvas.body.prepend(toolbar);
|
||||||
const bounds = block.getBoundingClientRect();
|
const bounds = block.getBoundingClientRect();
|
||||||
toolbar.style.top = Math.max(0, bounds.top + (canvas.defaultView?.scrollY ?? 0) - toolbar.offsetHeight) + "px";
|
toolbar.style.top = Math.max(0, bounds.top + (canvas.defaultView?.scrollY ?? 0) - toolbar.offsetHeight) + "px";
|
||||||
toolbar.style.left = Math.max(0, Math.min(bounds.left, canvas.documentElement.clientWidth - toolbar.offsetWidth)) + "px";
|
toolbar.style.left = Math.max(0, Math.min(bounds.right - toolbar.offsetWidth, canvas.documentElement.clientWidth - toolbar.offsetWidth)) + "px";
|
||||||
block.setAttribute("aria-label", label + " selected");
|
block.setAttribute("aria-label", label + " selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { Window } from "happy-dom";
|
||||||
|
import { showBuilderBlockActions } from "../src/live-canvas.ts";
|
||||||
|
|
||||||
|
test("compact selection actions retain accessible names and callbacks without interpreting labels", () => {
|
||||||
|
const window = new Window(), document = window.document;
|
||||||
|
document.body.innerHTML = '<div data-block-id="text">Content</div>';
|
||||||
|
let calls = 0;
|
||||||
|
showBuilderBlockActions(document as unknown as Document, "text", "Text — <script>example</script>", { Edit: () => calls++, Delete: () => calls++ });
|
||||||
|
const toolbar = document.querySelector('[role="toolbar"]')!;
|
||||||
|
expect(toolbar.getAttribute("aria-label")).toBe("Text — <script>example</script> actions");
|
||||||
|
expect(toolbar.querySelector("strong")?.textContent).toBe("Text");
|
||||||
|
expect(toolbar.querySelector("script")).toBeNull();
|
||||||
|
const edit = toolbar.querySelector('[aria-label="Edit"]') as any;
|
||||||
|
expect(edit.title).toBe("Edit");
|
||||||
|
expect(edit.querySelector("svg")).not.toBeNull();
|
||||||
|
edit.click();
|
||||||
|
expect(calls).toBe(1);
|
||||||
|
showBuilderBlockActions(document as unknown as Document, "text", "Text", { Edit: () => calls++ });
|
||||||
|
expect(document.querySelectorAll('[role="toolbar"]')).toHaveLength(1);
|
||||||
|
window.happyDOM.abort();
|
||||||
|
});
|
||||||
Reference in new issue
Block a user