From 2f9dc7b28ae45095575584486524e493678c1ab5 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Mon, 28 Sep 2026 20:40:11 -0700 Subject: [PATCH] fix(core): picker labels honour data-label, then an image's alt Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/core/src/runtime/picker.test.ts | 51 ++++++++++++++++++++++++ packages/core/src/runtime/picker.ts | 7 +++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/core/src/runtime/picker.test.ts b/packages/core/src/runtime/picker.test.ts index 20fb8b1c97..da958f0f79 100644 --- a/packages/core/src/runtime/picker.test.ts +++ b/packages/core/src/runtime/picker.test.ts @@ -882,4 +882,55 @@ describe("createPickerModule", () => { } }); }); + + describe("buildElementLabel names pictures by what the author called them", () => { + function pickLabel(el: Element): string | undefined { + Object.assign((el as HTMLElement).style, { + position: "absolute", + left: "0px", + top: "0px", + width: "40px", + height: "40px", + }); + document.body.appendChild(el); + const restore = emulateHitTest(() => [el]); + try { + const picker = createPickerModule({ postMessage: createMockPostMessage() }); + picker.installPickerApi(); + const api = ( + window as { + __HF_PICKER_API?: { pickAtPoint?: (x: number, y: number) => { label: string } | null }; + } + ).__HF_PICKER_API; + return api?.pickAtPoint?.(10, 10)?.label; + } finally { + restore(); + } + } + const img = (attrs: Record) => { + const el = document.createElement("img"); + for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value); + return el; + }; + + it("uses data-label first, then alt, then Image", () => { + expect( + pickLabel(img({ "data-label": "Player cutout (from Stadium photo)", alt: "Player" })), + ).toBe("Player cutout (from Stadium photo)"); + expect(pickLabel(img({ alt: " Player holding\n a basketball " }))).toBe( + "Player holding a basketball", + ); + expect(pickLabel(img({ alt: "" }))).toBe("Image"); + expect(pickLabel(img({ "data-label": " " }))).toBe("Image"); + }); + + it("honours data-label on any element and trims a long one", () => { + const div = document.createElement("div"); + div.setAttribute("data-label", "Intro card"); + div.textContent = "Welcome to the show"; + expect(pickLabel(div)).toBe("Intro card"); + const long = img({ alt: "x".repeat(80) }); + expect(pickLabel(long)).toBe(`${"x".repeat(55)}…`); + }); + }); }); diff --git a/packages/core/src/runtime/picker.ts b/packages/core/src/runtime/picker.ts index b598b7e397..b6313a0295 100644 --- a/packages/core/src/runtime/picker.ts +++ b/packages/core/src/runtime/picker.ts @@ -196,15 +196,18 @@ export function createPickerModule(deps: PickerModuleDeps): PickerModule { function buildElementLabel(el: Element): string { const tag = el.tagName.toLowerCase(); - const text = (el.textContent ?? "").trim().replace(/\s+/g, " "); + const words = (value: string | null) => (value ?? "").trim().replace(/\s+/g, " "); + const text = words(el.textContent); const trimLabel = (value: string, maxChars: number) => { const chars = Array.from(value); return chars.length > maxChars ? `${chars.slice(0, maxChars - 1).join("")}…` : value; }; + const authorsLabel = words(el.getAttribute("data-label")); + if (authorsLabel) return trimLabel(authorsLabel, 56); const heading = /^h[1-6]$/.test(tag); if (heading || tag === "p" || tag === "span" || tag === "div") return text.length > 0 ? trimLabel(text, 56) : heading ? "Heading" : "Text"; - if (tag === "img") return "Image"; + if (tag === "img") return trimLabel(words(el.getAttribute("alt")), 56) || "Image"; if (tag === "video") return "Video"; if (tag === "audio") return "Audio"; if (tag === "svg") return "Shape";