@fusion.dev/helix-headless 0.171.0 → 0.172.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 (33) hide show
  1. package/custom-elements.json +439 -65
  2. package/dist/chat/composer/chat-composer.d.ts +28 -1
  3. package/dist/chat/composer/chat-composer.js +22 -3
  4. package/dist/chat/root/chat-root.d.ts +38 -2
  5. package/dist/chat/root/chat-root.js +38 -1
  6. package/dist/color-input/root/color-input-root.d.ts +15 -1
  7. package/dist/color-input/root/color-input-root.js +19 -1
  8. package/dist/color-picker/root/color-picker-root.d.ts +5 -1
  9. package/dist/color-picker/root/color-picker-root.js +1 -1
  10. package/dist/data-table/root/data-table-root.d.ts +5 -1
  11. package/dist/data-table/root/data-table-root.js +1 -0
  12. package/dist/dialog/root/dialog-root.js +1 -0
  13. package/dist/drawer/root/drawer-root.d.ts +4 -0
  14. package/dist/drawer/root/drawer-root.js +1 -0
  15. package/dist/element-events.generated.js +1 -0
  16. package/dist/lightbox/root/lightbox-root.js +1 -0
  17. package/dist/listbox/root/listbox-root.d.ts +2 -1
  18. package/dist/listbox/root/listbox-root.js +3 -2
  19. package/dist/menu/item/menu-item.d.ts +2 -0
  20. package/dist/menu/item/menu-item.js +2 -0
  21. package/dist/menu/root/menu-root.d.ts +73 -12
  22. package/dist/menu/root/menu-root.js +103 -7
  23. package/dist/popover/root/popover-root.d.ts +59 -6
  24. package/dist/popover/root/popover-root.js +51 -3
  25. package/dist/select/root/select-root.d.ts +2 -2
  26. package/dist/select/root/select-root.js +2 -2
  27. package/dist/textarea/root/textarea-root.d.ts +8 -0
  28. package/dist/textarea/root/textarea-root.js +16 -0
  29. package/lint.json +1 -1
  30. package/package.json +5 -5
  31. package/types/jsx.d.ts +312 -30
  32. package/types/vue.d.ts +59 -8
  33. package/types/words.d.ts +8 -0
@@ -28,6 +28,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
28
28
  * Sending an empty required composer counts as trying. Tabbing past an empty
29
29
  * composer marks nothing.
30
30
  *
31
+ * **`feedback` is a message of your own about the draft**, such as one your
32
+ * server sent back. While it is set, the composer is invalid at once, does
33
+ * not send or submit, shows the message under the field, and a screen reader
34
+ * hears it as it appears. Clear it to let the draft go.
35
+ *
31
36
  * **`typing` says whether the user is writing**, and `helix-typing` is sent
32
37
  * each time it changes: when they start, and when they send or stop for
33
38
  * `typing-timeout` milliseconds. Pass it on to show "is typing" to the other
@@ -48,13 +53,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
48
53
  *
49
54
  * @csspart composer - The whole composer.
50
55
  * @csspart end - The box around the `end` slot.
56
+ * @csspart feedback - The `feedback` message, under the field while it is set.
51
57
  * @csspart field - The text field.
52
58
  * @csspart mark - The icon inside the send button.
53
59
  * @csspart send - The send button.
54
60
  * @csspart start - The box around the `start` slot.
55
61
  * @csspart tools - The row under the field.
56
62
  *
57
- * @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it.
63
+ * @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it. Or `feedback` is set.
58
64
  */
59
65
  export declare class HelixHeadlessChatComposer extends HelixFormElement {
60
66
  /**
@@ -62,6 +68,12 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
62
68
  * `registerWords('de', { 'helix-chat-composer': { sendLabel: 'Senden' } })`.
63
69
  */
64
70
  static words: string;
71
+ /**
72
+ * A message about the draft, shown under the field. While set, the composer
73
+ * is invalid, does not send, and does not submit, as a field's `feedback`
74
+ * does, and the message is announced as it appears.
75
+ */
76
+ feedback: string;
65
77
  /**
66
78
  * The field's name, read by screen readers.
67
79
  */
@@ -123,6 +135,17 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
123
135
  private writing;
124
136
  /** Whether the reader has tried to send or submit the draft, or edited it and left. */
125
137
  private touched;
138
+ /**
139
+ * Whether the reader has typed in the field since it last held a fresh
140
+ * draft: since it rendered, since a send cleared it, or since a reset.
141
+ *
142
+ * **A send clears the field, and the field still sends `change` as it is
143
+ * left**, because the platform compares the text with what it held when it
144
+ * was focused. Measured in Chromium before this: a required composer sent
145
+ * "hi", cleared, and on Tab showed itself invalid over an empty draft
146
+ * nobody had written.
147
+ */
148
+ private edited;
126
149
  /** Whether the composer shows that its draft is wrong. */
127
150
  private shownInvalid;
128
151
  /** The pause after which the user has stopped writing. */
@@ -147,10 +170,14 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
147
170
  * back to. `value` does not reflect, so its attribute stays as written.
148
171
  */
149
172
  private get declared();
173
+ /** @internal `feedback` marks the composer invalid, as a field's does. */
174
+ protected get validityFeedback(): string;
150
175
  /** Puts back the draft the markup declared when the form is reset. */
151
176
  formResetCallback(): void;
152
177
  /** The reader has had a go at the draft, so whether it is wrong can be shown. */
153
178
  private readonly touch;
179
+ /** The field's `change`, which counts only after the reader typed in it. */
180
+ private readonly handleChange;
154
181
  /** Brings back the draft when the browser restores the form. */
155
182
  formStateRestoreCallback(state: string | File | FormData | null): void;
156
183
  /**
@@ -13,6 +13,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
13
13
  let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFormElement {
14
14
  constructor() {
15
15
  super(...arguments);
16
+ this.feedback = '';
16
17
  this.label = 'Message';
17
18
  this.placeholder = '';
18
19
  this.required = false;
@@ -24,15 +25,22 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
24
25
  this.isChatComposer = true;
25
26
  this.writing = false;
26
27
  this.touched = false;
28
+ this.edited = false;
27
29
  this.shownInvalid = false;
28
30
  this.pause = new DelayController(this, { renders: false });
29
31
  this.touch = () => {
30
32
  this.touched = true;
31
33
  this.requestUpdate();
32
34
  };
35
+ this.handleChange = () => {
36
+ if (this.edited) {
37
+ this.touch();
38
+ }
39
+ };
33
40
  this.handleInput = (event) => {
34
41
  event.stopPropagation();
35
42
  this.value = event.target.value;
43
+ this.edited = true;
36
44
  this.dispatchEvent(new Event('input'));
37
45
  this.markTyping(this.value !== '');
38
46
  };
@@ -87,16 +95,21 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
87
95
  }
88
96
  this.value = '';
89
97
  this.touched = false;
98
+ this.edited = false;
90
99
  this.markTyping(false);
91
100
  return true;
92
101
  }
93
102
  get declared() {
94
103
  return this.getAttribute('value') ?? '';
95
104
  }
105
+ get validityFeedback() {
106
+ return this.feedback;
107
+ }
96
108
  formResetCallback() {
97
109
  super.formResetCallback();
98
110
  this.value = this.declared;
99
111
  this.touched = false;
112
+ this.edited = false;
100
113
  this.requestUpdate();
101
114
  }
102
115
  formStateRestoreCallback(state) {
@@ -117,7 +130,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
117
130
  if (field?.validity) {
118
131
  this.internals.setValidity(field.validity, field.validationMessage, field);
119
132
  }
120
- const invalid = this.touched && !this.internals.validity.valid;
133
+ const invalid = this.feedback !== '' || (this.touched && !this.internals.validity.valid);
121
134
  this.toggleState('invalid', invalid);
122
135
  this.internals.ariaInvalid = invalid ? 'true' : 'false';
123
136
  if (invalid !== this.shownInvalid) {
@@ -166,8 +179,9 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
166
179
  minlength=${this.minLength ?? nothing}
167
180
  maxlength=${this.maxLength ?? nothing}
168
181
  aria-invalid=${this.shownInvalid ? 'true' : nothing}
182
+ aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
169
183
  ?disabled=${this.isDisabled}
170
- @change=${this.touch}
184
+ @change=${this.handleChange}
171
185
  @input=${this.handleInput}
172
186
  @keydown=${this.handleKey}
173
187
  ></textarea>
@@ -176,9 +190,14 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
176
190
  <span part="end"><slot name="end"></slot></span>
177
191
  ${this.renderSend()}
178
192
  </div>
179
- </div>`;
193
+ </div>${this.feedback === ''
194
+ ? nothing
195
+ : html `<p part="feedback" id="feedback" role="alert">${this.feedback}</p>`}`;
180
196
  }
181
197
  };
198
+ __decorate([
199
+ property()
200
+ ], HelixHeadlessChatComposer.prototype, "feedback", void 0);
182
201
  __decorate([
183
202
  words,
184
203
  property()
@@ -37,6 +37,13 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
37
37
  * `loading` while you fetch, then prepend the older turns. The turn the user
38
38
  * was reading stays in place on screen.
39
39
  *
40
+ * The transcript is a `log`, the role for content added in order where the
41
+ * newest matters most, and it answers the APG feed's keys. The `feed` role
42
+ * reads its articles as a list from the top, with no newest end to stay
43
+ * pinned to, so the log keeps the role a conversation has. While `loading` is
44
+ * set or older turns load, the log carries `aria-busy`, as the APG asks of a
45
+ * feed.
46
+ *
40
47
  * The log is `aria-live="off"`. A separate status region announces each new
41
48
  * turn once, when it has finished arriving. It skips the user's own turns
42
49
  * unless they failed to send, turns loaded as history, and the turns already
@@ -49,6 +56,12 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
49
56
  * conversations. `turns` draws a long
50
57
  * transcript one window at a time, so the log needs a block size of its own.
51
58
  *
59
+ * The log scrolls on this tier. It carries `data-scroller`, and `core` ships
60
+ * `[data-scroller] { overflow: auto }` into every shadow root, so pinning,
61
+ * history and the window work with no stylesheet. You still need to give the
62
+ * log a block size, such as `helix-headless-chat::part(log) { block-size:
63
+ * 20rem }`.
64
+ *
52
65
  * **A conversation is built from three elements.** The chat holds the
53
66
  * transcript, a `Chat.Message` stands for each turn, and a `Chat.Composer` sits
54
67
  * in the `composer` slot. When the user sends, `helix-send` reaches the chat:
@@ -65,7 +78,9 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
65
78
  * turn says its place, such as "3 of 40", windowed or not. The transcript is
66
79
  * a tab stop while it holds a turn, and the turn a reader moved to is one
67
80
  * too. Moving to an older turn unpins a transcript that scrolls and scrolls
68
- * the turn into view, and moving to the newest pins it again.
81
+ * the turn into view, and moving to the newest pins it again. When the turn
82
+ * holding focus is removed, focus moves to the turn that took its place, or to
83
+ * the one before it where it was the last.
69
84
  *
70
85
  * | Key | What it does |
71
86
  * |---|---|
@@ -120,7 +135,7 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
120
135
  * @csspart indicator - The three dots before the words that say who is typing, which the styled tier animates as a message's are. Hidden from screen readers.
121
136
  * @csspart live - The region where screen readers announce each turn once it has arrived.
122
137
  * @csspart loading - Shown while the history loads.
123
- * @csspart log - The transcript, which scrolls.
138
+ * @csspart log - The transcript, which scrolls. On this tier, it needs only a block size.
124
139
  * @csspart message - The message drawn for a turn from `turns`, forwarded whole.
125
140
  * @csspart message-actions - The controls on a turn from `turns`, beside its time.
126
141
  * @csspart message-avatar - The avatar box of a turn from `turns`.
@@ -518,6 +533,27 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
518
533
  */
519
534
  private follow;
520
535
  protected updated(changed: PropertyValues): void;
536
+ /** The turn focus is in, by its place and its identity, once a reader is in one. */
537
+ private held;
538
+ /** Notes the turn focus went into, or that it went somewhere else. */
539
+ private readonly handleFocusIn;
540
+ /**
541
+ * Forgets the turn once focus moves to something outside the transcript.
542
+ * A move to nothing is kept, since removing the turn that held focus is
543
+ * one.
544
+ */
545
+ private readonly handleFocusOut;
546
+ /**
547
+ * Moves focus to a neighbor where removing the turn that held it dropped
548
+ * focus to the body: the turn that took its place, or the one before it
549
+ * where it was the last.
550
+ *
551
+ * **Focus went to the body**, and Page Down did nothing until the reader
552
+ * tabbed back into the transcript. List and Virtual List move focus to a
553
+ * neighbor the same way, as React Aria's GridList does. Focus anywhere but
554
+ * the body is where you put it, and is left there.
555
+ */
556
+ private followRemoved;
521
557
  /**
522
558
  * Hands each turn written as a child the words it is named with, once it
523
559
  * has hydrated, so its first render is the server's.
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessChat_1;
8
- import { ChildrenController, customElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, reportRuntime, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
8
+ import { ChildrenController, customElement, deepActiveElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, reportRuntime, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
9
9
  import { arrowDown } from '@fusion.dev/helix-icons';
10
10
  import { nothing } from 'lit';
11
11
  import { property, state } from 'lit/decorators.js';
@@ -98,6 +98,21 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
98
98
  this.resume();
99
99
  };
100
100
  this.warnedMixed = false;
101
+ this.handleFocusIn = (event) => {
102
+ const at = this.turnFrom(event.composedPath());
103
+ if (at === undefined || at < 0) {
104
+ this.held = undefined;
105
+ return;
106
+ }
107
+ const id = this.fed ? this.identity(this.turns[at]) : this.messages[at];
108
+ this.held = { at, id };
109
+ };
110
+ this.handleFocusOut = (event) => {
111
+ const to = event.relatedTarget;
112
+ if (to instanceof Node && !this.contains(to) && !this.renderRoot?.contains?.(to)) {
113
+ this.held = undefined;
114
+ }
115
+ };
101
116
  this.handleKeydown = (event) => {
102
117
  if (event.defaultPrevented || event.altKey || event.metaKey || event.shiftKey) {
103
118
  return;
@@ -423,6 +438,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
423
438
  connectedCallback() {
424
439
  super.connectedCallback();
425
440
  this.addEventListener('keydown', this.handleKeydown);
441
+ this.addEventListener('focusin', this.handleFocusIn);
442
+ this.addEventListener('focusout', this.handleFocusOut);
426
443
  this.settling = new MutationObserver(this.handleSettle);
427
444
  this.settling.observe(this, { attributeFilter: ['state'], subtree: true });
428
445
  this.opening = true;
@@ -436,6 +453,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
436
453
  disconnectedCallback() {
437
454
  super.disconnectedCallback();
438
455
  this.removeEventListener('keydown', this.handleKeydown);
456
+ this.removeEventListener('focusin', this.handleFocusIn);
457
+ this.removeEventListener('focusout', this.handleFocusOut);
439
458
  this.settling?.disconnect();
440
459
  this.settling = undefined;
441
460
  this.grower?.disconnect();
@@ -506,6 +525,23 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
506
525
  this.remember();
507
526
  this.reachHistory();
508
527
  this.announce();
528
+ this.followRemoved();
529
+ }
530
+ followRemoved() {
531
+ const held = this.held;
532
+ if (held === undefined) {
533
+ return;
534
+ }
535
+ const ids = this.fed ? this.turns.map((turn) => this.identity(turn)) : this.messages;
536
+ if (ids.includes(held.id)) {
537
+ return;
538
+ }
539
+ this.held = undefined;
540
+ const active = deepActiveElement();
541
+ if (ids.length === 0 || (active !== null && active !== document.body)) {
542
+ return;
543
+ }
544
+ void this.focusTurn(Math.min(held.at, ids.length - 1));
509
545
  }
510
546
  tellTurns() {
511
547
  const messages = this.messages;
@@ -670,6 +706,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
670
706
  part="log"
671
707
  role="log"
672
708
  aria-live="off"
709
+ data-scroller
673
710
  aria-label=${this.label || this.fallbackLabel}
674
711
  aria-busy=${busy ? 'true' : nothing}
675
712
  ?data-windowed=${fed}
@@ -31,7 +31,8 @@ declare const HelixHeadlessColorInput_base: (new (...args: any[]) => import("@fu
31
31
  *
32
32
  * The trigger and ArrowDown open the panel with focus in the square. Tab moves
33
33
  * between the panel's controls without leaving it, and Escape closes it and
34
- * returns focus to the field.
34
+ * returns focus to the control that opened it: the trigger after a press, and
35
+ * the field after ArrowDown.
35
36
  *
36
37
  * The component sets `--color-input-swatch`, so do not set it yourself. It
37
38
  * holds the resolved color, so the styled tier can paint the swatch without
@@ -305,6 +306,19 @@ export declare class HelixHeadlessColorInput extends HelixHeadlessColorInput_bas
305
306
  */
306
307
  protected sync(): void;
307
308
  protected updated(changed: PropertyValues): void;
309
+ /** Whether the trigger's press opened the panel that is open now. */
310
+ private pressedOpen;
311
+ /**
312
+ * The trigger's press, which notes that it opened the panel, so Escape hands
313
+ * focus back to the trigger.
314
+ */
315
+ private readonly handlePress;
316
+ /**
317
+ * Hands focus back to the control that opened the panel: the trigger after
318
+ * its press, which is the APG date picker dialog's rule, and the text after
319
+ * ArrowDown from it.
320
+ */
321
+ protected returnFocus(): void;
308
322
  /**
309
323
  * The picker the field opens, behind a seam the styled tier can move.
310
324
  *
@@ -55,6 +55,12 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
55
55
  this.handleSampleEnd = () => {
56
56
  this.requestOpen(true);
57
57
  };
58
+ this.pressedOpen = false;
59
+ this.handlePress = () => {
60
+ const opening = !this.open;
61
+ this.handleTriggerClick();
62
+ this.pressedOpen = opening && this.open;
63
+ };
58
64
  }
59
65
  static { this.words = 'helix-color-input'; }
60
66
  requestValue(next) {
@@ -117,6 +123,18 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
117
123
  if (changed.has('value') && parseColor(this.value)) {
118
124
  this.settled = this.value;
119
125
  }
126
+ if (changed.has('open') && !this.open) {
127
+ this.pressedOpen = false;
128
+ }
129
+ }
130
+ returnFocus() {
131
+ const trigger = this.pressedOpen ? this.partNamed('trigger') : null;
132
+ this.pressedOpen = false;
133
+ if (trigger) {
134
+ trigger.focus();
135
+ return;
136
+ }
137
+ super.returnFocus();
120
138
  }
121
139
  renderPicker() {
122
140
  return html `<${this.tag('color-picker')}
@@ -204,7 +222,7 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
204
222
  : 'swatch-value'
205
223
  : nothing}
206
224
  ?disabled=${this.isDisabled}
207
- @click=${this.handleTriggerClick}
225
+ @click=${this.handlePress}
208
226
  >
209
227
  <span
210
228
  part="preview"
@@ -445,7 +445,11 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
445
445
  * field shows the color the reset put back.
446
446
  */
447
447
  formResetCallback(): void;
448
- /** The line `said` holds, or nothing while it is empty. */
448
+ /**
449
+ * The line `said` holds, or nothing while it is empty. `role="alert"`, as
450
+ * every field's feedback line has, so the message is announced when it
451
+ * appears.
452
+ */
449
453
  protected renderPickerFeedback(): TemplateResult | typeof nothing;
450
454
  /** Whether a pointer is dragging the square's handle, said as `:state(dragging)`. */
451
455
  private dragging;
@@ -309,7 +309,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
309
309
  }
310
310
  renderPickerFeedback() {
311
311
  const said = this.said;
312
- return said === '' ? nothing : html `<p part="feedback" id="feedback">${said}</p>`;
312
+ return said === '' ? nothing : html `<p part="feedback" id="feedback" role="alert">${said}</p>`;
313
313
  }
314
314
  settle() {
315
315
  if (this.unsettled) {
@@ -208,7 +208,7 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
208
208
  * @csspart select - The cell that holds a row's checkbox.
209
209
  * @csspart select-all - The heading that holds the checkbox that selects every row.
210
210
  * @csspart select-selected - Added to `select` in a selected row.
211
- * @csspart table - The grid.
211
+ * @csspart table - The grid. While `windowed`, the box that scrolls, which needs only a block size on this tier.
212
212
  * @csspart toggle - The mark in a row header that a press opens and closes its row by, in a tree grid.
213
213
  * @csspart toggle-mark - The caret in `toggle`. It points along the line while the row is closed and down while it is open.
214
214
  * @csspart toggle-open - Added to `toggle` while its row is open.
@@ -586,6 +586,10 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
586
586
  * Off by default. Windowing costs a scroll listener, a measurement per row
587
587
  * drawn, and a `block-size` on the scrolling box, and a table of twenty rows
588
588
  * should pay none of them.
589
+ *
590
+ * While windowed, the `table` part scrolls on this tier: it carries
591
+ * `data-scroller`, and `core` ships `[data-scroller] { overflow: auto }` into
592
+ * every shadow root. You still need to give it a block size.
589
593
  */
590
594
  windowed: boolean;
591
595
  private sortState;
@@ -1228,6 +1228,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
1228
1228
  part="table"
1229
1229
  role="rowgroup"
1230
1230
  aria-busy=${this.loading ? 'true' : nothing}
1231
+ ?data-scroller=${this.windowed}
1231
1232
  style=${template ? `--data-table-template: ${template}` : nothing}
1232
1233
  @scroll=${this.handleScroll}
1233
1234
  >
@@ -31,6 +31,7 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
31
31
  '--show': () => this.show(),
32
32
  '--toggle': () => this.toggleOpen(),
33
33
  },
34
+ expanded: () => this.open,
34
35
  });
35
36
  this.persistent = false;
36
37
  this.returned = '';
@@ -74,6 +74,10 @@ declare const HelixHeadlessDrawer_base: (new (...args: any[]) => import("@fusion
74
74
  * <helix-headless-drawer id="nav">…</helix-headless-drawer>
75
75
  * ```
76
76
  *
77
+ * From its first press, a button that opens the drawer this way says whether
78
+ * the drawer is open: a Helix button through `expanded`, and any other
79
+ * element with `aria-expanded`.
80
+ *
77
81
  * @since 0.1.0
78
82
  * @status stable
79
83
  * @category overlays
@@ -19,6 +19,7 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
19
19
  '--show': () => this.requestOpen(true),
20
20
  '--toggle': () => this.toggleOpen(),
21
21
  },
22
+ expanded: () => this.open,
22
23
  });
23
24
  this.dismissible = false;
24
25
  this.dismissLabel = 'Close';
@@ -196,6 +196,7 @@ registerElementEvents({
196
196
  'helix-headless-map-terrain': ['helix-map-part-change'],
197
197
  'helix-headless-megamenu': ['helix-before-close', 'helix-before-open', 'toggle'],
198
198
  'helix-headless-menu': [
199
+ 'change',
199
200
  'helix-before-close',
200
201
  'helix-before-open',
201
202
  'helix-filter',
@@ -91,6 +91,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
91
91
  '--show': () => this.show(),
92
92
  '--toggle': () => this.toggleOpen(),
93
93
  },
94
+ expanded: () => this.open,
94
95
  });
95
96
  this.handleSlotChange = () => {
96
97
  this.requestUpdate();
@@ -394,7 +394,8 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
394
394
  * sounded the same. Said when the filter changes and the count with it, so
395
395
  * a keystroke that leaves the same options says nothing new. An empty filter
396
396
  * says nothing, being the list as it was given; no matches is `empty-text`,
397
- * which the region says in place of this.
397
+ * which the region says in place of this. A load that ends says the count
398
+ * too, whatever the filter.
398
399
  */
399
400
  private tell;
400
401
  /**
@@ -260,13 +260,14 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
260
260
  if (!this.hasUpdated) {
261
261
  return;
262
262
  }
263
- if (!changed.has('filter')) {
263
+ const loaded = changed.get('loading') === true && !this.loading;
264
+ if (!changed.has('filter') && !loaded) {
264
265
  this.tellActive(changed);
265
266
  return;
266
267
  }
267
268
  const count = this.shown.length;
268
269
  this.announced =
269
- this.filter.trim() && count
270
+ (this.filter.trim() || loaded) && count
270
271
  ? this.resultLabel
271
272
  ? countLabel(count, this.resultLabel, this.resultsLabel, wordsLocale(this))
272
273
  : formatWords(this, 'resultsLabel', { count })
@@ -81,6 +81,8 @@ import '../../kbd/index.js';
81
81
  * @csspart surface - The panel a submenu opens in beside the row, under the menu's `submenus="flyout"`.
82
82
  *
83
83
  * @cssstate checked - The row is checked.
84
+ * @cssstate disabled - The row is disabled.
85
+ * @cssstate expanded - Under `submenus="flyout"`, the row's submenu is open beside it.
84
86
  * @cssstate highlighted - In a menu with `filterable`, the arrow keys or the pointer reached the row while focus stays in the filter field.
85
87
  * @cssstate submenu-mark - Under `submenus="flyout"`, a row in this row's submenu can be ticked or opens a submenu, so the submenu keeps its trailing column.
86
88
  * @cssstate submenu-start - Under `submenus="flyout"`, a row in this row's submenu has content before its label, so the submenu keeps its leading column.
@@ -93,6 +93,8 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
93
93
  super.willUpdate(changed);
94
94
  this.toggleState('checked', this.checked);
95
95
  this.toggleState('highlighted', this.virtual && this.highlighted);
96
+ this.toggleState('disabled', this.disabled);
97
+ this.toggleState('expanded', this.expanded);
96
98
  if (!this.childrenKnown) {
97
99
  return;
98
100
  }