@hypen-space/web 0.5.2 → 0.5.3
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.
- package/dist/canvas/accessibility.d.ts +118 -11
- package/dist/canvas/accessibility.js +470 -108
- package/dist/canvas/accessibility.js.map +1 -1
- package/dist/canvas/dispatch.d.ts +24 -0
- package/dist/canvas/dispatch.js +62 -0
- package/dist/canvas/dispatch.js.map +1 -0
- package/dist/canvas/editing.d.ts +178 -0
- package/dist/canvas/editing.js +590 -0
- package/dist/canvas/editing.js.map +1 -0
- package/dist/canvas/events.d.ts +28 -23
- package/dist/canvas/events.js +76 -128
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/focus.d.ts +78 -0
- package/dist/canvas/focus.js +182 -0
- package/dist/canvas/focus.js.map +1 -0
- package/dist/canvas/index.d.ts +3 -1
- package/dist/canvas/index.js +3 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/paint.d.ts +16 -3
- package/dist/canvas/paint.js +283 -41
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +20 -1
- package/dist/canvas/renderer.js +181 -50
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/selection.js +31 -177
- package/dist/canvas/selection.js.map +1 -1
- package/dist/canvas/text-geometry.d.ts +72 -0
- package/dist/canvas/text-geometry.js +244 -0
- package/dist/canvas/text-geometry.js.map +1 -0
- package/dist/canvas/text.js +42 -9
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +7 -1
- package/dist/canvas/variants.d.ts +4 -1
- package/dist/canvas/variants.js +22 -3
- package/dist/canvas/variants.js.map +1 -1
- package/dist/dom/a11y-styles.d.ts +20 -0
- package/dist/dom/a11y-styles.js +61 -0
- package/dist/dom/a11y-styles.js.map +1 -0
- package/dist/dom/applicators/aria.d.ts +19 -0
- package/dist/dom/applicators/aria.js +36 -0
- package/dist/dom/applicators/aria.js.map +1 -0
- package/dist/dom/applicators/events.d.ts +7 -0
- package/dist/dom/applicators/events.js +35 -8
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/index.js +4 -0
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/components/hypenapp.d.ts +25 -1
- package/dist/dom/components/hypenapp.js +213 -245
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/index.js +7 -0
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/spinner.js +24 -10
- package/dist/dom/components/spinner.js.map +1 -1
- package/dist/dom/components/tabs.d.ts +27 -0
- package/dist/dom/components/tabs.js +69 -0
- package/dist/dom/components/tabs.js.map +1 -0
- package/dist/dom/components/visuallyhidden.d.ts +9 -0
- package/dist/dom/components/visuallyhidden.js +26 -0
- package/dist/dom/components/visuallyhidden.js.map +1 -0
- package/dist/dom/operability.d.ts +100 -0
- package/dist/dom/operability.js +298 -0
- package/dist/dom/operability.js.map +1 -0
- package/dist/dom/renderer.d.ts +98 -5
- package/dist/dom/renderer.js +398 -63
- package/dist/dom/renderer.js.map +1 -1
- package/dist/dom/route-focus.d.ts +42 -0
- package/dist/dom/route-focus.js +88 -0
- package/dist/dom/route-focus.js.map +1 -0
- package/dist/dom/semantics.d.ts +35 -0
- package/dist/dom/semantics.js +184 -0
- package/dist/dom/semantics.js.map +1 -0
- package/package.json +2 -2
- package/src/canvas/QUICKSTART.md +4 -4
- package/src/canvas/README.md +55 -29
- package/src/canvas/accessibility.ts +507 -115
- package/src/canvas/dispatch.ts +78 -0
- package/src/canvas/editing.ts +697 -0
- package/src/canvas/events.ts +89 -142
- package/src/canvas/focus.ts +216 -0
- package/src/canvas/index.ts +8 -1
- package/src/canvas/paint.ts +339 -42
- package/src/canvas/renderer.ts +218 -70
- package/src/canvas/selection.ts +52 -210
- package/src/canvas/text-geometry.ts +311 -0
- package/src/canvas/text.ts +43 -9
- package/src/canvas/types.ts +14 -1
- package/src/canvas/variants.ts +24 -5
- package/src/dom/a11y-styles.ts +65 -0
- package/src/dom/applicators/aria.ts +41 -0
- package/src/dom/applicators/events.ts +38 -7
- package/src/dom/applicators/index.ts +5 -0
- package/src/dom/components/hypenapp.ts +244 -272
- package/src/dom/components/index.ts +7 -0
- package/src/dom/components/spinner.ts +31 -13
- package/src/dom/components/tabs.ts +76 -0
- package/src/dom/components/visuallyhidden.ts +30 -0
- package/src/dom/operability.ts +312 -0
- package/src/dom/renderer.ts +455 -63
- package/src/dom/route-focus.ts +98 -0
- package/src/dom/semantics.ts +199 -0
- package/dist/canvas/input.d.ts +0 -76
- package/dist/canvas/input.js +0 -207
- package/dist/canvas/input.js.map +0 -1
- package/src/canvas/input.ts +0 -251
package/src/canvas/text.ts
CHANGED
|
@@ -36,9 +36,15 @@ function isPretextAvailable(): boolean {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Text metrics cache
|
|
39
|
+
* Text metrics cache — bounded LRU. Every distinct (text, font, width,
|
|
40
|
+
* clamp) combination measured on the layout/paint hot path lands here, so
|
|
41
|
+
* without a cap a long-lived session (live feeds, ticking clocks, per-width
|
|
42
|
+
* generations from resizes) grows it forever. Map iteration order is
|
|
43
|
+
* insertion order; hits re-insert to keep hot entries at the tail and the
|
|
44
|
+
* oldest entry is evicted past the cap.
|
|
40
45
|
*/
|
|
41
46
|
const textMetricsCache = new Map<string, TextMetrics>();
|
|
47
|
+
const MAX_TEXT_METRICS_CACHE_SIZE = 4096;
|
|
42
48
|
|
|
43
49
|
/**
|
|
44
50
|
* Get cache key for text metrics
|
|
@@ -55,6 +61,13 @@ function wrapTextFallback(ctx: CanvasRenderingContext2D, text: string, maxWidth:
|
|
|
55
61
|
const paragraphs = text.split("\n");
|
|
56
62
|
|
|
57
63
|
for (const paragraph of paragraphs) {
|
|
64
|
+
// An empty paragraph is a blank line — dropping it would collapse
|
|
65
|
+
// consecutive newlines (and a trailing Enter in a Textarea).
|
|
66
|
+
if (paragraph === "") {
|
|
67
|
+
lines.push("");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
const words = paragraph.split(" ");
|
|
59
72
|
let currentLine = "";
|
|
60
73
|
|
|
@@ -70,9 +83,7 @@ function wrapTextFallback(ctx: CanvasRenderingContext2D, text: string, maxWidth:
|
|
|
70
83
|
}
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
|
|
74
|
-
lines.push(currentLine);
|
|
75
|
-
}
|
|
86
|
+
lines.push(currentLine);
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
return lines.length > 0 ? lines : [""];
|
|
@@ -98,7 +109,12 @@ export function measureText(
|
|
|
98
109
|
): TextMetrics {
|
|
99
110
|
const cacheKey = `${getCacheKey(text, fontStyle, maxWidth)}|${maxLines ?? ""}|${textOverflow ?? ""}`;
|
|
100
111
|
const cached = textMetricsCache.get(cacheKey);
|
|
101
|
-
if (cached)
|
|
112
|
+
if (cached) {
|
|
113
|
+
// Refresh recency so steady-state entries survive eviction.
|
|
114
|
+
textMetricsCache.delete(cacheKey);
|
|
115
|
+
textMetricsCache.set(cacheKey, cached);
|
|
116
|
+
return cached;
|
|
117
|
+
}
|
|
102
118
|
|
|
103
119
|
const font = createFontString(fontStyle.fontSize, fontStyle.fontWeight, fontStyle.fontFamily);
|
|
104
120
|
const lineHeight = fontStyle.lineHeight || fontStyle.fontSize * 1.2;
|
|
@@ -107,11 +123,25 @@ export function measureText(
|
|
|
107
123
|
let width: number;
|
|
108
124
|
|
|
109
125
|
if (isPretextAvailable()) {
|
|
110
|
-
const prepared = prepareWithSegments(text, font);
|
|
111
126
|
const effectiveMaxWidth = maxWidth || Infinity;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
// Hard line breaks: pretext treats "\n" as ordinary whitespace, so
|
|
128
|
+
// paragraphs are laid out separately (matching the fallback path and
|
|
129
|
+
// what Textarea editing needs). Empty paragraphs stay as blank lines.
|
|
130
|
+
lines = [];
|
|
131
|
+
width = 0;
|
|
132
|
+
for (const paragraph of text.split("\n")) {
|
|
133
|
+
if (paragraph === "") {
|
|
134
|
+
lines.push("");
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const prepared = prepareWithSegments(paragraph, font);
|
|
138
|
+
const linesResult = layoutWithLines(prepared, effectiveMaxWidth, lineHeight);
|
|
139
|
+
const paragraphLines = linesResult.lines.map((l) => l.text);
|
|
140
|
+
lines.push(...(paragraphLines.length > 0 ? paragraphLines : [""]));
|
|
141
|
+
for (const l of linesResult.lines) {
|
|
142
|
+
width = Math.max(width, l.width);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
115
145
|
if (lines.length === 0) lines = [""];
|
|
116
146
|
} else {
|
|
117
147
|
ctx.save();
|
|
@@ -170,6 +200,10 @@ export function measureText(
|
|
|
170
200
|
};
|
|
171
201
|
|
|
172
202
|
textMetricsCache.set(cacheKey, result);
|
|
203
|
+
if (textMetricsCache.size > MAX_TEXT_METRICS_CACHE_SIZE) {
|
|
204
|
+
const oldest = textMetricsCache.keys().next().value;
|
|
205
|
+
if (oldest !== undefined) textMetricsCache.delete(oldest);
|
|
206
|
+
}
|
|
173
207
|
return result;
|
|
174
208
|
}
|
|
175
209
|
|
package/src/canvas/types.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Shared type definitions for the canvas renderer
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { Semantics } from "@hypen-space/core/types";
|
|
8
|
+
|
|
7
9
|
export interface VirtualNode {
|
|
8
10
|
id: string;
|
|
9
11
|
type: string;
|
|
@@ -11,6 +13,13 @@ export interface VirtualNode {
|
|
|
11
13
|
children: VirtualNode[];
|
|
12
14
|
parent: VirtualNode | null;
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Engine-derived accessibility semantics (role, name, hidden, …) carried
|
|
18
|
+
* from the Create patch. Drives the transparent accessibility overlay,
|
|
19
|
+
* since the canvas bitmap itself exposes nothing to assistive technology.
|
|
20
|
+
*/
|
|
21
|
+
semantics?: Semantics;
|
|
22
|
+
|
|
14
23
|
// Computed layout
|
|
15
24
|
layout?: Layout;
|
|
16
25
|
|
|
@@ -112,9 +121,13 @@ export interface CanvasRendererOptions {
|
|
|
112
121
|
backgroundColor?: string;
|
|
113
122
|
|
|
114
123
|
// Features
|
|
124
|
+
//
|
|
125
|
+
// enableAccessibility also gates text-input editing and keyboard focus:
|
|
126
|
+
// the accessibility mirror (a transparent positioned overlay above the
|
|
127
|
+
// canvas) is the renderer's focus system, and Input/Textarea edit
|
|
128
|
+
// sessions start from mirror focus.
|
|
115
129
|
enableAccessibility?: boolean;
|
|
116
130
|
enableHitTesting?: boolean;
|
|
117
|
-
enableInputOverlay?: boolean;
|
|
118
131
|
|
|
119
132
|
// Performance
|
|
120
133
|
enableDirtyRects?: boolean;
|
package/src/canvas/variants.ts
CHANGED
|
@@ -87,10 +87,20 @@ function activeStatesFor(node: VirtualNode): ActiveStates {
|
|
|
87
87
|
*
|
|
88
88
|
* No-op for nodes without any variant keys (the common case), so the per-frame
|
|
89
89
|
* cost on a plain tree is one cached set lookup per node.
|
|
90
|
+
*
|
|
91
|
+
* Returns true when any base value differs from the previous frame's
|
|
92
|
+
* effective value — the renderer uses this to decide whether layout must
|
|
93
|
+
* re-run (variant winners can change spacing/size, not just paint props).
|
|
90
94
|
*/
|
|
91
|
-
function applyVariantsToNode(node: VirtualNode, width: number):
|
|
95
|
+
function applyVariantsToNode(node: VirtualNode, width: number): boolean {
|
|
92
96
|
const bases = getVariantBases(node);
|
|
93
|
-
if (bases.size === 0) return;
|
|
97
|
+
if (bases.size === 0) return false;
|
|
98
|
+
|
|
99
|
+
// Snapshot the previous frame's effective values for change detection.
|
|
100
|
+
const previous: Record<string, unknown> = {};
|
|
101
|
+
for (const base of bases) {
|
|
102
|
+
previous[base] = node.props[base];
|
|
103
|
+
}
|
|
94
104
|
|
|
95
105
|
// Restore originals from the previous frame so we resolve from a clean base.
|
|
96
106
|
if (node.variantOriginals) {
|
|
@@ -123,18 +133,27 @@ function applyVariantsToNode(node: VirtualNode, width: number): void {
|
|
|
123
133
|
node.props[base] = resolved[base];
|
|
124
134
|
}
|
|
125
135
|
}
|
|
136
|
+
|
|
137
|
+
for (const base of bases) {
|
|
138
|
+
if (node.props[base] !== previous[base]) return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
126
141
|
}
|
|
127
142
|
|
|
128
143
|
/**
|
|
129
144
|
* Walk the tree from `root` and apply variant overrides to every node using
|
|
130
145
|
* `width` (the canvas/content width) as the viewport for breakpoint matching.
|
|
131
146
|
* Called once per frame before layout.
|
|
147
|
+
*
|
|
148
|
+
* Returns true when any node's effective props changed since the previous
|
|
149
|
+
* frame (breakpoint crossed, hover/focus/active/disabled state flipped).
|
|
132
150
|
*/
|
|
133
|
-
export function applyVariants(root: VirtualNode, width: number):
|
|
134
|
-
applyVariantsToNode(root, width);
|
|
151
|
+
export function applyVariants(root: VirtualNode, width: number): boolean {
|
|
152
|
+
let changed = applyVariantsToNode(root, width);
|
|
135
153
|
for (const child of root.children) {
|
|
136
|
-
applyVariants(child, width);
|
|
154
|
+
if (applyVariants(child, width)) changed = true;
|
|
137
155
|
}
|
|
156
|
+
return changed;
|
|
138
157
|
}
|
|
139
158
|
|
|
140
159
|
/**
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global accessibility stylesheet.
|
|
3
|
+
*
|
|
4
|
+
* Injects a single <style> element (idempotent, guarded by id) that:
|
|
5
|
+
* - honours `prefers-reduced-motion: reduce` by disabling transitions,
|
|
6
|
+
* animations and smooth scrolling on Hypen-rendered nodes;
|
|
7
|
+
* - renders a visible `:focus-visible` outline so keyboard users can see
|
|
8
|
+
* which element is focused;
|
|
9
|
+
* - keeps that focus indicator visible under `forced-colors: active`
|
|
10
|
+
* (Windows High Contrast), which strips box-shadow but honours outline; and
|
|
11
|
+
* - strengthens the outline under `prefers-contrast: more`.
|
|
12
|
+
*
|
|
13
|
+
* Scoped to `[data-hypen-id]` so it only affects Hypen-rendered elements and
|
|
14
|
+
* never the host page.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const STYLE_ID = "hypen-a11y-styles";
|
|
18
|
+
|
|
19
|
+
const A11Y_CSS = `
|
|
20
|
+
@media (prefers-reduced-motion: reduce) {
|
|
21
|
+
[data-hypen-id] {
|
|
22
|
+
transition: none !important;
|
|
23
|
+
animation: none !important;
|
|
24
|
+
scroll-behavior: auto !important;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
[data-hypen-id]:focus-visible {
|
|
28
|
+
outline: 2px solid #1a73e8;
|
|
29
|
+
outline-offset: 2px;
|
|
30
|
+
}
|
|
31
|
+
@media (forced-colors: active) {
|
|
32
|
+
/* Forced-colors mode removes box-shadow and author colors but honours
|
|
33
|
+
outline: pin the focus indicator to an outline in the system Highlight
|
|
34
|
+
color, and drop any box-shadow-based indicator a component style set. */
|
|
35
|
+
[data-hypen-id]:focus-visible {
|
|
36
|
+
outline: 2px solid Highlight;
|
|
37
|
+
box-shadow: none;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
@media (prefers-contrast: more) {
|
|
41
|
+
/* A thicker outline for users who asked for higher contrast. */
|
|
42
|
+
[data-hypen-id]:focus-visible {
|
|
43
|
+
outline-width: 3px;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Inject the accessibility stylesheet once. Safe to call repeatedly and in
|
|
50
|
+
* non-DOM environments (server/tests without `document`), where it no-ops.
|
|
51
|
+
*/
|
|
52
|
+
export function ensureA11yStyles(): void {
|
|
53
|
+
if (typeof document === "undefined" || !document.head) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof document.getElementById === "function" && document.getElementById(STYLE_ID)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const style = document.createElement("style");
|
|
62
|
+
style.id = STYLE_ID;
|
|
63
|
+
style.textContent = A11Y_CSS;
|
|
64
|
+
document.head.appendChild(style);
|
|
65
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARIA escape hatch applicator
|
|
3
|
+
*
|
|
4
|
+
* `.aria(key, value)` sets an arbitrary `aria-<key>` attribute on the element,
|
|
5
|
+
* e.g. `.aria("expanded", "true")` -> `aria-expanded="true"`.
|
|
6
|
+
*
|
|
7
|
+
* WEB-ONLY / NON-PORTABLE: this is a raw DOM escape hatch. It bypasses the
|
|
8
|
+
* engine-derived typed `Semantics` block, so the value does NOT reach the
|
|
9
|
+
* Canvas, iOS or Android renderers. Prefer the portable accessibility
|
|
10
|
+
* applicators (e.g. `.role()`, `.label()`) when one exists; reach for `.aria`
|
|
11
|
+
* only for DOM-specific ARIA attributes the typed semantics do not cover.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ApplicatorHandler } from "./types.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The grouped args arrive as `{ "0": key, "1": value }` for `.aria(key, value)`.
|
|
18
|
+
* A single positional arg (`.aria("busy")`) arrives as the bare key string.
|
|
19
|
+
*/
|
|
20
|
+
export const ariaHandler: ApplicatorHandler = (el, value) => {
|
|
21
|
+
let key: unknown;
|
|
22
|
+
let attrValue: unknown = "";
|
|
23
|
+
|
|
24
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
25
|
+
const args = value as Record<string, unknown>;
|
|
26
|
+
key = args["0"];
|
|
27
|
+
attrValue = "1" in args ? args["1"] : "";
|
|
28
|
+
} else {
|
|
29
|
+
key = value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (key === undefined || key === null || key === "") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
el.setAttribute("aria-" + String(key), String(attrValue));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const ariaHandlers: Record<string, ApplicatorHandler> = {
|
|
40
|
+
aria: ariaHandler,
|
|
41
|
+
};
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
unregisterEvent,
|
|
22
22
|
getKeyTarget,
|
|
23
23
|
setKeyTarget,
|
|
24
|
+
getMeta,
|
|
25
|
+
setMeta,
|
|
24
26
|
} from "../element-data.js";
|
|
25
27
|
|
|
26
28
|
const log = frameworkLoggers.events;
|
|
@@ -197,6 +199,7 @@ function createEventHandler(
|
|
|
197
199
|
eventType: string,
|
|
198
200
|
options: EventHandlerOptions = {}
|
|
199
201
|
): ApplicatorHandler {
|
|
202
|
+
const actionMetaKey = `event:${eventType}`;
|
|
200
203
|
return (element: HTMLElement, value: unknown) => {
|
|
201
204
|
const { actionName, payload: customPayload } = extractActionDetails(value);
|
|
202
205
|
|
|
@@ -205,14 +208,18 @@ function createEventHandler(
|
|
|
205
208
|
return;
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
// Store the current action on the element; the persistent listener reads
|
|
212
|
+
// it per event, so re-applying with a different action (e.g. a keyed-
|
|
213
|
+
// reused node) replaces the dispatch target instead of stacking listeners.
|
|
214
|
+
setMeta(element, actionMetaKey, { actionName, customPayload });
|
|
215
|
+
|
|
208
216
|
const disposables = getElementDisposables(element);
|
|
209
217
|
|
|
210
218
|
// Track that we've registered this event type
|
|
211
219
|
// The disposable stack handles cleanup automatically
|
|
212
|
-
const eventKey =
|
|
220
|
+
const eventKey = eventType;
|
|
213
221
|
if (getRegisteredEvents(element).has(eventKey)) {
|
|
214
|
-
//
|
|
215
|
-
// This can happen during re-renders
|
|
222
|
+
// Listener already installed — the meta update above retargets it
|
|
216
223
|
return;
|
|
217
224
|
}
|
|
218
225
|
registerEvent(element, eventKey);
|
|
@@ -222,6 +229,12 @@ function createEventHandler(
|
|
|
222
229
|
|
|
223
230
|
// Create the event listener
|
|
224
231
|
const listener = (event: Event) => {
|
|
232
|
+
const current = getMeta<{ actionName: string; customPayload: Record<string, unknown> }>(
|
|
233
|
+
element,
|
|
234
|
+
actionMetaKey,
|
|
235
|
+
);
|
|
236
|
+
if (!current) return;
|
|
237
|
+
|
|
225
238
|
// Handle throttling
|
|
226
239
|
if (options.throttleMs && throttleTimer) {
|
|
227
240
|
return;
|
|
@@ -240,8 +253,8 @@ function createEventHandler(
|
|
|
240
253
|
|
|
241
254
|
// Build payload
|
|
242
255
|
const payload =
|
|
243
|
-
Object.keys(customPayload).length > 0
|
|
244
|
-
? { ...customPayload }
|
|
256
|
+
Object.keys(current.customPayload).length > 0
|
|
257
|
+
? { ...current.customPayload }
|
|
245
258
|
: options.extractPayload
|
|
246
259
|
? options.extractPayload(event, element)
|
|
247
260
|
: extractEventData(event, element);
|
|
@@ -250,9 +263,9 @@ function createEventHandler(
|
|
|
250
263
|
const engine = getEngine(element);
|
|
251
264
|
if (engine) {
|
|
252
265
|
try {
|
|
253
|
-
engine.dispatchAction(actionName, payload);
|
|
266
|
+
engine.dispatchAction(current.actionName, payload);
|
|
254
267
|
} catch (err) {
|
|
255
|
-
log.error(`Error dispatching action "${actionName}":`, err);
|
|
268
|
+
log.error(`Error dispatching action "${current.actionName}":`, err);
|
|
256
269
|
}
|
|
257
270
|
}
|
|
258
271
|
};
|
|
@@ -274,6 +287,24 @@ function createEventHandler(
|
|
|
274
287
|
};
|
|
275
288
|
}
|
|
276
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Dispatch the action encoded in an applicator value (e.g. `"@actions.save"`)
|
|
292
|
+
* to the element's engine. Shared by click wiring and keyboard activation so
|
|
293
|
+
* both routes dispatch identically. No-op if the value carries no action or
|
|
294
|
+
* the element has no engine.
|
|
295
|
+
*/
|
|
296
|
+
export function triggerElementAction(element: HTMLElement, value: unknown): void {
|
|
297
|
+
const { actionName, payload } = extractActionDetails(value);
|
|
298
|
+
if (!actionName) return;
|
|
299
|
+
const engine = getEngine(element);
|
|
300
|
+
if (!engine) return;
|
|
301
|
+
try {
|
|
302
|
+
engine.dispatchAction(actionName, payload);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
log.error(`Error dispatching action "${actionName}":`, err);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
277
308
|
/**
|
|
278
309
|
* Create a keyboard event handler that filters by key
|
|
279
310
|
*/
|
|
@@ -41,6 +41,7 @@ import { backgroundHandlers } from "./background.js";
|
|
|
41
41
|
import { displayHandlers } from "./display.js";
|
|
42
42
|
import { transitionHandlers } from "./transition.js";
|
|
43
43
|
import { BREAKPOINTS as VARIANT_BREAKPOINTS, VALID_STATES } from "../../variants.js";
|
|
44
|
+
import { ariaHandlers } from "./aria.js";
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* Tailwind breakpoint values for responsive variants.
|
|
@@ -520,5 +521,9 @@ export class ApplicatorRegistry {
|
|
|
520
521
|
for (const [name, handler] of Object.entries(transitionHandlers)) {
|
|
521
522
|
this.register(name, handler as ApplicatorHandler);
|
|
522
523
|
}
|
|
524
|
+
|
|
525
|
+
for (const [name, handler] of Object.entries(ariaHandlers)) {
|
|
526
|
+
this.register(name, handler as ApplicatorHandler);
|
|
527
|
+
}
|
|
523
528
|
}
|
|
524
529
|
}
|