@fusion.dev/helix-headless 0.155.0 → 0.157.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.
Files changed (74) hide show
  1. package/custom-elements.json +2695 -137
  2. package/dist/canvas/node/canvas-node.js +1 -1
  3. package/dist/canvas/root/canvas-root.d.ts +17 -2
  4. package/dist/canvas/root/canvas-root.js +23 -6
  5. package/dist/chat/composer/chat-composer.js +1 -1
  6. package/dist/chat/root/chat-root.d.ts +14 -1
  7. package/dist/chat/root/chat-root.js +14 -4
  8. package/dist/combobox/root/combobox-root.d.ts +10 -0
  9. package/dist/combobox/root/combobox-root.js +18 -3
  10. package/dist/command/root/command-root.d.ts +116 -1
  11. package/dist/command/root/command-root.js +124 -15
  12. package/dist/data-table/root/data-table-root.d.ts +25 -0
  13. package/dist/data-table/root/data-table-root.js +13 -3
  14. package/dist/date-input/root/date-input-root.d.ts +32 -1
  15. package/dist/date-input/root/date-input-root.js +7 -1
  16. package/dist/drilldown/item/drilldown-item.d.ts +49 -1
  17. package/dist/drilldown/item/drilldown-item.js +77 -17
  18. package/dist/drilldown/root/drilldown-root.d.ts +45 -5
  19. package/dist/drilldown/root/drilldown-root.js +51 -5
  20. package/dist/element-events.generated.js +2 -1
  21. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  22. package/dist/filter-input/menu/filter-menu.js +4 -4
  23. package/dist/graph/root/graph-root.d.ts +13 -0
  24. package/dist/graph/root/graph-root.js +16 -0
  25. package/dist/list/root/list-root.d.ts +74 -1
  26. package/dist/list/root/list-root.js +92 -4
  27. package/dist/list/selection.js +3 -3
  28. package/dist/map/root/map-root.d.ts +6 -0
  29. package/dist/map/root/map-root.js +8 -1
  30. package/dist/megamenu/root/megamenu-root.d.ts +50 -13
  31. package/dist/megamenu/root/megamenu-root.js +44 -6
  32. package/dist/menu/item/menu-item.d.ts +24 -1
  33. package/dist/menu/item/menu-item.js +42 -2
  34. package/dist/menu/root/menu-root.d.ts +120 -9
  35. package/dist/menu/root/menu-root.js +329 -26
  36. package/dist/notifications/root/notifications-root.d.ts +20 -0
  37. package/dist/notifications/root/notifications-root.js +32 -1
  38. package/dist/number-input/root/number-input-root.d.ts +63 -6
  39. package/dist/number-input/root/number-input-root.js +96 -11
  40. package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
  41. package/dist/object-inspector/root/object-inspector-root.js +26 -2
  42. package/dist/option/root/option-root.d.ts +32 -0
  43. package/dist/option/root/option-root.js +44 -5
  44. package/dist/outline/root/outline-root.d.ts +6 -2
  45. package/dist/outline/root/outline-root.js +7 -3
  46. package/dist/table/root/table-root.d.ts +20 -0
  47. package/dist/table/root/table-root.js +13 -3
  48. package/dist/time-input/root/time-input-root.d.ts +32 -1
  49. package/dist/time-input/root/time-input-root.js +9 -2
  50. package/dist/time-picker/root/time-picker-root.d.ts +63 -1
  51. package/dist/time-picker/root/time-picker-root.js +58 -7
  52. package/dist/timeline/item/timeline-item.d.ts +25 -0
  53. package/dist/timeline/item/timeline-item.js +27 -2
  54. package/dist/toast/item/toast-item.d.ts +26 -0
  55. package/dist/toast/item/toast-item.js +55 -3
  56. package/dist/toast/root/toast-root.d.ts +32 -0
  57. package/dist/toast/root/toast-root.js +47 -2
  58. package/dist/video/root/keys.d.ts +6 -2
  59. package/dist/video/root/keys.js +18 -1
  60. package/dist/video/root/video-root.d.ts +61 -1
  61. package/dist/video/root/video-root.js +79 -11
  62. package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
  63. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  64. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  65. package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
  66. package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
  67. package/dist/video-playlist/root/video-playlist-root.js +69 -2
  68. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  69. package/dist/virtual-list/root/virtual-list-root.js +3 -0
  70. package/lint.json +1 -1
  71. package/package.json +5 -5
  72. package/types/jsx.d.ts +3346 -266
  73. package/types/vue.d.ts +910 -66
  74. package/types/words.d.ts +124 -0
@@ -38,8 +38,12 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
38
38
  holdWhileHidden: true,
39
39
  paused: (paused) => {
40
40
  this.toggleAttribute('data-paused', paused);
41
+ this.toggleState('paused', paused);
41
42
  },
42
43
  });
44
+ this.waiting = false;
45
+ this.started = false;
46
+ this.holding = false;
43
47
  this.openState = undefined;
44
48
  this.handlePause = () => {
45
49
  this.countdown.hold();
@@ -47,6 +51,15 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
47
51
  this.handleResume = () => {
48
52
  this.countdown.release();
49
53
  };
54
+ this.handleKeydown = (event) => {
55
+ if (event.key !== 'Escape' || event.defaultPrevented || !this.open) {
56
+ return;
57
+ }
58
+ if (this.dismiss()) {
59
+ event.preventDefault();
60
+ event.stopPropagation();
61
+ }
62
+ };
50
63
  this.name = () => {
51
64
  const words = this.partNamed('content');
52
65
  if (!words) {
@@ -72,6 +85,23 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
72
85
  const physical = isRtl(this) ? -travel : travel;
73
86
  this.partNamed('item')?.style.setProperty('--toast-swipe-x', travel === 0 ? '' : `${Math.round(physical)}px`);
74
87
  }
88
+ wait(waiting) {
89
+ if (waiting) {
90
+ if (this.started && !this.holding) {
91
+ this.holding = true;
92
+ this.countdown.hold();
93
+ }
94
+ return;
95
+ }
96
+ if (this.holding) {
97
+ this.holding = false;
98
+ this.countdown.release();
99
+ }
100
+ else if (!this.started && this.hasUpdated) {
101
+ this.started = true;
102
+ this.countdown.start(this.duration);
103
+ }
104
+ }
75
105
  dismiss() {
76
106
  if (!this.open) {
77
107
  return true;
@@ -97,19 +127,35 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
97
127
  connectedCallback() {
98
128
  super.connectedCallback();
99
129
  if (this.hasUpdated && this.open) {
100
- this.resume();
130
+ if (this.waiting) {
131
+ this.wait(true);
132
+ }
133
+ else {
134
+ this.resume();
135
+ }
101
136
  }
137
+ this.addEventListener('keydown', this.handleKeydown);
138
+ }
139
+ disconnectedCallback() {
140
+ super.disconnectedCallback();
141
+ this.holding = false;
142
+ this.removeEventListener('keydown', this.handleKeydown);
102
143
  }
103
144
  willUpdate(changed) {
104
145
  super.willUpdate(changed);
105
146
  this.toggleAttribute('data-closed', !this.open);
106
- this.toggleAttribute('hidden', !this.open);
147
+ this.toggleAttribute('hidden', !this.open || this.waiting);
148
+ this.toggleState('closed', !this.open);
149
+ this.toggleState('waiting', this.waiting);
107
150
  hostAria(this.internals, { role: 'alertdialog' });
108
151
  this.internals.ariaModal = 'false';
109
152
  }
110
153
  updated(changed) {
111
154
  super.updated(changed);
112
155
  this.name();
156
+ if (changed.has('waiting')) {
157
+ this.wait(this.waiting);
158
+ }
113
159
  }
114
160
  assigned(name) {
115
161
  return (this.renderRoot
@@ -118,7 +164,10 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
118
164
  }
119
165
  firstUpdated(changed) {
120
166
  super.firstUpdated(changed);
121
- this.countdown.start(this.duration);
167
+ if (!this.waiting) {
168
+ this.started = true;
169
+ this.countdown.start(this.duration);
170
+ }
122
171
  }
123
172
  renderIcon(mark) {
124
173
  return html `<slot name="icon">
@@ -183,6 +232,9 @@ __decorate([
183
232
  __decorate([
184
233
  property({ reflect: true })
185
234
  ], HelixHeadlessToastItem.prototype, "tone", void 0);
235
+ __decorate([
236
+ property({ attribute: false, type: Boolean })
237
+ ], HelixHeadlessToastItem.prototype, "waiting", void 0);
186
238
  HelixHeadlessToastItem = __decorate([
187
239
  customElement('helix-headless-toast-item')
188
240
  ], HelixHeadlessToastItem);
@@ -49,6 +49,19 @@ import { type PropertyValues, type TemplateResult } from 'lit';
49
49
  * message, then the one before it, then back to where it was before it entered
50
50
  * the stack.
51
51
  *
52
+ * **`max-visible` limits how many messages show at once**, as Zag's `max`
53
+ * does. The messages after that many, in document order, wait: each is hidden,
54
+ * its timer has not started, and the region says it once it shows, when a
55
+ * message before it goes. A waiting message has `:state(waiting)`.
56
+ *
57
+ * **Keyboard.**
58
+ *
59
+ * | Key | What it does |
60
+ * |---|---|
61
+ * | F8, or your `hotkey` | Moves focus to the first control in the stack, and back to where it was when pressed in the stack. |
62
+ * | Tab, Shift+Tab | Moves between the controls of the messages. |
63
+ * | Escape | Dismisses the message holding focus. Focus moves to the next message, then the one before it, then back to where it was. |
64
+ *
52
65
  * @since 0.1.0
53
66
  * @status stable
54
67
  * @category overlays
@@ -81,6 +94,12 @@ export declare class HelixHeadlessToast extends HelixElement {
81
94
  * the rest of your interface.
82
95
  */
83
96
  label: string;
97
+ /**
98
+ * How many messages show at once. The messages after that many wait, hidden
99
+ * and with their timers stopped, until a message before them goes. Unset,
100
+ * every message shows.
101
+ */
102
+ maxVisible?: number;
84
103
  /**
85
104
  * Which corner or edge the stack sits at.
86
105
  *
@@ -171,6 +190,18 @@ export declare class HelixHeadlessToast extends HelixElement {
171
190
  private watcher;
172
191
  /** The messages that are showing, in order. */
173
192
  private get showing();
193
+ /** The messages `max-visible` holds back. */
194
+ private readonly held;
195
+ /**
196
+ * Holds back the messages past `max-visible`, and returns the ones it has
197
+ * just let go. A message that has been dismissed takes no place in the line.
198
+ */
199
+ private queue;
200
+ /**
201
+ * Lets go of the messages past `max-visible` that now have room, raises the
202
+ * stack and says them, once each has shown.
203
+ */
204
+ private restack;
174
205
  private get region();
175
206
  /**
176
207
  * Puts the stack in the top layer while a message shows, and on top of it.
@@ -215,6 +246,7 @@ export declare class HelixHeadlessToast extends HelixElement {
215
246
  connectedCallback(): void;
216
247
  disconnectedCallback(): void;
217
248
  protected firstUpdated(changed: PropertyValues): void;
249
+ protected updated(changed: PropertyValues): void;
218
250
  protected render(): TemplateResult;
219
251
  }
220
252
  declare global {
@@ -40,6 +40,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
40
40
  }
41
41
  };
42
42
  this.made = new WeakSet();
43
+ this.held = new WeakSet();
43
44
  this.handleFocusIn = (event) => {
44
45
  const from = event.relatedTarget;
45
46
  if (from instanceof HTMLElement && !deepContains(this, from)) {
@@ -165,7 +166,8 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
165
166
  }
166
167
  sweep() {
167
168
  for (const one of [...this.children]) {
168
- if (!(one instanceof HTMLElement) || !one.hidden || !this.made.has(one)) {
169
+ const closed = one.open === false;
170
+ if (!(one instanceof HTMLElement) || !closed || !one.hidden || !this.made.has(one)) {
169
171
  continue;
170
172
  }
171
173
  this.made.delete(one);
@@ -173,7 +175,38 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
173
175
  }
174
176
  }
175
177
  get showing() {
176
- return [...this.children].filter((one) => one instanceof HTMLElement && !one.hidden);
178
+ return [...this.children].filter((one) => one instanceof HTMLElement && !one.hidden && !this.held.has(one));
179
+ }
180
+ queue() {
181
+ const max = this.maxVisible;
182
+ const line = [...this.children].filter((one) => one instanceof HTMLElement && one.open !== false);
183
+ const released = [];
184
+ line.forEach((one, at) => {
185
+ const wait = max !== undefined && max >= 0 && at >= max;
186
+ if (this.held.has(one) && !wait) {
187
+ released.push(one);
188
+ }
189
+ if (wait) {
190
+ this.held.add(one);
191
+ }
192
+ else {
193
+ this.held.delete(one);
194
+ }
195
+ if ((one.waiting ?? false) !== wait) {
196
+ one.waiting = wait;
197
+ }
198
+ });
199
+ return released;
200
+ }
201
+ restack() {
202
+ const released = this.queue();
203
+ if (released.length === 0) {
204
+ return;
205
+ }
206
+ void Promise.all(released.map((one) => one.updateComplete)).then(() => {
207
+ this.place(released);
208
+ this.tell(released);
209
+ });
177
210
  }
178
211
  get region() {
179
212
  return this.partNamed('region');
@@ -238,6 +271,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
238
271
  document.addEventListener('keydown', this.handleHotkey);
239
272
  this.watcher = new MutationObserver((records) => {
240
273
  const added = records.flatMap((record) => [...record.addedNodes]);
274
+ this.restack();
241
275
  this.place(added);
242
276
  this.tell(added);
243
277
  this.sweep();
@@ -262,8 +296,16 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
262
296
  }
263
297
  firstUpdated(changed) {
264
298
  super.firstUpdated(changed);
299
+ this.queue();
265
300
  this.place();
266
301
  }
302
+ updated(changed) {
303
+ super.updated(changed);
304
+ if (changed.has('maxVisible')) {
305
+ this.restack();
306
+ this.place();
307
+ }
308
+ }
267
309
  render() {
268
310
  return html `<div part="region" popover="manual" tabindex="-1"><slot></slot><span part="live" role="status">${this.said}</span><span part="live" role="alert">${this.urgent}</span></div>`;
269
311
  }
@@ -275,6 +317,9 @@ __decorate([
275
317
  words,
276
318
  property()
277
319
  ], HelixHeadlessToast.prototype, "label", void 0);
320
+ __decorate([
321
+ property({ attribute: 'max-visible', type: Number })
322
+ ], HelixHeadlessToast.prototype, "maxVisible", void 0);
278
323
  __decorate([
279
324
  property({ reflect: true })
280
325
  ], HelixHeadlessToast.prototype, "placement", void 0);
@@ -3,18 +3,22 @@
3
3
  *
4
4
  * Separable because the table is a convention rather than a fact about this
5
5
  * element: Space and `k` play, the arrows scrub and change the volume, `m`
6
- * mutes, `f` fills the screen, and `j` and `l` jump the distance the bar's own
7
- * buttons jump. What it needs is the media element and four actions, which is
6
+ * mutes, `f` fills the screen, `c` turns captions on and off, Home, End and
7
+ * the digits go to the start, the end and each tenth, and `j` and `l` jump the
8
+ * distance the bar's own buttons jump. What it needs is the media element and four actions, which is
8
9
  * the same handful the media session hands to a headset. Whether the keys are
9
10
  * bound at all, and what a taken press does to the event, stay with the element.
10
11
  */
11
12
  /** What a press asks the player for, which is all a key can do. */
12
13
  export interface KeyActions {
14
+ captions: () => void;
13
15
  fullscreen: () => void;
14
16
  seek: (seconds: number) => void;
15
17
  skipBy: (seconds: number) => void;
16
18
  toggle: () => void;
17
19
  }
20
+ /** The keys `takeKey` answers, as `aria-keyshortcuts` names them. */
21
+ export declare const PLAYER_KEYS = "Space K ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight J L ArrowUp ArrowDown M F C Home End 0 1 2 3 4 5 6 7 8 9";
18
22
  /**
19
23
  * Answers one press, and says whether it was one of these.
20
24
  *
@@ -1,8 +1,9 @@
1
1
  import { SKIP_SECONDS } from '@fusion.dev/helix-core';
2
2
  const PRESSED_BY_SPACE = new Set(['a', 'button', 'select', 'summary', 'textarea']);
3
3
  const PRESSED_ROLES = /^(button|checkbox|link|menuitem\w*|option|radio|switch|tab)$/;
4
+ export const PLAYER_KEYS = 'Space K ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight J L ArrowUp ArrowDown M F C Home End 0 1 2 3 4 5 6 7 8 9';
4
5
  export const takeKey = (event, video, actions) => {
5
- if (event.defaultPrevented) {
6
+ if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
6
7
  return false;
7
8
  }
8
9
  const origin = (event.composedPath?.()[0] ?? event.target);
@@ -54,7 +55,23 @@ export const takeKey = (event, video, actions) => {
54
55
  actions.fullscreen();
55
56
  break;
56
57
  }
58
+ case 'c': {
59
+ actions.captions();
60
+ break;
61
+ }
62
+ case 'Home': {
63
+ actions.seek(0);
64
+ break;
65
+ }
66
+ case 'End': {
67
+ actions.seek(video.duration);
68
+ break;
69
+ }
57
70
  default: {
71
+ if (/^[0-9]$/.test(event.key) && Number.isFinite(video.duration)) {
72
+ actions.seek((video.duration * Number(event.key)) / 10);
73
+ break;
74
+ }
58
75
  return false;
59
76
  }
60
77
  }
@@ -29,8 +29,16 @@ import '../../menu/index.js';
29
29
  * | Up, Down | Raises or lowers the volume by a tenth. |
30
30
  * | `m` | Mutes or unmutes. |
31
31
  * | `f` | Enters or leaves fullscreen. |
32
+ * | `c` | Turns captions on or off. |
33
+ * | Home, End | Goes to the start or the end. |
34
+ * | `0` to `9` | Goes to that tenth of the video: `5` is the middle. |
32
35
  * | Tab | Moves through the controls on the bar. |
33
36
  *
37
+ * A key with Control, Command, or Alt held is left to the browser. The
38
+ * player lists its keys in `aria-keyshortcuts`, and a live region says what a
39
+ * key changed that is not on the focused control: playing or paused, muted,
40
+ * the volume, captions on or off, and where the video is after a seek.
41
+ *
34
42
  * On the scrubber and the volume slider, the arrow keys, Page Up, Page Down,
35
43
  * Home, and End move the slider, as on any range input.
36
44
  *
@@ -121,11 +129,12 @@ import '../../menu/index.js';
121
129
  * @csspart entry-shortcut-key - One key of a row's shortcut chord, inside `entry-shortcut`. The player's rows set none.
122
130
  * @csspart entry-shortcut-separator - The `+` between two keys of a row's shortcut, where the platform writes one.
123
131
  * @csspart entry-start - The leading column that holds the `start` slot. It is always rendered.
132
+ * @csspart entry-surface - The panel a row's submenu opens in beside it. The player's rows have none.
124
133
  * @csspart failure - The message shown when the source cannot be played.
125
134
  * @csspart forward - The button that skips forward ten seconds.
126
135
  * @csspart fullscreen - The button that enters and leaves fullscreen.
127
136
  * @csspart header - The region at the top left that holds the `header` slot.
128
- * @csspart live - A live region that repeats the loading text, so screen readers announce it when it changes. Visually hidden in the styled tier.
137
+ * @csspart live - A live region that says the loading text, and what a key changed, such as the volume. Visually hidden in the styled tier.
129
138
  * @csspart loading - The text shown while the video waits for more data. Draw the spinner
130
139
  * with `::part(loading)::before` in your stylesheet. This tier ships no CSS and renders
131
140
  * no extra box for it.
@@ -187,6 +196,10 @@ export declare class HelixHeadlessVideo extends HelixElement {
187
196
  * looking at.
188
197
  */
189
198
  autoplay: boolean;
199
+ /**
200
+ * What the live region says when `c` turns captions off.
201
+ */
202
+ captionsHiddenLabel: string;
190
203
  /**
191
204
  * The name of the captions control when the video has one track. The control
192
205
  * turns that track on and off.
@@ -201,6 +214,10 @@ export declare class HelixHeadlessVideo extends HelixElement {
201
214
  * tracks.
202
215
  */
203
216
  captionsOffLabel: string;
217
+ /**
218
+ * What the live region says when `c` turns captions on.
219
+ */
220
+ captionsShownLabel: string;
204
221
  /**
205
222
  * Whether the browser draws its native controls. When set, this element draws
206
223
  * no controls of its own.
@@ -326,6 +343,10 @@ export declare class HelixHeadlessVideo extends HelixElement {
326
343
  * What the button that opens `overflow-controls` is called.
327
344
  */
328
345
  moreLabel: string;
346
+ /**
347
+ * What the live region says when a key mutes the video.
348
+ */
349
+ mutedLabel: string;
329
350
  /**
330
351
  * The name of the mute control. The control shows as pressed while the video
331
352
  * is muted.
@@ -353,6 +374,10 @@ export declare class HelixHeadlessVideo extends HelixElement {
353
374
  * bar: two of the same control is worse than either placement.
354
375
  */
355
376
  overflowControls: string;
377
+ /**
378
+ * What the live region says when a key pauses the video.
379
+ */
380
+ pausedLabel: string;
356
381
  /**
357
382
  * The name of the play control while pressing it would pause the video.
358
383
  */
@@ -372,6 +397,10 @@ export declare class HelixHeadlessVideo extends HelixElement {
372
397
  * this is what a screen reader calls it on arriving.
373
398
  */
374
399
  playerLabel: string;
400
+ /**
401
+ * What the live region says when a key plays the video.
402
+ */
403
+ playingLabel: string;
375
404
  /**
376
405
  * The name of the play control while pressing it would start the video.
377
406
  */
@@ -453,6 +482,15 @@ export declare class HelixHeadlessVideo extends HelixElement {
453
482
  * `{duration}` filled in.
454
483
  */
455
484
  timeLabel: string;
485
+ /**
486
+ * What the live region says when a key unmutes the video.
487
+ */
488
+ unmutedLabel: string;
489
+ /**
490
+ * What the live region says when a key changes the volume. `{percent}` is the
491
+ * volume from 0 to 100, written in the locale.
492
+ */
493
+ volumeChangedLabel: string;
456
494
  /**
457
495
  * What the volume slider is called.
458
496
  */
@@ -773,6 +811,28 @@ export declare class HelixHeadlessVideo extends HelixElement {
773
811
  * key means and this says that nothing else gets it.
774
812
  */
775
813
  private readonly handleKeydown;
814
+ /**
815
+ * What a reader hears after a key, which changed something they may not be
816
+ * looking at: the focus is on the player or a control, and the bar may be
817
+ * hidden.
818
+ *
819
+ * **Said from the video's state after the press**, except for playing,
820
+ * which starts on a promise: whether the video was paused before the press
821
+ * says which way it went. Nothing for fullscreen, which a reader sees and a
822
+ * screen reader reports as the window changing, nor for `c` where there are
823
+ * no captions to turn on.
824
+ */
825
+ private sayKey;
826
+ /**
827
+ * What the live region says after a key, until the video waits for data,
828
+ * which it says over this.
829
+ */
830
+ private said;
831
+ /**
832
+ * Puts words in the live region. The same words twice differ by a space no
833
+ * reader hears, since a region whose text does not change is not read again.
834
+ */
835
+ private say;
776
836
  /** The caption tracks among the copies, named as the menu names them. */
777
837
  private captions;
778
838
  /**