@fusion.dev/helix-headless 0.155.0 → 0.156.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 (54) hide show
  1. package/custom-elements.json +1787 -92
  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/element-events.generated.js +1 -0
  17. package/dist/graph/root/graph-root.d.ts +13 -0
  18. package/dist/graph/root/graph-root.js +16 -0
  19. package/dist/list/root/list-root.d.ts +74 -1
  20. package/dist/list/root/list-root.js +92 -4
  21. package/dist/list/selection.js +3 -3
  22. package/dist/map/root/map-root.d.ts +6 -0
  23. package/dist/map/root/map-root.js +8 -1
  24. package/dist/number-input/root/number-input-root.d.ts +63 -6
  25. package/dist/number-input/root/number-input-root.js +96 -11
  26. package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
  27. package/dist/object-inspector/root/object-inspector-root.js +26 -2
  28. package/dist/option/root/option-root.d.ts +32 -0
  29. package/dist/option/root/option-root.js +44 -5
  30. package/dist/outline/root/outline-root.d.ts +6 -2
  31. package/dist/outline/root/outline-root.js +7 -3
  32. package/dist/table/root/table-root.d.ts +20 -0
  33. package/dist/table/root/table-root.js +13 -3
  34. package/dist/time-input/root/time-input-root.d.ts +32 -1
  35. package/dist/time-input/root/time-input-root.js +9 -2
  36. package/dist/time-picker/root/time-picker-root.d.ts +63 -1
  37. package/dist/time-picker/root/time-picker-root.js +58 -7
  38. package/dist/timeline/item/timeline-item.d.ts +25 -0
  39. package/dist/timeline/item/timeline-item.js +27 -2
  40. package/dist/video/root/keys.d.ts +6 -2
  41. package/dist/video/root/keys.js +18 -1
  42. package/dist/video/root/video-root.d.ts +60 -1
  43. package/dist/video/root/video-root.js +70 -2
  44. package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
  45. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  46. package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
  47. package/dist/video-playlist/root/video-playlist-root.js +69 -2
  48. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  49. package/dist/virtual-list/root/virtual-list-root.js +3 -0
  50. package/lint.json +1 -1
  51. package/package.json +5 -5
  52. package/types/jsx.d.ts +2901 -232
  53. package/types/vue.d.ts +824 -58
  54. package/types/words.d.ts +110 -0
@@ -69,6 +69,23 @@ import { type ListSelectionBehavior, type ListSelectionMode } from '../selection
69
69
  * no `start` counts down from the number of rows, which a server cannot know,
70
70
  * so served markup counts from one until the list runs.
71
71
  *
72
+ * **An empty list.** Set `empty-label`, or put your own content in the `empty`
73
+ * slot, for a row shown while the list has no rows and none are loading, such
74
+ * as "No files yet" and a link to add one. Without either, an empty list shows
75
+ * nothing. The list takes the `empty` state while it has no rows, whatever it
76
+ * shows.
77
+ *
78
+ * **Loading more.** `load-more` sends `helix-load-more` once the end of the
79
+ * list comes within a screen of view, in the box that scrolls it or the page,
80
+ * once for each number of rows: add rows and it can send it again. While you
81
+ * fetch, set `loading`: the list is busy, and a row after the last says
82
+ * "Loading more" (`loading-label`, or your own content in the `loading`
83
+ * slot). The list asks for nothing while `loading` is set.
84
+ *
85
+ * An interactive list with no name is reported once in a development build,
86
+ * since a screen reader announces it as a grid and nothing more. Name it with
87
+ * `label` or `aria-label`.
88
+ *
72
89
  * @since 0.1.0
73
90
  * @status stable
74
91
  * @category data
@@ -76,18 +93,26 @@ import { type ListSelectionBehavior, type ListSelectionMode } from '../selection
76
93
  * @documentation https://fusion.dev/helix/components/list
77
94
  *
78
95
  * @slot - The rows.
96
+ * @slot empty - Content for the row shown while the list has no rows and none are loading. Falls back to `empty-label`.
97
+ * @slot loading - Content for the row after the last while `loading` is set. Falls back to `loading-label`.
79
98
  *
80
99
  * @event {Event} change - Fired once a press has changed which rows are selected.
81
100
  * @event {HelixActionEvent} helix-action - Sent from a row when the reader acts on it: Enter, or a press in a list that selects nothing. Read the row from `target`. A row with `href` follows its link instead.
82
101
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired before a press changes which rows are selected, with the `value` it would leave. Cancelable.
83
102
  * @event {HelixBeforeReorderEvent} helix-before-reorder - Fired when a row is dropped at a new place, in a `reorderable` list, with every row's value in the new order. Cancelable: cancel it to keep the row where it is.
103
+ * @event {HelixLoadMoreEvent} helix-load-more - Fired, in a list with `load-more`, when the end of the list comes within a screen of view, once for each number of rows.
84
104
  * @event {HelixReorderEvent} helix-reorder - Fired after `helix-before-reorder` when nobody canceled it. Move your rows to the order in `keys`. Cancelable: cancel it to say the move did not happen.
85
105
  *
86
106
  * @csspart drag-preview - A copy of the row a pointer drags, which follows the pointer in the top layer until the row is let go, in a `reorderable` list. Hidden from assistive technology.
107
+ * @csspart empty - The row shown while the list has no rows and none are loading, where `empty-label` or the `empty` slot gives it content. A grid row with one cell in a list that selects.
87
108
  * @csspart live - The region that says each step of a move to screen readers, in a `reorderable` list. Drawn from the first move. Visually hidden in the styled tier.
109
+ * @csspart loading - The row after the last while `loading` is set. A grid row with one cell in a list that selects.
88
110
  *
89
111
  * @cssproperty --list-first - The number of the first row, written where `start` or `reversed` moves it.
90
112
  * @cssproperty --list-step - What each row adds to the number before it: `1`, or `-1` where `reversed`.
113
+ *
114
+ * @cssstate empty - While the list has no rows.
115
+ * @cssstate loading - While `loading` is set.
91
116
  */
92
117
  export declare class HelixHeadlessList extends HelixElement {
93
118
  /**
@@ -102,6 +127,11 @@ export declare class HelixHeadlessList extends HelixElement {
102
127
  canceledLabel: string;
103
128
  /** What screen readers hear when a row is dropped, in a `reorderable` list. */
104
129
  droppedLabel: string;
130
+ /**
131
+ * The text of the row shown while the list has no rows, where the `empty`
132
+ * slot is empty. Empty, the default, shows no row.
133
+ */
134
+ emptyLabel: string;
105
135
  /**
106
136
  * The kind of list, which follows from the content.
107
137
  *
@@ -129,6 +159,18 @@ export declare class HelixHeadlessList extends HelixElement {
129
159
  * The list's name, read by screen readers.
130
160
  */
131
161
  label: string;
162
+ /**
163
+ * Marks the list as busy fetching more rows, and draws a row after the last
164
+ * that says so. The list asks for no more while it is set.
165
+ */
166
+ loading: boolean;
167
+ /** The text of the row drawn while `loading` is set, where the `loading` slot is empty. */
168
+ loadingLabel: string;
169
+ /**
170
+ * Sends `helix-load-more` once the end of the list comes within a screen of
171
+ * view, so you can add the next rows as a reader nears them.
172
+ */
173
+ loadMore: boolean;
132
174
  /**
133
175
  * What screen readers hear each time an arrow key moves a held row, in a
134
176
  * `reorderable` list. `{name}` is the row's words, `{position}` the place it
@@ -185,7 +227,7 @@ export declare class HelixHeadlessList extends HelixElement {
185
227
  */
186
228
  type?: '1' | 'a' | 'A' | 'i' | 'I';
187
229
  /**
188
- * The selected rows' values, separated by commas, in a list that selects.
230
+ * The selected rows' values, separated by commas with no spaces, such as `a,c`, in a list that selects.
189
231
  *
190
232
  * Set it to start with rows selected. A row's value is its `value`, or its
191
233
  * words where it has none.
@@ -303,6 +345,37 @@ export declare class HelixHeadlessList extends HelixElement {
303
345
  */
304
346
  private get numbers();
305
347
  protected render(): TemplateResult;
348
+ /**
349
+ * The row shown while the list has no rows: a list item, or a grid row with
350
+ * one cell in a list that selects. Drawn only once the children are known,
351
+ * so a server and a hydrating render never claim an empty list, and hidden
352
+ * while it has neither `empty-label` nor content in the `empty` slot.
353
+ */
354
+ private drawEmpty;
355
+ /**
356
+ * The row after the last while `loading` is set: a list item, or a grid row
357
+ * with one cell in a list that selects. It is not one of the rows, so it
358
+ * carries no value and a key never lands on it.
359
+ */
360
+ private drawLoading;
361
+ /** Watches the end of a `load-more` list coming near the box that scrolls it. */
362
+ private endWatcher;
363
+ /** The end being watched, so a new one is watched again. */
364
+ private watchedEnd;
365
+ /** Whether the end of the list was in reach when last measured. */
366
+ private endInView;
367
+ /** The number of rows `helix-load-more` was last sent for. */
368
+ private askedAt;
369
+ /**
370
+ * Starts or stops watching the end of the list. The box it is measured
371
+ * against is the one that scrolls the list, or the page, and a screen of
372
+ * room is added below it, so the next rows are asked for before a reader
373
+ * reaches the last.
374
+ */
375
+ private watchEnd;
376
+ /** Sends `helix-load-more`, once for each number of rows, and never while loading. */
377
+ private askForMore;
378
+ private readonly handleEmptyChange;
306
379
  private readonly handleSlotChange;
307
380
  }
308
381
  declare global {
@@ -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 { commaList, customElement, deepActiveElement, drawStatus, focusItemOf, formatWords, HelixActionEvent, HelixElement, hostAria, inlineStep, KeyboardNavigationController, placeKey, pressedOwnControl, ReorderController, requestReorder, requestValue, rovingStop, StatusController, scrollingAncestor, scrollRoom, selectsAll, typeaheadByWords, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { auditName, commaList, customElement, deepActiveElement, drawStatus, focusItemOf, formatWords, HelixActionEvent, HelixElement, HelixLoadMoreEvent, hostAria, inlineStep, KeyboardNavigationController, placeKey, pressedOwnControl, ReorderController, requestReorder, requestValue, rovingStop, StatusController, scrollingAncestor, scrollRoom, selectsAll, typeaheadByWords, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessListItem } from '../item/list-item.js';
@@ -15,7 +15,11 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
15
15
  super(...arguments);
16
16
  this.canceledLabel = 'Canceled';
17
17
  this.droppedLabel = 'Dropped';
18
+ this.emptyLabel = '';
18
19
  this.label = '';
20
+ this.loading = false;
21
+ this.loadingLabel = 'Loading more';
22
+ this.loadMore = false;
19
23
  this.movedLabel = '{name} moved to position {position} of {count}';
20
24
  this.moveLabel = 'Move {name}';
21
25
  this.pickedUpLabel = 'Picked up {name}';
@@ -81,7 +85,7 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
81
85
  }
82
86
  if (selectsAll(event) && this.selectionMode === 'multiple') {
83
87
  event.preventDefault();
84
- const every = this.reachable.map((one) => this.valueFor(one)).join(', ');
88
+ const every = this.reachable.map((one) => this.valueFor(one)).join(',');
85
89
  this.choose(every === this.value ? null : every);
86
90
  }
87
91
  else if (event.key === 'Escape' && this.value !== '' && this.selectionMode !== 'none') {
@@ -125,6 +129,10 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
125
129
  this.sync();
126
130
  }
127
131
  };
132
+ this.endInView = false;
133
+ this.handleEmptyChange = () => {
134
+ this.requestUpdate();
135
+ };
128
136
  this.handleSlotChange = () => {
129
137
  this.rowsChanged = true;
130
138
  this.requestUpdate();
@@ -306,9 +314,15 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
306
314
  this.addEventListener('keydown', this.handleKeydown);
307
315
  this.addEventListener('click', this.handleClick);
308
316
  this.addEventListener('focusin', this.handleFocusin);
317
+ if (this.hasUpdated) {
318
+ this.watchEnd();
319
+ }
309
320
  }
310
321
  disconnectedCallback() {
311
322
  super.disconnectedCallback();
323
+ this.endWatcher?.disconnect();
324
+ this.endWatcher = undefined;
325
+ this.watchedEnd = undefined;
312
326
  this.removeEventListener('keydown', this.handleKeyCapture, true);
313
327
  this.removeEventListener('keydown', this.handleKeydown);
314
328
  this.removeEventListener('click', this.handleClick);
@@ -323,6 +337,17 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
323
337
  if (this.interactive || changed.get('selectionMode') !== undefined) {
324
338
  this.sync();
325
339
  }
340
+ if (this.interactive) {
341
+ auditName(this, () => null, 'Give it a `label`, or an `aria-label` that says what the rows are.', () => this.label !== '');
342
+ }
343
+ if (this.childrenKnown) {
344
+ this.toggleState('empty', this.rows.length === 0);
345
+ }
346
+ this.toggleState('loading', this.loading);
347
+ this.watchEnd();
348
+ if (this.endInView) {
349
+ this.askForMore();
350
+ }
326
351
  if (this.reorderable || changed.get('reorderable') !== undefined) {
327
352
  this.syncMoves();
328
353
  this.refocus();
@@ -331,14 +356,64 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
331
356
  willUpdate(changed) {
332
357
  super.willUpdate(changed);
333
358
  hostAria(this.internals, { label: this.label, role: this.interactive ? 'grid' : 'list' });
359
+ this.internals.ariaBusy = this.loading ? 'true' : null;
334
360
  this.internals.ariaMultiSelectable = this.selectionMode === 'multiple' ? 'true' : null;
335
361
  }
336
362
  get numbers() {
337
- const counted = numbering(this.start, this.reversed, this.childrenKnown ? this.knownChildren.length : undefined);
363
+ const counted = numbering(this.start, this.reversed, this.childrenKnown
364
+ ? this.knownChildren.filter((child) => !child.hasAttribute('slot')).length
365
+ : undefined);
338
366
  return counted ? `--list-first: ${counted.first}; --list-step: ${counted.step}` : nothing;
339
367
  }
340
368
  render() {
341
- return html `<slot style=${this.numbers} @slotchange=${this.handleSlotChange}></slot>${drawStatus(this.status)}${this.reorder.drawDragPreview()}`;
369
+ return html `<slot style=${this.numbers} @slotchange=${this.handleSlotChange}></slot>${this.drawLoading()}${this.drawEmpty()}${this.loadMore ? html `<span data-end aria-hidden="true"></span>` : nothing}${drawStatus(this.status)}${this.reorder.drawDragPreview()}`;
370
+ }
371
+ drawEmpty() {
372
+ if (!this.childrenKnown || this.rows.length > 0 || this.loading) {
373
+ return nothing;
374
+ }
375
+ const shown = this.emptyLabel !== '' || this.querySelector(':scope > [slot="empty"]') !== null;
376
+ const content = html `<slot name="empty" @slotchange=${this.handleEmptyChange}>${this.emptyLabel}</slot>`;
377
+ return this.interactive
378
+ ? html `<div part="empty" role="row" ?hidden=${!shown}><div role="gridcell">${content}</div></div>`
379
+ : html `<div part="empty" role="listitem" ?hidden=${!shown}>${content}</div>`;
380
+ }
381
+ drawLoading() {
382
+ if (!this.loading) {
383
+ return nothing;
384
+ }
385
+ const content = html `<slot name="loading">${this.loadingLabel}</slot>`;
386
+ return this.interactive
387
+ ? html `<div part="loading" role="row"><div role="gridcell">${content}</div></div>`
388
+ : html `<div part="loading" role="listitem">${content}</div>`;
389
+ }
390
+ watchEnd() {
391
+ const end = this.loadMore ? this.renderRoot.querySelector('[data-end]') : null;
392
+ if (end === this.watchedEnd) {
393
+ return;
394
+ }
395
+ this.endWatcher?.disconnect();
396
+ this.endWatcher = undefined;
397
+ this.endInView = false;
398
+ this.watchedEnd = end ?? undefined;
399
+ if (!end || typeof IntersectionObserver === 'undefined') {
400
+ return;
401
+ }
402
+ this.endWatcher = new IntersectionObserver((entries) => {
403
+ this.endInView = entries.some((entry) => entry.isIntersecting);
404
+ if (this.endInView) {
405
+ this.askForMore();
406
+ }
407
+ }, { root: scrollingAncestor(this, { axis: 'y' }), rootMargin: '0px 0px 100% 0px' });
408
+ this.endWatcher.observe(end);
409
+ }
410
+ askForMore() {
411
+ const count = this.rows.length;
412
+ if (!this.loadMore || this.loading || this.askedAt === count) {
413
+ return;
414
+ }
415
+ this.askedAt = count;
416
+ this.dispatchEvent(new HelixLoadMoreEvent());
342
417
  }
343
418
  };
344
419
  __decorate([
@@ -349,12 +424,25 @@ __decorate([
349
424
  words,
350
425
  property({ attribute: 'dropped-label' })
351
426
  ], HelixHeadlessList.prototype, "droppedLabel", void 0);
427
+ __decorate([
428
+ property({ attribute: 'empty-label' })
429
+ ], HelixHeadlessList.prototype, "emptyLabel", void 0);
352
430
  __decorate([
353
431
  property({ reflect: true })
354
432
  ], HelixHeadlessList.prototype, "kind", void 0);
355
433
  __decorate([
356
434
  property()
357
435
  ], HelixHeadlessList.prototype, "label", void 0);
436
+ __decorate([
437
+ property({ type: Boolean })
438
+ ], HelixHeadlessList.prototype, "loading", void 0);
439
+ __decorate([
440
+ words,
441
+ property({ attribute: 'loading-label' })
442
+ ], HelixHeadlessList.prototype, "loadingLabel", void 0);
443
+ __decorate([
444
+ property({ attribute: 'load-more', type: Boolean })
445
+ ], HelixHeadlessList.prototype, "loadMore", void 0);
358
446
  __decorate([
359
447
  words,
360
448
  property({ attribute: 'moved-label' })
@@ -7,7 +7,7 @@ export const choiceValue = ({ anchor, arrived = false, behavior, extend = false,
7
7
  if (extend && mode === 'multiple') {
8
8
  const from = anchor === undefined ? -1 : shown.indexOf(anchor);
9
9
  const range = rangeAdd(now, shown, from, shown.indexOf(pressed));
10
- return range ? range.join(', ') : null;
10
+ return range ? range.join(',') : null;
11
11
  }
12
12
  if (behavior === 'replace' && !toggle) {
13
13
  return now.length === 1 && now[0] === pressed ? null : pressed;
@@ -15,8 +15,8 @@ export const choiceValue = ({ anchor, arrived = false, behavior, extend = false,
15
15
  if (arrived) {
16
16
  return null;
17
17
  }
18
- const next = pressedValue(mode, value, pressed);
19
- return next === value ? null : next;
18
+ const next = commaList(pressedValue(mode, value, pressed)).join(',');
19
+ return next === commaList(value).join(',') ? null : next;
20
20
  };
21
21
  export const rowStop = (count, at, step) => {
22
22
  if (count <= 1) {
@@ -214,6 +214,12 @@ export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
214
214
  * above `0`.
215
215
  */
216
216
  pitch: number;
217
+ /**
218
+ * What a screen reader calls the map in place of its role, after its
219
+ * name: "Edinburgh, map". Said only where the map has a name of its
220
+ * own, since the fallback name is the same word. Empty leaves the role.
221
+ */
222
+ roleLabel: string;
217
223
  /** The rotate left button's label. */
218
224
  rotateLeftLabel: string;
219
225
  /** The rotate right button's label. */
@@ -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, HelixActivateEvent, HelixBeforeChangeEvent, HelixBeforeToggleEvent, HelixElement, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixToggleEvent, hostAria, KeyboardNavigationController, MixinViewport, requestChange, words, } from '@fusion.dev/helix-core';
7
+ import { auditName, customElement, focusItemOf, formatWords, HelixActivateEvent, HelixBeforeChangeEvent, HelixBeforeToggleEvent, HelixElement, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixToggleEvent, hostAria, KeyboardNavigationController, MixinViewport, requestChange, words, } from '@fusion.dev/helix-core';
8
8
  import { buildings, cloudRain, dotsNine, leaf, mountains, path, polygon, stack, target, } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -60,6 +60,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
60
60
  this.movedUpLabel = 'Moved up';
61
61
  this.northLabel = 'Facing north';
62
62
  this.pitch = 0;
63
+ this.roleLabel = 'map';
63
64
  this.rotateLeftLabel = 'Rotate left';
64
65
  this.rotateRightLabel = 'Rotate right';
65
66
  this.satelliteLabel = 'Satellite';
@@ -340,6 +341,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
340
341
  firstUpdated(changed) {
341
342
  super.firstUpdated(changed);
342
343
  this.finishHydration();
344
+ auditName(this, () => null, 'Give it a `label` that says what the map shows, such as "Stores in Edinburgh".', () => this.label !== '');
343
345
  }
344
346
  updated(changed) {
345
347
  super.updated(changed);
@@ -609,6 +611,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
609
611
  part="region"
610
612
  role="group"
611
613
  aria-label=${this.label || this.getAttribute('aria-label') || this.fallbackLabel}
614
+ aria-roledescription=${(this.label || this.getAttribute('aria-label')) && this.roleLabel ? this.roleLabel : nothing}
612
615
  aria-keyshortcuts=${this.keyShortcuts}
613
616
  tabindex="0"
614
617
  @focusin=${this.idle.stir}
@@ -705,6 +708,10 @@ __decorate([
705
708
  __decorate([
706
709
  property({ type: Number })
707
710
  ], HelixHeadlessMap.prototype, "pitch", void 0);
711
+ __decorate([
712
+ words,
713
+ property({ attribute: 'role-label' })
714
+ ], HelixHeadlessMap.prototype, "roleLabel", void 0);
708
715
  __decorate([
709
716
  words,
710
717
  property({ attribute: 'rotate-left-label' })
@@ -25,17 +25,24 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
25
25
  * The locale's group separators are read in every field, so `1,234` is a
26
26
  * number in `en-US`.
27
27
  *
28
- * On iOS and iPadOS the field is a plain text field, with no
29
- * `role="spinbutton"`, which VoiceOver reads as a slider you cannot type in,
30
- * and no role description, so VoiceOver says the field is required. React
31
- * Aria's NumberField does the same.
28
+ * The field and its two buttons sit in a `role="group"`, and the field carries
29
+ * `role-label`, "number field" in English, as its `aria-roledescription`, as
30
+ * React Aria's NumberField does. On iOS and iPadOS the field is a plain text
31
+ * field, with no `role="spinbutton"`, which VoiceOver reads as a slider you
32
+ * cannot type in, and no role description, so VoiceOver says the field is
33
+ * required. React Aria's NumberField does the same.
32
34
  *
33
35
  * Validity follows the platform's rules: `min`, `max`, and `step` are checked
34
36
  * and reported through the host's own `setValidity`, under the platform's flag
35
37
  * names.
36
38
  *
37
39
  * Stepping is separate from typing, so a number is not corrected while you type
38
- * it.
40
+ * it. While you type, the form holds the number the text reads as, so a field
41
+ * showing `$2,500.50` gives the form 2500.5, and text that is not a number
42
+ * gives it nothing, as a native number field does.
43
+ *
44
+ * Holding a stepper button down steps once, then again after 400 milliseconds,
45
+ * then every 60 milliseconds until you let go, as React Aria's NumberField does.
39
46
  *
40
47
  * **Keyboard.** The field is one tab stop, and the stepper buttons are not in
41
48
  * the tab order.
@@ -56,9 +63,11 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
56
63
  * @aliases NumberField, Stepper, InputNumber
57
64
  * @documentation https://fusion.dev/helix/components/number-input
58
65
  *
66
+ * @slot decrement-icon - Replaces the icon on the step down button.
59
67
  * @slot end - Content after the number, such as a unit.
60
68
  * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
61
69
  * @slot hint - Help under the control, for content richer than the `hint` text.
70
+ * @slot increment-icon - Replaces the icon on the step up button.
62
71
  * @slot label - The label, for content richer than the `label` text.
63
72
  * @slot start - Content before the number, such as a unit or an icon.
64
73
  *
@@ -70,7 +79,7 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
70
79
  * @csspart decrement - The button that steps the value down.
71
80
  * @csspart decrement-mark - The icon on the step down button.
72
81
  * @csspart feedback - The message that says what is wrong.
73
- * @csspart field - The box that holds the whole control.
82
+ * @csspart field - The box that holds the whole control, a `role="group"` around the field and its buttons.
74
83
  * @csspart hint - Help text under the field, shown while there is no feedback.
75
84
  * @csspart increment - The button that steps the value up.
76
85
  * @csspart increment-mark - The icon on the step up button.
@@ -175,6 +184,15 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
175
184
  * Prevents editing. The value is still submitted with the form.
176
185
  */
177
186
  readOnly: boolean;
187
+ /**
188
+ * The words screen readers announce for the field's role, set as its
189
+ * `aria-roledescription`. Translate it with the rest of your text, or empty
190
+ * it to leave the role as `spinbutton`.
191
+ *
192
+ * Never written on iOS and iPadOS, where a role description stops VoiceOver
193
+ * saying that the field is required.
194
+ */
195
+ roleLabel: string;
178
196
  /**
179
197
  * Makes the label a grip that changes the value when it is dragged.
180
198
  *
@@ -292,6 +310,26 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
292
310
  * having to repeat both.
293
311
  */
294
312
  stepDown(large?: boolean): void;
313
+ /** The timer of a held stepper button. */
314
+ private repeat;
315
+ /** Whether a pointer press stepped already, so the click that ends it does not step again. */
316
+ private pressStepped;
317
+ /**
318
+ * A stepper button pressed with a pointer steps at once, and again while it
319
+ * is held.
320
+ *
321
+ * Captured, so letting go outside the button still ends the hold. A click
322
+ * with no press before it, from a screen reader or a script, steps once in
323
+ * `handleStepperClick`.
324
+ */
325
+ private readonly handleStepperDown;
326
+ /**
327
+ * Ends a hold. The click that follows a release arrives in the same task,
328
+ * so the press is forgotten after it.
329
+ */
330
+ private readonly stopRepeat;
331
+ private readonly handleStepperClick;
332
+ disconnectedCallback(): void;
295
333
  /**
296
334
  * The label is the surface, which is where the convention already is.
297
335
  *
@@ -391,6 +429,17 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
391
429
  * what a reader pressing them meant.
392
430
  */
393
431
  private readonly handleKeydown;
432
+ /**
433
+ * What the form holds for the field: the number the text reads as.
434
+ *
435
+ * The text as written where it is already a plain number, so a value of
436
+ * `0.50` is sent as written. A number written in the locale or the format,
437
+ * `$2,500.50` or `1.234,5`, is sent as the number, 2500.5 or 1234.5, so a
438
+ * form read while the field still has focus gets what a submit would. Text
439
+ * that is not a number is sent as empty, as a native number field sends it.
440
+ * The text itself is the state the browser restores.
441
+ */
442
+ private submitted;
394
443
  /**
395
444
  * Reports the value and a verdict on it.
396
445
  *
@@ -433,6 +482,14 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
433
482
  private readonly handleKeypress;
434
483
  /** @internal A single line of text counts against implicit submission. */
435
484
  protected get blocksImplicitSubmission(): boolean;
485
+ /**
486
+ * The step down button's icon, as the `decrement-icon` slot's fallback, so an
487
+ * icon of your own replaces it by being slotted, and a tier above by
488
+ * overriding this.
489
+ */
490
+ protected renderDecrementIcon(): TemplateResult;
491
+ /** The step up button's icon, behind the same seam as `renderDecrementIcon`. */
492
+ protected renderIncrementIcon(): TemplateResult;
436
493
  protected render(): TemplateResult;
437
494
  /**
438
495
  * The value as a number, or `NaN` while the control is empty.