@artymclabin/qa-review 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,31 @@ import { type QAJourneyConfig } from "./journey.js";
4
4
  /** Was the pointer gesture a click (vs a drag)? Exported for tests. */
5
5
  export declare function isClickGesture(dx: number, dy: number): boolean;
6
6
  /** Note-reference text for a picked element: « label » (truncated) or tag. */
7
- export declare function pickedElementRef(textContent: string | null, tagName: string): string;
7
+ export interface PickedElementDescriptor {
8
+ textContent?: string | null;
9
+ tagName: string;
10
+ /** aria-label, or the aria-labelledby target's text. */
11
+ ariaLabel?: string | null;
12
+ /** Text of an associated <label>, whether wrapping or `for=`-linked. */
13
+ labelText?: string | null;
14
+ placeholder?: string | null;
15
+ alt?: string | null;
16
+ title?: string | null;
17
+ /** Current value, for a control the reviewer has already filled in. */
18
+ value?: string | null;
19
+ name?: string | null;
20
+ }
21
+ /**
22
+ * 🚨 A form control has no `textContent`, so reading only that made every input,
23
+ * textarea and select come out as its bare tag name: a reviewer picking seven link
24
+ * fields got seven « input » refs with no way to say which row they meant. The
25
+ * order below is "what a human would call it" - its own text, then the accessible
26
+ * name, then the label a designer wrote, then what it holds - and the tag name only
27
+ * when the element genuinely has no name at all.
28
+ */
29
+ export declare function pickedElementRef(el: PickedElementDescriptor): string;
30
+ /** Read a live DOM element into the descriptor above. */
31
+ export declare function describePickedElement(el: Element): PickedElementDescriptor;
8
32
  /**
9
33
  * Element-pick mode semantics (0.3.1): LEFT click picks and exits the mode;
10
34
  * RIGHT click picks and STAYS for multi-select (context menu suppressed).
@@ -52,11 +52,63 @@ const CLICK_DRAG_THRESHOLD_PX = 6;
52
52
  export function isClickGesture(dx, dy) {
53
53
  return Math.hypot(dx, dy) < CLICK_DRAG_THRESHOLD_PX;
54
54
  }
55
- /** Note-reference text for a picked element: « label » (truncated) or tag. */
56
- export function pickedElementRef(textContent, tagName) {
57
- const label = (textContent || "").replace(/\s+/g, " ").trim().slice(0, 48) || tagName.toLowerCase();
55
+ /**
56
+ * 🚨 A form control has no `textContent`, so reading only that made every input,
57
+ * textarea and select come out as its bare tag name: a reviewer picking seven link
58
+ * fields got seven « input » refs with no way to say which row they meant. The
59
+ * order below is "what a human would call it" - its own text, then the accessible
60
+ * name, then the label a designer wrote, then what it holds - and the tag name only
61
+ * when the element genuinely has no name at all.
62
+ */
63
+ export function pickedElementRef(el) {
64
+ const clean = (v) => (v || "").replace(/\s+/g, " ").trim();
65
+ const label = [
66
+ clean(el.textContent),
67
+ clean(el.ariaLabel),
68
+ clean(el.labelText),
69
+ clean(el.placeholder),
70
+ clean(el.alt),
71
+ clean(el.title),
72
+ clean(el.value),
73
+ clean(el.name),
74
+ ]
75
+ .find((c) => c.length > 0)
76
+ ?.slice(0, 48) || el.tagName.toLowerCase();
58
77
  return ` «${label}» `;
59
78
  }
79
+ /** Read a live DOM element into the descriptor above. */
80
+ export function describePickedElement(el) {
81
+ const labelledBy = el.getAttribute("aria-labelledby");
82
+ const labelledByText = labelledBy
83
+ ? labelledBy
84
+ .split(/\s+/)
85
+ .map((id) => el.ownerDocument?.getElementById(id)?.textContent ?? "")
86
+ .join(" ")
87
+ : null;
88
+ // A control's label is either an ancestor <label> or one pointing at its id.
89
+ let labelText = null;
90
+ const id = el.getAttribute("id");
91
+ if (id) {
92
+ const escaped = id.replace(/["\\]/g, "\\$&");
93
+ labelText = el.ownerDocument?.querySelector(`label[for="${escaped}"]`)?.textContent ?? null;
94
+ }
95
+ if (!labelText)
96
+ labelText = el.closest("label")?.textContent ?? null;
97
+ const isControl = ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName);
98
+ return {
99
+ // A control's own textContent is empty anyway; skipping it explicitly keeps the
100
+ // intent readable rather than relying on that emptiness.
101
+ textContent: isControl ? null : el.textContent,
102
+ tagName: el.tagName,
103
+ ariaLabel: el.getAttribute("aria-label") || labelledByText,
104
+ labelText,
105
+ placeholder: el.getAttribute("placeholder"),
106
+ alt: el.getAttribute("alt"),
107
+ title: el.getAttribute("title"),
108
+ value: isControl ? (el.value ?? null) : null,
109
+ name: el.getAttribute("name"),
110
+ };
111
+ }
60
112
  /**
61
113
  * Element-pick mode semantics (0.3.1): LEFT click picks and exits the mode;
62
114
  * RIGHT click picks and STAYS for multi-select (context menu suppressed).
@@ -378,7 +430,7 @@ export function QAReviewOverlay({ items, target, gateParam, stateUrl, submitUrl,
378
430
  return; // ignore the panel itself
379
431
  e.preventDefault();
380
432
  e.stopPropagation();
381
- const ref = pickedElementRef(el.textContent, el.tagName);
433
+ const ref = pickedElementRef(describePickedElement(el));
382
434
  const ta = noteRef.current;
383
435
  if (ta) {
384
436
  const s = ta.selectionStart ?? note.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artymclabin/qa-review",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Interactive on-page QA review overlay (spotlight walkthrough, approve/reject verdict ledger) with a framework-agnostic server handler factory and a self-provisioning Postgres store.",
5
5
  "license": "MIT",
6
6
  "repository": {