@fusion.dev/helix-headless 0.159.0 → 0.161.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 (61) hide show
  1. package/custom-elements.json +2980 -189
  2. package/dist/canvas/root/canvas-root.d.ts +59 -2
  3. package/dist/canvas/root/canvas-root.js +123 -8
  4. package/dist/chat/message/chat-message.d.ts +8 -0
  5. package/dist/chat/message/chat-message.js +11 -0
  6. package/dist/chat/root/chat-root.d.ts +47 -1
  7. package/dist/chat/root/chat-root.js +130 -4
  8. package/dist/code-block/lines.d.ts +12 -0
  9. package/dist/code-block/lines.js +41 -0
  10. package/dist/code-block/root/code-block-root.d.ts +88 -5
  11. package/dist/code-block/root/code-block-root.js +154 -10
  12. package/dist/date-input/root/date-input-root.d.ts +108 -19
  13. package/dist/date-input/root/date-input-root.js +112 -13
  14. package/dist/date-picker/root/date-picker-root.d.ts +81 -16
  15. package/dist/date-picker/root/date-picker-root.js +140 -36
  16. package/dist/element-events.generated.js +6 -0
  17. package/dist/format-date/fields.js +1 -0
  18. package/dist/format-date/options.d.ts +3 -1
  19. package/dist/format-date/options.js +4 -1
  20. package/dist/format-date/root/format-date-root.d.ts +27 -11
  21. package/dist/format-date/root/format-date-root.js +6 -0
  22. package/dist/listbox/root/listbox-root.js +5 -0
  23. package/dist/notifications/notification/notification.d.ts +1 -0
  24. package/dist/notifications/notification/notification.js +2 -1
  25. package/dist/slider/root/slider-root.d.ts +54 -3
  26. package/dist/slider/root/slider-root.js +124 -2
  27. package/dist/splitter/root/splitter-root.d.ts +15 -0
  28. package/dist/splitter/root/splitter-root.js +16 -2
  29. package/dist/table/grid-controller.d.ts +7 -1
  30. package/dist/table/grid-controller.js +9 -1
  31. package/dist/table/root/table-root.d.ts +122 -0
  32. package/dist/table/root/table-root.js +293 -17
  33. package/dist/table/row/table-row.d.ts +41 -0
  34. package/dist/table/row/table-row.js +56 -2
  35. package/dist/time-input/root/time-input-root.d.ts +55 -7
  36. package/dist/time-input/root/time-input-root.js +70 -10
  37. package/dist/time-picker/root/time-picker-root.d.ts +27 -5
  38. package/dist/time-picker/root/time-picker-root.js +52 -13
  39. package/dist/timeline/item/timeline-item.d.ts +47 -10
  40. package/dist/timeline/item/timeline-item.js +36 -4
  41. package/dist/toggle/group/toggle-group.d.ts +18 -1
  42. package/dist/toggle/group/toggle-group.js +8 -2
  43. package/dist/toggle/root/toggle-root.d.ts +2 -2
  44. package/dist/toggle/root/toggle-root.js +2 -1
  45. package/dist/video-playlist/item/video-playlist-item.d.ts +11 -0
  46. package/dist/video-playlist/item/video-playlist-item.js +5 -0
  47. package/dist/video-playlist/menu/video-playlist-menu.d.ts +6 -0
  48. package/dist/video-playlist/menu/video-playlist-menu.js +11 -0
  49. package/dist/video-playlist/root/video-playlist-root.d.ts +59 -5
  50. package/dist/video-playlist/root/video-playlist-root.js +82 -10
  51. package/dist/virtual-list/fill.d.ts +6 -1
  52. package/dist/virtual-list/fill.js +1 -1
  53. package/dist/virtual-list/keys.d.ts +8 -0
  54. package/dist/virtual-list/keys.js +20 -0
  55. package/dist/virtual-list/root/virtual-list-root.d.ts +98 -13
  56. package/dist/virtual-list/root/virtual-list-root.js +195 -20
  57. package/lint.json +1 -1
  58. package/package.json +5 -5
  59. package/types/jsx.d.ts +1596 -224
  60. package/types/vue.d.ts +404 -49
  61. package/types/words.d.ts +151 -0
@@ -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
  import { customElement, followLink, HelixElement, hostAria, pressedOwnControl, } from '@fusion.dev/helix-core';
8
- import { dotsSixVertical } from '@fusion.dev/helix-icons';
8
+ import { caretRight, dotsSixVertical } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
@@ -17,6 +17,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
17
17
  this.isTableRow = true;
18
18
  this.disabled = false;
19
19
  this.href = '';
20
+ this.parent = '';
20
21
  this.rel = '';
21
22
  this.target = '';
22
23
  this.value = '';
@@ -29,6 +30,12 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
29
30
  this.move = undefined;
30
31
  this.gridCell = false;
31
32
  this.selectStop = false;
33
+ this.level = 0;
34
+ this.branch = false;
35
+ this.open = false;
36
+ this.expandLabel = '';
37
+ this.expandStop = false;
38
+ this.expandWith = undefined;
32
39
  this.columnRules = undefined;
33
40
  this.handleClick = (event) => {
34
41
  if (this.disabled || this.href === '' || event.button !== 0 || event.defaultPrevented) {
@@ -70,6 +77,10 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
70
77
  hostAria(this.internals, { role: 'row' });
71
78
  this.internals.ariaSelected = this.gridCell && this.selectable ? String(this.selected) : null;
72
79
  this.internals.ariaDisabled = this.disabled ? 'true' : null;
80
+ const tree = this.gridCell && this.level > 0;
81
+ this.internals.ariaLevel = tree ? String(this.level) : null;
82
+ this.internals.ariaExpanded = tree && this.branch ? String(this.open) : null;
83
+ this.toggleState('expanded', this.branch && this.open);
73
84
  this.toggleState('selected', this.selectable && this.selected);
74
85
  this.toggleState('link', this.href !== '' && !this.disabled);
75
86
  this.toggleState('dragging', this.move === 'dragging');
@@ -125,8 +136,30 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
125
136
  ><svg aria-hidden="true" fill="currentColor" part="grip-mark" viewBox=${dotsSixVertical.viewBox}>${unsafeSVG(dotsSixVertical.content)}</svg></button
126
137
  ></span>`;
127
138
  }
139
+ renderExpand() {
140
+ if (this.level === 0) {
141
+ return nothing;
142
+ }
143
+ const grid = this.gridCell;
144
+ return html `<span
145
+ part="expand"
146
+ role=${grid ? 'gridcell' : 'cell'}
147
+ style="--table-row-level: ${this.level}"
148
+ tabindex=${grid ? (this.expandStop ? '0' : '-1') : nothing}
149
+ >${this.branch
150
+ ? html `<button
151
+ part="expand-button"
152
+ type="button"
153
+ tabindex=${grid ? '-1' : nothing}
154
+ aria-label=${this.expandLabel}
155
+ aria-expanded=${this.open ? 'true' : 'false'}
156
+ @click=${() => this.expandWith?.()}
157
+ ><svg aria-hidden="true" data-mirror fill="currentColor" part="expand-mark" viewBox=${caretRight.viewBox}>${unsafeSVG(caretRight.content)}</svg></button>`
158
+ : nothing}</span
159
+ >`;
160
+ }
128
161
  render() {
129
- return html `${this.renderGrip()}${this.renderSelect()}<slot @slotchange=${this.handleSlotChange}></slot>`;
162
+ return html `${this.renderGrip()}${this.renderSelect()}${this.renderExpand()}<slot @slotchange=${this.handleSlotChange}></slot>`;
130
163
  }
131
164
  };
132
165
  __decorate([
@@ -135,6 +168,9 @@ __decorate([
135
168
  __decorate([
136
169
  property()
137
170
  ], HelixHeadlessTableRow.prototype, "href", void 0);
171
+ __decorate([
172
+ property()
173
+ ], HelixHeadlessTableRow.prototype, "parent", void 0);
138
174
  __decorate([
139
175
  property()
140
176
  ], HelixHeadlessTableRow.prototype, "rel", void 0);
@@ -171,6 +207,24 @@ __decorate([
171
207
  __decorate([
172
208
  state()
173
209
  ], HelixHeadlessTableRow.prototype, "selectStop", void 0);
210
+ __decorate([
211
+ state()
212
+ ], HelixHeadlessTableRow.prototype, "level", void 0);
213
+ __decorate([
214
+ state()
215
+ ], HelixHeadlessTableRow.prototype, "branch", void 0);
216
+ __decorate([
217
+ state()
218
+ ], HelixHeadlessTableRow.prototype, "open", void 0);
219
+ __decorate([
220
+ state()
221
+ ], HelixHeadlessTableRow.prototype, "expandLabel", void 0);
222
+ __decorate([
223
+ state()
224
+ ], HelixHeadlessTableRow.prototype, "expandStop", void 0);
225
+ __decorate([
226
+ state()
227
+ ], HelixHeadlessTableRow.prototype, "expandWith", void 0);
174
228
  HelixHeadlessTableRow = __decorate([
175
229
  customElement('helix-headless-table-row')
176
230
  ], HelixHeadlessTableRow);
@@ -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
@@ -4,8 +4,8 @@ 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 { AriaForwardController, valueAsTime as asTime, clockLocale, customElement, formatWords, timeAsValue as fromTime, HelixFormElement, handedLocale, isClockValue, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerEditing, MixinPickerField, MixinSegmentedField, MixinTokens, timeFault, words, wordsLocale, writeClock, } from '@fusion.dev/helix-core';
8
- import { clock } from '@fusion.dev/helix-icons';
7
+ import { AriaForwardController, valueAsTime as asTime, clockLocale, customElement, drawIcon, formatWords, timeAsValue as fromTime, HelixClearEvent, HelixFormElement, handedLocale, isClockValue, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerEditing, MixinPickerField, MixinSegmentedField, MixinTokens, readWrittenTime, timeFault, words, wordsLocale, writeClock, } from '@fusion.dev/helix-core';
8
+ import { clock, x } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
@@ -19,6 +19,8 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
19
19
  target: () => this.partNamed('control') ?? this.segmentGroup,
20
20
  });
21
21
  this.autocomplete = '';
22
+ this.clearable = false;
23
+ this.clearLabel = 'Clear';
22
24
  this.announcesInput = true;
23
25
  this.disableTimes = '';
24
26
  this.locale = '';
@@ -34,16 +36,28 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
34
36
  this.timeStyle = 'local';
35
37
  this.triggerLabel = 'Choose a time';
36
38
  this.unavailableMessage = 'Choose a time that is available.';
39
+ this.unreadableMessage = '{mode, select, single {Enter a time as {shape}.} other {Enter times as {shape}, separated by commas.}}';
37
40
  this.draft = '';
41
+ this.handleTextCommit = (event) => {
42
+ if (this.mode !== 'multiple') {
43
+ const held = this.held;
44
+ const read = held.map((one) => this.read(one));
45
+ if (held.length > 0 && !held.every((one) => this.isTime(one)) && read.every(Boolean)) {
46
+ this.requestValue(read.join(','));
47
+ }
48
+ }
49
+ this.handleCommit(event);
50
+ };
38
51
  this.handleDraftKey = (event) => {
39
52
  if (this.mode !== 'multiple') {
40
53
  return;
41
54
  }
42
55
  const held = this.held;
43
- if (event.key === 'Enter' && this.isTime(this.draft)) {
56
+ const draft = this.read(this.draft.trim());
57
+ if (event.key === 'Enter' && draft !== '') {
44
58
  event.preventDefault();
45
- if (!held.includes(this.draft)) {
46
- this.requestPicked([...held, this.draft].sort().join(','));
59
+ if (!held.includes(draft)) {
60
+ this.requestPicked([...held, draft].sort().join(','));
47
61
  }
48
62
  this.draft = '';
49
63
  }
@@ -91,6 +105,13 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
91
105
  this.returnFocus();
92
106
  }
93
107
  };
108
+ this.handleClear = () => {
109
+ if (this.value !== '' && this.requestValue('')) {
110
+ this.announceChange();
111
+ this.dispatchEvent(new HelixClearEvent());
112
+ }
113
+ void this.updateComplete.then(() => this.returnFocus());
114
+ };
94
115
  }
95
116
  static { this.words = 'helix-time-input'; }
96
117
  get language() {
@@ -104,7 +125,10 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
104
125
  .filter(Boolean);
105
126
  }
106
127
  get segmentable() {
107
- return this.mode === undefined;
128
+ return this.mode !== 'multiple';
129
+ }
130
+ get segmentGroups() {
131
+ return this.mode === 'range' ? 2 : 1;
108
132
  }
109
133
  get segmentedKind() {
110
134
  return 'time';
@@ -141,6 +165,9 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
141
165
  get restCount() {
142
166
  return Math.max(0, this.held.length - this.shownTimes.length);
143
167
  }
168
+ read(text) {
169
+ return this.isTime(text) ? text : readWrittenTime(text, this.language, this.seconds);
170
+ }
144
171
  get picked() {
145
172
  const held = this.held;
146
173
  return held.length > 0 && held.every((one) => this.isTime(one)) ? held.join(',') : '';
@@ -157,9 +184,7 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
157
184
  const shape = this.seconds ? 'HH:mm:ss' : 'HH:mm';
158
185
  if (held.length > 0 && !held.every((one) => this.isTime(one))) {
159
186
  return (this.badInputMessage ||
160
- (this.mode === undefined
161
- ? `Enter a time as ${shape}.`
162
- : `Enter times as ${shape}, separated by commas.`));
187
+ formatWords(this, 'unreadableMessage', { mode: this.mode ?? 'single', shape }));
163
188
  }
164
189
  const rules = { disableTimes: this.disableTimes, max: this.max, min: this.min };
165
190
  const faults = held.map((one) => timeFault(one, rules));
@@ -249,6 +274,12 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
249
274
  ${unsafeSVG(clock.content)}
250
275
  </svg></slot>`;
251
276
  }
277
+ get showsClear() {
278
+ return this.clearable && this.value !== '' && !this.isDisabled && !this.readOnly;
279
+ }
280
+ renderClearIcon() {
281
+ return html `<slot name="clear-icon">${drawIcon(x)}</slot>`;
282
+ }
252
283
  renderControl() {
253
284
  return html `<input
254
285
  part="control"
@@ -257,6 +288,10 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
257
288
  .value=${this.display}
258
289
  name=${this.name || nothing}
259
290
  placeholder=${this.mode === 'multiple' && this.held.length > 0 ? nothing : this.placeholder || nothing}
291
+ role="combobox"
292
+ aria-controls="surface"
293
+ aria-expanded=${this.open ? 'true' : 'false'}
294
+ aria-haspopup="dialog"
260
295
  autocomplete=${this.autocomplete || nothing}
261
296
  aria-describedby=${this.describedBy ?? nothing}
262
297
  aria-invalid=${this.invalidity ?? nothing}
@@ -264,13 +299,17 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
264
299
  ?readonly=${this.readOnly}
265
300
  ?required=${this.required}
266
301
  @blur=${this.handleBlur}
267
- @change=${this.handleCommit}
302
+ @change=${this.handleTextCommit}
268
303
  @focus=${this.handleFocus}
269
304
  @input=${this.handleInput}
270
305
  @keydown=${this.handleDraftKey}
271
306
  @keypress=${this.handleKeypress}
272
307
  />`;
273
308
  }
309
+ willUpdate(changed) {
310
+ super.willUpdate(changed);
311
+ this.toggleState('blank', this.value === '');
312
+ }
274
313
  render() {
275
314
  return html `${this.renderLabel()}
276
315
  <div part="field" @keydown=${this.handleKeydown}>
@@ -280,6 +319,16 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
280
319
  : nothing}
281
320
  ${this.segmentsShown ? this.renderSegments() : this.renderControl()}
282
321
  <slot name="end"></slot>
322
+ ${this.showsClear
323
+ ? html `<button
324
+ part="clear"
325
+ type="button"
326
+ aria-label=${this.clearLabel}
327
+ @click=${this.handleClear}
328
+ >
329
+ ${this.renderClearIcon()}
330
+ </button>`
331
+ : nothing}
283
332
  <button
284
333
  part="trigger"
285
334
  type="button"
@@ -327,6 +376,13 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
327
376
  __decorate([
328
377
  property()
329
378
  ], HelixHeadlessTimeInput.prototype, "autocomplete", void 0);
379
+ __decorate([
380
+ property({ type: Boolean })
381
+ ], HelixHeadlessTimeInput.prototype, "clearable", void 0);
382
+ __decorate([
383
+ words,
384
+ property({ attribute: 'clear-label' })
385
+ ], HelixHeadlessTimeInput.prototype, "clearLabel", void 0);
330
386
  __decorate([
331
387
  property({ attribute: 'disable-times' })
332
388
  ], HelixHeadlessTimeInput.prototype, "disableTimes", void 0);
@@ -382,6 +438,10 @@ __decorate([
382
438
  words,
383
439
  property({ attribute: 'unavailable-message' })
384
440
  ], HelixHeadlessTimeInput.prototype, "unavailableMessage", void 0);
441
+ __decorate([
442
+ words,
443
+ property({ attribute: 'unreadable-message' })
444
+ ], HelixHeadlessTimeInput.prototype, "unreadableMessage", void 0);
385
445
  __decorate([
386
446
  state()
387
447
  ], HelixHeadlessTimeInput.prototype, "draft", void 0);
@@ -1,4 +1,4 @@
1
- import { HelixFormElement, type HourCycle } from '@fusion.dev/helix-core';
1
+ import { HelixFormElement, type HourCycle, type PickerFailure } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../listbox/index.js';
4
4
  import '../../option/index.js';
@@ -51,7 +51,10 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
51
51
  * host's `aria-label` or its `aria-labelledby`. A development build warns
52
52
  * where all four are empty. The list is described by the hint, or by the
53
53
  * message in `feedback` in its place, and carries `aria-multiselectable` with
54
- * a `mode` and `aria-invalid` while `feedback` says what is wrong. The wheels
54
+ * a `mode`. It carries `aria-invalid` while `feedback` says what is wrong,
55
+ * while it holds a time outside `min` and `max` or one the picker does not
56
+ * offer, and, for a `required` picker with no time, once the reader has
57
+ * chosen one or a form has refused the picker. The wheels
55
58
  * are a `group` with that name, holding one `listbox` per wheel, named "Hour",
56
59
  * "Minute", "Second" and "AM/PM" in the page's language, and each described
57
60
  * the same way.
@@ -100,7 +103,7 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
100
103
  * @csspart list-loading - The text shown while the options load.
101
104
  * @csspart list-search - The region that holds the filter field.
102
105
  * @csspart list-search-field - The filter field.
103
- * @csspart option - One time in the list. It carries `data-between` inside a range.
106
+ * @csspart option - One time in the list. It carries `data-between` inside a range, and inside the run to the time under the pointer while a range waits for its second end.
104
107
  * @csspart option-between - A time inside a chosen range, excluding its ends, beside `option`.
105
108
  * @csspart option-description - The caption under the time's label, when there is one.
106
109
  * @csspart option-item - The row.
@@ -110,7 +113,8 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
110
113
  * @csspart option-text - The label and the caption, as one column.
111
114
  *
112
115
  * @cssstate blank - While no time is chosen.
113
- * @cssstate invalid - While `invalid` is set or `feedback` says what is wrong.
116
+ * @cssstate invalid - While `invalid` is set, `feedback` says what is wrong, or the list carries `aria-invalid`.
117
+ * @cssstate range-pending - While a `range` holds its first time and waits for the second.
114
118
  */
115
119
  export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_base {
116
120
  /**
@@ -318,7 +322,18 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
318
322
  * sequence.
319
323
  */
320
324
  private readonly handleChange;
321
- /** Whether a time falls inside a range, which is what gets banded. */
325
+ /**
326
+ * The time under the pointer while a range holds one end, which the list
327
+ * bands the range to before it is chosen, as Date Picker does.
328
+ */
329
+ private previewed;
330
+ private readonly handleListHover;
331
+ private readonly handleListLeave;
332
+ /**
333
+ * Whether a time falls inside a range, which is what gets banded: the
334
+ * chosen range, or while one end waits, the run to the time under the
335
+ * pointer.
336
+ */
322
337
  private between;
323
338
  /** The readings the wheels show, from the value or from `min`. */
324
339
  private get parts();
@@ -463,6 +478,13 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
463
478
  * browser points its bubble at.
464
479
  */
465
480
  private reportRequired;
481
+ /**
482
+ * What is wrong with the answer, as validity flags and a message, or nothing.
483
+ * `reportRequired` hands it to the form, and `failure` to the mark.
484
+ */
485
+ private get problem();
486
+ /** A missing time waits for the reader; a refused one is marked at once. */
487
+ protected get failure(): PickerFailure | undefined;
466
488
  /**
467
489
  * Reports the times to the form, one entry each.
468
490
  *
@@ -53,6 +53,19 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
53
53
  const added = next.find((one) => !before.has(one));
54
54
  this.requestValue(added ?? '');
55
55
  };
56
+ this.previewed = '';
57
+ this.handleListHover = (event) => {
58
+ const option = event
59
+ .composedPath()
60
+ .find((one) => one instanceof HTMLElement && one.hasAttribute('value'));
61
+ const at = option?.getAttribute('value') ?? '';
62
+ if (this.mode === 'range' && this.held.length === 1 && at !== this.previewed) {
63
+ this.previewed = at;
64
+ }
65
+ };
66
+ this.handleListLeave = () => {
67
+ this.previewed = '';
68
+ };
56
69
  this.moveColumn = (part, to) => {
57
70
  if (this.isDisabled) {
58
71
  return;
@@ -146,8 +159,11 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
146
159
  if (this.mode !== 'range') {
147
160
  return false;
148
161
  }
149
- const [from, to] = this.held;
150
- return from !== undefined && to !== undefined && at > from && at < to;
162
+ const [from, to = this.previewed] = this.held;
163
+ if (from === undefined || to === '' || this.held.length > 2) {
164
+ return false;
165
+ }
166
+ return at > (from < to ? from : to) && at < (from < to ? to : from);
151
167
  }
152
168
  get parts() {
153
169
  const at = valueAsSeconds(this.value) ?? valueAsSeconds(this.min) ?? 0;
@@ -218,27 +234,41 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
218
234
  }
219
235
  }
220
236
  reportRequired() {
221
- const anchor = this.partNamed('list') ?? undefined;
222
- if (this.required && this.value === '') {
223
- this.internals.setValidity({ valueMissing: true }, this.valueMissingMessage, anchor);
237
+ const problem = this.problem;
238
+ if (problem) {
239
+ this.internals.setValidity(problem[0], problem[1], this.partNamed('list') ?? undefined);
224
240
  return;
225
241
  }
242
+ this.internals.setValidity({});
243
+ }
244
+ get problem() {
245
+ if (this.required && this.value === '') {
246
+ return [{ valueMissing: true }, this.valueMissingMessage];
247
+ }
226
248
  const held = this.held;
227
249
  if (held.length > 0 && held.some((one) => valueAsSeconds(one) === null)) {
228
- this.internals.setValidity({ badInput: true }, formatWords(this, 'badInputMessage', {
229
- mode: this.mode ?? 'single',
230
- shape: this.seconds ? 'HH:mm:ss' : 'HH:mm',
231
- }), anchor);
232
- return;
250
+ return [
251
+ { badInput: true },
252
+ formatWords(this, 'badInputMessage', {
253
+ mode: this.mode ?? 'single',
254
+ shape: this.seconds ? 'HH:mm:ss' : 'HH:mm',
255
+ }),
256
+ ];
233
257
  }
234
258
  for (const one of held) {
235
259
  const fault = this.fault(one);
236
260
  if (fault) {
237
- this.internals.setValidity({ [fault[0]]: true }, fault[1], anchor);
238
- return;
261
+ return [{ [fault[0]]: true }, fault[1]];
239
262
  }
240
263
  }
241
- this.internals.setValidity({});
264
+ return undefined;
265
+ }
266
+ get failure() {
267
+ const problem = this.problem;
268
+ if (!problem) {
269
+ return undefined;
270
+ }
271
+ return problem[0].valueMissing ? 'missing' : 'refused';
242
272
  }
243
273
  report() {
244
274
  if (this.isDisabled) {
@@ -290,7 +320,11 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
290
320
  willUpdate(changed) {
291
321
  super.willUpdate(changed);
292
322
  this.report();
323
+ if (changed.has('value')) {
324
+ this.previewed = '';
325
+ }
293
326
  this.toggleState('blank', this.value === '');
327
+ this.toggleState('range-pending', this.mode === 'range' && this.held.length === 1);
294
328
  }
295
329
  firstUpdated(changed) {
296
330
  super.firstUpdated(changed);
@@ -386,6 +420,8 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
386
420
  .value=${this.value}
387
421
  @helix-before-change=${this.handleChange}
388
422
  @input=${this.handleListInput}
423
+ @pointerleave=${this.handleListLeave}
424
+ @pointerover=${this.handleListHover}
389
425
  >
390
426
  ${this.times.map((seconds) => {
391
427
  const at = this.valueFor(seconds);
@@ -481,6 +517,9 @@ __decorate([
481
517
  words,
482
518
  property({ attribute: 'range-underflow-message' })
483
519
  ], HelixHeadlessTimePicker.prototype, "rangeUnderflowMessage", void 0);
520
+ __decorate([
521
+ state()
522
+ ], HelixHeadlessTimePicker.prototype, "previewed", void 0);
484
523
  __decorate([
485
524
  property({ attribute: 'readonly', reflect: true, type: Boolean })
486
525
  ], HelixHeadlessTimePicker.prototype, "readOnly", void 0);