@fusion.dev/helix-headless 0.158.0 → 0.160.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 (58) hide show
  1. package/custom-elements.json +1925 -174
  2. package/dist/app-header/root/app-header-root.d.ts +59 -12
  3. package/dist/app-header/root/app-header-root.js +68 -29
  4. package/dist/button/root/button-root.d.ts +8 -0
  5. package/dist/button/root/button-root.js +4 -0
  6. package/dist/button-group/root/button-group-root.d.ts +20 -7
  7. package/dist/button-group/root/button-group-root.js +30 -5
  8. package/dist/date-input/root/date-input-root.d.ts +108 -19
  9. package/dist/date-input/root/date-input-root.js +112 -13
  10. package/dist/date-picker/root/date-picker-root.d.ts +81 -16
  11. package/dist/date-picker/root/date-picker-root.js +140 -36
  12. package/dist/element-events.generated.js +9 -1
  13. package/dist/format-date/fields.js +1 -0
  14. package/dist/format-date/options.d.ts +3 -1
  15. package/dist/format-date/options.js +4 -1
  16. package/dist/format-date/root/format-date-root.d.ts +27 -11
  17. package/dist/format-date/root/format-date-root.js +6 -0
  18. package/dist/lightbox/item/lightbox-item.d.ts +7 -1
  19. package/dist/lightbox/item/lightbox-item.js +15 -2
  20. package/dist/lightbox/root/lightbox-root.d.ts +8 -4
  21. package/dist/lightbox/root/lightbox-root.js +9 -1
  22. package/dist/lightbox/zoom-controller.d.ts +13 -4
  23. package/dist/lightbox/zoom-controller.js +17 -0
  24. package/dist/listbox/root/listbox-root.js +5 -0
  25. package/dist/notifications/notification/notification.d.ts +1 -0
  26. package/dist/notifications/notification/notification.js +2 -1
  27. package/dist/slider/root/slider-root.d.ts +54 -3
  28. package/dist/slider/root/slider-root.js +124 -2
  29. package/dist/steps/root/steps-root.js +8 -6
  30. package/dist/steps/step/step.d.ts +9 -0
  31. package/dist/steps/step/step.js +19 -2
  32. package/dist/tabs/panel/tab-panel.d.ts +22 -1
  33. package/dist/tabs/panel/tab-panel.js +22 -1
  34. package/dist/tabs/root/tabs-root.d.ts +21 -5
  35. package/dist/tabs/root/tabs-root.js +24 -13
  36. package/dist/tabs/tab/tab.d.ts +4 -2
  37. package/dist/tabs/tab/tab.js +3 -1
  38. package/dist/time-input/root/time-input-root.d.ts +55 -7
  39. package/dist/time-input/root/time-input-root.js +70 -10
  40. package/dist/time-picker/root/time-picker-root.d.ts +27 -5
  41. package/dist/time-picker/root/time-picker-root.js +52 -13
  42. package/dist/toast/item/toast-item.d.ts +27 -8
  43. package/dist/toast/item/toast-item.js +47 -11
  44. package/dist/toast/root/toast-root.d.ts +24 -1
  45. package/dist/toast/root/toast-root.js +54 -0
  46. package/dist/toggle/group/toggle-group.d.ts +18 -1
  47. package/dist/toggle/group/toggle-group.js +8 -2
  48. package/dist/toggle/root/toggle-root.d.ts +2 -2
  49. package/dist/toggle/root/toggle-root.js +2 -1
  50. package/dist/tour/root/tour-root.d.ts +34 -0
  51. package/dist/tour/root/tour-root.js +92 -3
  52. package/dist/tour/stop/tour-stop.d.ts +33 -1
  53. package/dist/tour/stop/tour-stop.js +12 -1
  54. package/lint.json +1 -1
  55. package/package.json +5 -5
  56. package/types/jsx.d.ts +1119 -218
  57. package/types/vue.d.ts +278 -51
  58. package/types/words.d.ts +72 -0
@@ -34,14 +34,16 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
34
34
  wrap: false,
35
35
  });
36
36
  this.handleSelection = (event) => {
37
- if (event.defaultPrevented || !this.steps.includes(event.target)) {
37
+ if (!this.steps.includes(event.target)) {
38
38
  return;
39
39
  }
40
- const index = this.steps.indexOf(event.target);
41
- if (!this.reachable(index) || !this.valid(index)) {
42
- return;
43
- }
44
- this.requestActive(event.detail.value);
40
+ queueMicrotask(() => {
41
+ const index = this.steps.indexOf(event.target);
42
+ if (event.defaultPrevented || !this.reachable(index) || !this.valid(index)) {
43
+ return;
44
+ }
45
+ this.requestActive(event.detail.value);
46
+ });
45
47
  };
46
48
  this.handleSlotChange = () => {
47
49
  this.sync();
@@ -27,6 +27,10 @@ import '../../button/index.js';
27
27
  * region. The current stage's button carries `aria-current="step"`, and only
28
28
  * the button, so the state is said once, on the control.
29
29
  *
30
+ * A stage's `content`, while it holds anything, is a group named by the
31
+ * stage's name, so a screen reader moving into it hears which stage it
32
+ * belongs to.
33
+ *
30
34
  * @since 0.1.0
31
35
  * @status stable
32
36
  * @aliases Stage
@@ -105,6 +109,11 @@ export declare class HelixHeadlessStep extends HelixElement {
105
109
  * The value the step sends in `helix-select`.
106
110
  */
107
111
  value: string;
112
+ /** Whether anything is slotted into `content`, which makes it a named group. */
113
+ private holding;
114
+ /** Reads whether the `content` slot holds anything. */
115
+ private readonly readContent;
116
+ protected firstUpdated(changed: PropertyValues): void;
108
117
  /** Says a stage was finished, from the first time one is. */
109
118
  private readonly status;
110
119
  /**
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, drawStatus, formatWords, HelixElement, HelixSelectEvent, hostAria, StatusController, slottedWords, words, } from '@fusion.dev/helix-core';
8
8
  import { check, circle, record } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
- import { property } from 'lit/decorators.js';
10
+ import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
13
  import '../../button/index.js';
@@ -23,9 +23,18 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
23
23
  this.locked = false;
24
24
  this.optional = false;
25
25
  this.value = '';
26
+ this.holding = false;
27
+ this.readContent = () => {
28
+ const slot = this.renderRoot.querySelector('slot[name="content"]');
29
+ this.holding = (slot?.assignedNodes() ?? []).some((node) => node.nodeType === Node.ELEMENT_NODE || (node.textContent ?? '').trim() !== '');
30
+ };
26
31
  this.status = new StatusController(this);
27
32
  }
28
33
  static { this.words = 'helix-step'; }
34
+ firstUpdated(changed) {
35
+ super.firstUpdated(changed);
36
+ this.readContent();
37
+ }
29
38
  focus(options) {
30
39
  this.partNamed('label')?.focus(options);
31
40
  }
@@ -60,6 +69,7 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
60
69
  }
61
70
  renderLabel() {
62
71
  return html `<${this.tag('button')}
72
+ id="label"
63
73
  part="label"
64
74
  exportparts="button: label-button, live: label-live, spinner: label-spinner, text: label-text"
65
75
  emphasis="ghost"
@@ -79,7 +89,11 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
79
89
  ${this.renderLabel()}
80
90
  <span part="description"><slot name="description"></slot></span>
81
91
  </span>
82
- <div part="content"><slot name="content"></slot></div>${drawStatus(this.status)}`;
92
+ <div
93
+ part="content"
94
+ role=${this.holding ? 'group' : nothing}
95
+ aria-labelledby=${this.holding ? 'label' : nothing}
96
+ ><slot name="content" @slotchange=${this.readContent}></slot></div>${drawStatus(this.status)}`;
83
97
  }
84
98
  };
85
99
  __decorate([
@@ -111,6 +125,9 @@ __decorate([
111
125
  __decorate([
112
126
  property()
113
127
  ], HelixHeadlessStep.prototype, "value", void 0);
128
+ __decorate([
129
+ state()
130
+ ], HelixHeadlessStep.prototype, "holding", void 0);
114
131
  HelixHeadlessStep = __decorate([
115
132
  customElement('helix-headless-step')
116
133
  ], HelixHeadlessStep);
@@ -15,15 +15,24 @@ import { type PropertyValues, type TemplateResult } from 'lit';
15
15
  * the tab list lands on the panel's first link or control where there is one,
16
16
  * and on the panel itself where there is none.
17
17
  *
18
+ * **Set `lazy` for a panel that is expensive to draw.** Its content waits in a
19
+ * `<template>` inside it, and goes into the panel the first time the panel is
20
+ * shown, so it costs nothing until a reader opens that tab. Once in, it stays.
21
+ * The panel sends `helix-load` at the same moment, for content your code
22
+ * renders itself.
23
+ *
18
24
  * @since 0.1.0
19
25
  * @status stable
20
26
  * @aliases TabPanel
21
27
  * @documentation https://fusion.dev/helix/components/tabs
22
28
  *
23
- * @slot - The panel's content.
29
+ * @slot - The panel's content. Under `lazy`, a `<template>` holding it.
30
+ *
31
+ * @event {HelixLoadEvent} helix-load - Fired the first time a `lazy` panel is shown, once its template's content is in.
24
32
  *
25
33
  * @csspart panel - The panel container, which takes the focus while it holds nothing focusable.
26
34
  *
35
+ * @cssstate loaded - Once a `lazy` panel has been shown and its content is in.
27
36
  * @cssstate selected - While its tab is the one selected.
28
37
  */
29
38
  export declare class HelixHeadlessTabPanel extends HelixElement {
@@ -40,6 +49,18 @@ export declare class HelixHeadlessTabPanel extends HelixElement {
40
49
  * the pattern names one anyway.
41
50
  */
42
51
  label: string;
52
+ /**
53
+ * Whether the panel's content waits in a `<template>` until the panel is
54
+ * first shown.
55
+ */
56
+ lazy: boolean;
57
+ /** Whether a `lazy` panel has put its content in. */
58
+ private loaded;
59
+ /**
60
+ * Puts a `lazy` panel's content in the first time the panel is shown: the
61
+ * content of each `<template>` it holds, in place of the template.
62
+ */
63
+ private load;
43
64
  /**
44
65
  * Whether this panel is the visible one.
45
66
  */
@@ -4,13 +4,15 @@ 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, focusableElements, HelixElement, hostAria } from '@fusion.dev/helix-core';
7
+ import { customElement, focusableElements, HelixElement, HelixLoadEvent, hostAria, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessTabPanel = class HelixHeadlessTabPanel extends HelixElement {
11
11
  constructor() {
12
12
  super(...arguments);
13
13
  this.label = '';
14
+ this.lazy = false;
15
+ this.loaded = false;
14
16
  this.selected = false;
15
17
  this.value = '';
16
18
  this.holdsStop = false;
@@ -21,6 +23,19 @@ let HelixHeadlessTabPanel = class HelixHeadlessTabPanel extends HelixElement {
21
23
  this.holdsStop = this.selected && focusableElements(this).length > 0;
22
24
  };
23
25
  }
26
+ load() {
27
+ if (!this.lazy || !this.selected || this.loaded) {
28
+ return;
29
+ }
30
+ this.loaded = true;
31
+ for (const template of [...this.children]) {
32
+ if (template instanceof HTMLTemplateElement) {
33
+ template.replaceWith(document.importNode(template.content, true));
34
+ }
35
+ }
36
+ this.toggleState('loaded', true);
37
+ this.dispatchEvent(new HelixLoadEvent());
38
+ }
24
39
  connectedCallback() {
25
40
  super.connectedCallback();
26
41
  this.watcher?.observe(this, {
@@ -36,6 +51,9 @@ let HelixHeadlessTabPanel = class HelixHeadlessTabPanel extends HelixElement {
36
51
  }
37
52
  updated(changed) {
38
53
  super.updated(changed);
54
+ if (changed.has('selected') || changed.has('lazy')) {
55
+ this.load();
56
+ }
39
57
  if (changed.has('selected')) {
40
58
  this.readStops();
41
59
  }
@@ -58,6 +76,9 @@ let HelixHeadlessTabPanel = class HelixHeadlessTabPanel extends HelixElement {
58
76
  __decorate([
59
77
  property({ attribute: false })
60
78
  ], HelixHeadlessTabPanel.prototype, "label", void 0);
79
+ __decorate([
80
+ property({ type: Boolean })
81
+ ], HelixHeadlessTabPanel.prototype, "lazy", void 0);
61
82
  __decorate([
62
83
  property({ type: Boolean })
63
84
  ], HelixHeadlessTabPanel.prototype, "selected", void 0);
@@ -46,9 +46,9 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
46
46
  * | Enter, Space | Selects the tab with focus, or follows its link. |
47
47
  * | Tab | Leaves the list for the selected panel. |
48
48
  *
49
- * Removing the selected tab selects the first enabled tab that is left, and
50
- * `active` says so, with `helix-activate`. Where the removed tab had focus,
51
- * focus moves to the tab now selected.
49
+ * Removing the selected tab selects the enabled tab beside it, the next one
50
+ * or else the one before, and `active` says so, with `helix-activate`. Where
51
+ * the removed tab had focus, focus moves to the tab now selected.
52
52
  *
53
53
  * **More tabs than fit.** With `scroll-controls`, a strip that scrolls shows a
54
54
  * button at each end it can scroll toward, as Web Awesome's tab group does,
@@ -74,6 +74,8 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
74
74
  * @slot - The tabs, as direct children of this element.
75
75
  * @slot indicator - A mark of your own, drawn inside the `indicator` part, where `--indicator-x`, `--indicator-y`, `--indicator-inline-size` and `--indicator-block-size` reach it.
76
76
  * @slot panels - The panels, as direct children of this element. Panels inside a wrapper are not paired with tabs, because pairing reads only the direct children.
77
+ * @slot scroll-end-icon - Replaces the glyph on the button that scrolls the strip toward its end.
78
+ * @slot scroll-start-icon - Replaces the glyph on the button that scrolls the strip toward its start.
77
79
  *
78
80
  * @event {HelixActivateEvent} helix-activate - Fired after the active tab changes, so the element already holds the new value.
79
81
  * @event {HelixBeforeActivateEvent} helix-before-activate - Fired before the active tab changes, and cancelable. Canceling it is the only way to block a change.
@@ -85,7 +87,9 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
85
87
  * and animate yourself.
86
88
  * @csspart panels - The panel container.
87
89
  * @csspart scroll-end - The button that scrolls the strip toward its end, shown under `scroll-controls` while there is more that way.
90
+ * @csspart scroll-end-mark - The glyph on that button, where none is slotted.
88
91
  * @csspart scroll-start - The button that scrolls the strip toward its start, shown under `scroll-controls` while there is more that way.
92
+ * @csspart scroll-start-mark - The glyph on that button, where none is slotted.
89
93
  * @csspart tabs - The tab list.
90
94
  */
91
95
  export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
@@ -169,6 +173,12 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
169
173
  private scrollStrip;
170
174
  /** The values of the tabs the last pairing read, to tell a removed tab from one not yet here. */
171
175
  private held;
176
+ /**
177
+ * The enabled tab nearest to where a removed one stood: the next ones
178
+ * nearest first, then the ones before it nearest first. The same order a
179
+ * dismissed tab hands focus in.
180
+ */
181
+ private beside;
172
182
  /** Pushes the current value down to whichever tab and panel own it. */
173
183
  private sync;
174
184
  /** The tab that last had focus, so its removal can hand focus on. */
@@ -205,10 +215,16 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
205
215
  private readonly handleSlotChange;
206
216
  protected updated(changed: PropertyValues): void;
207
217
  /**
208
- * One of the two scroll buttons, pointing the way it scrolls. A caret along
209
- * the line is mirrored on a right-to-left page, and one down a column is not.
218
+ * One of the two scroll buttons, with the glyph pointing the way it scrolls.
210
219
  */
211
220
  private renderScroll;
221
+ /**
222
+ * The glyph on a scroll button, behind a seam: slot fallback, so a consumer
223
+ * replaces it by slotting, and a method, so a tier above replaces it by
224
+ * overriding. A caret along the line is mirrored on a right-to-left page, and
225
+ * one down a column is not.
226
+ */
227
+ protected renderScrollIcon(toward: 1 | -1): TemplateResult;
212
228
  protected render(): TemplateResult;
213
229
  }
214
230
  declare global {
@@ -33,10 +33,14 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
33
33
  this.manual = false;
34
34
  this.scrollControls = false;
35
35
  this.handleSelection = (event) => {
36
- if (event.defaultPrevented || !this.tabs.includes(event.target)) {
36
+ if (!this.tabs.includes(event.target)) {
37
37
  return;
38
38
  }
39
- this.requestActive(event.detail.value);
39
+ queueMicrotask(() => {
40
+ if (!event.defaultPrevented) {
41
+ this.requestActive(event.detail.value);
42
+ }
43
+ });
40
44
  };
41
45
  this.more = { end: false, start: false };
42
46
  this.measure = () => {
@@ -89,12 +93,19 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
89
93
  const behavior = matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
90
94
  strip.scrollBy(down ? { behavior, top: by } : { behavior, left: isRtl(this) ? -by : by });
91
95
  }
96
+ beside(gone, tabs) {
97
+ const at = this.held.indexOf(gone);
98
+ const by = new Map(tabs.map((tab) => [tab.value, tab]));
99
+ const order = [...this.held.slice(at + 1), ...this.held.slice(0, at).reverse()];
100
+ return order.map((value) => by.get(value)).find((tab) => tab !== undefined && !tab.disabled);
101
+ }
92
102
  sync() {
93
103
  const tabs = this.tabs;
94
104
  const declared = this.active === '' ? tabs.find((tab) => tab.selected) : undefined;
95
- const value = (declared ?? this.resolveActive(tabs))?.value ?? '';
96
105
  const previous = this.active;
97
106
  const gone = this.held.includes(previous) && !tabs.some((tab) => tab.value === previous);
107
+ const value = (declared ?? (gone ? this.beside(previous, tabs) : undefined) ?? this.resolveActive(tabs))
108
+ ?.value ?? '';
98
109
  this.held = tabs.map((tab) => tab.value);
99
110
  if (gone && value !== '') {
100
111
  this.active = value;
@@ -192,19 +203,19 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
192
203
  if (!this.scrollControls) {
193
204
  return nothing;
194
205
  }
195
- const down = this.orientation === 'vertical';
196
- const icon = down ? (toward > 0 ? caretDown : caretUp) : toward > 0 ? caretRight : caretLeft;
197
- const glyph = html `<svg
198
- data-mirror=${down ? nothing : ''}
199
- aria-hidden="true"
200
- fill="currentColor"
201
- viewBox=${icon.viewBox}
202
- >${unsafeSVG(icon.content)}</svg>`;
203
206
  const shown = toward > 0 ? this.more.end : this.more.start;
204
207
  const keepFocus = (event) => event.preventDefault();
205
208
  return toward > 0
206
- ? html `<button part="scroll-end" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(1)}>${glyph}</button>`
207
- : html `<button part="scroll-start" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(-1)}>${glyph}</button>`;
209
+ ? html `<button part="scroll-end" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(1)}>${this.renderScrollIcon(1)}</button>`
210
+ : html `<button part="scroll-start" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(-1)}>${this.renderScrollIcon(-1)}</button>`;
211
+ }
212
+ renderScrollIcon(toward) {
213
+ const down = this.orientation === 'vertical';
214
+ const icon = down ? (toward > 0 ? caretDown : caretUp) : toward > 0 ? caretRight : caretLeft;
215
+ const mirror = down ? nothing : '';
216
+ return toward > 0
217
+ ? html `<slot name="scroll-end-icon"><svg part="scroll-end-mark" data-mirror=${mirror} aria-hidden="true" fill="currentColor" viewBox=${icon.viewBox}>${unsafeSVG(icon.content)}</svg></slot>`
218
+ : html `<slot name="scroll-start-icon"><svg part="scroll-start-mark" data-mirror=${mirror} aria-hidden="true" fill="currentColor" viewBox=${icon.viewBox}>${unsafeSVG(icon.content)}</svg></slot>`;
208
219
  }
209
220
  render() {
210
221
  return html `<div
@@ -16,8 +16,8 @@ declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.de
16
16
  * can: a press on its `dismiss` mark, or Delete or Backspace while the tab
17
17
  * has focus, sends `helix-dismiss`. The tab does not remove itself, because
18
18
  * your code owns the set, so remove the tab and its panel in your handler.
19
- * Focus moves to the tab beside it, and the tab set selects a tab that is
20
- * left.
19
+ * Focus moves to the tab beside it, and where the tab was selected, the tab
20
+ * set selects that same tab.
21
21
  * The tab says the key through `aria-keyshortcuts`, and the mark is for the
22
22
  * pointer, hidden from screen readers.
23
23
  *
@@ -27,11 +27,13 @@ declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.de
27
27
  * @documentation https://fusion.dev/helix/components/tabs
28
28
  *
29
29
  * @slot - The tab's label.
30
+ * @slot dismiss-icon - Replaces the glyph on the mark that closes a `dismissible` tab.
30
31
  *
31
32
  * @event {HelixDismissEvent} helix-dismiss - A `dismissible` tab was asked to close. The tab does not remove itself, so remove it and its panel in response. Cancelable.
32
33
  * @event {HelixSelectEvent} helix-select - Fired when the tab is chosen. Cancelable: canceling it stops the change and leaves the decision to you.
33
34
  *
34
35
  * @csspart dismiss - The mark a pointer presses to close a `dismissible` tab.
36
+ * @csspart dismiss-mark - The glyph on that mark, where none is slotted.
35
37
  * @csspart tab - The tab itself.
36
38
  *
37
39
  * @cssstate selected - While it is the tab selected.
@@ -53,7 +53,9 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinDismissible(MixinSele
53
53
  return nothing;
54
54
  }
55
55
  return html `<span part="dismiss" aria-hidden="true" title=${this.dismissLabel} @click=${this.handleDismissPress}
56
- ><svg fill="currentColor" viewBox=${x.viewBox} aria-hidden="true">${unsafeSVG(x.content)}</svg></span
56
+ ><slot name="dismiss-icon"
57
+ ><svg part="dismiss-mark" fill="currentColor" viewBox=${x.viewBox} aria-hidden="true">${unsafeSVG(x.content)}</svg></slot
58
+ ></span
57
59
  >`;
58
60
  }
59
61
  updated(changed) {
@@ -19,20 +19,25 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
19
19
  * `seconds`, because that is what the form
20
20
  * submits, what `min` and `max` compare against, and what sorts correctly as a
21
21
  * string. While idle, the field shows the time in the user's locale format.
22
- * Once the caret is in the field, it shows the value itself, because a
23
- * formatted time such as `2:30 PM` cannot be typed back and `Intl` has no
24
- * parser for it.
22
+ * Once the caret is in the field, it shows the value itself. You can type
23
+ * `14:30`, or a time as you write one, such as `2:30 PM`, `2.30pm`, or
24
+ * `14h30`, and the field reads it as you leave it, with AM and PM in the
25
+ * locale's words.
25
26
  *
26
27
  * Typing sends `input` on each keystroke and `change` when you leave the
27
28
  * field. The trigger and ArrowDown open the list, and so does a press into the
28
- * field; Tab into the field opens nothing. In the list, the arrow keys, typed digits and PageUp and PageDown
29
+ * field; Tab into the field opens nothing. The text field is a `combobox`
30
+ * with `aria-haspopup="dialog"`, `aria-expanded`, and `aria-controls`, since
31
+ * Down opens the list from it. In the list, the arrow keys, typed digits and PageUp and PageDown
29
32
  * move through the times, and Enter, Space or a click chooses one and closes
30
33
  * the panel. With `variant="wheels"` each wheel moves the time as you turn it,
31
34
  * the panel stays open, and Enter or Escape closes it.
32
35
  *
33
36
  * With `segmented`, the field is a row of segments in place of the text: an
34
37
  * hour, a minute, and AM or PM where the clock has them, each set with digits
35
- * or the arrow keys. Alt+Down opens the list from a segment.
38
+ * or the arrow keys. Alt+Down opens the list from a segment. A `range` is two
39
+ * rows with a dash between them, the start and the end, and `multiple` keeps
40
+ * the text.
36
41
  *
37
42
  * Text that is not a time gives the form an empty value, as a native time
38
43
  * field does, and `value` keeps the text so it can be corrected.
@@ -62,6 +67,7 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
62
67
  * @aliases TimeField, InputTime
63
68
  * @documentation https://fusion.dev/helix/components/time-input
64
69
  *
70
+ * @slot clear-icon - Replaces the cross on the clear button, under `clearable`.
65
71
  * @slot end - Content after the time, before the trigger, such as a clear button.
66
72
  * @slot feedback - The message that says what is wrong with the value.
67
73
  * @slot hint - Help under the field.
@@ -75,9 +81,11 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
75
81
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the time changes. Cancelable: canceling it stops the change and leaves the decision to you.
76
82
  * @event {HelixBeforeCloseEvent} helix-before-close - Fired before the list closes. Cancelable: canceling it keeps the list open and leaves the decision to you.
77
83
  * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the list opens. Cancelable: canceling it keeps the list closed and leaves the decision to you.
84
+ * @event {HelixClearEvent} helix-clear - Fired after the clear button has emptied the field, after `input` and `change`.
78
85
  * @event {Event} input - Fired after the value changes.
79
86
  * @event {ToggleEvent} toggle - Fired after the list opens or closes, with `newState` and `oldState`.
80
87
  *
88
+ * @csspart clear - The button that empties the field, under `clearable`, while it holds a time.
81
89
  * @csspart control - The text input.
82
90
  * @csspart feedback - The message that says what is wrong with the value.
83
91
  * @csspart field - The box that holds the text and the trigger.
@@ -123,6 +131,7 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
123
131
  * @csspart picker-option-item-selected - The row of a chosen time: the one time, or either end of a range.
124
132
  * @csspart picker-option-label - The option's label.
125
133
  * @csspart picker-option-text - The label and the caption, as one column.
134
+ * @csspart range-separator - The dash between a range's two rows of segments, in `segmented`, beside `literal`.
126
135
  * @csspart rest - The count of times held beyond the ones shown. A `<button>` with `expandable`, and a `<span>` without it.
127
136
  * @csspart segment - One part of the time, an hour, a minute, a second or AM and PM, in `segmented`.
128
137
  * @csspart segment-placeholder - A segment with nothing in it yet. Applied with `segment`.
@@ -139,6 +148,7 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
139
148
  * @csspart trigger - The control that opens the list.
140
149
  * @csspart trigger-icon - The icon on the button that opens the clock.
141
150
  *
151
+ * @cssstate blank - The field holds no time.
142
152
  * @cssstate invalid - The control has an error to show, from the browser's validation or from your `feedback`.
143
153
  * @cssstate open - While the clock is open.
144
154
  */
@@ -156,6 +166,14 @@ export declare class HelixHeadlessTimeInput extends HelixHeadlessTimeInput_base
156
166
  * lets autofill work at all.
157
167
  */
158
168
  autocomplete: string;
169
+ /**
170
+ * Whether the field shows a button that empties it, as Input's `clearable`
171
+ * does. Shown only while the field holds something to clear and is neither
172
+ * disabled nor read-only.
173
+ */
174
+ clearable: boolean;
175
+ /** The accessible name of the clear button, which shows only an icon. */
176
+ clearLabel: string;
159
177
  /** Typed, so a keystroke is announced as `input` alone. See `announcesInput`. */
160
178
  protected readonly announcesInput = true;
161
179
  /**
@@ -256,13 +274,20 @@ export declare class HelixHeadlessTimeInput extends HelixHeadlessTimeInput_base
256
274
  * The message shown when `disable-times` bars a time.
257
275
  */
258
276
  unavailableMessage: string;
277
+ /**
278
+ * The message for text that is no time, where `bad-input-message` is not
279
+ * set. `{mode}` is `single`, `range`, or `multiple`, and `{shape}` is `HH:mm`, or `HH:mm:ss` with `seconds`.
280
+ */
281
+ unreadableMessage: string;
259
282
  /** Every time in the value, which may hold more than one. */
260
283
  private get held();
261
284
  /**
262
- * Segments hold one time, so `range` and `multiple` keep the line of text,
263
- * whose separators and tags are how those modes write several.
285
+ * A row of segments holds one time, and `range` takes two rows with a dash
286
+ * between them, as React Aria's range fields do. `multiple` keeps the line
287
+ * of text, whose tags are how it writes several.
264
288
  */
265
289
  protected get segmentable(): boolean;
290
+ protected get segmentGroups(): number;
266
291
  protected get segmentedKind(): 'time';
267
292
  protected get segmentLocale(): string;
268
293
  protected get segmentOptions(): {
@@ -287,6 +312,18 @@ export declare class HelixHeadlessTimeInput extends HelixHeadlessTimeInput_base
287
312
  private get shownTimes();
288
313
  /** How many are held beyond the ones shown. */
289
314
  private get restCount();
315
+ /**
316
+ * A time as the reader wrote it, as the value writes one: `HH:mm` as it
317
+ * is, and `2:30 PM`, `2.30pm`, `14h30`, or `1430` read back, with the half
318
+ * of the day in the locale's words. Empty where it is no time.
319
+ */
320
+ private read;
321
+ /**
322
+ * The text the reader leaves, read: a time written as a reader writes one
323
+ * becomes the value as the field commits. Not on each key, where `2:3` is
324
+ * a time a moment before it is `2:30`.
325
+ */
326
+ private readonly handleTextCommit;
290
327
  /** Adds what was typed to the set, or takes the last one back out. */
291
328
  private readonly handleDraftKey;
292
329
  /** Takes one time back out of the set. */
@@ -368,8 +405,19 @@ export declare class HelixHeadlessTimeInput extends HelixHeadlessTimeInput_base
368
405
  * consumer gets an inert unknown element and no mark whatsoever.
369
406
  */
370
407
  protected renderTrigger(): TemplateResult;
408
+ /** Whether there is a clear button on screen right now. */
409
+ private get showsClear();
410
+ /**
411
+ * Empties the field, through `requestValue` so a consumer who owns the
412
+ * value decides, then says `change` and `helix-clear` and puts focus back
413
+ * in the field, the button having gone.
414
+ */
415
+ private readonly handleClear;
416
+ /** The cross on the clear button, behind a seam a tier can move. */
417
+ protected renderClearIcon(): TemplateResult;
371
418
  /** The line of text the times are typed into, where the field shows no segments. */
372
419
  private renderControl;
420
+ protected willUpdate(changed: PropertyValues): void;
373
421
  protected render(): TemplateResult;
374
422
  /**
375
423
  * The value as a `Date` on January 1, 1970 UTC, or `null` while there is no