@hypen-space/core 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/a11y.d.ts +82 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +45 -1
- package/dist/index.js.map +5 -4
- package/dist/types.d.ts +129 -1
- package/package.json +1 -1
- package/src/a11y.ts +127 -0
- package/src/index.ts +5 -0
- package/src/types.ts +131 -1
package/dist/a11y.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility conformance diagnostics — host-side surfacing.
|
|
3
|
+
*
|
|
4
|
+
* The engine's dev-mode conformance pass (`checkAccessibility(source)` on the
|
|
5
|
+
* WASM engine, wired through the package `Engine` wrappers) returns findings
|
|
6
|
+
* as `A11yDiagnostic[]`. This module provides a small, pure helper for
|
|
7
|
+
* surfacing those findings in a dev console.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Byte range `[start, end)` of an element's name token in its source string.
|
|
11
|
+
* Mirrors the Rust `SourceSpan` serialization.
|
|
12
|
+
*/
|
|
13
|
+
export interface A11ySourceSpan {
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A single accessibility conformance finding produced by the engine.
|
|
19
|
+
*
|
|
20
|
+
* Shape mirrors the Rust located-diagnostic serialization
|
|
21
|
+
* (`{ rule, elementType, message, span?, line?, col? }`). The location
|
|
22
|
+
* fields are present when the engine binding ships span support and the
|
|
23
|
+
* finding maps to a source element; older bindings omit them.
|
|
24
|
+
*/
|
|
25
|
+
export interface A11yDiagnostic {
|
|
26
|
+
/** Kebab-case rule identifier (e.g. `"icon-only-control"`). */
|
|
27
|
+
rule: string;
|
|
28
|
+
/** The element type the finding applies to (e.g. `"Button"`). */
|
|
29
|
+
elementType: string;
|
|
30
|
+
/** Human-readable description of the gap. */
|
|
31
|
+
message: string;
|
|
32
|
+
/** Byte span of the offending element's name token in the source. */
|
|
33
|
+
span?: A11ySourceSpan;
|
|
34
|
+
/** 1-based source line of the finding. */
|
|
35
|
+
line?: number;
|
|
36
|
+
/** 1-based column in Unicode codepoints (human/CLI convention). */
|
|
37
|
+
col?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `console.warn` each accessibility finding as
|
|
41
|
+
* `a11y[<rule>] <elementType>: <message>`.
|
|
42
|
+
*
|
|
43
|
+
* Pure with respect to its input (it mutates nothing and returns the
|
|
44
|
+
* formatted lines), so it is unit-testable: the returned array is exactly
|
|
45
|
+
* what was warned, in order. A `console`-less environment is tolerated — the
|
|
46
|
+
* lines are still returned.
|
|
47
|
+
*/
|
|
48
|
+
export declare function logA11yDiagnostics(diags: A11yDiagnostic[]): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Kebab-case ids of every accessibility rule this SDK version expects the
|
|
51
|
+
* engine's conformance pass to implement. Mirrors the engine's `A11yRule`
|
|
52
|
+
* enum via `ALL_RULES` in `hypen-engine-rs/src/ir/conformance.rs` (the Rust
|
|
53
|
+
* side pins these exact strings in a unit test).
|
|
54
|
+
*
|
|
55
|
+
* Used for rule-set drift detection: a prebuilt WASM that predates a rule
|
|
56
|
+
* still exposes `checkAccessibility` and looks current while silently never
|
|
57
|
+
* firing the newer rule.
|
|
58
|
+
*/
|
|
59
|
+
export declare const EXPECTED_A11Y_RULES: readonly string[];
|
|
60
|
+
/** Result of {@link checkRuleDrift}. */
|
|
61
|
+
export interface A11yRuleDrift {
|
|
62
|
+
/**
|
|
63
|
+
* Whether the binding exposes `a11yRules()` at all. `false` means the
|
|
64
|
+
* build predates rule-list stamping entirely (every expected rule is
|
|
65
|
+
* reported missing).
|
|
66
|
+
*/
|
|
67
|
+
hasRuleList: boolean;
|
|
68
|
+
/** Expected rule ids the binding does not implement; empty means no drift. */
|
|
69
|
+
missing: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Compare an engine binding's advertised accessibility rules against
|
|
73
|
+
* {@link EXPECTED_A11Y_RULES}. `engine` is anything exposing the WASM
|
|
74
|
+
* binding surface (a raw `WasmEngine` or a wrapper forwarding `a11yRules`).
|
|
75
|
+
*
|
|
76
|
+
* Drift is a warning, not a failure: findings from the rules the binding
|
|
77
|
+
* does implement remain valid, so callers should surface the missing rules
|
|
78
|
+
* and continue rather than abort the check.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkRuleDrift(engine: {
|
|
81
|
+
a11yRules?: () => string[];
|
|
82
|
+
} | null | undefined): A11yRuleDrift;
|
package/dist/index.d.ts
CHANGED
|
@@ -80,3 +80,5 @@ export { Logger, createLogger, logger, log, frameworkLoggers, setLogLevel, getLo
|
|
|
80
80
|
export type { LogLevel, LoggerConfig, LogHandler } from "./logger.js";
|
|
81
81
|
export { validatePatches } from "./validate.js";
|
|
82
82
|
export type { PatchValidationResult } from "./validate.js";
|
|
83
|
+
export { logA11yDiagnostics, checkRuleDrift, EXPECTED_A11Y_RULES } from "./a11y.js";
|
|
84
|
+
export type { A11yDiagnostic, A11ySourceSpan, A11yRuleDrift } from "./a11y.js";
|
package/dist/index.js
CHANGED
|
@@ -132,6 +132,47 @@ function validatePatches(patches, knownTypes) {
|
|
|
132
132
|
}
|
|
133
133
|
return { unknownTypes: [...unknown] };
|
|
134
134
|
}
|
|
135
|
+
// src/a11y.ts
|
|
136
|
+
function logA11yDiagnostics(diags) {
|
|
137
|
+
const lines = diags.map((d) => `a11y[${d.rule}] ${d.elementType}: ${d.message}`);
|
|
138
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
139
|
+
for (const line of lines) {
|
|
140
|
+
console.warn(line);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
var EXPECTED_A11Y_RULES = [
|
|
146
|
+
"missing-accessible-name",
|
|
147
|
+
"image-missing-alt",
|
|
148
|
+
"heading-missing-level",
|
|
149
|
+
"nested-interactive",
|
|
150
|
+
"form-control-missing-label",
|
|
151
|
+
"unknown-role-token",
|
|
152
|
+
"unknown-dir-token",
|
|
153
|
+
"dangling-reference",
|
|
154
|
+
"duplicate-id",
|
|
155
|
+
"tablist-wiring-skipped",
|
|
156
|
+
"non-portable-aria",
|
|
157
|
+
"unknown-live-token",
|
|
158
|
+
"unknown-ignore-rule"
|
|
159
|
+
];
|
|
160
|
+
function checkRuleDrift(engine) {
|
|
161
|
+
if (typeof engine?.a11yRules !== "function") {
|
|
162
|
+
return { hasRuleList: false, missing: [...EXPECTED_A11Y_RULES] };
|
|
163
|
+
}
|
|
164
|
+
let advertised;
|
|
165
|
+
try {
|
|
166
|
+
advertised = engine.a11yRules();
|
|
167
|
+
} catch {
|
|
168
|
+
return { hasRuleList: false, missing: [...EXPECTED_A11Y_RULES] };
|
|
169
|
+
}
|
|
170
|
+
const have = new Set(advertised);
|
|
171
|
+
return {
|
|
172
|
+
hasRuleList: true,
|
|
173
|
+
missing: EXPECTED_A11Y_RULES.filter((rule) => !have.has(rule))
|
|
174
|
+
};
|
|
175
|
+
}
|
|
135
176
|
export {
|
|
136
177
|
withRetry,
|
|
137
178
|
validatePatches,
|
|
@@ -152,6 +193,7 @@ export {
|
|
|
152
193
|
mapErr,
|
|
153
194
|
map,
|
|
154
195
|
logger,
|
|
196
|
+
logA11yDiagnostics,
|
|
155
197
|
log,
|
|
156
198
|
item,
|
|
157
199
|
isStateProxy,
|
|
@@ -184,6 +226,7 @@ export {
|
|
|
184
226
|
configureLogger,
|
|
185
227
|
compositeDisposable,
|
|
186
228
|
classifyEngineError,
|
|
229
|
+
checkRuleDrift,
|
|
187
230
|
batchStateUpdates,
|
|
188
231
|
app,
|
|
189
232
|
all,
|
|
@@ -208,6 +251,7 @@ export {
|
|
|
208
251
|
HypenAppBuilder,
|
|
209
252
|
HypenApp,
|
|
210
253
|
Err,
|
|
254
|
+
EXPECTED_A11Y_RULES,
|
|
211
255
|
DisposableStack,
|
|
212
256
|
DisposableMixin,
|
|
213
257
|
DataSourceManager,
|
|
@@ -218,4 +262,4 @@ export {
|
|
|
218
262
|
ActionError
|
|
219
263
|
};
|
|
220
264
|
|
|
221
|
-
//# debugId=
|
|
265
|
+
//# debugId=316C720E20393D9564756E2164756E21
|
package/dist/index.js.map
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/validate.ts"],
|
|
3
|
+
"sources": ["../src/validate.ts", "../src/a11y.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Patch-stream validation. A `create` patch whose `elementType` is neither a\n * known primitive nor a resolved component is a silent resolver miss — the\n * renderer drops the node, no error surfaces. `validatePatches` turns that into\n * a one-line test/CI assertion.\n */\n\nimport type { Patch } from \"./types.js\";\n\nexport interface PatchValidationResult {\n /** Distinct `create` `elementType`s not in `knownTypes` (i.e. dropped nodes). */\n unknownTypes: string[];\n}\n\n/**\n * Distinct `create` `elementType`s not in `knownTypes`.\n *\n * `knownTypes` is caller-supplied (engine primitives + the app's resolvable\n * component names) rather than read from the engine: the wasm-bindgen build\n * exposes no primitive list, and hardcoding the built-ins would drift from Rust.\n */\nexport function validatePatches(\n patches: Patch[],\n knownTypes: Iterable<string>,\n): PatchValidationResult {\n const known = knownTypes instanceof Set ? knownTypes : new Set(knownTypes);\n const unknown = new Set<string>();\n\n for (const patch of patches) {\n if (patch.type !== \"create\") continue;\n const elementType = patch.elementType;\n if (typeof elementType !== \"string\") continue;\n if (!known.has(elementType)) unknown.add(elementType);\n }\n\n return { unknownTypes: [...unknown] };\n}\n"
|
|
5
|
+
"/**\n * Patch-stream validation. A `create` patch whose `elementType` is neither a\n * known primitive nor a resolved component is a silent resolver miss — the\n * renderer drops the node, no error surfaces. `validatePatches` turns that into\n * a one-line test/CI assertion.\n */\n\nimport type { Patch } from \"./types.js\";\n\nexport interface PatchValidationResult {\n /** Distinct `create` `elementType`s not in `knownTypes` (i.e. dropped nodes). */\n unknownTypes: string[];\n}\n\n/**\n * Distinct `create` `elementType`s not in `knownTypes`.\n *\n * `knownTypes` is caller-supplied (engine primitives + the app's resolvable\n * component names) rather than read from the engine: the wasm-bindgen build\n * exposes no primitive list, and hardcoding the built-ins would drift from Rust.\n */\nexport function validatePatches(\n patches: Patch[],\n knownTypes: Iterable<string>,\n): PatchValidationResult {\n const known = knownTypes instanceof Set ? knownTypes : new Set(knownTypes);\n const unknown = new Set<string>();\n\n for (const patch of patches) {\n if (patch.type !== \"create\") continue;\n const elementType = patch.elementType;\n if (typeof elementType !== \"string\") continue;\n if (!known.has(elementType)) unknown.add(elementType);\n }\n\n return { unknownTypes: [...unknown] };\n}\n",
|
|
6
|
+
"/**\n * Accessibility conformance diagnostics — host-side surfacing.\n *\n * The engine's dev-mode conformance pass (`checkAccessibility(source)` on the\n * WASM engine, wired through the package `Engine` wrappers) returns findings\n * as `A11yDiagnostic[]`. This module provides a small, pure helper for\n * surfacing those findings in a dev console.\n */\n\n/**\n * Byte range `[start, end)` of an element's name token in its source string.\n * Mirrors the Rust `SourceSpan` serialization.\n */\nexport interface A11ySourceSpan {\n start: number;\n end: number;\n}\n\n/**\n * A single accessibility conformance finding produced by the engine.\n *\n * Shape mirrors the Rust located-diagnostic serialization\n * (`{ rule, elementType, message, span?, line?, col? }`). The location\n * fields are present when the engine binding ships span support and the\n * finding maps to a source element; older bindings omit them.\n */\nexport interface A11yDiagnostic {\n /** Kebab-case rule identifier (e.g. `\"icon-only-control\"`). */\n rule: string;\n /** The element type the finding applies to (e.g. `\"Button\"`). */\n elementType: string;\n /** Human-readable description of the gap. */\n message: string;\n /** Byte span of the offending element's name token in the source. */\n span?: A11ySourceSpan;\n /** 1-based source line of the finding. */\n line?: number;\n /** 1-based column in Unicode codepoints (human/CLI convention). */\n col?: number;\n}\n\n/**\n * `console.warn` each accessibility finding as\n * `a11y[<rule>] <elementType>: <message>`.\n *\n * Pure with respect to its input (it mutates nothing and returns the\n * formatted lines), so it is unit-testable: the returned array is exactly\n * what was warned, in order. A `console`-less environment is tolerated — the\n * lines are still returned.\n */\nexport function logA11yDiagnostics(diags: A11yDiagnostic[]): string[] {\n const lines = diags.map(\n (d) => `a11y[${d.rule}] ${d.elementType}: ${d.message}`,\n );\n if (typeof console !== \"undefined\" && typeof console.warn === \"function\") {\n for (const line of lines) {\n console.warn(line);\n }\n }\n return lines;\n}\n\n/**\n * Kebab-case ids of every accessibility rule this SDK version expects the\n * engine's conformance pass to implement. Mirrors the engine's `A11yRule`\n * enum via `ALL_RULES` in `hypen-engine-rs/src/ir/conformance.rs` (the Rust\n * side pins these exact strings in a unit test).\n *\n * Used for rule-set drift detection: a prebuilt WASM that predates a rule\n * still exposes `checkAccessibility` and looks current while silently never\n * firing the newer rule.\n */\nexport const EXPECTED_A11Y_RULES: readonly string[] = [\n \"missing-accessible-name\",\n \"image-missing-alt\",\n \"heading-missing-level\",\n \"nested-interactive\",\n \"form-control-missing-label\",\n \"unknown-role-token\",\n \"unknown-dir-token\",\n \"dangling-reference\",\n \"duplicate-id\",\n \"tablist-wiring-skipped\",\n \"non-portable-aria\",\n \"unknown-live-token\",\n \"unknown-ignore-rule\",\n];\n\n/** Result of {@link checkRuleDrift}. */\nexport interface A11yRuleDrift {\n /**\n * Whether the binding exposes `a11yRules()` at all. `false` means the\n * build predates rule-list stamping entirely (every expected rule is\n * reported missing).\n */\n hasRuleList: boolean;\n /** Expected rule ids the binding does not implement; empty means no drift. */\n missing: string[];\n}\n\n/**\n * Compare an engine binding's advertised accessibility rules against\n * {@link EXPECTED_A11Y_RULES}. `engine` is anything exposing the WASM\n * binding surface (a raw `WasmEngine` or a wrapper forwarding `a11yRules`).\n *\n * Drift is a warning, not a failure: findings from the rules the binding\n * does implement remain valid, so callers should surface the missing rules\n * and continue rather than abort the check.\n */\nexport function checkRuleDrift(\n engine: { a11yRules?: () => string[] } | null | undefined,\n): A11yRuleDrift {\n if (typeof engine?.a11yRules !== \"function\") {\n return { hasRuleList: false, missing: [...EXPECTED_A11Y_RULES] };\n }\n let advertised: string[];\n try {\n advertised = engine.a11yRules();\n } catch {\n return { hasRuleList: false, missing: [...EXPECTED_A11Y_RULES] };\n }\n const have = new Set(advertised);\n return {\n hasRuleList: true,\n missing: EXPECTED_A11Y_RULES.filter((rule) => !have.has(rule)),\n };\n}\n"
|
|
6
7
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBO,SAAS,eAAe,CAC7B,SACA,YACuB;AAAA,EACvB,MAAM,QAAQ,sBAAsB,MAAM,aAAa,IAAI,IAAI,UAAU;AAAA,EACzE,MAAM,UAAU,IAAI;AAAA,EAEpB,WAAW,SAAS,SAAS;AAAA,IAC3B,IAAI,MAAM,SAAS;AAAA,MAAU;AAAA,IAC7B,MAAM,cAAc,MAAM;AAAA,IAC1B,IAAI,OAAO,gBAAgB;AAAA,MAAU;AAAA,IACrC,IAAI,CAAC,MAAM,IAAI,WAAW;AAAA,MAAG,QAAQ,IAAI,WAAW;AAAA,EACtD;AAAA,EAEA,OAAO,EAAE,cAAc,CAAC,GAAG,OAAO,EAAE;AAAA;",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBO,SAAS,eAAe,CAC7B,SACA,YACuB;AAAA,EACvB,MAAM,QAAQ,sBAAsB,MAAM,aAAa,IAAI,IAAI,UAAU;AAAA,EACzE,MAAM,UAAU,IAAI;AAAA,EAEpB,WAAW,SAAS,SAAS;AAAA,IAC3B,IAAI,MAAM,SAAS;AAAA,MAAU;AAAA,IAC7B,MAAM,cAAc,MAAM;AAAA,IAC1B,IAAI,OAAO,gBAAgB;AAAA,MAAU;AAAA,IACrC,IAAI,CAAC,MAAM,IAAI,WAAW;AAAA,MAAG,QAAQ,IAAI,WAAW;AAAA,EACtD;AAAA,EAEA,OAAO,EAAE,cAAc,CAAC,GAAG,OAAO,EAAE;AAAA;;ACe/B,SAAS,kBAAkB,CAAC,OAAmC;AAAA,EACpE,MAAM,QAAQ,MAAM,IAClB,CAAC,MAAM,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,SAChD;AAAA,EACA,IAAI,OAAO,YAAY,eAAe,OAAO,QAAQ,SAAS,YAAY;AAAA,IACxE,WAAW,QAAQ,OAAO;AAAA,MACxB,QAAQ,KAAK,IAAI;AAAA,IACnB;AAAA,EACF;AAAA,EACA,OAAO;AAAA;AAaF,IAAM,sBAAyC;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAuBO,SAAS,cAAc,CAC5B,QACe;AAAA,EACf,IAAI,OAAO,QAAQ,cAAc,YAAY;AAAA,IAC3C,OAAO,EAAE,aAAa,OAAO,SAAS,CAAC,GAAG,mBAAmB,EAAE;AAAA,EACjE;AAAA,EACA,IAAI;AAAA,EACJ,IAAI;AAAA,IACF,aAAa,OAAO,UAAU;AAAA,IAC9B,MAAM;AAAA,IACN,OAAO,EAAE,aAAa,OAAO,SAAS,CAAC,GAAG,mBAAmB,EAAE;AAAA;AAAA,EAEjE,MAAM,OAAO,IAAI,IAAI,UAAU;AAAA,EAC/B,OAAO;AAAA,IACL,aAAa;AAAA,IACb,SAAS,oBAAoB,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC;AAAA,EAC/D;AAAA;",
|
|
9
|
+
"debugId": "316C720E20393D9564756E2164756E21",
|
|
9
10
|
"names": []
|
|
10
11
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -16,10 +16,21 @@ export type Patch = {
|
|
|
16
16
|
* rebuild. If `remove` arrives for a detached id instead, the
|
|
17
17
|
* subtree is torn down normally.
|
|
18
18
|
*/
|
|
19
|
-
type: "create" | "setProp" | "removeProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent" | "detach" | "attach";
|
|
19
|
+
type: "create" | "setProp" | "removeProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent" | "detach" | "attach" | "setSemantics";
|
|
20
20
|
id?: string;
|
|
21
21
|
elementType?: string;
|
|
22
22
|
props?: Record<string, any>;
|
|
23
|
+
/**
|
|
24
|
+
* Accessibility semantics derived by the engine for this node. Carried on
|
|
25
|
+
* `create` (first paint) and on `setSemantics` (reactive change — a
|
|
26
|
+
* templated accessible name or bound state whose source path changed).
|
|
27
|
+
* A `setSemantics` always carries the node's **complete** resolved block;
|
|
28
|
+
* renderers re-apply it with the same translation they run at create,
|
|
29
|
+
* clearing anything the new block no longer sets. Absent on `create` when
|
|
30
|
+
* the node has nothing derivable; absent on `setSemantics` when the node
|
|
31
|
+
* lost all semantics (→ clear everything).
|
|
32
|
+
*/
|
|
33
|
+
semantics?: Semantics;
|
|
23
34
|
name?: string;
|
|
24
35
|
value?: any;
|
|
25
36
|
text?: string;
|
|
@@ -27,6 +38,123 @@ export type Patch = {
|
|
|
27
38
|
beforeId?: string;
|
|
28
39
|
eventName?: string;
|
|
29
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Platform-neutral accessibility semantics for a node, derived in the engine
|
|
43
|
+
* and carried on `create` patches. Each renderer maps this onto its native
|
|
44
|
+
* accessibility model (ARIA attributes on DOM, `Modifier.semantics` on
|
|
45
|
+
* Compose, accessibility traits on SwiftUI).
|
|
46
|
+
*
|
|
47
|
+
* Every field is optional; an absent field means "nothing derivable".
|
|
48
|
+
*/
|
|
49
|
+
export type Semantics = {
|
|
50
|
+
/**
|
|
51
|
+
* What this node is, as a platform-neutral role token (e.g. `"button"`).
|
|
52
|
+
* Mirrors the ARIA role vocabulary. Emitted only where the role is
|
|
53
|
+
* structurally certain from the component type.
|
|
54
|
+
*/
|
|
55
|
+
role?: string;
|
|
56
|
+
/** Heading level (1–6) when known. */
|
|
57
|
+
level?: number;
|
|
58
|
+
/** Whether the node represents in-progress content (maps to `aria-busy`). */
|
|
59
|
+
busy?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Live-region politeness (`.liveRegion("polite" | "assertive")`) →
|
|
62
|
+
* `aria-live` on DOM and the Canvas shadow tree. The engine validates the
|
|
63
|
+
* token, so only those two values arrive. Travels untranslated to the
|
|
64
|
+
* native renderers (no direct analog wired yet).
|
|
65
|
+
*/
|
|
66
|
+
live?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Accessible name derived from the element's text content, for roles that
|
|
69
|
+
* require one. On DOM this is usually left unapplied — the browser derives
|
|
70
|
+
* the name from visible content — but renderers without that affordance
|
|
71
|
+
* (Canvas shadow tree, iOS, Android) use it directly.
|
|
72
|
+
*/
|
|
73
|
+
name?: string;
|
|
74
|
+
/**
|
|
75
|
+
* `true` when the element's role requires a name but none could be derived
|
|
76
|
+
* (e.g. an icon-only button). Surfaced by dev-mode conformance checks; the
|
|
77
|
+
* fix is an explicit label.
|
|
78
|
+
*/
|
|
79
|
+
nameMissing?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* `true` when {@link name} came from an explicit author `.label(...)` rather
|
|
82
|
+
* than derived content. DOM applies an explicit name as `aria-label` (an
|
|
83
|
+
* intentional override) but leaves a derived name to the visible content.
|
|
84
|
+
*/
|
|
85
|
+
nameExplicit?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* `true` when the element was marked decorative via `.hidden()`. It is
|
|
88
|
+
* removed from the accessibility tree (`aria-hidden`).
|
|
89
|
+
*/
|
|
90
|
+
hidden?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Supplementary description (`.description(...)`) — extra context beyond the
|
|
93
|
+
* name. Applied as `aria-description` on DOM.
|
|
94
|
+
*/
|
|
95
|
+
description?: string;
|
|
96
|
+
/** Disclosure state → `aria-expanded`. */
|
|
97
|
+
expanded?: boolean;
|
|
98
|
+
/** Toggle-button state → `aria-pressed`. */
|
|
99
|
+
pressed?: boolean;
|
|
100
|
+
/** Selected state (e.g. a tab) → `aria-selected`. */
|
|
101
|
+
selected?: boolean;
|
|
102
|
+
/** Current-item token (page/step/…) → `aria-current`. */
|
|
103
|
+
current?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Checked state for a `Checkbox`/`Switch` bound via `.bind(@state.x)` →
|
|
106
|
+
* `aria-checked`. Resolved at reconcile from the control's bind-target value.
|
|
107
|
+
*/
|
|
108
|
+
checked?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Validity state for a form control (`.invalid(bool | @state.hasError)`) →
|
|
111
|
+
* `aria-invalid`. A bound value resolves at reconcile and stays live via
|
|
112
|
+
* `setSemantics` re-emits. DOM only for now.
|
|
113
|
+
*/
|
|
114
|
+
invalid?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Cross-node relationship: id of the element this node controls →
|
|
117
|
+
* `aria-controls`. Web-leaning (DOM only); id references do not survive to
|
|
118
|
+
* the string-hint native APIs. See the guide's "Platform support" section
|
|
119
|
+
* (`hypen-docs/content/docs/guide/accessibility.mdx`).
|
|
120
|
+
*/
|
|
121
|
+
controls?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Cross-node relationship: id of the element that describes this node →
|
|
124
|
+
* `aria-describedby`. Web-leaning (DOM only), as with `controls`.
|
|
125
|
+
*/
|
|
126
|
+
describedby?: string;
|
|
127
|
+
/**
|
|
128
|
+
* The author-supplied stable id of this node (`.id("details-panel")`) —
|
|
129
|
+
* the anchor id-reference relationships resolve against. Applied as the
|
|
130
|
+
* DOM `id` attribute.
|
|
131
|
+
*/
|
|
132
|
+
id?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Cross-node relationship: id of the element that labels this node →
|
|
135
|
+
* `aria-labelledby` (the tabpanel→tab half of the Tabs pattern). DOM only.
|
|
136
|
+
*/
|
|
137
|
+
labelledby?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Reactive cross-node reference: id of the currently-active descendant →
|
|
140
|
+
* `aria-activedescendant` (the roving focus pointer of composite widgets).
|
|
141
|
+
* Usually bound (`.activedescendant(@state.focusedId)`); kept live via
|
|
142
|
+
* `setSemantics` re-emits. DOM only.
|
|
143
|
+
*/
|
|
144
|
+
activeDescendant?: string;
|
|
145
|
+
/**
|
|
146
|
+
* Cross-node relationship: id of an element that is logically this node's
|
|
147
|
+
* child but rendered elsewhere (a portaled popup) → `aria-owns`. DOM only.
|
|
148
|
+
*/
|
|
149
|
+
owns?: string;
|
|
150
|
+
/**
|
|
151
|
+
* Base text direction (`.dir("rtl" | "ltr" | "auto")`) → the HTML `dir`
|
|
152
|
+
* attribute on DOM. The engine validates the token, so only those three
|
|
153
|
+
* values arrive. Native renderers translate to their layout-direction
|
|
154
|
+
* APIs (follow-up; the value already travels on the block).
|
|
155
|
+
*/
|
|
156
|
+
dir?: string;
|
|
157
|
+
};
|
|
30
158
|
export type Action = {
|
|
31
159
|
name: string;
|
|
32
160
|
payload?: any;
|
package/package.json
CHANGED
package/src/a11y.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility conformance diagnostics — host-side surfacing.
|
|
3
|
+
*
|
|
4
|
+
* The engine's dev-mode conformance pass (`checkAccessibility(source)` on the
|
|
5
|
+
* WASM engine, wired through the package `Engine` wrappers) returns findings
|
|
6
|
+
* as `A11yDiagnostic[]`. This module provides a small, pure helper for
|
|
7
|
+
* surfacing those findings in a dev console.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Byte range `[start, end)` of an element's name token in its source string.
|
|
12
|
+
* Mirrors the Rust `SourceSpan` serialization.
|
|
13
|
+
*/
|
|
14
|
+
export interface A11ySourceSpan {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A single accessibility conformance finding produced by the engine.
|
|
21
|
+
*
|
|
22
|
+
* Shape mirrors the Rust located-diagnostic serialization
|
|
23
|
+
* (`{ rule, elementType, message, span?, line?, col? }`). The location
|
|
24
|
+
* fields are present when the engine binding ships span support and the
|
|
25
|
+
* finding maps to a source element; older bindings omit them.
|
|
26
|
+
*/
|
|
27
|
+
export interface A11yDiagnostic {
|
|
28
|
+
/** Kebab-case rule identifier (e.g. `"icon-only-control"`). */
|
|
29
|
+
rule: string;
|
|
30
|
+
/** The element type the finding applies to (e.g. `"Button"`). */
|
|
31
|
+
elementType: string;
|
|
32
|
+
/** Human-readable description of the gap. */
|
|
33
|
+
message: string;
|
|
34
|
+
/** Byte span of the offending element's name token in the source. */
|
|
35
|
+
span?: A11ySourceSpan;
|
|
36
|
+
/** 1-based source line of the finding. */
|
|
37
|
+
line?: number;
|
|
38
|
+
/** 1-based column in Unicode codepoints (human/CLI convention). */
|
|
39
|
+
col?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `console.warn` each accessibility finding as
|
|
44
|
+
* `a11y[<rule>] <elementType>: <message>`.
|
|
45
|
+
*
|
|
46
|
+
* Pure with respect to its input (it mutates nothing and returns the
|
|
47
|
+
* formatted lines), so it is unit-testable: the returned array is exactly
|
|
48
|
+
* what was warned, in order. A `console`-less environment is tolerated — the
|
|
49
|
+
* lines are still returned.
|
|
50
|
+
*/
|
|
51
|
+
export function logA11yDiagnostics(diags: A11yDiagnostic[]): string[] {
|
|
52
|
+
const lines = diags.map(
|
|
53
|
+
(d) => `a11y[${d.rule}] ${d.elementType}: ${d.message}`,
|
|
54
|
+
);
|
|
55
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
console.warn(line);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Kebab-case ids of every accessibility rule this SDK version expects the
|
|
65
|
+
* engine's conformance pass to implement. Mirrors the engine's `A11yRule`
|
|
66
|
+
* enum via `ALL_RULES` in `hypen-engine-rs/src/ir/conformance.rs` (the Rust
|
|
67
|
+
* side pins these exact strings in a unit test).
|
|
68
|
+
*
|
|
69
|
+
* Used for rule-set drift detection: a prebuilt WASM that predates a rule
|
|
70
|
+
* still exposes `checkAccessibility` and looks current while silently never
|
|
71
|
+
* firing the newer rule.
|
|
72
|
+
*/
|
|
73
|
+
export const EXPECTED_A11Y_RULES: readonly string[] = [
|
|
74
|
+
"missing-accessible-name",
|
|
75
|
+
"image-missing-alt",
|
|
76
|
+
"heading-missing-level",
|
|
77
|
+
"nested-interactive",
|
|
78
|
+
"form-control-missing-label",
|
|
79
|
+
"unknown-role-token",
|
|
80
|
+
"unknown-dir-token",
|
|
81
|
+
"dangling-reference",
|
|
82
|
+
"duplicate-id",
|
|
83
|
+
"tablist-wiring-skipped",
|
|
84
|
+
"non-portable-aria",
|
|
85
|
+
"unknown-live-token",
|
|
86
|
+
"unknown-ignore-rule",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/** Result of {@link checkRuleDrift}. */
|
|
90
|
+
export interface A11yRuleDrift {
|
|
91
|
+
/**
|
|
92
|
+
* Whether the binding exposes `a11yRules()` at all. `false` means the
|
|
93
|
+
* build predates rule-list stamping entirely (every expected rule is
|
|
94
|
+
* reported missing).
|
|
95
|
+
*/
|
|
96
|
+
hasRuleList: boolean;
|
|
97
|
+
/** Expected rule ids the binding does not implement; empty means no drift. */
|
|
98
|
+
missing: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Compare an engine binding's advertised accessibility rules against
|
|
103
|
+
* {@link EXPECTED_A11Y_RULES}. `engine` is anything exposing the WASM
|
|
104
|
+
* binding surface (a raw `WasmEngine` or a wrapper forwarding `a11yRules`).
|
|
105
|
+
*
|
|
106
|
+
* Drift is a warning, not a failure: findings from the rules the binding
|
|
107
|
+
* does implement remain valid, so callers should surface the missing rules
|
|
108
|
+
* and continue rather than abort the check.
|
|
109
|
+
*/
|
|
110
|
+
export function checkRuleDrift(
|
|
111
|
+
engine: { a11yRules?: () => string[] } | null | undefined,
|
|
112
|
+
): A11yRuleDrift {
|
|
113
|
+
if (typeof engine?.a11yRules !== "function") {
|
|
114
|
+
return { hasRuleList: false, missing: [...EXPECTED_A11Y_RULES] };
|
|
115
|
+
}
|
|
116
|
+
let advertised: string[];
|
|
117
|
+
try {
|
|
118
|
+
advertised = engine.a11yRules();
|
|
119
|
+
} catch {
|
|
120
|
+
return { hasRuleList: false, missing: [...EXPECTED_A11Y_RULES] };
|
|
121
|
+
}
|
|
122
|
+
const have = new Set(advertised);
|
|
123
|
+
return {
|
|
124
|
+
hasRuleList: true,
|
|
125
|
+
missing: EXPECTED_A11Y_RULES.filter((rule) => !have.has(rule)),
|
|
126
|
+
};
|
|
127
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -331,3 +331,8 @@ export type { LogLevel, LoggerConfig, LogHandler } from "./logger.js";
|
|
|
331
331
|
// elementTypes) in CI / smoke tests off the wire.
|
|
332
332
|
export { validatePatches } from "./validate.js";
|
|
333
333
|
export type { PatchValidationResult } from "./validate.js";
|
|
334
|
+
|
|
335
|
+
// Accessibility conformance diagnostics — surface the engine's dev-mode
|
|
336
|
+
// `checkAccessibility(source)` findings in a host dev console.
|
|
337
|
+
export { logA11yDiagnostics, checkRuleDrift, EXPECTED_A11Y_RULES } from "./a11y.js";
|
|
338
|
+
export type { A11yDiagnostic, A11ySourceSpan, A11yRuleDrift } from "./a11y.js";
|
package/src/types.ts
CHANGED
|
@@ -28,10 +28,22 @@ export type Patch = {
|
|
|
28
28
|
| "attachEvent"
|
|
29
29
|
| "detachEvent"
|
|
30
30
|
| "detach"
|
|
31
|
-
| "attach"
|
|
31
|
+
| "attach"
|
|
32
|
+
| "setSemantics";
|
|
32
33
|
id?: string;
|
|
33
34
|
elementType?: string;
|
|
34
35
|
props?: Record<string, any>;
|
|
36
|
+
/**
|
|
37
|
+
* Accessibility semantics derived by the engine for this node. Carried on
|
|
38
|
+
* `create` (first paint) and on `setSemantics` (reactive change — a
|
|
39
|
+
* templated accessible name or bound state whose source path changed).
|
|
40
|
+
* A `setSemantics` always carries the node's **complete** resolved block;
|
|
41
|
+
* renderers re-apply it with the same translation they run at create,
|
|
42
|
+
* clearing anything the new block no longer sets. Absent on `create` when
|
|
43
|
+
* the node has nothing derivable; absent on `setSemantics` when the node
|
|
44
|
+
* lost all semantics (→ clear everything).
|
|
45
|
+
*/
|
|
46
|
+
semantics?: Semantics;
|
|
35
47
|
name?: string;
|
|
36
48
|
value?: any;
|
|
37
49
|
text?: string;
|
|
@@ -40,6 +52,124 @@ export type Patch = {
|
|
|
40
52
|
eventName?: string;
|
|
41
53
|
};
|
|
42
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Platform-neutral accessibility semantics for a node, derived in the engine
|
|
57
|
+
* and carried on `create` patches. Each renderer maps this onto its native
|
|
58
|
+
* accessibility model (ARIA attributes on DOM, `Modifier.semantics` on
|
|
59
|
+
* Compose, accessibility traits on SwiftUI).
|
|
60
|
+
*
|
|
61
|
+
* Every field is optional; an absent field means "nothing derivable".
|
|
62
|
+
*/
|
|
63
|
+
export type Semantics = {
|
|
64
|
+
/**
|
|
65
|
+
* What this node is, as a platform-neutral role token (e.g. `"button"`).
|
|
66
|
+
* Mirrors the ARIA role vocabulary. Emitted only where the role is
|
|
67
|
+
* structurally certain from the component type.
|
|
68
|
+
*/
|
|
69
|
+
role?: string;
|
|
70
|
+
/** Heading level (1–6) when known. */
|
|
71
|
+
level?: number;
|
|
72
|
+
/** Whether the node represents in-progress content (maps to `aria-busy`). */
|
|
73
|
+
busy?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Live-region politeness (`.liveRegion("polite" | "assertive")`) →
|
|
76
|
+
* `aria-live` on DOM and the Canvas shadow tree. The engine validates the
|
|
77
|
+
* token, so only those two values arrive. Travels untranslated to the
|
|
78
|
+
* native renderers (no direct analog wired yet).
|
|
79
|
+
*/
|
|
80
|
+
live?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Accessible name derived from the element's text content, for roles that
|
|
83
|
+
* require one. On DOM this is usually left unapplied — the browser derives
|
|
84
|
+
* the name from visible content — but renderers without that affordance
|
|
85
|
+
* (Canvas shadow tree, iOS, Android) use it directly.
|
|
86
|
+
*/
|
|
87
|
+
name?: string;
|
|
88
|
+
/**
|
|
89
|
+
* `true` when the element's role requires a name but none could be derived
|
|
90
|
+
* (e.g. an icon-only button). Surfaced by dev-mode conformance checks; the
|
|
91
|
+
* fix is an explicit label.
|
|
92
|
+
*/
|
|
93
|
+
nameMissing?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* `true` when {@link name} came from an explicit author `.label(...)` rather
|
|
96
|
+
* than derived content. DOM applies an explicit name as `aria-label` (an
|
|
97
|
+
* intentional override) but leaves a derived name to the visible content.
|
|
98
|
+
*/
|
|
99
|
+
nameExplicit?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* `true` when the element was marked decorative via `.hidden()`. It is
|
|
102
|
+
* removed from the accessibility tree (`aria-hidden`).
|
|
103
|
+
*/
|
|
104
|
+
hidden?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Supplementary description (`.description(...)`) — extra context beyond the
|
|
107
|
+
* name. Applied as `aria-description` on DOM.
|
|
108
|
+
*/
|
|
109
|
+
description?: string;
|
|
110
|
+
/** Disclosure state → `aria-expanded`. */
|
|
111
|
+
expanded?: boolean;
|
|
112
|
+
/** Toggle-button state → `aria-pressed`. */
|
|
113
|
+
pressed?: boolean;
|
|
114
|
+
/** Selected state (e.g. a tab) → `aria-selected`. */
|
|
115
|
+
selected?: boolean;
|
|
116
|
+
/** Current-item token (page/step/…) → `aria-current`. */
|
|
117
|
+
current?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Checked state for a `Checkbox`/`Switch` bound via `.bind(@state.x)` →
|
|
120
|
+
* `aria-checked`. Resolved at reconcile from the control's bind-target value.
|
|
121
|
+
*/
|
|
122
|
+
checked?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Validity state for a form control (`.invalid(bool | @state.hasError)`) →
|
|
125
|
+
* `aria-invalid`. A bound value resolves at reconcile and stays live via
|
|
126
|
+
* `setSemantics` re-emits. DOM only for now.
|
|
127
|
+
*/
|
|
128
|
+
invalid?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Cross-node relationship: id of the element this node controls →
|
|
131
|
+
* `aria-controls`. Web-leaning (DOM only); id references do not survive to
|
|
132
|
+
* the string-hint native APIs. See the guide's "Platform support" section
|
|
133
|
+
* (`hypen-docs/content/docs/guide/accessibility.mdx`).
|
|
134
|
+
*/
|
|
135
|
+
controls?: string;
|
|
136
|
+
/**
|
|
137
|
+
* Cross-node relationship: id of the element that describes this node →
|
|
138
|
+
* `aria-describedby`. Web-leaning (DOM only), as with `controls`.
|
|
139
|
+
*/
|
|
140
|
+
describedby?: string;
|
|
141
|
+
/**
|
|
142
|
+
* The author-supplied stable id of this node (`.id("details-panel")`) —
|
|
143
|
+
* the anchor id-reference relationships resolve against. Applied as the
|
|
144
|
+
* DOM `id` attribute.
|
|
145
|
+
*/
|
|
146
|
+
id?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Cross-node relationship: id of the element that labels this node →
|
|
149
|
+
* `aria-labelledby` (the tabpanel→tab half of the Tabs pattern). DOM only.
|
|
150
|
+
*/
|
|
151
|
+
labelledby?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Reactive cross-node reference: id of the currently-active descendant →
|
|
154
|
+
* `aria-activedescendant` (the roving focus pointer of composite widgets).
|
|
155
|
+
* Usually bound (`.activedescendant(@state.focusedId)`); kept live via
|
|
156
|
+
* `setSemantics` re-emits. DOM only.
|
|
157
|
+
*/
|
|
158
|
+
activeDescendant?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Cross-node relationship: id of an element that is logically this node's
|
|
161
|
+
* child but rendered elsewhere (a portaled popup) → `aria-owns`. DOM only.
|
|
162
|
+
*/
|
|
163
|
+
owns?: string;
|
|
164
|
+
/**
|
|
165
|
+
* Base text direction (`.dir("rtl" | "ltr" | "auto")`) → the HTML `dir`
|
|
166
|
+
* attribute on DOM. The engine validates the token, so only those three
|
|
167
|
+
* values arrive. Native renderers translate to their layout-direction
|
|
168
|
+
* APIs (follow-up; the value already travels on the block).
|
|
169
|
+
*/
|
|
170
|
+
dir?: string;
|
|
171
|
+
};
|
|
172
|
+
|
|
43
173
|
export type Action = {
|
|
44
174
|
name: string;
|
|
45
175
|
payload?: any;
|