@fusion.dev/helix-headless 0.170.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 (57) hide show
  1. package/custom-elements.json +1045 -154
  2. package/dist/chat/composer/chat-composer.d.ts +30 -2
  3. package/dist/chat/composer/chat-composer.js +29 -4
  4. package/dist/chat/root/chat-root.d.ts +73 -7
  5. package/dist/chat/root/chat-root.js +90 -5
  6. package/dist/code-block/root/code-block-root.d.ts +20 -2
  7. package/dist/code-block/root/code-block-root.js +29 -8
  8. package/dist/color-input/root/color-input-root.d.ts +15 -1
  9. package/dist/color-input/root/color-input-root.js +19 -1
  10. package/dist/color-picker/root/color-picker-root.d.ts +54 -7
  11. package/dist/color-picker/root/color-picker-root.js +62 -1
  12. package/dist/combobox/root/combobox-root.d.ts +16 -6
  13. package/dist/combobox/root/combobox-root.js +9 -2
  14. package/dist/command/root/command-root.d.ts +16 -1
  15. package/dist/command/root/command-root.js +8 -0
  16. package/dist/data-table/root/data-table-root.d.ts +5 -1
  17. package/dist/data-table/root/data-table-root.js +1 -0
  18. package/dist/dialog/root/dialog-root.js +1 -0
  19. package/dist/drawer/root/drawer-root.d.ts +4 -0
  20. package/dist/drawer/root/drawer-root.js +1 -0
  21. package/dist/element-events.generated.js +1 -0
  22. package/dist/filter-input/chip/filter-chip.d.ts +13 -3
  23. package/dist/filter-input/chip/filter-chip.js +5 -0
  24. package/dist/filter-input/menu/filter-menu.js +0 -1
  25. package/dist/filter-input/root/filter-input-root.d.ts +71 -4
  26. package/dist/filter-input/root/filter-input-root.js +87 -2
  27. package/dist/format-date/root/format-date-root.d.ts +3 -1
  28. package/dist/input/root/input-root.d.ts +6 -0
  29. package/dist/input/root/input-root.js +13 -0
  30. package/dist/lightbox/root/lightbox-root.js +1 -0
  31. package/dist/listbox/root/listbox-root.d.ts +8 -1
  32. package/dist/listbox/root/listbox-root.js +9 -3
  33. package/dist/menu/item/menu-item.d.ts +2 -0
  34. package/dist/menu/item/menu-item.js +2 -0
  35. package/dist/menu/root/menu-root.d.ts +73 -12
  36. package/dist/menu/root/menu-root.js +103 -7
  37. package/dist/number-input/root/number-input-root.d.ts +16 -6
  38. package/dist/number-input/root/number-input-root.js +15 -4
  39. package/dist/popover/root/popover-root.d.ts +59 -6
  40. package/dist/popover/root/popover-root.js +51 -3
  41. package/dist/select/root/select-root.d.ts +2 -2
  42. package/dist/select/root/select-root.js +4 -2
  43. package/dist/steps/root/steps-root.d.ts +32 -8
  44. package/dist/steps/root/steps-root.js +70 -9
  45. package/dist/steps/step/step.d.ts +9 -1
  46. package/dist/steps/step/step.js +5 -0
  47. package/dist/textarea/root/textarea-root.d.ts +8 -0
  48. package/dist/textarea/root/textarea-root.js +16 -0
  49. package/dist/video-playlist/root/video-playlist-root.d.ts +19 -1
  50. package/dist/video-playlist/root/video-playlist-root.js +14 -3
  51. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  52. package/dist/virtual-list/root/virtual-list-root.js +7 -2
  53. package/lint.json +1 -1
  54. package/package.json +5 -5
  55. package/types/jsx.d.ts +800 -144
  56. package/types/vue.d.ts +165 -31
  57. package/types/words.d.ts +20 -2
@@ -1,4 +1,4 @@
1
- import { HelixElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
1
+ import { CommandController, HelixElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import type { HelixHeadlessStep } from '../step/step.js';
4
4
  declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerActiveInterface) & typeof HelixElement;
@@ -14,12 +14,21 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
14
14
  * sequence is correct in the server's markup, where the root cannot count its
15
15
  * slotted children.
16
16
  *
17
+ * `active` names a step by its `value`. A step with no `value` is named by its
18
+ * place, counted from 1, so a sequence written without values moves as one
19
+ * written with them, and `helix-activate` says `1`, `2` and so on. Give every
20
+ * step a value or none, because a value of `2` and the second place are one
21
+ * name.
22
+ *
17
23
  * `linear` keeps a reader from passing a stage they have not finished, as Ark
18
24
  * UI's linear steps do: a stage ahead is out of reach, and its button disabled,
19
25
  * until every stage before it is `complete` or `optional`. Going back is always
20
26
  * allowed. `next()` and `previous()` move one stage either way, under the
21
27
  * same rules, for your own buttons. `reset()` goes back to the first stage and
22
- * clears every stage's `complete`, as Ark UI's `resetStep` does.
28
+ * clears every stage's `complete`, as Ark UI's `resetStep` does. A button
29
+ * anywhere on the page calls them with no script, as Ark UI's previous and
30
+ * next triggers do: give it `commandfor` with the sequence's `id`, and
31
+ * `command="--previous"`, `"--next"` or `"--reset"`.
23
32
  *
24
33
  * `completion="passed"` marks a stage `complete` once a reader moves past it,
25
34
  * as Ark UI counts a stage before the current one as completed. Going back
@@ -46,11 +55,13 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
46
55
  * button is described by its `complete-label`.
47
56
  *
48
57
  * **Keyboard.** Each stage's name is a button and its own tab stop, and Enter
49
- * or Space goes to the stage. Set `arrows` and the arrow keys move focus
50
- * between the names as well, as Ark UI's steps do: Left and Right along a row,
51
- * Up and Down a column, Home and End to the first and the last. The arrows
52
- * pass over a disabled stage and one a linear sequence holds out of reach,
53
- * and they move focus without going to the stage.
58
+ * or Space goes to the stage. Set `arrows` and the names are one tab stop, the
59
+ * current stage's, and the arrow keys move focus between them: Left and Right
60
+ * along a row, Up and Down a column, Home and End to the first and the last.
61
+ * The stop follows focus, so Tab leaves the sequence and Shift+Tab comes back
62
+ * to the name the reader left. The arrows pass over a disabled stage and one a
63
+ * linear sequence holds out of reach, and they move focus without going to the
64
+ * stage.
54
65
  *
55
66
  * @since 0.1.0
56
67
  * @status stable
@@ -78,7 +89,8 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
78
89
  static words: string;
79
90
  /**
80
91
  * Whether the arrow keys move focus between the stages' names, along the
81
- * way the steps run. Off by default, so the arrows keep scrolling the page.
92
+ * way the steps run, and the names are one tab stop. Off by default, so the
93
+ * arrows keep scrolling the page.
82
94
  */
83
95
  arrows: boolean;
84
96
  /**
@@ -148,6 +160,7 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
148
160
  */
149
161
  variant?: 'numeric';
150
162
  protected readonly keys: KeyboardNavigationController;
163
+ protected readonly commands: CommandController;
151
164
  /** The steps, in the order the consumer wrote them. */
152
165
  private get steps();
153
166
  /**
@@ -158,6 +171,8 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
158
171
  * calls to `next()` in one task then move two stages.
159
172
  */
160
173
  private get at();
174
+ /** The step `active` names, by `value` or by place, or the first enabled one. */
175
+ private named;
161
176
  /**
162
177
  * Requests a new current stage, and returns whether it moved.
163
178
  *
@@ -207,6 +222,15 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
207
222
  */
208
223
  private sync;
209
224
  private readonly handleSelection;
225
+ /** The step whose name holds the sequence's one tab stop under `arrows`, once focus has chosen it. */
226
+ private holder;
227
+ /**
228
+ * Gives the one tab stop to a name, under `arrows`: the one focus last left,
229
+ * else the current stage's, else the first a reader can go to. Without
230
+ * `arrows` every name is its own stop. Client only.
231
+ */
232
+ private rove;
233
+ private readonly handleFocus;
210
234
  private readonly handleSlotChange;
211
235
  connectedCallback(): void;
212
236
  disconnectedCallback(): void;
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, focusItemOf, formatWords, HelixElement, hostAria, KeyboardNavigationController, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { CommandController, customElement, focusItemOf, formatWords, HelixElement, hostAria, KeyboardNavigationController, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { canReach, progressOf } from '../reach.js';
@@ -19,6 +19,7 @@ const navigable = (step) => {
19
19
  },
20
20
  });
21
21
  };
22
+ const keyOf = (step, index) => step.value || String(index + 1);
22
23
  let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(HelixElement) {
23
24
  constructor() {
24
25
  super(...arguments);
@@ -33,6 +34,13 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
33
34
  selects: () => false,
34
35
  wrap: false,
35
36
  });
37
+ this.commands = new CommandController(this, {
38
+ commands: {
39
+ '--next': () => this.next(),
40
+ '--previous': () => this.previous(),
41
+ '--reset': () => this.reset(),
42
+ },
43
+ });
36
44
  this.handleSelection = (event) => {
37
45
  if (!this.steps.includes(event.target)) {
38
46
  return;
@@ -42,12 +50,27 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
42
50
  if (event.defaultPrevented || !this.reachable(index) || !this.valid(index)) {
43
51
  return;
44
52
  }
45
- this.requestActive(event.detail.value);
53
+ this.requestActive(keyOf(this.steps[index], index));
46
54
  });
47
55
  };
56
+ this.handleFocus = (event) => {
57
+ if (!this.arrows) {
58
+ return;
59
+ }
60
+ const path = event.composedPath();
61
+ const step = this.steps.find((one) => {
62
+ const name = one.shadowRoot?.querySelector('[part="label"]');
63
+ return name ? path.includes(name) : false;
64
+ });
65
+ if (step && step !== this.holder) {
66
+ this.holder = step;
67
+ this.rove();
68
+ }
69
+ };
48
70
  this.handleSlotChange = () => {
49
71
  this.sync();
50
72
  this.lock();
73
+ this.rove();
51
74
  };
52
75
  }
53
76
  static { this.words = 'helix-steps'; }
@@ -62,11 +85,20 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
62
85
  if (this.active === '') {
63
86
  return steps.findIndex((step) => step.current);
64
87
  }
65
- const item = this.resolveActive(steps);
88
+ const item = this.named(steps);
66
89
  return item ? steps.indexOf(item) : -1;
67
90
  }
91
+ named(steps) {
92
+ const keyed = steps.map((step, index) => ({
93
+ disabled: step.disabled,
94
+ step,
95
+ value: keyOf(step, index),
96
+ }));
97
+ return this.resolveActive(keyed)?.step;
98
+ }
68
99
  requestActive(next, cause) {
69
- const here = this.active === '' ? this.steps[this.at]?.value : undefined;
100
+ const at = this.active === '' ? this.at : -1;
101
+ const here = at === -1 ? undefined : keyOf(this.steps[at], at);
70
102
  if (here) {
71
103
  this.active = here;
72
104
  }
@@ -80,8 +112,8 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
80
112
  }
81
113
  reset() {
82
114
  const steps = this.steps;
83
- const first = steps.find((one) => !one.disabled);
84
- if (!first || !this.requestActive(first.value)) {
115
+ const first = steps.findIndex((one) => !one.disabled);
116
+ if (first === -1 || !this.requestActive(keyOf(steps[first], first))) {
85
117
  return false;
86
118
  }
87
119
  for (const one of steps) {
@@ -100,7 +132,7 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
100
132
  if (!target || !this.reachable(index) || !this.valid(index)) {
101
133
  return false;
102
134
  }
103
- return this.requestActive(target.value);
135
+ return this.requestActive(keyOf(target, index));
104
136
  }
105
137
  valid(index) {
106
138
  const from = this.steps[this.at];
@@ -128,8 +160,8 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
128
160
  return;
129
161
  }
130
162
  const steps = this.steps;
131
- const value = this.resolveActive(steps)?.value ?? '';
132
- const reached = steps.findIndex((step) => step.value === value);
163
+ const named = this.named(steps);
164
+ const reached = named ? steps.indexOf(named) : -1;
133
165
  steps.forEach((step, index) => {
134
166
  const current = index === reached;
135
167
  const passed = this.completion === 'passed' && index < reached;
@@ -141,11 +173,30 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
141
173
  });
142
174
  });
143
175
  }
176
+ rove() {
177
+ const steps = this.steps;
178
+ const open = steps.filter((step, index) => !step.disabled && this.reachable(index));
179
+ const current = steps[this.at];
180
+ const holder = !this.arrows
181
+ ? undefined
182
+ : this.holder && open.includes(this.holder)
183
+ ? this.holder
184
+ : current && open.includes(current)
185
+ ? current
186
+ : open[0];
187
+ for (const step of steps) {
188
+ whenHydrated(step, () => {
189
+ step.stop = !this.arrows || step === holder;
190
+ });
191
+ }
192
+ }
144
193
  connectedCallback() {
145
194
  super.connectedCallback();
146
195
  this.addEventListener('helix-select', this.handleSelection);
196
+ this.addEventListener('focusin', this.handleFocus);
147
197
  this.watcher = new MutationObserver(() => {
148
198
  this.lock();
199
+ this.rove();
149
200
  this.requestUpdate();
150
201
  });
151
202
  this.watcher.observe(this, {
@@ -157,6 +208,7 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
157
208
  disconnectedCallback() {
158
209
  super.disconnectedCallback();
159
210
  this.removeEventListener('helix-select', this.handleSelection);
211
+ this.removeEventListener('focusin', this.handleFocus);
160
212
  this.watcher?.disconnect();
161
213
  this.watcher = undefined;
162
214
  }
@@ -181,6 +233,15 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
181
233
  if (changed.has('linear') || changed.has('active') || changed.has('completion')) {
182
234
  this.lock();
183
235
  }
236
+ if (changed.has('active')) {
237
+ this.holder = undefined;
238
+ }
239
+ if (changed.has('arrows') ||
240
+ changed.has('linear') ||
241
+ changed.has('active') ||
242
+ changed.has('completion')) {
243
+ this.rove();
244
+ }
184
245
  }
185
246
  render() {
186
247
  const progress = progressOf(this.known);
@@ -106,7 +106,15 @@ export declare class HelixHeadlessStep extends HelixElement {
106
106
  */
107
107
  optional: boolean;
108
108
  /**
109
- * The value the step sends in `helix-select`.
109
+ * @internal Set by a sequence with `arrows`: whether the name holds the
110
+ * sequence's one tab stop. A property, because the sequence writes it after
111
+ * the step upgrades.
112
+ */
113
+ stop: boolean;
114
+ /**
115
+ * The value the step sends in `helix-select`, and what names the step in its
116
+ * sequence's `active`. Without one, the sequence names the step by its place,
117
+ * counted from 1.
110
118
  */
111
119
  value: string;
112
120
  /** Whether anything is slotted into `content`, which makes it a named group. */
@@ -22,6 +22,7 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
22
22
  this.invalid = false;
23
23
  this.locked = false;
24
24
  this.optional = false;
25
+ this.stop = true;
25
26
  this.value = '';
26
27
  this.holding = false;
27
28
  this.readContent = () => {
@@ -73,6 +74,7 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
73
74
  part="label"
74
75
  exportparts="button: label-button, live: label-live, spinner: label-spinner, text: label-text"
75
76
  emphasis="ghost"
77
+ .focusable=${this.stop}
76
78
  aria-current=${this.current ? 'step' : nothing}
77
79
  aria-describedby=${this.complete ? 'state' : nothing}
78
80
  ?disabled=${this.disabled || this.locked}
@@ -122,6 +124,9 @@ __decorate([
122
124
  __decorate([
123
125
  property({ reflect: true, type: Boolean })
124
126
  ], HelixHeadlessStep.prototype, "optional", void 0);
127
+ __decorate([
128
+ property({ attribute: false })
129
+ ], HelixHeadlessStep.prototype, "stop", void 0);
125
130
  __decorate([
126
131
  property()
127
132
  ], HelixHeadlessStep.prototype, "value", void 0);
@@ -217,6 +217,14 @@ export declare class HelixHeadlessTextarea extends HelixHeadlessTextarea_base {
217
217
  * and so does this.
218
218
  */
219
219
  setSelectionRange(start: number, end: number, direction?: SelectionDirection): void;
220
+ /**
221
+ * Replaces a range of the text, as the native method does: the selection
222
+ * where `start` and `end` are left out, and `selectMode` says what is
223
+ * selected after. It sends no event, as the native method sends none.
224
+ */
225
+ setRangeText(replacement: string, start?: number, end?: number, selectMode?: SelectionMode): void;
226
+ /** Selects everything in the field. */
227
+ select(): void;
220
228
  /** How many characters the value holds. */
221
229
  get textLength(): number;
222
230
  /** The control's type, which is always `textarea`. */
@@ -111,6 +111,22 @@ let HelixHeadlessTextarea = class HelixHeadlessTextarea extends MixinControllerV
111
111
  setSelectionRange(start, end, direction) {
112
112
  this.control?.setSelectionRange(start, end, direction);
113
113
  }
114
+ setRangeText(replacement, start, end, selectMode) {
115
+ const control = this.control;
116
+ if (!control) {
117
+ return;
118
+ }
119
+ if (start === undefined || end === undefined) {
120
+ control.setRangeText(replacement);
121
+ }
122
+ else {
123
+ control.setRangeText(replacement, start, end, selectMode);
124
+ }
125
+ this.value = control.value;
126
+ }
127
+ select() {
128
+ this.control?.select();
129
+ }
114
130
  get textLength() {
115
131
  return this.value.length;
116
132
  }
@@ -11,6 +11,12 @@ declare const HelixHeadlessVideoPlaylist_base: (new (...args: any[]) => import("
11
11
  * captions, and poster treatment. `Sandbox` takes its preview and `Menu` its
12
12
  * trigger the same way.
13
13
  *
14
+ * **The player is a `Video` or a plain `<video>`.** Either takes `src` and
15
+ * `poster` from the entry, is named by it, plays on, and answers Shift with N
16
+ * or P while focused. A plain `<video>` has no previous and next controls
17
+ * that send `helix-previous` or `helix-next`, so its moves are the entries,
18
+ * the menu, and your own buttons calling `previous()` and `next()`.
19
+ *
14
20
  * **Entries are markup.** React skips properties it would set imperatively
15
21
  * during hydration, so an array property on a custom element is missing on the
16
22
  * first paint of a server-rendered page. `using-the-elements.mdx` has the
@@ -43,7 +49,8 @@ declare const HelixHeadlessVideoPlaylist_base: (new (...args: any[]) => import("
43
49
  *
44
50
  * **The player is named by the current entry**, and the device's own media
45
51
  * controls show the entry as the title. A player you gave a `label` keeps
46
- * your name, and only the title follows the entry.
52
+ * your name, and only the title follows the entry. A plain `<video>` is
53
+ * named through `aria-label`, unless you wrote one.
47
54
  *
48
55
  * **Keyboard.** The entries take one stop in the tab order: the entry you
49
56
  * last focused, or the current entry before you have focused one. Enter or
@@ -235,6 +242,17 @@ export declare class HelixHeadlessVideoPlaylist extends HelixHeadlessVideoPlayli
235
242
  * Before they focus one, the current entry holds it.
236
243
  */
237
244
  private stop;
245
+ /** The name this playlist last wrote on a plain media player, to tell it from yours. */
246
+ private wroteName;
247
+ /**
248
+ * Names the player by the entry.
249
+ *
250
+ * The consumer's own name for the player stands. Only a player named by
251
+ * nobody takes the entry's words as its name: `label` on a player that has
252
+ * one, and `aria-label` on a plain `<video>` or `<audio>`, which has none and
253
+ * was heard as an unnamed video.
254
+ */
255
+ private name;
238
256
  /** The entry the player was last pointed at. */
239
257
  private pointed;
240
258
  /** Hears entries arrive and leave, which change no property here. */
@@ -161,6 +161,19 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
161
161
  get items() {
162
162
  return [...this.querySelectorAll('*')].filter((one) => one instanceof HelixHeadlessVideoPlaylistItem && one.value !== '');
163
163
  }
164
+ name(player, text) {
165
+ if (player instanceof HTMLMediaElement) {
166
+ const own = player.getAttribute('aria-label');
167
+ if (own === null || own === this.wroteName) {
168
+ player.setAttribute('aria-label', text);
169
+ this.wroteName = text;
170
+ }
171
+ return;
172
+ }
173
+ if (!player.hasAttribute('label')) {
174
+ player.label = text;
175
+ }
176
+ }
164
177
  get player() {
165
178
  const slot = this.renderRoot?.querySelector('slot[name="player"]');
166
179
  return slot?.assignedElements()[0] ?? null;
@@ -299,9 +312,7 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
299
312
  }
300
313
  player.poster = current.poster;
301
314
  player.mediaTitle = current.text;
302
- if (!player.hasAttribute('label')) {
303
- player.label = current.text;
304
- }
315
+ this.name(player, current.text);
305
316
  if (this.chased) {
306
317
  this.status.say(formatWords(this, 'playingLabel', { title: current.text }));
307
318
  void player.updateComplete?.then(() => player.play()).catch(() => { });
@@ -119,6 +119,11 @@ export interface VirtualListRange {
119
119
  * commas. Their rows carry `aria-disabled`, the keys and typing pass over
120
120
  * them, and a press, Space, or Control with A does not select them.
121
121
  *
122
+ * **Rows a stylesheet can tell apart.** A chosen row carries `data-selected`,
123
+ * a disabled one `data-disabled`, as React Aria's rows do, and the row the
124
+ * keys are on, which holds the tab stop, `data-active`, whether or not the
125
+ * list has focus.
126
+ *
122
127
  * **Loading more.** `helix-range-change` fires as the items on screen change,
123
128
  * and `range` holds them. `helix-load-more` fires once a reader is within one
124
129
  * screen of the end, once for each length of the set: add to `items` and it can
@@ -560,6 +565,9 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
560
565
  * `option` with `aria-selected` there, and `listitem` otherwise, and
561
566
  * `aria-disabled` on an item `disabled-keys` names. Every pass,
562
567
  * since a choice changes no row's content.
568
+ *
569
+ * `data-selected` and `data-disabled` say the same to a stylesheet, as
570
+ * React Aria's rows do, so one rule reads the same in either kind of list.
563
571
  */
564
572
  private mark;
565
573
  /** Writes a row's place in the set. */
@@ -532,18 +532,22 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
532
532
  }
533
533
  mark(element, at, chosen, off) {
534
534
  element.setAttribute('role', this.selectionMode ? 'option' : 'listitem');
535
+ const selected = Boolean(this.selectionMode) && chosen.has(this.keyAt(at));
535
536
  if (this.selectionMode) {
536
- element.setAttribute('aria-selected', String(chosen.has(this.keyAt(at))));
537
+ element.setAttribute('aria-selected', String(selected));
537
538
  }
538
539
  else {
539
540
  element.removeAttribute('aria-selected');
540
541
  }
541
- if (off.size > 0 && off.has(this.keyAt(at))) {
542
+ element.toggleAttribute('data-selected', selected);
543
+ const disabled = off.size > 0 && off.has(this.keyAt(at));
544
+ if (disabled) {
542
545
  element.setAttribute('aria-disabled', 'true');
543
546
  }
544
547
  else {
545
548
  element.removeAttribute('aria-disabled');
546
549
  }
550
+ element.toggleAttribute('data-disabled', disabled);
547
551
  }
548
552
  stamp(element, at) {
549
553
  element.setAttribute('aria-posinset', String(at + 1));
@@ -660,6 +664,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
660
664
  }
661
665
  const stop = at === this.active;
662
666
  group[0]?.setAttribute('tabindex', stop ? '0' : '-1');
667
+ group[0]?.toggleAttribute('data-active', stop);
663
668
  if (stop && (fresh.has(at) || this.released !== at)) {
664
669
  releaseControls(group);
665
670
  }