@flighthq/accessibility 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @flighthq/accessibility
2
2
 
3
- Assistive-technology (ARIA) bridge over a swappable web/native backend (a visually-hidden ARIA DOM overlay by default)
3
+ Host-explicit assistive-technology bridge for mirrored semantic nodes, focus, and announcements
4
4
 
5
5
  ## Install
6
6
 
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/accessibility`. The
13
13
  This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
14
14
 
15
15
  - [Flight project](https://github.com/flighthq/flight)
16
- - [Source for @flighthq/accessibility@0.5.0-next.1944.cd6b6e7](https://github.com/flighthq/flight/tree/cd6b6e71eb472b3a4249830a7743333f019fa7b7/packages/accessibility)
17
- - [License](https://github.com/flighthq/flight/blob/cd6b6e71eb472b3a4249830a7743333f019fa7b7/LICENSE.md)
16
+ - [Source for @flighthq/accessibility@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/accessibility)
17
+ - [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
@@ -1,15 +1,8 @@
1
- import type { AccessibilityBackend, AccessibilityLiveness, AccessibilityNode } from '@flighthq/types/contract';
2
- import type { BackendExplanation } from '@flighthq/types/contract';
3
- export declare function announceAccessibility(message: string, liveness?: AccessibilityLiveness): void;
4
- export declare function clearAccessibilityTree(): void;
5
- export declare function createWebAccessibilityBackend(container?: HTMLElement): AccessibilityBackend;
6
- export declare function explainAccessibilityBackend(): BackendExplanation;
7
- export declare function getAccessibilityBackend(): AccessibilityBackend;
8
- export declare function installAccessibilityHostBackend(backend: AccessibilityBackend): void;
9
- export declare function observeAccessibilityHostResult(operation: string, succeeded: boolean): void;
10
- export declare function removeAccessibilityNode(id: string): void;
11
- export declare function resetAccessibilityBackendForTest(): void;
12
- export declare function setAccessibilityBackend(backend: AccessibilityBackend | null): void;
13
- export declare function setAccessibilityFocus(id: string): boolean;
14
- export declare function setAccessibilityNode(node: Readonly<AccessibilityNode>): void;
1
+ import type { AccessibilityLiveness, AccessibilityNode, AccessibilityOperationOutcome, HasAccessibilityProvider } from '@flighthq/types/contract';
2
+ export declare function announceAccessibility(host: HasAccessibilityProvider, message: string, liveness?: AccessibilityLiveness): AccessibilityOperationOutcome<'destroyed' | 'no-dom'>;
3
+ export declare function clearAccessibilityTree(host: HasAccessibilityProvider): AccessibilityOperationOutcome<'destroyed' | 'no-dom'>;
4
+ export declare function destroyAccessibility(host: HasAccessibilityProvider): void;
5
+ export declare function removeAccessibilityNode(host: HasAccessibilityProvider, id: string): AccessibilityOperationOutcome<'destroyed' | 'no-dom' | 'node-not-found'>;
6
+ export declare function setAccessibilityFocus(host: HasAccessibilityProvider, id: string): AccessibilityOperationOutcome<'destroyed' | 'focus-not-moved' | 'no-dom' | 'node-not-found'>;
7
+ export declare function setAccessibilityNode(host: HasAccessibilityProvider, node: Readonly<AccessibilityNode>): AccessibilityOperationOutcome<'destroyed' | 'no-dom'>;
15
8
  //# sourceMappingURL=accessibility.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../src/accessibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EAElB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAKnE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,qBAAgC,GAAG,IAAI,CAEvG;AAID,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAQD,wBAAgB,6BAA6B,CAAC,SAAS,CAAC,EAAE,WAAW,GAAG,oBAAoB,CAgE3F;AAED,wBAAgB,2BAA2B,IAAI,kBAAkB,CAahE;AAGD,wBAAgB,uBAAuB,IAAI,oBAAoB,CAE9D;AAED,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAMnF;AAED,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAK1F;AAGD,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,wBAAgB,gCAAgC,IAAI,IAAI,CAKvD;AAGD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,GAAG,IAAI,CAElF;AAID,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEzD;AAID,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAE5E"}
1
+ {"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../src/accessibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,iBAAiB,EACjB,6BAA6B,EAC7B,wBAAwB,EACzB,MAAM,0BAA0B,CAAC;AAGlC,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,wBAAwB,EAC9B,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,qBAAgC,GACzC,6BAA6B,CAAC,WAAW,GAAG,QAAQ,CAAC,CAEvD;AAGD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,wBAAwB,GAC7B,6BAA6B,CAAC,WAAW,GAAG,QAAQ,CAAC,CAEvD;AAID,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,wBAAwB,GAAG,IAAI,CAEzE;AAGD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,wBAAwB,EAC9B,EAAE,EAAE,MAAM,GACT,6BAA6B,CAAC,WAAW,GAAG,QAAQ,GAAG,gBAAgB,CAAC,CAE1E;AAGD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,wBAAwB,EAC9B,EAAE,EAAE,MAAM,GACT,6BAA6B,CAAC,WAAW,GAAG,iBAAiB,GAAG,QAAQ,GAAG,gBAAgB,CAAC,CAE9F;AAGD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,wBAAwB,EAC9B,IAAI,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAChC,6BAA6B,CAAC,WAAW,GAAG,QAAQ,CAAC,CAEvD"}
@@ -1,277 +1,26 @@
1
- // Speaks a transient message through the platform's live region. `liveness` picks urgency: 'polite'
2
- // waits for current speech, 'assertive' interrupts. Defaults to 'polite'. No-op when no backend can
3
- // speak.
4
- export function announceAccessibility(message, liveness = 'polite') {
5
- getAccessibilityBackend().announce(message, liveness);
1
+ // Speaks a transient message through the explicitly selected Host provider.
2
+ export function announceAccessibility(host, message, liveness = 'polite') {
3
+ return host.accessibility.provider.announce(message, liveness);
6
4
  }
7
- // Empties the mirrored accessibility tree, removing every published node. Announcement live regions
8
- // are recreated lazily on the next announce.
9
- export function clearAccessibilityTree() {
10
- getAccessibilityBackend().clear();
5
+ // Empties the selected Host provider's mirrored tree while leaving the provider reusable.
6
+ export function clearAccessibilityTree(host) {
7
+ return host.accessibility.provider.clear();
11
8
  }
12
- // Builds the default web backend: a visually-hidden ARIA DOM overlay. Created lazily by
13
- // getAccessibilityBackend — nothing touches the DOM at import time, so importing the package has no
14
- // side effect. Each published node becomes an element carrying `role` + `aria-*` attributes, nested
15
- // under its `parentId`'s element (or the container root). By default a hidden container is appended
16
- // to document.body on first use; pass `container` to host the overlay elsewhere. With no DOM present
17
- // every method is a sentinel no-op (setFocus returns false) rather than throwing.
18
- export function createWebAccessibilityBackend(container) {
19
- const elements = new Map();
20
- const liveRegions = new Map();
21
- let root = container ?? null;
22
- let rootResolved = container !== undefined;
23
- // Resolves the overlay root, lazily creating and appending a hidden container on first use. Returns
24
- // null when no DOM is available, which flips every method into a no-op.
25
- function getRoot() {
26
- if (rootResolved)
27
- return root;
28
- rootResolved = true;
29
- if (typeof document === 'undefined' || document.body === null) {
30
- root = null;
31
- return null;
32
- }
33
- root = _createHiddenAccessibilityContainer(document);
34
- document.body.appendChild(root);
35
- return root;
36
- }
37
- return {
38
- setNode(node) {
39
- const overlayRoot = getRoot();
40
- if (overlayRoot === null)
41
- return;
42
- let element = elements.get(node.id);
43
- if (element === undefined) {
44
- element = overlayRoot.ownerDocument.createElement('div');
45
- element.setAttribute('data-flight-accessibility-id', node.id);
46
- elements.set(node.id, element);
47
- }
48
- _applyAccessibilityElementAttributes(element, node);
49
- _reparentAccessibilityElement(element, node.parentId, elements, overlayRoot);
50
- },
51
- removeNode(id) {
52
- const element = elements.get(id);
53
- if (element === undefined)
54
- return;
55
- // Drop the node and its entire descendant subtree (Node.contains is true for the element
56
- // itself and every DOM descendant, matching the nested overlay layout).
57
- for (const [key, other] of elements) {
58
- if (element.contains(other))
59
- elements.delete(key);
60
- }
61
- if (element.parentNode !== null)
62
- element.parentNode.removeChild(element);
63
- },
64
- clear() {
65
- const overlayRoot = getRoot();
66
- elements.clear();
67
- liveRegions.clear();
68
- if (overlayRoot !== null)
69
- overlayRoot.replaceChildren();
70
- },
71
- setFocus(id) {
72
- const overlayRoot = getRoot();
73
- if (overlayRoot === null)
74
- return false;
75
- const element = elements.get(id);
76
- if (element === undefined)
77
- return false;
78
- element.focus();
79
- return overlayRoot.ownerDocument.activeElement === element;
80
- },
81
- announce(message, liveness) {
82
- const overlayRoot = getRoot();
83
- if (overlayRoot === null)
84
- return;
85
- const region = _getAccessibilityLiveRegion(overlayRoot, liveRegions, liveness);
86
- region.textContent = message;
87
- },
88
- };
9
+ // Terminates the selected Host provider and frees the non-GC resources it owns. Provider destruction
10
+ // is idempotent; callers that share one provider across Hosts retain responsibility for its final release.
11
+ export function destroyAccessibility(host) {
12
+ host.accessibility.provider.destroy();
89
13
  }
90
- export function explainAccessibilityBackend() {
91
- if (_custom !== null) {
92
- return { conflict: _hostConflict, layer: 'custom', operation: null, viability: 'unobserved' };
93
- }
94
- if (_host !== null) {
95
- return {
96
- conflict: _hostConflict,
97
- layer: 'host',
98
- operation: _hostObservation !== null ? _hostObservation.operation : null,
99
- viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
100
- };
101
- }
102
- return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
14
+ // Removes a node and its entire descendant subtree from the selected Host provider.
15
+ export function removeAccessibilityNode(host, id) {
16
+ return host.accessibility.provider.removeNode(id);
103
17
  }
104
- // The active accessibility backend. Precedence: custom > host > sentinel.
105
- export function getAccessibilityBackend() {
106
- return _custom ?? _host ?? _sentinel;
18
+ // Moves platform focus to a node published by the selected Host provider.
19
+ export function setAccessibilityFocus(host, id) {
20
+ return host.accessibility.provider.setFocus(id);
107
21
  }
108
- export function installAccessibilityHostBackend(backend) {
109
- if (_host !== null) {
110
- if (_host !== backend)
111
- _hostConflict = true;
112
- return;
113
- }
114
- _host = backend;
22
+ // Registers or updates a node in the selected Host provider's mirrored tree.
23
+ export function setAccessibilityNode(host, node) {
24
+ return host.accessibility.provider.setNode(node);
115
25
  }
116
- export function observeAccessibilityHostResult(operation, succeeded) {
117
- _hostObservation = {
118
- operation,
119
- viability: succeeded ? 'available' : 'runtime-api-unavailable',
120
- };
121
- }
122
- // Removes a node and its entire descendant subtree from the mirrored tree.
123
- export function removeAccessibilityNode(id) {
124
- getAccessibilityBackend().removeNode(id);
125
- }
126
- export function resetAccessibilityBackendForTest() {
127
- _custom = null;
128
- _host = null;
129
- _hostConflict = false;
130
- _hostObservation = null;
131
- }
132
- // Installs a custom accessibility backend; pass null to revert to precedence fallback.
133
- export function setAccessibilityBackend(backend) {
134
- _custom = backend;
135
- }
136
- // Moves platform focus to the published node. Returns false when the node is missing or the platform
137
- // could not focus it — a sentinel, never a throw.
138
- export function setAccessibilityFocus(id) {
139
- return getAccessibilityBackend().setFocus(id);
140
- }
141
- // Registers or updates a node in the mirrored tree, keyed by `node.id` and parented by
142
- // `node.parentId`. Re-issuing with the same id updates the existing node in place.
143
- export function setAccessibilityNode(node) {
144
- getAccessibilityBackend().setNode(node);
145
- }
146
- let _custom = null;
147
- let _host = null;
148
- let _hostConflict = false;
149
- let _hostObservation = null;
150
- const _sentinel = {
151
- setNode() { },
152
- removeNode() { },
153
- clear() { },
154
- setFocus() {
155
- return false;
156
- },
157
- announce() { },
158
- };
159
- const _TEXT_NODE = 3;
160
- // Reflects a node's semantics onto its overlay element: role, the label/description/value text and
161
- // their ARIA attributes, focusability, and the mapped state attributes. Attributes are cleared when
162
- // their source field is absent, so an update that drops a field clears the stale attribute.
163
- function _applyAccessibilityElementAttributes(element, node) {
164
- element.setAttribute('role', node.role);
165
- _reflectAccessibilityAttribute(element, 'aria-label', node.label);
166
- _reflectAccessibilityAttribute(element, 'aria-description', node.description);
167
- _reflectAccessibilityAttribute(element, 'title', node.description);
168
- _reflectAccessibilityAttribute(element, 'aria-valuetext', node.value);
169
- _setAccessibilityElementValueText(element, node.value);
170
- // Every node element is focusable so setFocus can move platform focus to it, including
171
- // non-interactive roles (heading/region) a caller may want to move a screen reader to.
172
- element.setAttribute('tabindex', '-1');
173
- _applyAccessibilityStateAttributes(element, node.states ?? _EMPTY_STATE);
174
- }
175
- // Maps each AccessibilityState field to its ARIA attribute. Absent fields clear the attribute so a
176
- // state that was set then dropped does not linger.
177
- function _applyAccessibilityStateAttributes(element, state) {
178
- _reflectAccessibilityBoolean(element, 'aria-disabled', state.disabled);
179
- _reflectAccessibilityBoolean(element, 'aria-checked', state.checked);
180
- _reflectAccessibilityBoolean(element, 'aria-expanded', state.expanded);
181
- _reflectAccessibilityBoolean(element, 'aria-selected', state.selected);
182
- _reflectAccessibilityBoolean(element, 'aria-pressed', state.pressed);
183
- _reflectAccessibilityBoolean(element, 'aria-busy', state.busy);
184
- _reflectAccessibilityBoolean(element, 'aria-hidden', state.hidden);
185
- _reflectAccessibilityBoolean(element, 'aria-readonly', state.readonly);
186
- _reflectAccessibilityBoolean(element, 'aria-required', state.required);
187
- _reflectAccessibilityNumber(element, 'aria-level', state.level);
188
- _reflectAccessibilityNumber(element, 'aria-valuemin', state.valueMin);
189
- _reflectAccessibilityNumber(element, 'aria-valuemax', state.valueMax);
190
- _reflectAccessibilityNumber(element, 'aria-valuenow', state.valueNow);
191
- }
192
- // Builds the visually-hidden overlay container. Uses the standard clip-rect "sr-only" inline styles
193
- // so the elements stay in the accessibility tree while drawing nothing on screen.
194
- function _createHiddenAccessibilityContainer(doc) {
195
- const container = doc.createElement('div');
196
- container.setAttribute('data-flight-accessibility', 'true');
197
- const style = container.style;
198
- style.position = 'absolute';
199
- style.width = '1px';
200
- style.height = '1px';
201
- style.margin = '-1px';
202
- style.padding = '0';
203
- style.border = '0';
204
- style.overflow = 'hidden';
205
- style.clip = 'rect(0 0 0 0)';
206
- style.clipPath = 'inset(50%)';
207
- style.whiteSpace = 'nowrap';
208
- return container;
209
- }
210
- // Resolves the persistent aria-live region for the given urgency, creating and appending it under
211
- // the container on first use (or when a prior clear removed it).
212
- function _getAccessibilityLiveRegion(root, liveRegions, liveness) {
213
- let region = liveRegions.get(liveness);
214
- if (region === undefined || region.parentNode === null) {
215
- region = root.ownerDocument.createElement('div');
216
- region.setAttribute('aria-live', liveness);
217
- region.setAttribute('aria-atomic', 'true');
218
- region.setAttribute('data-flight-accessibility-live', liveness);
219
- liveRegions.set(liveness, region);
220
- root.appendChild(region);
221
- }
222
- return region;
223
- }
224
- // Sets a string attribute, or removes it when the value is absent.
225
- function _reflectAccessibilityAttribute(element, attribute, value) {
226
- if (value === undefined) {
227
- element.removeAttribute(attribute);
228
- return;
229
- }
230
- element.setAttribute(attribute, value);
231
- }
232
- // Sets a boolean attribute as 'true'/'false', or removes it when the value is absent.
233
- function _reflectAccessibilityBoolean(element, attribute, value) {
234
- if (value === undefined) {
235
- element.removeAttribute(attribute);
236
- return;
237
- }
238
- element.setAttribute(attribute, value ? 'true' : 'false');
239
- }
240
- // Sets a numeric attribute as its decimal string, or removes it when the value is absent.
241
- function _reflectAccessibilityNumber(element, attribute, value) {
242
- if (value === undefined) {
243
- element.removeAttribute(attribute);
244
- return;
245
- }
246
- element.setAttribute(attribute, String(value));
247
- }
248
- // Moves the element under the element of `parentId` (or the container root when the parent is unknown
249
- // or unset). Only touches the DOM when the parent actually changes, so re-issuing an unchanged node
250
- // causes no reparenting churn.
251
- function _reparentAccessibilityElement(element, parentId, elements, root) {
252
- let parent = root;
253
- if (parentId !== undefined) {
254
- const found = elements.get(parentId);
255
- if (found !== undefined)
256
- parent = found;
257
- }
258
- if (element.parentNode !== parent)
259
- parent.appendChild(element);
260
- }
261
- // Maintains the node's value as a leading text node so nested child elements survive value updates
262
- // (setting element.textContent would wipe the appended child nodes).
263
- function _setAccessibilityElementValueText(element, value) {
264
- const first = element.firstChild;
265
- if (value === undefined) {
266
- if (first !== null && first.nodeType === _TEXT_NODE)
267
- element.removeChild(first);
268
- return;
269
- }
270
- if (first !== null && first.nodeType === _TEXT_NODE) {
271
- first.nodeValue = value;
272
- return;
273
- }
274
- element.insertBefore(element.ownerDocument.createTextNode(value), first);
275
- }
276
- const _EMPTY_STATE = {};
277
26
  //# sourceMappingURL=accessibility.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../src/accessibility.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../src/accessibility.ts"],"names":[],"mappings":"AAOA,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CACnC,IAA8B,EAC9B,OAAe,EACf,WAAkC,QAAQ;IAE1C,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AACjE,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,sBAAsB,CACpC,IAA8B;IAE9B,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;AAC7C,CAAC;AAED,qGAAqG;AACrG,2GAA2G;AAC3G,MAAM,UAAU,oBAAoB,CAAC,IAA8B;IACjE,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;AACxC,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,uBAAuB,CACrC,IAA8B,EAC9B,EAAU;IAEV,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,qBAAqB,CACnC,IAA8B,EAC9B,EAAU;IAEV,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAClD,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,oBAAoB,CAClC,IAA8B,EAC9B,IAAiC;IAEjC,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AACnD,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { explainAccessibilityBackend, announceAccessibility, clearAccessibilityTree, removeAccessibilityNode, setAccessibilityFocus, setAccessibilityNode, } from './contract';
1
+ export { announceAccessibility, clearAccessibilityTree, destroyAccessibility, removeAccessibilityNode, setAccessibilityFocus, setAccessibilityNode, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { explainAccessibilityBackend, announceAccessibility, clearAccessibilityTree, removeAccessibilityNode, setAccessibilityFocus, setAccessibilityNode, } from './contract';
1
+ export { announceAccessibility, clearAccessibilityTree, destroyAccessibility, removeAccessibilityNode, setAccessibilityFocus, setAccessibilityNode, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/accessibility",
3
- "version": "0.5.0-next.1944.cd6b6e7",
3
+ "version": "0.5.1-edge.638.d4dbd4e",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,11 +38,11 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/types": "0.5.0-next.1944.cd6b6e7"
41
+ "@flighthq/types": "0.5.1-edge.638.d4dbd4e"
42
42
  },
43
43
  "devDependencies": {
44
44
  "typescript": "^5.3.0"
45
45
  },
46
- "description": "Assistive-technology (ARIA) bridge over a swappable web/native backend (a visually-hidden ARIA DOM overlay by default)",
46
+ "description": "Host-explicit assistive-technology bridge for mirrored semantic nodes, focus, and announcements",
47
47
  "sideEffects": false
48
48
  }
@@ -1,368 +1,90 @@
1
- import type { AccessibilityBackend, AccessibilityLiveness, AccessibilityNode } from '@flighthq/types/contract';
2
- import { afterEach, describe, expect, it, vi } from 'vitest';
1
+ import { EntityRuntimeKey } from '@flighthq/types/contract';
2
+ import type { AccessibilityBackend, AccessibilityNode, EntityWithoutRuntime } from '@flighthq/types/contract';
3
+ import { vi } from 'vitest';
3
4
 
4
5
  import {
5
6
  announceAccessibility,
6
7
  clearAccessibilityTree,
7
- createWebAccessibilityBackend,
8
- getAccessibilityBackend,
8
+ destroyAccessibility,
9
9
  removeAccessibilityNode,
10
- setAccessibilityBackend,
11
10
  setAccessibilityFocus,
12
11
  setAccessibilityNode,
13
- explainAccessibilityBackend,
14
- installAccessibilityHostBackend,
15
- observeAccessibilityHostResult,
16
- resetAccessibilityBackendForTest,
17
12
  } from './accessibility';
18
13
 
19
- afterEach(() => {
20
- setAccessibilityBackend(null);
21
- vi.unstubAllGlobals();
22
- document.body.replaceChildren();
23
- });
24
-
25
14
  describe('announceAccessibility', () => {
26
- it('dispatches the message and liveness to the backend', () => {
27
- const mock = createMockAccessibilityBackend();
28
- setAccessibilityBackend(mock.backend);
29
- announceAccessibility('saved', 'assertive');
30
- expect(mock.calls.announce).toEqual([['saved', 'assertive']]);
31
- });
15
+ it('returns the selected Host provider outcome and supplies the default liveness', () => {
16
+ const provider = createProvider();
17
+ const host = { accessibility: { provider } };
32
18
 
33
- it('defaults liveness to polite', () => {
34
- const mock = createMockAccessibilityBackend();
35
- setAccessibilityBackend(mock.backend);
36
- announceAccessibility('loading');
37
- expect(mock.calls.announce).toEqual([['loading', 'polite']]);
19
+ expect(announceAccessibility(host, 'saved')).toEqual({ reason: 'ok' });
20
+ expect(provider.announce).toHaveBeenCalledWith('saved', 'polite');
38
21
  });
39
22
  });
40
23
 
41
24
  describe('clearAccessibilityTree', () => {
42
- it('dispatches a clear to the backend', () => {
43
- const mock = createMockAccessibilityBackend();
44
- setAccessibilityBackend(mock.backend);
45
- clearAccessibilityTree();
46
- expect(mock.calls.clear).toBe(1);
47
- });
48
- });
49
-
50
- describe('createWebAccessibilityBackend', () => {
51
- it('creates an element carrying role, aria-label, and mapped state attributes', () => {
52
- const container = document.createElement('div');
53
- const backend = createWebAccessibilityBackend(container);
54
- backend.setNode(node('play', 'button', { label: 'Play', states: { disabled: true, pressed: false } }));
55
- const element = container.querySelector('[data-flight-accessibility-id="play"]');
56
- expect(element).not.toBeNull();
57
- expect(element?.getAttribute('role')).toBe('button');
58
- expect(element?.getAttribute('aria-label')).toBe('Play');
59
- expect(element?.getAttribute('aria-disabled')).toBe('true');
60
- expect(element?.getAttribute('aria-pressed')).toBe('false');
61
- });
62
-
63
- it('maps description to aria-description and title, and value to text and aria-valuetext', () => {
64
- const container = document.createElement('div');
65
- const backend = createWebAccessibilityBackend(container);
66
- backend.setNode(node('vol', 'slider', { description: 'Volume level', value: '70%' }));
67
- const element = container.querySelector('[data-flight-accessibility-id="vol"]');
68
- expect(element?.getAttribute('aria-description')).toBe('Volume level');
69
- expect(element?.getAttribute('title')).toBe('Volume level');
70
- expect(element?.getAttribute('aria-valuetext')).toBe('70%');
71
- expect(element?.textContent).toBe('70%');
72
- });
73
-
74
- it('maps range and level numerics to their aria attributes', () => {
75
- const container = document.createElement('div');
76
- const backend = createWebAccessibilityBackend(container);
77
- backend.setNode(node('bar', 'progressbar', { states: { valueMin: 0, valueMax: 100, valueNow: 42, level: 2 } }));
78
- const element = container.querySelector('[data-flight-accessibility-id="bar"]');
79
- expect(element?.getAttribute('aria-valuemin')).toBe('0');
80
- expect(element?.getAttribute('aria-valuemax')).toBe('100');
81
- expect(element?.getAttribute('aria-valuenow')).toBe('42');
82
- expect(element?.getAttribute('aria-level')).toBe('2');
83
- });
84
-
85
- it('updates an existing node in place without duplicating the element', () => {
86
- const container = document.createElement('div');
87
- const backend = createWebAccessibilityBackend(container);
88
- backend.setNode(node('cb', 'checkbox', { label: 'Mute', states: { checked: false } }));
89
- backend.setNode(node('cb', 'checkbox', { label: 'Muted', states: { checked: true } }));
90
- const matches = container.querySelectorAll('[data-flight-accessibility-id="cb"]');
91
- expect(matches.length).toBe(1);
92
- expect(matches[0].getAttribute('aria-label')).toBe('Muted');
93
- expect(matches[0].getAttribute('aria-checked')).toBe('true');
94
- });
95
-
96
- it('clears an attribute when an updated node drops the field', () => {
97
- const container = document.createElement('div');
98
- const backend = createWebAccessibilityBackend(container);
99
- backend.setNode(node('cb', 'checkbox', { label: 'Mute', states: { checked: true } }));
100
- backend.setNode(node('cb', 'checkbox', {}));
101
- const element = container.querySelector('[data-flight-accessibility-id="cb"]');
102
- expect(element?.hasAttribute('aria-label')).toBe(false);
103
- expect(element?.hasAttribute('aria-checked')).toBe(false);
104
- expect(element?.textContent).toBe('');
105
- });
106
-
107
- it('nests a child element under its parentId element', () => {
108
- const container = document.createElement('div');
109
- const backend = createWebAccessibilityBackend(container);
110
- backend.setNode(node('menu', 'menu', { label: 'File' }));
111
- backend.setNode(node('item', 'menuitem', { label: 'Open', parentId: 'menu' }));
112
- const parent = container.querySelector('[data-flight-accessibility-id="menu"]');
113
- const child = container.querySelector('[data-flight-accessibility-id="item"]');
114
- expect(child?.parentElement).toBe(parent);
115
- });
25
+ it('returns the selected Host provider outcome', () => {
26
+ const provider = createProvider({ clear: vi.fn(() => ({ reason: 'no-dom' as const })) });
116
27
 
117
- it('re-parents an element when parentId changes', () => {
118
- const container = document.createElement('div');
119
- const backend = createWebAccessibilityBackend(container);
120
- backend.setNode(node('a', 'group', {}));
121
- backend.setNode(node('b', 'group', {}));
122
- backend.setNode(node('leaf', 'button', { parentId: 'a' }));
123
- const child = () => container.querySelector('[data-flight-accessibility-id="leaf"]');
124
- expect(child()?.parentElement?.getAttribute('data-flight-accessibility-id')).toBe('a');
125
- backend.setNode(node('leaf', 'button', { parentId: 'b' }));
126
- expect(child()?.parentElement?.getAttribute('data-flight-accessibility-id')).toBe('b');
127
- });
128
-
129
- it('removes a node and its descendant subtree', () => {
130
- const container = document.createElement('div');
131
- const backend = createWebAccessibilityBackend(container);
132
- backend.setNode(node('menu', 'menu', {}));
133
- backend.setNode(node('item', 'menuitem', { parentId: 'menu' }));
134
- backend.removeNode('menu');
135
- expect(container.querySelector('[data-flight-accessibility-id="menu"]')).toBeNull();
136
- expect(container.querySelector('[data-flight-accessibility-id="item"]')).toBeNull();
137
- // The descendant id is dropped from tracking too, so re-adding it lands at the root.
138
- backend.setNode(node('item', 'menuitem', { parentId: 'menu' }));
139
- expect(container.querySelector('[data-flight-accessibility-id="item"]')?.parentElement).toBe(container);
140
- });
141
-
142
- it('focuses a node element and reports missing ids as false', () => {
143
- const container = document.createElement('div');
144
- document.body.appendChild(container);
145
- const backend = createWebAccessibilityBackend(container);
146
- backend.setNode(node('field', 'textbox', { label: 'Name' }));
147
- expect(backend.setFocus('field')).toBe(true);
148
- const element = container.querySelector('[data-flight-accessibility-id="field"]');
149
- expect(document.activeElement).toBe(element);
150
- expect(backend.setFocus('missing')).toBe(false);
151
- });
152
-
153
- it('writes announcements into the polite and assertive live regions', () => {
154
- const container = document.createElement('div');
155
- const backend = createWebAccessibilityBackend(container);
156
- backend.announce('level complete', 'polite');
157
- backend.announce('warning', 'assertive');
158
- const polite = container.querySelector('[data-flight-accessibility-live="polite"]');
159
- const assertive = container.querySelector('[data-flight-accessibility-live="assertive"]');
160
- expect(polite?.getAttribute('aria-live')).toBe('polite');
161
- expect(polite?.textContent).toBe('level complete');
162
- expect(assertive?.getAttribute('aria-live')).toBe('assertive');
163
- expect(assertive?.textContent).toBe('warning');
164
- });
165
-
166
- it('empties the container on clear', () => {
167
- const container = document.createElement('div');
168
- const backend = createWebAccessibilityBackend(container);
169
- backend.setNode(node('a', 'button', {}));
170
- backend.announce('hi', 'polite');
171
- expect(container.childElementCount).toBeGreaterThan(0);
172
- backend.clear();
173
- expect(container.childElementCount).toBe(0);
174
- });
175
-
176
- it('defaults its container into document.body', () => {
177
- const backend = createWebAccessibilityBackend();
178
- backend.setNode(node('a', 'button', {}));
179
- expect(document.querySelector('[data-flight-accessibility-id="a"]')).not.toBeNull();
180
- expect(document.body.querySelector('[data-flight-accessibility]')).not.toBeNull();
181
- });
182
-
183
- it('is a sentinel no-op when no DOM is available', () => {
184
- vi.stubGlobal('document', undefined);
185
- const backend = createWebAccessibilityBackend();
186
- expect(() => backend.setNode(node('a', 'button', {}))).not.toThrow();
187
- expect(() => backend.removeNode('a')).not.toThrow();
188
- expect(() => backend.announce('hi', 'polite')).not.toThrow();
189
- expect(() => backend.clear()).not.toThrow();
190
- expect(backend.setFocus('a')).toBe(false);
28
+ expect(clearAccessibilityTree({ accessibility: { provider } })).toEqual({ reason: 'no-dom' });
191
29
  });
192
30
  });
193
31
 
194
- describe('explainAccessibilityBackend', () => {
195
- afterEach(() => resetAccessibilityBackendForTest());
32
+ describe('destroyAccessibility', () => {
33
+ it('destroys the selected Host provider and no other provider', () => {
34
+ const first = createProvider();
35
+ const second = createProvider();
196
36
 
197
- it('reports host-not-enabled when no backend is installed', () => {
198
- resetAccessibilityBackendForTest();
199
- const explanation = explainAccessibilityBackend();
200
- expect(explanation.layer).toBe('host-not-enabled');
201
- expect(explanation.conflict).toBe(false);
202
- expect(explanation.viability).toBe('unobserved');
203
- });
204
-
205
- it('reports custom layer when a custom backend is set', () => {
206
- setAccessibilityBackend(getAccessibilityBackend());
207
- expect(explainAccessibilityBackend().layer).toBe('custom');
208
- });
37
+ destroyAccessibility({ accessibility: { provider: first } });
209
38
 
210
- it('reports host layer when a host backend is installed', () => {
211
- installAccessibilityHostBackend(getAccessibilityBackend());
212
- expect(explainAccessibilityBackend().layer).toBe('host');
213
- });
214
-
215
- it('reports conflict when two different host backends are installed', () => {
216
- installAccessibilityHostBackend({ ...getAccessibilityBackend() });
217
- installAccessibilityHostBackend({ ...getAccessibilityBackend() });
218
- expect(explainAccessibilityBackend().conflict).toBe(true);
39
+ expect(first.destroy).toHaveBeenCalledOnce();
40
+ expect(second.destroy).not.toHaveBeenCalled();
219
41
  });
220
42
  });
221
43
 
222
- describe('getAccessibilityBackend', () => {
223
- it('lazily returns a stable web default', () => {
224
- const first = getAccessibilityBackend();
225
- expect(first).toBe(getAccessibilityBackend());
226
- });
44
+ describe('removeAccessibilityNode', () => {
45
+ it('forwards the node id and returns a node-not-found outcome', () => {
46
+ const provider = createProvider({ removeNode: vi.fn(() => ({ reason: 'node-not-found' as const })) });
227
47
 
228
- it('returns the installed backend', () => {
229
- const mock = createMockAccessibilityBackend();
230
- setAccessibilityBackend(mock.backend);
231
- expect(getAccessibilityBackend()).toBe(mock.backend);
48
+ expect(removeAccessibilityNode({ accessibility: { provider } }, 'missing')).toEqual({
49
+ reason: 'node-not-found',
50
+ });
51
+ expect(provider.removeNode).toHaveBeenCalledWith('missing');
232
52
  });
233
53
  });
234
54
 
235
- describe('installAccessibilityHostBackend', () => {
236
- afterEach(() => resetAccessibilityBackendForTest());
237
-
238
- it('installs a host backend that getAccessibilityBackend returns', () => {
239
- const backend = getAccessibilityBackend();
240
- installAccessibilityHostBackend(backend);
241
- expect(getAccessibilityBackend()).toBe(backend);
242
- });
55
+ describe('setAccessibilityFocus', () => {
56
+ it('forwards the node id and returns a focus-not-moved outcome', () => {
57
+ const provider = createProvider({ setFocus: vi.fn(() => ({ reason: 'focus-not-moved' as const })) });
243
58
 
244
- it('is first-host-wins: a second different backend sets conflict', () => {
245
- const first = { ...getAccessibilityBackend() };
246
- const second = { ...getAccessibilityBackend() };
247
- installAccessibilityHostBackend(first);
248
- installAccessibilityHostBackend(second);
249
- expect(getAccessibilityBackend()).toBe(first);
250
- expect(explainAccessibilityBackend().conflict).toBe(true);
59
+ expect(setAccessibilityFocus({ accessibility: { provider } }, 'subject')).toEqual({
60
+ reason: 'focus-not-moved',
61
+ });
62
+ expect(provider.setFocus).toHaveBeenCalledWith('subject');
251
63
  });
252
64
  });
253
65
 
254
- describe('observeAccessibilityHostResult', () => {
255
- afterEach(() => resetAccessibilityBackendForTest());
256
-
257
- it('records a successful observation', () => {
258
- installAccessibilityHostBackend(getAccessibilityBackend());
259
- observeAccessibilityHostResult('setNode', true);
260
- const explanation = explainAccessibilityBackend();
261
- expect(explanation.operation).toBe('setNode');
262
- expect(explanation.viability).toBe('available');
263
- });
66
+ describe('setAccessibilityNode', () => {
67
+ it('publishes only through the explicitly supplied Host', () => {
68
+ const first = createProvider();
69
+ const second = createProvider();
70
+ const subject: AccessibilityNode = { id: 'subject', label: 'Subject', role: 'button' };
264
71
 
265
- it('records a failed observation', () => {
266
- installAccessibilityHostBackend(getAccessibilityBackend());
267
- observeAccessibilityHostResult('setNode', false);
268
- expect(explainAccessibilityBackend().viability).toBe('runtime-api-unavailable');
269
- });
270
- });
72
+ expect(setAccessibilityNode({ accessibility: { provider: first } }, subject)).toEqual({ reason: 'ok' });
271
73
 
272
- describe('removeAccessibilityNode', () => {
273
- it('dispatches the id to the backend', () => {
274
- const mock = createMockAccessibilityBackend();
275
- setAccessibilityBackend(mock.backend);
276
- removeAccessibilityNode('gone');
277
- expect(mock.calls.removeNode).toEqual(['gone']);
74
+ expect(first.setNode).toHaveBeenCalledWith(subject);
75
+ expect(second.setNode).not.toHaveBeenCalled();
278
76
  });
279
77
  });
280
78
 
281
- function node(
282
- id: string,
283
- role: AccessibilityNode['role'],
284
- rest: Readonly<Omit<AccessibilityNode, 'id' | 'role'>>,
285
- ): AccessibilityNode {
286
- return { id, role, ...rest };
287
- }
288
-
289
- interface MockAccessibilityCalls {
290
- setNode: AccessibilityNode[];
291
- removeNode: string[];
292
- clear: number;
293
- setFocus: string[];
294
- announce: (readonly [string, AccessibilityLiveness])[];
295
- }
296
-
297
- function createMockAccessibilityBackend(): { backend: AccessibilityBackend; calls: MockAccessibilityCalls } {
298
- const calls: MockAccessibilityCalls = { setNode: [], removeNode: [], clear: 0, setFocus: [], announce: [] };
299
- const backend: AccessibilityBackend = {
300
- setNode(target) {
301
- calls.setNode.push(target as AccessibilityNode);
302
- },
303
- removeNode(id) {
304
- calls.removeNode.push(id);
305
- },
306
- clear() {
307
- calls.clear += 1;
308
- },
309
- setFocus(id) {
310
- calls.setFocus.push(id);
311
- return id === 'ok';
312
- },
313
- announce(message, liveness) {
314
- calls.announce.push([message, liveness]);
315
- },
79
+ function createProvider(overrides: Partial<EntityWithoutRuntime<AccessibilityBackend>> = {}): AccessibilityBackend {
80
+ return {
81
+ [EntityRuntimeKey]: undefined,
82
+ announce: vi.fn(() => ({ reason: 'ok' as const })),
83
+ clear: vi.fn(() => ({ reason: 'ok' as const })),
84
+ destroy: vi.fn(),
85
+ removeNode: vi.fn(() => ({ reason: 'ok' as const })),
86
+ setFocus: vi.fn(() => ({ reason: 'ok' as const })),
87
+ setNode: vi.fn(() => ({ reason: 'ok' as const })),
88
+ ...overrides,
316
89
  };
317
- return { backend, calls };
318
90
  }
319
-
320
- describe('resetAccessibilityBackendForTest', () => {
321
- it('clears all backend slots', () => {
322
- setAccessibilityBackend(getAccessibilityBackend());
323
- installAccessibilityHostBackend(getAccessibilityBackend());
324
- observeAccessibilityHostResult('setNode', true);
325
- resetAccessibilityBackendForTest();
326
- expect(explainAccessibilityBackend().layer).toBe('host-not-enabled');
327
- expect(explainAccessibilityBackend().conflict).toBe(false);
328
- expect(explainAccessibilityBackend().viability).toBe('unobserved');
329
- });
330
- });
331
-
332
- describe('setAccessibilityBackend', () => {
333
- it('installs a backend that later commands dispatch through', () => {
334
- const mock = createMockAccessibilityBackend();
335
- setAccessibilityBackend(mock.backend);
336
- setAccessibilityNode(node('a', 'button', {}));
337
- expect(mock.calls.setNode.length).toBe(1);
338
- });
339
-
340
- it('reverts to the sentinel when passed null', () => {
341
- const sentinel = getAccessibilityBackend();
342
- const mock = createMockAccessibilityBackend();
343
- setAccessibilityBackend(mock.backend);
344
- expect(getAccessibilityBackend()).toBe(mock.backend);
345
- setAccessibilityBackend(null);
346
- expect(getAccessibilityBackend()).toBe(sentinel);
347
- });
348
- });
349
-
350
- describe('setAccessibilityFocus', () => {
351
- it('dispatches the id and returns the backend result', () => {
352
- const mock = createMockAccessibilityBackend();
353
- setAccessibilityBackend(mock.backend);
354
- expect(setAccessibilityFocus('ok')).toBe(true);
355
- expect(setAccessibilityFocus('nope')).toBe(false);
356
- expect(mock.calls.setFocus).toEqual(['ok', 'nope']);
357
- });
358
- });
359
-
360
- describe('setAccessibilityNode', () => {
361
- it('dispatches the node to the backend', () => {
362
- const mock = createMockAccessibilityBackend();
363
- setAccessibilityBackend(mock.backend);
364
- const target = node('a', 'button', { label: 'Go' });
365
- setAccessibilityNode(target);
366
- expect(mock.calls.setNode).toEqual([target]);
367
- });
368
- });