@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 +3 -3
- package/dist/accessibility.d.ts +7 -14
- package/dist/accessibility.d.ts.map +1 -1
- package/dist/accessibility.js +19 -270
- package/dist/accessibility.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/accessibility.test.ts +51 -329
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @flighthq/accessibility
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
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)
|
package/dist/accessibility.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
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,
|
|
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"}
|
package/dist/accessibility.js
CHANGED
|
@@ -1,277 +1,26 @@
|
|
|
1
|
-
// Speaks a transient message through the
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
//
|
|
105
|
-
export function
|
|
106
|
-
return
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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":"
|
|
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 {
|
|
1
|
+
export { announceAccessibility, clearAccessibilityTree, destroyAccessibility, removeAccessibilityNode, setAccessibilityFocus, setAccessibilityNode, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
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 {
|
|
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,
|
|
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.
|
|
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.
|
|
41
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"typescript": "^5.3.0"
|
|
45
45
|
},
|
|
46
|
-
"description": "
|
|
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
|
|
2
|
-
import {
|
|
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
|
-
|
|
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('
|
|
27
|
-
const
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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('
|
|
43
|
-
const
|
|
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
|
-
|
|
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('
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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('
|
|
223
|
-
it('
|
|
224
|
-
const
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
expect(
|
|
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('
|
|
236
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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('
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
):
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
});
|