@lexical/a11y 0.0.0-bootstrap.0 → 0.46.1-nightly.20260706.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/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # @lexical/a11y
1
+ # `@lexical/a11y`
2
2
 
3
- This 0.0.0-bootstrap.0 placeholder was published only to claim the npm package name so trusted publishing can be configured on npmjs.com. It contains no code; a future release will replace it.
3
+ [![See API Documentation](https://lexical.dev/img/see-api-documentation.svg)](https://lexical.dev/docs/api/modules/lexical_a11y)
4
+
5
+ Framework-agnostic accessibility helpers for Lexical: ARIA live regions, focus management, roving tab index, and focus trap. Usable from React, Svelte, Vue, Solid, or vanilla DOM. React-specific wrappers around the same primitives ship from `@lexical/react`.
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ var extension = require('@lexical/extension');
12
+ var lexical = require('lexical');
13
+
14
+ /**
15
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
16
+ *
17
+ * This source code is licensed under the MIT license found in the
18
+ * LICENSE file in the root directory of this source tree.
19
+ *
20
+ */
21
+
22
+ function applyVisuallyHidden(el) {
23
+ const style = el.style;
24
+ style.border = '0';
25
+ style.clip = 'rect(0 0 0 0)';
26
+ style.height = '1px';
27
+ style.margin = '-1px';
28
+ style.overflow = 'hidden';
29
+ style.padding = '0';
30
+ style.position = 'absolute';
31
+ style.whiteSpace = 'nowrap';
32
+ style.width = '1px';
33
+ }
34
+
35
+ /**
36
+ * Creates a visually hidden live-region element as a child of `owner` and
37
+ * returns it. WAI-ARIA status message pattern (WCAG 4.1.3). The caller owns
38
+ * the element's lifetime (removal) and applies the `aria-live` politeness and
39
+ * `textContent` reactively.
40
+ */
41
+ function createLiveRegion(owner) {
42
+ const region = owner.ownerDocument.createElement('div');
43
+ region.setAttribute('aria-atomic', 'true');
44
+ region.setAttribute('role', 'status');
45
+ applyVisuallyHidden(region);
46
+ owner.appendChild(region);
47
+ return region;
48
+ }
49
+ const FOCUSABLE_SELECTOR = ['a[href]', 'button:not([disabled])', 'input:not([disabled]):not([type="hidden"])', 'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex="-1"])', '[contenteditable="true"]'].join(',');
50
+ function getFocusableElements(container) {
51
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
52
+ }
53
+ function containsComposed(container, target) {
54
+ let current = target;
55
+ while (current !== null) {
56
+ if (current === container) {
57
+ return true;
58
+ }
59
+ if (lexical.isDOMShadowRoot(current)) {
60
+ current = current.host;
61
+ } else {
62
+ current = current.parentNode;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+ /**
68
+ * Traps Tab / Shift+Tab focus inside `container` and restores focus to
69
+ * the previously-focused element when the returned dispose runs.
70
+ * Intended for modal dialogs and other transient overlays.
71
+ *
72
+ * While active, *any* focus that lands outside the container is pulled
73
+ * back inside via a document-level `focusin` listener. This recovers
74
+ * from Safari's default Tab routing through the browser chrome, but it
75
+ * also means descendants that mount into a portal outside `container`
76
+ * (autocomplete panels, tooltips, toasts that auto-focus themselves)
77
+ * will be yanked back as soon as they take focus. Portal them inside
78
+ * `container`, or skip this helper for those dialogs. The pull-back
79
+ * always lands on the first focusable descendant (or the container as a
80
+ * fallback) — `initialFocus` only applies to the activation-time
81
+ * landing, not to subsequent escape recoveries. Only one trap should be
82
+ * mounted at a time — two active traps install competing document-level
83
+ * `focusin` listeners and will fight over focus.
84
+ *
85
+ * Escape is not intercepted — the owner handles close-key behavior.
86
+ */
87
+ function registerFocusTrap(container, options = {}) {
88
+ const initialFocus = options.initialFocus ?? 'firstFocusable';
89
+ const doc = container.ownerDocument;
90
+ const deepActive = lexical.getActiveElementDeep(doc);
91
+ const previouslyFocused = lexical.isHTMLElement(deepActive) ? deepActive : null;
92
+ const focusable = getFocusableElements(container);
93
+ if (initialFocus === 'container' && container.hasAttribute('tabindex')) {
94
+ container.focus();
95
+ } else if (focusable.length > 0) {
96
+ focusable[0].focus();
97
+ } else if (container.hasAttribute('tabindex')) {
98
+ container.focus();
99
+ }
100
+
101
+ // Full Tab management — every Tab / Shift+Tab is handled here.
102
+ // Boundary-only cycling let Safari's default Tab route out to the
103
+ // browser chrome (URL bar) between presses, producing a visible
104
+ // focus flash. This trades contentEditable Tab indent for a reliable
105
+ // trap; the helper is Modal-only today.
106
+ const keydownHandler = event => {
107
+ if (event.key !== 'Tab') {
108
+ return;
109
+ }
110
+ const currentFocusable = getFocusableElements(container);
111
+ if (currentFocusable.length === 0) {
112
+ event.preventDefault();
113
+ return;
114
+ }
115
+ event.preventDefault();
116
+ const first = currentFocusable[0];
117
+ const last = currentFocusable[currentFocusable.length - 1];
118
+ const active = lexical.getActiveElementDeep(doc);
119
+ const activeIndex = lexical.isHTMLElement(active) && containsComposed(container, active) ? currentFocusable.indexOf(active) : -1;
120
+ if (event.shiftKey) {
121
+ (activeIndex <= 0 ? last : currentFocusable[activeIndex - 1]).focus();
122
+ } else {
123
+ (activeIndex === -1 || activeIndex === currentFocusable.length - 1 ? first : currentFocusable[activeIndex + 1]).focus();
124
+ }
125
+ };
126
+
127
+ // Safety net — if focus ever escapes the container (e.g. portal
128
+ // ordering routes the browser's default Tab past the modal in
129
+ // Safari), pull it back to the first focusable inside.
130
+ const focusinHandler = event => {
131
+ const target = lexical.getComposedEventTarget(event);
132
+ if (!lexical.isHTMLElement(target) || containsComposed(container, target)) {
133
+ return;
134
+ }
135
+ if (options.allowOutside != null && options.allowOutside(target)) {
136
+ return;
137
+ }
138
+ const currentFocusable = getFocusableElements(container);
139
+ if (currentFocusable.length > 0) {
140
+ currentFocusable[0].focus();
141
+ } else if (container.hasAttribute('tabindex')) {
142
+ container.focus();
143
+ }
144
+ };
145
+ return lexical.mergeRegister(
146
+ // Placed first so mergeRegister's reverse (LIFO) teardown runs it last,
147
+ // after the focusin listener is removed — otherwise restoring focus to an
148
+ // element outside the container would re-trigger the trap.
149
+ () => {
150
+ if (previouslyFocused !== null && typeof previouslyFocused.focus === 'function' && containsComposed(doc, previouslyFocused)) {
151
+ previouslyFocused.focus();
152
+ }
153
+ }, lexical.registerEventListener(container, 'keydown', keydownHandler), lexical.registerEventListener(doc, 'focusin', focusinHandler));
154
+ }
155
+ const DEFAULT_ROVING_SELECTOR = ':scope > button:not([disabled])';
156
+
157
+ /**
158
+ * Implements the WAI-ARIA roving-tabindex pattern on `container`. One
159
+ * item carries `tabindex="0"` at a time; the rest are `-1`. Arrow keys
160
+ * move focus inside the group; Home / End jump to the ends. Tab leaves
161
+ * the group as a unit, matching the toolbar / menubar pattern.
162
+ *
163
+ * Items are queried lazily on every interaction so additions or
164
+ * removals during the lifetime of the group are picked up without
165
+ * extra wiring.
166
+ */
167
+ function registerRovingTabIndex(container, options = {}) {
168
+ const orientation = options.orientation ?? 'horizontal';
169
+ const selector = options.itemSelector ?? DEFAULT_ROVING_SELECTOR;
170
+ const getItems = () => Array.from(container.querySelectorAll(selector));
171
+ const applyTabIndex = (items, activeIndex) => {
172
+ items.forEach((item, i) => {
173
+ item.tabIndex = i === activeIndex ? 0 : -1;
174
+ });
175
+ };
176
+ const init = () => {
177
+ const items = getItems();
178
+ if (items.length === 0) {
179
+ return;
180
+ }
181
+ const active = lexical.getActiveElementDeep(container.ownerDocument);
182
+ const activeIdx = items.findIndex(el => el === active);
183
+ applyTabIndex(items, activeIdx >= 0 ? activeIdx : 0);
184
+ };
185
+ init();
186
+ const handler = event => {
187
+ const items = getItems();
188
+ if (items.length === 0) {
189
+ return;
190
+ }
191
+ const active = lexical.getActiveElementDeep(container.ownerDocument);
192
+ const currentIdx = items.findIndex(el => el === active);
193
+ if (currentIdx < 0) {
194
+ // Firefox can focus the container itself (e.g. role="toolbar") rather
195
+ // than a child item. Treat any arrow/Home/End as "enter the group at
196
+ // the first item" so the user isn't stuck.
197
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown' || event.key === 'ArrowLeft' || event.key === 'ArrowUp' || event.key === 'Home' || event.key === 'End') {
198
+ event.preventDefault();
199
+ applyTabIndex(items, 0);
200
+ items[0].focus();
201
+ }
202
+ return;
203
+ }
204
+ const horizontal = orientation === 'horizontal' || orientation === 'both';
205
+ const vertical = orientation === 'vertical' || orientation === 'both';
206
+ let nextIdx = currentIdx;
207
+ switch (event.key) {
208
+ case 'ArrowRight':
209
+ if (!horizontal) {
210
+ return;
211
+ }
212
+ nextIdx = currentIdx + 1;
213
+ break;
214
+ case 'ArrowLeft':
215
+ if (!horizontal) {
216
+ return;
217
+ }
218
+ nextIdx = currentIdx - 1;
219
+ break;
220
+ case 'ArrowDown':
221
+ if (!vertical) {
222
+ return;
223
+ }
224
+ nextIdx = currentIdx + 1;
225
+ break;
226
+ case 'ArrowUp':
227
+ if (!vertical) {
228
+ return;
229
+ }
230
+ nextIdx = currentIdx - 1;
231
+ break;
232
+ case 'Home':
233
+ nextIdx = 0;
234
+ break;
235
+ case 'End':
236
+ nextIdx = items.length - 1;
237
+ break;
238
+ default:
239
+ return;
240
+ }
241
+ event.preventDefault();
242
+ // Single wrap for every case: the only out-of-range value is -1 (left/up
243
+ // from the first item), and `currentIdx >= 0` keeps `nextIdx >= -1`, so one
244
+ // `+ length` is enough to normalize.
245
+ nextIdx = (nextIdx + items.length) % items.length;
246
+ applyTabIndex(items, nextIdx);
247
+ items[nextIdx].focus();
248
+ };
249
+ return lexical.mergeRegister(lexical.registerEventListener(container, 'keydown', handler), () => {
250
+ const items = getItems();
251
+ items.forEach(item => {
252
+ item.tabIndex = 0;
253
+ });
254
+ });
255
+ }
256
+ const DEFAULT_TOOLBAR_FOCUSABLE_SELECTOR = ':scope > button:not([disabled]), :scope > [tabindex="0"]';
257
+ /**
258
+ * Implements the editor-to-toolbar focus jump recommended by the
259
+ * WAI-ARIA APG editor menubar pattern: Alt+F10 inside the editor moves
260
+ * focus to the first focusable in `toolbar`, and Escape inside the
261
+ * toolbar returns focus to the editor.
262
+ *
263
+ * Wires the navigation only. Selection restoration relies on the
264
+ * editor's own focus handling; the editor's last selection is preserved
265
+ * across the jump so toolbar commands act on the same range.
266
+ */
267
+ function registerFocusManager(editor, toolbar, options = {}) {
268
+ const selector = options.toolbarItemSelector ?? DEFAULT_TOOLBAR_FOCUSABLE_SELECTOR;
269
+ const handler = event => {
270
+ if (event.key !== 'Escape') {
271
+ return;
272
+ }
273
+ const target = lexical.getComposedEventTarget(event);
274
+ if (lexical.isHTMLElement(target)) {
275
+ const items = toolbar.querySelectorAll(selector);
276
+ let isRovingItem = false;
277
+ for (const item of items) {
278
+ if (item === target || containsComposed(item, target)) {
279
+ isRovingItem = true;
280
+ break;
281
+ }
282
+ }
283
+ if (!isRovingItem) {
284
+ return;
285
+ }
286
+ }
287
+ const rootElement = editor.getRootElement();
288
+ if (rootElement === null) {
289
+ return;
290
+ }
291
+ event.preventDefault();
292
+ // Keep Escape inside the toolbar — don't let it bubble to any
293
+ // window-level Modal close handler.
294
+ event.stopPropagation();
295
+ // `editor.focus()` restores the prior selection so commands resume
296
+ // where the cursor was before the jump; the `rootElement.focus()`
297
+ // fallback covers test environments where `editor.focus()` does not
298
+ // also move DOM focus.
299
+ editor.focus();
300
+ rootElement.focus();
301
+ };
302
+ return lexical.mergeRegister(editor.registerCommand(lexical.KEY_DOWN_COMMAND, event => {
303
+ if (!event.altKey || event.key !== 'F10') {
304
+ return false;
305
+ }
306
+ const firstItem = toolbar.querySelector('[tabindex="0"]') ?? toolbar.querySelector(selector);
307
+ if (firstItem === null) {
308
+ return false;
309
+ }
310
+ event.preventDefault();
311
+ firstItem.focus();
312
+ return true;
313
+ }, lexical.COMMAND_PRIORITY_LOW), lexical.registerEventListener(toolbar, 'keydown', handler));
314
+ }
315
+
316
+ /**
317
+ * Public output of {@link AriaLiveRegionExtension}. The `politeness` and
318
+ * `owner` config values are exposed as runtime-tunable signals — setting
319
+ * `politeness` updates the mounted region's `aria-live`, setting `owner`
320
+ * re-mounts the region — alongside a stable `announce` sink that is valid for
321
+ * the editor's lifetime (callers never see the region element or its disposal).
322
+ */
323
+
324
+ /**
325
+ * Platform-independent extension that owns a single `aria-live` region for
326
+ * the editor and exposes a stable {@link AriaLiveRegion} sink as its output.
327
+ * Other a11y extensions (`HistoryAnnounceExtension`,
328
+ * `EditorModeAnnounceExtension`) depend on it and announce through
329
+ * `output.announce`.
330
+ *
331
+ * The sink writes to a private `message` signal created in `init`. The
332
+ * `politeness` / `owner` config are exposed as signals (via `namedSignals`).
333
+ * `register` tracks the editor's root element reactively and runs three
334
+ * effects: one creates / disposes the region element as the root document (or
335
+ * `owner`) changes, one applies the current `politeness` to the mounted region,
336
+ * and one mirrors the current message into it. Buffering through signals
337
+ * decouples the stable `announce` (from `build`) from the region element —
338
+ * which comes and goes with the root — and creates the region in the editor's
339
+ * own document (e.g. an iframe-portaled editor) rather than the top-level
340
+ * `document`.
341
+ */
342
+ const AriaLiveRegionExtension = /* @__PURE__ */lexical.defineExtension({
343
+ build(_editor, config, state) {
344
+ const message = state.getInitResult();
345
+ return {
346
+ ...extension.namedSignals(config),
347
+ announce(text) {
348
+ // Toggle a trailing zero-width space on repeats so re-announcing the
349
+ // same text still registers as a textContent change downstream.
350
+ message.value = text === message.peek() ? text + '\u200B' : text;
351
+ }
352
+ };
353
+ },
354
+ config: /* @__PURE__ */lexical.safeCast({
355
+ owner: null,
356
+ politeness: 'polite'
357
+ }),
358
+ dependencies: [extension.RootElementExtension],
359
+ // Private reactive message buffer shared by `build` (writer) and `register`
360
+ // (renderer); see the extension doc comment.
361
+ init: () => extension.signal(''),
362
+ name: '@lexical/a11y/AriaLiveRegion',
363
+ register(_editor, _config, state) {
364
+ const message = state.getInitResult();
365
+ const {
366
+ owner,
367
+ politeness
368
+ } = state.getOutput();
369
+ const rootElement = state.getDependency(extension.RootElementExtension).output;
370
+ const region = extension.signal(null);
371
+ return lexical.mergeRegister(
372
+ // Create / dispose the region as the root document (or `owner`) changes.
373
+ // Subscribe to the root only when no explicit `owner` is configured, so a
374
+ // fixed-owner region isn't torn down and rebuilt on unrelated root churn.
375
+ extension.effect(() => {
376
+ const ownerEl = owner.value;
377
+ const root = ownerEl ? null : rootElement.value;
378
+ const host = ownerEl ?? (root ? root.ownerDocument.body : null);
379
+ if (!host) {
380
+ return undefined;
381
+ }
382
+ const el = createLiveRegion(host);
383
+ region.value = el;
384
+ return () => {
385
+ el.remove();
386
+ region.value = null;
387
+ };
388
+ }),
389
+ // Apply the (runtime-tunable) politeness to whatever region is mounted.
390
+ // This tracks `region` so it re-applies the attribute to a re-created
391
+ // region (an attribute, not an announcement).
392
+ extension.effect(() => {
393
+ const el = region.value;
394
+ if (el) {
395
+ el.setAttribute('aria-live', politeness.value);
396
+ }
397
+ }),
398
+ // Mirror the current message into whatever region is mounted. Always read
399
+ // `message.value` (to stay subscribed) but read the region with peek() so
400
+ // this re-runs only when the message changes, not when the region is
401
+ // (re)created: replaying the buffered message into a freshly mounted
402
+ // region would make the screen reader re-announce it on every editor
403
+ // remount with no user action.
404
+ extension.effect(() => {
405
+ const text = message.value;
406
+ const el = region.peek();
407
+ if (el) {
408
+ el.textContent = text;
409
+ }
410
+ }));
411
+ }
412
+ });
413
+ /**
414
+ * Platform-independent extension that announces undo / redo through the
415
+ * `AriaLiveRegionExtension`'s shared sink.
416
+ */
417
+ const HistoryAnnounceExtension = /* @__PURE__ */lexical.defineExtension({
418
+ build: (_editor, config) => extension.namedSignals(config),
419
+ config: /* @__PURE__ */lexical.safeCast({
420
+ disabled: false,
421
+ redone: 'Redone',
422
+ undone: 'Undone'
423
+ }),
424
+ dependencies: [AriaLiveRegionExtension],
425
+ name: '@lexical/a11y/HistoryAnnounce',
426
+ register(editor, _config, state) {
427
+ const {
428
+ disabled,
429
+ redone,
430
+ undone
431
+ } = state.getOutput();
432
+ const {
433
+ announce
434
+ } = state.getDependency(AriaLiveRegionExtension).output;
435
+ // Gate registration on `disabled` from an effect so a disabled announcer
436
+ // adds no command overhead at all. Peek the message signals at dispatch
437
+ // time so changing them doesn't re-register. Both return false to keep the
438
+ // history command chain intact.
439
+ return extension.effect(() => disabled.value ? undefined : lexical.mergeRegister(editor.registerCommand(lexical.UNDO_COMMAND, () => {
440
+ announce(undone.peek());
441
+ return false;
442
+ }, lexical.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical.REDO_COMMAND, () => {
443
+ announce(redone.peek());
444
+ return false;
445
+ }, lexical.COMMAND_PRIORITY_LOW)));
446
+ }
447
+ });
448
+ /**
449
+ * Platform-independent extension that announces
450
+ * `editor.setEditable(true|false)` transitions through the
451
+ * `AriaLiveRegionExtension`'s shared sink.
452
+ */
453
+ const EditorModeAnnounceExtension = /* @__PURE__ */lexical.defineExtension({
454
+ build: (_editor, config) => extension.namedSignals(config),
455
+ config: /* @__PURE__ */lexical.safeCast({
456
+ disabled: false,
457
+ editable: 'Editor is editable',
458
+ readOnly: 'Editor is read-only'
459
+ }),
460
+ dependencies: [AriaLiveRegionExtension],
461
+ name: '@lexical/a11y/EditorModeAnnounce',
462
+ register(editor, _config, state) {
463
+ const {
464
+ disabled,
465
+ editable,
466
+ readOnly
467
+ } = state.getOutput();
468
+ const {
469
+ announce
470
+ } = state.getDependency(AriaLiveRegionExtension).output;
471
+ // Gate registration on `disabled` from an effect so a disabled announcer
472
+ // adds no listener overhead. Peek the message signals at dispatch time so
473
+ // changing them doesn't re-register.
474
+ return extension.effect(() => disabled.value ? undefined : editor.registerEditableListener(isEditable => {
475
+ announce(isEditable ? editable.peek() : readOnly.peek());
476
+ }));
477
+ }
478
+ });
479
+
480
+ /**
481
+ * The public output of the focus-trap, roving-tabindex and focus-manager
482
+ * extensions: register a container (reference counted) and get back an
483
+ * idempotent disposer. Callers register through this method instead of a
484
+ * mutable map and never see the container bookkeeping. It is the core
485
+ * {@link RefCountedRegistry} keyed by `HTMLElement`.
486
+ *
487
+ * Each extension creates the registry in `build` (where the editor is
488
+ * available for the per-container activation) and disposes it on `register`
489
+ * teardown via {@link RefCountedRegistry.dispose}.
490
+ */
491
+
492
+ /**
493
+ * Platform-independent extension that traps Tab / Shift+Tab focus inside one
494
+ * or more containers. Register a container through the extension output
495
+ * ({@link ContainerRegistry.register}); the React adapter is
496
+ * `useLexicalFocusTrapRef` from `@lexical/react`.
497
+ */
498
+ const FocusTrapExtension = /* @__PURE__ */lexical.defineExtension({
499
+ build: () => lexical.createRefCountedRegistry(registerFocusTrap),
500
+ name: '@lexical/a11y/FocusTrap',
501
+ register: (_editor, _config, state) => () => state.getOutput().dispose()
502
+ });
503
+
504
+ /**
505
+ * Platform-independent extension that wires the WAI-ARIA roving-tabindex
506
+ * pattern on one or more containers. Register a container through the
507
+ * extension output ({@link ContainerRegistry.register}); the React adapter is
508
+ * `useLexicalRovingTabIndexRef` from `@lexical/react`.
509
+ */
510
+ const RovingTabIndexExtension = /* @__PURE__ */lexical.defineExtension({
511
+ build: () => lexical.createRefCountedRegistry(registerRovingTabIndex),
512
+ name: '@lexical/a11y/RovingTabIndex',
513
+ register: (_editor, _config, state) => () => state.getOutput().dispose()
514
+ });
515
+
516
+ /**
517
+ * Platform-independent extension that wires the editor-to-toolbar focus jump
518
+ * (Alt+F10 / Escape return) on one or more toolbars. Register a toolbar
519
+ * through the extension output ({@link ContainerRegistry.register}); the React
520
+ * adapter is `useLexicalFocusManagerRef` from `@lexical/react`.
521
+ */
522
+ const FocusManagerExtension = /* @__PURE__ */lexical.defineExtension({
523
+ build: editor => lexical.createRefCountedRegistry((toolbar, options) => registerFocusManager(editor, toolbar, options)),
524
+ name: '@lexical/a11y/FocusManager',
525
+ register: (_editor, _config, state) => () => state.getOutput().dispose()
526
+ });
527
+
528
+ exports.AriaLiveRegionExtension = AriaLiveRegionExtension;
529
+ exports.EditorModeAnnounceExtension = EditorModeAnnounceExtension;
530
+ exports.FocusManagerExtension = FocusManagerExtension;
531
+ exports.FocusTrapExtension = FocusTrapExtension;
532
+ exports.HistoryAnnounceExtension = HistoryAnnounceExtension;
533
+ exports.RovingTabIndexExtension = RovingTabIndexExtension;