From 640dadaa75a874b5546427499deb0eb05f22eb7f Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 5 Sep 2026 20:52:29 +0530 Subject: [PATCH] Use compact accessible SVG actions for selected blocks --- packages/block-editor/package.json | 2 +- packages/block-editor/src/live-canvas.ts | 29 +++++++++++++++---- .../test/selection-toolbar.test.ts | 22 ++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 packages/block-editor/test/selection-toolbar.test.ts diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index a3536ec3..c86d2ffa 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/block-editor", - "version": "0.1.9", + "version": "0.1.10", "type": "module", "description": "Hydrated accessible editor runtime for WRNexus block documents.", "exports": { diff --git a/packages/block-editor/src/live-canvas.ts b/packages/block-editor/src/live-canvas.ts index cf7c209f..49023b0e 100644 --- a/packages/block-editor/src/live-canvas.ts +++ b/packages/block-editor/src/live-canvas.ts @@ -21,16 +21,33 @@ export function showBuilderBlockActions(canvas: Document, id: string, label: str toolbar.dataset.builderBlockActions = ""; toolbar.setAttribute("role", "toolbar"); 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"); - name.textContent = label; - name.style.cssText = "margin-inline-end:auto"; + name.textContent = label.split(" — ")[0]; + name.title = label; + name.style.cssText = "padding:0 6px;max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"; toolbar.append(name); for (const [name, action] of Object.entries(actions)) { const button = canvas.createElement("button"); button.type = "button"; - button.textContent = name; - button.style.cssText = "border:1px solid #64748b;border-radius:4px;background:#1f2937;color:white;padding:5px 8px;cursor:pointer"; + button.setAttribute("aria-label", name); + button.title = name; + const paths: Record = { + 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(); }); toolbar.append(button); } @@ -38,7 +55,7 @@ export function showBuilderBlockActions(canvas: Document, id: string, label: str canvas.body.prepend(toolbar); const bounds = block.getBoundingClientRect(); 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"); } diff --git a/packages/block-editor/test/selection-toolbar.test.ts b/packages/block-editor/test/selection-toolbar.test.ts new file mode 100644 index 00000000..ab15bb8d --- /dev/null +++ b/packages/block-editor/test/selection-toolbar.test.ts @@ -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 = '
Content
'; + let calls = 0; + showBuilderBlockActions(document as unknown as Document, "text", "Text — ", { Edit: () => calls++, Delete: () => calls++ }); + const toolbar = document.querySelector('[role="toolbar"]')!; + expect(toolbar.getAttribute("aria-label")).toBe("Text — 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(); +});