@generative-a11y/dom 0.0.0
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/LICENSE +21 -0
- package/README.md +317 -0
- package/dist/index.cjs +1013 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +976 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bhavesh Chowdhury
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# `@generative-a11y/dom`
|
|
2
|
+
|
|
3
|
+
DOM announcement delivery for `@generative-a11y/core`. The package mounts or
|
|
4
|
+
adopts live regions without changing the host application's visible interface.
|
|
5
|
+
It reports DOM delivery actions; it does not claim that assistive technology
|
|
6
|
+
produced speech.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @generative-a11y/core @generative-a11y/dom
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Connect a runtime
|
|
15
|
+
|
|
16
|
+
`connectRuntimeToDOM(runtime, options?)` creates a `DOMAnnouncer`, subscribes it
|
|
17
|
+
to a `GenerativeA11yRuntime`, and returns a `DOMRuntimeBinding`. Disposing the
|
|
18
|
+
binding unsubscribes and disposes the announcer, but never disposes the borrowed
|
|
19
|
+
runtime.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createGenerativeA11y } from "@generative-a11y/core";
|
|
23
|
+
import { connectRuntimeToDOM } from "@generative-a11y/dom";
|
|
24
|
+
|
|
25
|
+
const runtime = createGenerativeA11y({});
|
|
26
|
+
const binding = connectRuntimeToDOM(runtime);
|
|
27
|
+
|
|
28
|
+
// Later:
|
|
29
|
+
binding.dispose();
|
|
30
|
+
runtime.dispose();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`DOMRuntimeBinding` exposes the connected `announcer` and an idempotent
|
|
34
|
+
`dispose()` method. Once disposed, the binding cannot mutate its regions even if
|
|
35
|
+
the runtime was already delivering an event. If subscription fails, any regions
|
|
36
|
+
created for the attempted binding are removed before the error is rethrown.
|
|
37
|
+
|
|
38
|
+
## Create an announcer directly
|
|
39
|
+
|
|
40
|
+
`createDOMAnnouncer(options?)` returns a `DOMAnnouncer`. When a document is
|
|
41
|
+
available, it synchronously mounts one polite and one assertive region before
|
|
42
|
+
returning. Each announcer owns an isolated pair. Without an injected or global
|
|
43
|
+
document it remains inert, so the module and constructor are safe in server
|
|
44
|
+
environments.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createDOMAnnouncer } from "@generative-a11y/dom";
|
|
48
|
+
|
|
49
|
+
const announcer = createDOMAnnouncer({ mode: "auto" });
|
|
50
|
+
const result = announcer.announce(intent);
|
|
51
|
+
announcer.dispose();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`DOMAnnouncer` provides:
|
|
55
|
+
|
|
56
|
+
- `announce(intent)`, which returns a `DOMDeliveryResult`.
|
|
57
|
+
- `getRegions()`, which returns the `DOMLiveRegions` pair or `undefined` when
|
|
58
|
+
the DOM is unavailable.
|
|
59
|
+
- An idempotent `dispose()`. It removes regions created by the announcer and
|
|
60
|
+
leaves supplied regions mounted. Later announcements report `disposed`.
|
|
61
|
+
|
|
62
|
+
## Options and modes
|
|
63
|
+
|
|
64
|
+
`DOMAnnouncerOptions` accepts:
|
|
65
|
+
|
|
66
|
+
- `document`: an injected `Document`. If omitted, creation uses a supplied
|
|
67
|
+
region's owner document and then the current global document, when available.
|
|
68
|
+
- `mode`: a `DOMAnnouncementMode` of `"auto"`, `"aria-notify"`, or
|
|
69
|
+
`"live-region"`. Both progressive-enhancement modes try a callable
|
|
70
|
+
`ariaNotify` on the selected region and fall back when it is absent.
|
|
71
|
+
`"live-region"` always uses text mutation.
|
|
72
|
+
- `regions`: a pre-mounted `DOMLiveRegions` pair with `polite` and `assertive`
|
|
73
|
+
elements. Supply dedicated, connected, empty elements in the same document;
|
|
74
|
+
neither element may contain the other, and an explicitly supplied `document`
|
|
75
|
+
must be their owner document. The driver normalizes direct hiding attributes
|
|
76
|
+
and inline `display`, `visibility`, and `content-visibility`, then applies the
|
|
77
|
+
required live-region attributes and visually hidden inline styles. The caller
|
|
78
|
+
must ensure ancestors and external CSS keep both regions in the accessibility
|
|
79
|
+
tree. Supplied elements remain owned by the caller.
|
|
80
|
+
- `onDiagnostic`: a callback invoked with the `DOMDeliveryResult` for each
|
|
81
|
+
attempted announcement. Callback errors are isolated from delivery.
|
|
82
|
+
|
|
83
|
+
If `ariaNotify` throws, that notifier is disabled for the announcer. The same
|
|
84
|
+
intent is delivered once through live-region mutation, and later intents remain
|
|
85
|
+
on that fallback path.
|
|
86
|
+
|
|
87
|
+
## Delivery results
|
|
88
|
+
|
|
89
|
+
`DOMDeliveryResult` is serializable and contains:
|
|
90
|
+
|
|
91
|
+
- `status`: `"notified"`, `"mutated"`, `"unavailable"`, or `"disposed"`.
|
|
92
|
+
- `method`: `"aria-notify"`, `"live-region"`, or `"none"`.
|
|
93
|
+
- `channel`: the selected core announcement channel.
|
|
94
|
+
- `error`, when notifier invocation failed, with serializable `name` and
|
|
95
|
+
`message` strings.
|
|
96
|
+
|
|
97
|
+
`DOMLiveRegions` contains the stable outer `polite` and `assertive` elements.
|
|
98
|
+
Live-region delivery sets or clears `lang` from the intent locale and replaces
|
|
99
|
+
the region's text content on every delivery, including repeated identical text.
|
|
100
|
+
Announcement strings are inserted as literal text, never HTML.
|
|
101
|
+
|
|
102
|
+
## Observe attention signals
|
|
103
|
+
|
|
104
|
+
`createAttentionStore(options?)` returns an `AttentionStore`, an external-store
|
|
105
|
+
compatible source of conservative browser signals. It observes raw visibility,
|
|
106
|
+
window focus, DOM focus area, and whether the currently registered newest
|
|
107
|
+
response intersects the viewport. These signals do not reveal a screen-reader
|
|
108
|
+
virtual cursor or user intent.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { createAttentionStore } from "@generative-a11y/dom";
|
|
112
|
+
|
|
113
|
+
const attention = createAttentionStore();
|
|
114
|
+
const unregisterComposer = attention.registerComposer(composerElement);
|
|
115
|
+
const unsubscribe = attention.subscribe(() => {
|
|
116
|
+
const snapshot = attention.getSnapshot();
|
|
117
|
+
console.log(snapshot.mode);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Later:
|
|
121
|
+
unsubscribe();
|
|
122
|
+
unregisterComposer();
|
|
123
|
+
attention.dispose();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`ExternalStore<T>` exposes `subscribe(listener)`, `getSnapshot()`, and
|
|
127
|
+
`getServerSnapshot()`. `AttentionStore` implements
|
|
128
|
+
`ExternalStore<AttentionSnapshot>` and adds:
|
|
129
|
+
|
|
130
|
+
- `registerComposer(element)` and `registerConversation(element)`. Any
|
|
131
|
+
registered element containing `document.activeElement` matches. Composer wins
|
|
132
|
+
when registered areas overlap. Repeated registrations of the same element are
|
|
133
|
+
reference-counted. Each returned unregister function is idempotent.
|
|
134
|
+
- `registerNewestResponse(element)`. Only one newest response is current; a
|
|
135
|
+
later registration replaces the prior target. Unregistering an older target
|
|
136
|
+
cannot remove its replacement. Each registration receives a fresh observer
|
|
137
|
+
epoch, including when the same element is registered again, so queued records
|
|
138
|
+
from an older registration are ignored. When one callback contains multiple
|
|
139
|
+
records for the current target, the final record is the current transition.
|
|
140
|
+
- `dispose()`, which is idempotent and removes listeners, registrations,
|
|
141
|
+
subscribers, and the intersection observer. Subscribing or registering after
|
|
142
|
+
disposal throws. Stores created without a document are permanently inert; all
|
|
143
|
+
their methods remain safe no-ops.
|
|
144
|
+
|
|
145
|
+
Subscribers present when a snapshot changes are invoked at most once for that
|
|
146
|
+
transition. Adding, removing, or re-subscribing listeners during notification
|
|
147
|
+
affects later transitions, not the stable listener snapshot already in flight.
|
|
148
|
+
Listener errors remain isolated.
|
|
149
|
+
|
|
150
|
+
`AttentionSnapshot` is frozen and cached by value, so `getSnapshot()` returns
|
|
151
|
+
the same reference until a raw or derived value changes. Its fields are:
|
|
152
|
+
|
|
153
|
+
- `visibility`: `"visible"`, `"hidden"`, or `"unknown"`.
|
|
154
|
+
- `windowFocus`: `"focused"`, `"blurred"`, or `"unknown"`.
|
|
155
|
+
- `focusArea`: `"composer"`, `"conversation"`, `"elsewhere"`, `"none"`, or
|
|
156
|
+
`"unknown"`.
|
|
157
|
+
- `newestResponse`: `"visible"`, `"outside"`, `"unobserved"`, or `"unknown"`.
|
|
158
|
+
- `mode`: `"background"` when hidden; `"away"` when visible and blurred;
|
|
159
|
+
`"reading-history"` when visible, focused, and the newest response is outside;
|
|
160
|
+
`"foreground"` when visible, focused, and the newest response is visible;
|
|
161
|
+
otherwise `"unknown"`.
|
|
162
|
+
|
|
163
|
+
Creation uses an injected document or the current browser document. Without
|
|
164
|
+
either, client and server snapshots are the same constant all-`"unknown"` value.
|
|
165
|
+
With a document but no registered newest response, `newestResponse` is
|
|
166
|
+
`"unobserved"`. Registering a newest response without IntersectionObserver
|
|
167
|
+
support produces `"unknown"`, never optimistic `"visible"`.
|
|
168
|
+
|
|
169
|
+
`AttentionStoreOptions` accepts an injected `document`, an optional
|
|
170
|
+
`createIntersectionObserver` (`AttentionIntersectionObserverFactory`), and an
|
|
171
|
+
optional `intersectionObserverInit`. The factory returns the minimal
|
|
172
|
+
`AttentionIntersectionObserver` interface (`observe`, `unobserve`, and
|
|
173
|
+
`disconnect`), allowing deterministic tests without browser globals. The store
|
|
174
|
+
does not use timers and never changes focus or scroll position. Observer
|
|
175
|
+
creation, observation, and cleanup failures are treated as unavailable
|
|
176
|
+
intersection evidence and deliberately suppressed; DOM listener and subscriber
|
|
177
|
+
cleanup still completes.
|
|
178
|
+
|
|
179
|
+
## Use conservative focus helpers
|
|
180
|
+
|
|
181
|
+
Focus helpers are never invoked automatically. They are explicit host actions
|
|
182
|
+
for workflows, such as restoring focus after an application-owned interaction.
|
|
183
|
+
Ordinary streaming, status changes, and announcements do not move focus.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { captureFocus, restoreFocus } from "@generative-a11y/dom";
|
|
187
|
+
|
|
188
|
+
const capture = captureFocus();
|
|
189
|
+
// The host opens and later closes its interaction.
|
|
190
|
+
const result = restoreFocus(capture, { onlyIfFocusWithin: interactionElement });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`captureFocus(document?)` returns a frozen `FocusCapture` containing the exact
|
|
194
|
+
originating `Document` and deepest active `Element` references. It follows
|
|
195
|
+
`activeElement` through nested open shadow roots and uses the current browser
|
|
196
|
+
document when none is injected. Missing documents, throwing `activeElement`
|
|
197
|
+
accessors, and focus on `body` or `documentElement` produce a capture with no
|
|
198
|
+
restorable target. Closed shadow roots deliberately expose only their host, so
|
|
199
|
+
their internal focused element is unavailable to these helpers.
|
|
200
|
+
|
|
201
|
+
`focusElement(target, options?)` requests focus through the target's public
|
|
202
|
+
`focus()` method. `FocusElementOptions.preventScroll` defaults to `true` and can
|
|
203
|
+
be set to `false`. Success is verified with `ownerDocument.activeElement`.
|
|
204
|
+
|
|
205
|
+
`restoreFocus(capture, options?)` checks that the captured target still belongs
|
|
206
|
+
to its originating document, then delegates to `focusElement`. A capture can be
|
|
207
|
+
used repeatedly. `RestoreFocusOptions` adds `onlyIfFocusWithin`: restoration
|
|
208
|
+
proceeds only while the current active element is that guard or its descendant.
|
|
209
|
+
If focus has moved elsewhere or is unavailable, restoration is skipped so a
|
|
210
|
+
later user focus move is preserved. Guard containment follows the composed tree
|
|
211
|
+
through assigned slots and across open shadow-root hosts.
|
|
212
|
+
|
|
213
|
+
Both operations return a finite `FocusResult`: either
|
|
214
|
+
`{ status: "focused", target }` or `{ status: "skipped", reason, target }`.
|
|
215
|
+
`FocusSkippedReason` is one of `"unavailable"`, `"cross-document"`,
|
|
216
|
+
`"disconnected"`, `"disabled"`, `"hidden"`, `"aria-hidden"`, `"inert"`,
|
|
217
|
+
`"missing-focus"`, `"guard-mismatch"`, `"focus-error"`, or
|
|
218
|
+
`"focus-not-applied"`.
|
|
219
|
+
|
|
220
|
+
Eligibility is intentionally conservative. The target must be an element from
|
|
221
|
+
its owner document, remain connected, expose a callable public focus method, not
|
|
222
|
+
be effectively disabled, and have no self or composed ancestor with `hidden`,
|
|
223
|
+
`aria-hidden="true"`, or `inert`. Effective disabled checks use the browser's
|
|
224
|
+
`:disabled` matching, including disabled-fieldset and first-legend behavior,
|
|
225
|
+
with guarded direct-state fallback. Composed ancestry follows assigned slots
|
|
226
|
+
before light-DOM parents, then crosses open shadow roots through their hosts.
|
|
227
|
+
These checks do not claim perfect browser focusability: layout, computed CSS,
|
|
228
|
+
tab order, and device behavior require real browser and assistive-technology
|
|
229
|
+
testing.
|
|
230
|
+
|
|
231
|
+
Eligibility is checked both before and immediately after the focus call. If the
|
|
232
|
+
target becomes ineligible while remaining deeply active, the helper restores the
|
|
233
|
+
previous target only when that previous target is still conservatively eligible.
|
|
234
|
+
A synchronous third-party redirect is never overwritten: if focus ends on
|
|
235
|
+
another element, the helper reports `"focus-not-applied"` or `"focus-error"` and
|
|
236
|
+
preserves that destination. All rollback and verification remain best-effort at
|
|
237
|
+
hostile DOM boundaries without leaking errors.
|
|
238
|
+
|
|
239
|
+
`FocusElementOptions`, `RestoreFocusOptions`, `FocusCapture`, `FocusResult`, and
|
|
240
|
+
`FocusSkippedReason` are exported for typed integrations. The module is safe to
|
|
241
|
+
import during SSR, uses no timers, performs no scrolling itself, creates no
|
|
242
|
+
focus trap, queries no host DOM, and has no automatic lifecycle behavior.
|
|
243
|
+
|
|
244
|
+
## Store announcement preferences
|
|
245
|
+
|
|
246
|
+
`createPreferenceStore(options?)` creates a small external store for validated,
|
|
247
|
+
versioned announcement preferences. The default is the frozen v1 value
|
|
248
|
+
`{ version: 1, preset: "balanced", streaming: "preset", tools: "preset" }`. The
|
|
249
|
+
other granular presets are `"minimal"` and `"verbose"`; streaming can be
|
|
250
|
+
`"preset"`, `"off"`, `"completion"`, `"paragraph"`, or `"sentence"`, and tool
|
|
251
|
+
verbosity can be `"preset"`, `"off"`, `"failures"`, `"status"`, or `"progress"`.
|
|
252
|
+
The separate `"completion-only"` schema has no granular fields. Every accepted
|
|
253
|
+
`PreferenceSchemaV1` snapshot is frozen, and validation rejects missing, extra,
|
|
254
|
+
invalid, or unsupported-version fields. Runtime input fields must be own,
|
|
255
|
+
enumerable data properties; accessor-backed and symbol-keyed input is rejected
|
|
256
|
+
without invoking its getters.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import {
|
|
260
|
+
createPreferenceStore,
|
|
261
|
+
preferencesToCoreConfiguration,
|
|
262
|
+
} from "@generative-a11y/dom";
|
|
263
|
+
|
|
264
|
+
const preferences = createPreferenceStore({
|
|
265
|
+
persistence: { key: "my-app:a11y-preferences" },
|
|
266
|
+
});
|
|
267
|
+
const unsubscribe = preferences.subscribe(() => {
|
|
268
|
+
console.log(preferences.getSnapshot());
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
preferences.setPreferences({
|
|
272
|
+
version: 1,
|
|
273
|
+
preset: "balanced",
|
|
274
|
+
streaming: "sentence",
|
|
275
|
+
tools: "status",
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`PreferenceStore` implements `ExternalStore<PreferenceSchemaV1>` and adds
|
|
280
|
+
`setPreferences(value)` and idempotent `dispose()`. Its server snapshot is the
|
|
281
|
+
configured default, while its client snapshot may contain a loaded preference.
|
|
282
|
+
This deliberate split supports SSR and hydration without accessing browser
|
|
283
|
+
globals during module evaluation. After disposal, snapshots remain readable; new
|
|
284
|
+
subscriptions and writes throw.
|
|
285
|
+
|
|
286
|
+
`normalizePreferences(value)` validates, canonicalizes, and freezes a v1
|
|
287
|
+
snapshot. `samePreferences(left, right)` compares canonical preference values.
|
|
288
|
+
These helpers are exported for thin integrations such as the React adapter to
|
|
289
|
+
reuse the DOM package's schema invariants.
|
|
290
|
+
|
|
291
|
+
Persistence is opt-in through `PreferencePersistence`. It accepts a `key`, an
|
|
292
|
+
optional `PreferenceStorage` (`getItem`/`setItem`), and an optional
|
|
293
|
+
`PreferenceStorageEventSource`. When persistence is requested without injected
|
|
294
|
+
storage, a safely available browser `localStorage` and native `storage` events
|
|
295
|
+
are used. Server and restricted-browser environments remain in-memory. Injected
|
|
296
|
+
event sources take precedence; custom storage without an event source has no
|
|
297
|
+
cross-tab synchronization. Native storage events synchronize other documents,
|
|
298
|
+
but browser storage events do not form a same-tab bus. Invalid, corrupt, and
|
|
299
|
+
forward-version values are preserved in storage and reported through the
|
|
300
|
+
isolated, serializable `PreferenceDiagnostic` callback.
|
|
301
|
+
|
|
302
|
+
`PreferenceStoreOptions` contains `defaultValue`, `persistence`, and
|
|
303
|
+
`onDiagnostic`. A `PreferenceStorageEvent` carries `key`, `newValue`, and an
|
|
304
|
+
optional `storageArea`; a `PreferenceStorageEventSource` supplies a subscribing
|
|
305
|
+
callback and cleanup function. `PreferenceDiagnostic` contains a
|
|
306
|
+
`PreferenceDiagnosticSource`, a `PreferenceDiagnosticCode`, and an optional
|
|
307
|
+
serialized `{ name, message }` error. Sources distinguish storage reads, writes,
|
|
308
|
+
external events, event subscription, and event unsubscription. Codes distinguish
|
|
309
|
+
operation failures, invalid JSON, invalid preferences, and unsupported versions.
|
|
310
|
+
|
|
311
|
+
`preferencesToCoreConfiguration(value)` validates a preference and translates it
|
|
312
|
+
to `{ preset, policy? }`. Use that result only when the host explicitly
|
|
313
|
+
constructs or replaces a core runtime. A preference store never mutates,
|
|
314
|
+
recreates, or disposes an active runtime. `"preset"` fields inherit the selected
|
|
315
|
+
core preset; granular values override only text strategy or tool-event flags,
|
|
316
|
+
leaving timing and progress thresholds inherited. The `"completion-only"`
|
|
317
|
+
preference returns that preset without granular policy overrides.
|