@fusion.dev/helix-headless 0.177.0 → 0.177.2

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.
@@ -173,6 +173,7 @@ let HelixHeadlessButton = class HelixHeadlessButton extends HelixFormElement {
173
173
  this.watchForm(null);
174
174
  }
175
175
  formAssociatedCallback(form) {
176
+ super.formAssociatedCallback(form);
176
177
  this.watchForm(form);
177
178
  }
178
179
  render() {
@@ -49,7 +49,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
49
49
  *
50
50
  * @event {HelixSendEvent} helix-send - Fired when the user sends their turn, and cancelable.
51
51
  * @event {HelixTypingEvent} helix-typing - The user started or stopped writing, with `typing` saying which, as the composer's `typing` does.
52
- * @event {Event} input - The user changed the text. Read it from `value`, for a typing indicator or a saved draft. Sent on the composer and does not bubble.
52
+ * @event {Event} input - The user changed the text. Read it from `value`, for a typing indicator or a saved draft. Bubbles and is composed, as a native field's is.
53
53
  *
54
54
  * @csspart composer - The whole composer.
55
55
  * @csspart end - The box around the `end` slot.
@@ -41,7 +41,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
41
41
  event.stopPropagation();
42
42
  this.value = event.target.value;
43
43
  this.edited = true;
44
- this.dispatchEvent(new Event('input'));
44
+ this.announceInput();
45
45
  this.markTyping(this.value !== '');
46
46
  };
47
47
  this.handleSend = () => {
@@ -306,19 +306,6 @@ export declare class HelixHeadlessColorInput extends HelixHeadlessColorInput_bas
306
306
  */
307
307
  protected sync(): void;
308
308
  protected updated(changed: PropertyValues): void;
309
- /** Whether the trigger's press opened the panel that is open now. */
310
- private pressedOpen;
311
- /**
312
- * The trigger's press, which notes that it opened the panel, so Escape hands
313
- * focus back to the trigger.
314
- */
315
- private readonly handlePress;
316
- /**
317
- * Hands focus back to the control that opened the panel: the trigger after
318
- * its press, which is the APG date picker dialog's rule, and the text after
319
- * ArrowDown from it.
320
- */
321
- protected returnFocus(): void;
322
309
  /**
323
310
  * The picker the field opens, behind a seam the styled tier can move.
324
311
  *
@@ -55,12 +55,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
55
55
  this.handleSampleEnd = () => {
56
56
  this.requestOpen(true);
57
57
  };
58
- this.pressedOpen = false;
59
- this.handlePress = () => {
60
- const opening = !this.open;
61
- this.handleTriggerClick();
62
- this.pressedOpen = opening && this.open;
63
- };
64
58
  }
65
59
  static { this.words = 'helix-color-input'; }
66
60
  requestValue(next) {
@@ -123,18 +117,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
123
117
  if (changed.has('value') && parseColor(this.value)) {
124
118
  this.settled = this.value;
125
119
  }
126
- if (changed.has('open') && !this.open) {
127
- this.pressedOpen = false;
128
- }
129
- }
130
- returnFocus() {
131
- const trigger = this.pressedOpen ? this.partNamed('trigger') : null;
132
- this.pressedOpen = false;
133
- if (trigger) {
134
- trigger.focus();
135
- return;
136
- }
137
- super.returnFocus();
138
120
  }
139
121
  renderPicker() {
140
122
  return html `<${this.tag('color-picker')}
@@ -222,7 +204,7 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
222
204
  : 'swatch-value'
223
205
  : nothing}
224
206
  ?disabled=${this.isDisabled}
225
- @click=${this.handlePress}
207
+ @click=${this.handleTriggerClick}
226
208
  >
227
209
  <span
228
210
  part="preview"
@@ -633,6 +633,16 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
633
633
  * is redrawn though the value it settles on is the one drawn before.
634
634
  */
635
635
  protected renderFields(color: Color): TemplateResult;
636
+ /**
637
+ * A channel's name, after the picker's own where the picker has one.
638
+ *
639
+ * **Every channel the same way**, as React Aria's color area names its own:
640
+ * "Brand, Saturation" says which picker on a page with two. The square's
641
+ * axes and the wheel took the picker's name, and the hue and opacity ramps
642
+ * and the number fields did not, so two pickers on a page gave two sliders
643
+ * named "Hue", measured in Chromium's tree.
644
+ */
645
+ protected channelName(channel: string): string;
636
646
  /**
637
647
  * One of the ramps under the square.
638
648
  *
@@ -407,11 +407,15 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
407
407
  }
408
408
  renderFields(color) {
409
409
  return html `<div part="fields">
410
- ${channelFields(this.value, color, this.format ?? 'hex', this.alpha).map((field) => html `<label part="field">
411
- <span part="field-label">${this.channelWord(field.channel)}</span>
410
+ ${channelFields(this.value, color, this.format ?? 'hex', this.alpha).map((field) => {
411
+ const word = this.channelWord(field.channel);
412
+ const name = this.channelName(word);
413
+ return html `<label part="field">
414
+ <span part="field-label">${word}</span>
412
415
  <input
413
416
  part="field-input"
414
417
  type="number"
418
+ aria-label=${name === word ? nothing : name}
415
419
  inputmode=${field.step < 1 ? 'decimal' : 'numeric'}
416
420
  min=${field.min}
417
421
  max=${field.max}
@@ -421,14 +425,19 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
421
425
  ?readonly=${this.readOnly}
422
426
  @input=${(event) => this.handleField(field.channel, event)}
423
427
  @change=${(event) => {
424
- event.stopPropagation();
425
- this.settle();
426
- this.requestUpdate();
427
- }}
428
+ event.stopPropagation();
429
+ this.settle();
430
+ this.requestUpdate();
431
+ }}
428
432
  />
429
- </label>`)}
433
+ </label>`;
434
+ })}
430
435
  </div>`;
431
436
  }
437
+ channelName(channel) {
438
+ const owner = this.getAttribute('aria-label')?.trim() || this.named;
439
+ return owner ? `${owner}, ${channel}` : channel;
440
+ }
432
441
  renderChannel(channel, label, max, value, spoken = '') {
433
442
  return html `<${this.tag('slider')}
434
443
  part="channel"
@@ -439,7 +448,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
439
448
  tooltips: channel-tooltips"
440
449
  data-channel=${channel}
441
450
  aria-invalid=${this.marked ? 'true' : nothing}
442
- label=${label}
451
+ label=${this.channelName(label)}
443
452
  max=${max}
444
453
  min="0"
445
454
  value-text=${spoken || nothing}
@@ -568,8 +577,6 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
568
577
  const reversed = square('brightness');
569
578
  const hue = hueName({ alpha: 100, brightness: 100, hue: color.hue, saturation: 100 }, this.colorWords, locale);
570
579
  const hueSaid = (value) => new Intl.ListFormat(locale, { style: 'short', type: 'unit' }).format([String(value), hue]);
571
- const owner = this.getAttribute('aria-label')?.trim() || this.named;
572
- const axisName = (channel) => (owner ? `${owner}, ${channel}` : channel);
573
580
  const area = html `<div
574
581
  part="grid"
575
582
  style="--color-picker-hue: ${color.hue}; --color-picker-saturation: ${color.saturation}%; --color-picker-brightness: ${color.brightness}%"
@@ -584,7 +591,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
584
591
  type="range"
585
592
  min="0"
586
593
  max="100"
587
- aria-label=${axisName(this.saturationLabel)}
594
+ aria-label=${this.channelName(this.saturationLabel)}
588
595
  aria-describedby=${this.describedBy}
589
596
  aria-valuetext=${both}
590
597
  aria-invalid=${this.marked ? 'true' : nothing}
@@ -601,7 +608,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
601
608
  max="100"
602
609
  tabindex="-1"
603
610
  style="writing-mode: vertical-lr; direction: rtl"
604
- aria-label=${axisName(this.brightnessLabel)}
611
+ aria-label=${this.channelName(this.brightnessLabel)}
605
612
  aria-describedby=${this.describedBy}
606
613
  aria-valuetext=${reversed}
607
614
  aria-invalid=${this.marked ? 'true' : nothing}
@@ -630,7 +637,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
630
637
  min="0"
631
638
  max="360"
632
639
  dir="ltr"
633
- aria-label=${axisName(this.hueLabel)}
640
+ aria-label=${this.channelName(this.hueLabel)}
634
641
  aria-describedby=${this.describedBy}
635
642
  aria-valuetext=${hueSaid(color.hue)}
636
643
  aria-invalid=${this.marked ? 'true' : nothing}
@@ -395,6 +395,8 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
395
395
  * "France", and the form received two entries.
396
396
  */
397
397
  private get values();
398
+ /** The values a value holds, read as `values` reads this element's own. */
399
+ private valuesIn;
398
400
  /** The answers created here, which no option holds, kept so their commas read as theirs. */
399
401
  private readonly created;
400
402
  /**
@@ -577,6 +579,17 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
577
579
  * `result-label` for one where a page still sets it.
578
580
  */
579
581
  private countSaid;
582
+ /**
583
+ * Says the option a choice changed, for VoiceOver, which says nothing when
584
+ * `aria-selected` changes: `Beta, selected` for one taken, and its words
585
+ * alone for one let go, as the highlight says an option not chosen.
586
+ *
587
+ * **The option the choice changed, read from the values before and after.**
588
+ * With `multiple` the value is the whole list, and the option was looked up
589
+ * by it, so no option matched and no choice was ever said: measured with
590
+ * Beta chosen, Enter let it go and the live region kept `Beta, selected`.
591
+ */
592
+ private sayChoice;
580
593
  /**
581
594
  * Takes what the list announced and stops it there.
582
595
  *
@@ -188,12 +188,12 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
188
188
  this.handleChange = (event) => {
189
189
  event.stopPropagation();
190
190
  const { value } = event.detail;
191
+ const before = this.values;
191
192
  if (!this.requestValue(value)) {
192
193
  return;
193
194
  }
194
- const chosen = this.listbox?.options.find((option) => option.value === value);
195
- if (chosen && onApple()) {
196
- this.status.say(formatWords(this, 'selectedLabel', { name: chosen.text }));
195
+ if (onApple()) {
196
+ this.sayChoice(before, this.valuesIn(value));
197
197
  }
198
198
  this.emptied = false;
199
199
  this.typed = '';
@@ -253,11 +253,14 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
253
253
  return this.partNamed('listbox');
254
254
  }
255
255
  get values() {
256
+ return this.valuesIn(this.value);
257
+ }
258
+ valuesIn(value) {
256
259
  const known = new Set(this.created);
257
260
  for (const option of optionsAmong(this.knownChildren)) {
258
261
  known.add(option.value);
259
262
  }
260
- return chosenValues(this.value, known, this.multiple);
263
+ return chosenValues(value, known, this.multiple);
261
264
  }
262
265
  get chosen() {
263
266
  const options = optionsAmong(this.knownChildren);
@@ -460,6 +463,15 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
460
463
  ? countLabel(count, this.resultLabel, this.resultsLabel, wordsLocale(this))
461
464
  : formatWords(this, 'resultsLabel', { count });
462
465
  }
466
+ sayChoice(before, after) {
467
+ const taken = this.multiple ? after.find((one) => !before.includes(one)) : after[0];
468
+ const changed = taken ?? before.find((one) => !after.includes(one));
469
+ const option = this.listbox?.options.find((one) => one.value === changed);
470
+ if (!option) {
471
+ return;
472
+ }
473
+ this.status.say(taken === undefined ? option.text : formatWords(this, 'selectedLabel', { name: option.text }));
474
+ }
463
475
  removeLastToken() {
464
476
  const last = this.values.at(-1);
465
477
  if (last !== undefined) {
@@ -486,7 +486,13 @@ export declare class HelixHeadlessDateInput extends HelixHeadlessDateInput_base
486
486
  unavailableMessage: string;
487
487
  /**
488
488
  * The message for text that is no date, where `bad-input-message` is not
489
- * set. `{mode}` is `single`, `range`, or `multiple`.
489
+ * set. `{mode}` is `single`, `range`, or `multiple`, and `{example}` is a
490
+ * date written as the field writes one, `August 24, 2026` in `en-US` and
491
+ * `24. August 2026` in `de-DE` at the default `date-style`.
492
+ *
493
+ * **The default names a date the field reads back.** It said "Enter a date
494
+ * as YYYY-MM-DD." to a reader whose field showed `August 4, 2026`, and a
495
+ * pattern token is words for a developer. Time Input says the same.
490
496
  */
491
497
  unreadableMessage: string;
492
498
  /**
@@ -12,6 +12,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
13
  import '../../date-picker/index.js';
14
14
  import '../../tag/root/index.js';
15
+ const EXAMPLE = '2026-08-24';
15
16
  const dates = (value) => value
16
17
  .replace(/\s+[–-]\s+/g, ',')
17
18
  .split(',')
@@ -119,7 +120,7 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
119
120
  }
120
121
  };
121
122
  this.unavailableMessage = 'Choose a date that is available.';
122
- this.unreadableMessage = '{mode, select, single {Enter a date as YYYY-MM-DD.} other {Enter dates as YYYY-MM-DD, separated by commas.}}';
123
+ this.unreadableMessage = '{mode, select, single {Enter a date such as {example}.} other {Enter dates such as {example}, separated by commas.}}';
123
124
  this.yearViewLabel = '{heading}. Choose a year';
124
125
  this.handleIconChange = () => {
125
126
  this.requestUpdate();
@@ -232,7 +233,10 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
232
233
  const held = dates(this.value);
233
234
  if (held.length > 0 && !held.every(isDateValue)) {
234
235
  return (this.badInputMessage ||
235
- formatWords(this, 'unreadableMessage', { mode: this.mode ?? 'single' }));
236
+ formatWords(this, 'unreadableMessage', {
237
+ example: this.written(EXAMPLE),
238
+ mode: this.mode ?? 'single',
239
+ }));
236
240
  }
237
241
  if (this.min !== '' && held.some((one) => one < this.min)) {
238
242
  return [
@@ -117,8 +117,12 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
117
117
  * **Every sentence a control raises in the browser's bubble is a property**,
118
118
  * named for the validity flag it reports, so a form in another language
119
119
  * speaks it. Unset is the sentence registered for the page's language, then
120
- * the English. `{mode}` is `single`, `range` or `multiple`, so each can have
121
- * its own sentence.
120
+ * the English. `{example}` is a date written as the calendar names one,
121
+ * `August 24, 2026` in `en-US` and `24. August 2026` in `de-DE`, and `{mode}`
122
+ * is `single`, `range` or `multiple`, so each can have its own sentence.
123
+ *
124
+ * **The default names a date in words.** It said "Enter a date as
125
+ * YYYY-MM-DD.", and a pattern token is words for a developer.
122
126
  */
123
127
  badInputMessage: string;
124
128
  /**
@@ -9,6 +9,7 @@ import { caretLeft, caretRight } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
+ const EXAMPLE = '2026-08-24';
12
13
  const KEYS = new Set([
13
14
  'ArrowDown',
14
15
  'ArrowLeft',
@@ -25,7 +26,7 @@ const firstOf = (year, month) => addMonths(asDate('2000-01-01'), (year - 2000) *
25
26
  let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerFeedback(MixinControllerValue(HelixFormElement)) {
26
27
  constructor() {
27
28
  super(...arguments);
28
- this.badInputMessage = '{mode, select, single {Enter a date as YYYY-MM-DD.} other {Enter dates as YYYY-MM-DD, separated by commas.}}';
29
+ this.badInputMessage = '{mode, select, single {Enter a date such as {example}.} other {Enter dates such as {example}, separated by commas.}}';
29
30
  this.defaultMonth = '';
30
31
  this.disableDates = '';
31
32
  this.disableDays = '';
@@ -491,7 +492,10 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
491
492
  if (held.length > 0 && held.some((one) => asDate(one) === null)) {
492
493
  return [
493
494
  { badInput: true },
494
- formatWords(this, 'badInputMessage', { mode: this.mode ?? 'single' }),
495
+ formatWords(this, 'badInputMessage', {
496
+ example: this.spoken(EXAMPLE),
497
+ mode: this.mode ?? 'single',
498
+ }),
495
499
  ];
496
500
  }
497
501
  for (const one of held) {
@@ -360,8 +360,9 @@ export declare class HelixHeadlessFileInput extends HelixHeadlessFileInput_base
360
360
  *
361
361
  * Not through `commit`, and that is the point of the split: a reset is the
362
362
  * form's decision and there is nothing here for a listener to refuse. What it
363
- * owes is the report and the platform's own announcement, which is what
364
- * `settle` is.
363
+ * owes is the report to the form, and no `input` or `change`: a native file
364
+ * input sends neither on `form.reset()`, measured in Chromium, where this
365
+ * sent both.
365
366
  */
366
367
  formResetCallback(): void;
367
368
  /**
@@ -224,10 +224,13 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
224
224
  }
225
225
  control.files = holding.files;
226
226
  }
227
- settle() {
227
+ settle(announce = true) {
228
228
  this.committed = this.files;
229
229
  this.sync();
230
230
  this.requestUpdate();
231
+ if (!announce) {
232
+ return;
233
+ }
231
234
  this.announceInput();
232
235
  this.announceChange();
233
236
  }
@@ -265,7 +268,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
265
268
  if (this.control) {
266
269
  restoreValue(this.control, '');
267
270
  }
268
- this.settle();
271
+ this.settle(false);
269
272
  }
270
273
  renderMeta(file) {
271
274
  return html `<${this.tag('tag')} part="size"
@@ -133,6 +133,7 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
133
133
  }
134
134
  this.placeCaret(true);
135
135
  if (applied && this.value === code && code.length === this.count) {
136
+ this.runPendingUpdate();
136
137
  this.dispatchEvent(new HelixCompleteEvent({ value: code }));
137
138
  this.commit();
138
139
  if (this.autosubmit) {
@@ -277,7 +277,13 @@ export declare class HelixHeadlessTimeInput extends HelixHeadlessTimeInput_base
277
277
  unavailableMessage: string;
278
278
  /**
279
279
  * The message for text that is no time, where `bad-input-message` is not
280
- * set. `{mode}` is `single`, `range`, or `multiple`, and `{shape}` is `HH:mm`, or `HH:mm:ss` with `seconds`.
280
+ * set. `{mode}` is `single`, `range`, or `multiple`, `{example}` is a time
281
+ * written as the field writes one, `2:30 PM` in `en-US` and `14:30` in
282
+ * `de-DE`, and `{shape}` is `HH:mm`, or `HH:mm:ss` with `seconds`.
283
+ *
284
+ * **The default names a time the field reads back.** It said "Enter a time
285
+ * as HH:mm." to a reader whose field showed `2:30 PM`, and a pattern token
286
+ * is words for a developer.
281
287
  */
282
288
  unreadableMessage: string;
283
289
  /** Every time in the value, which may hold more than one. */
@@ -36,7 +36,7 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
36
36
  this.timeStyle = 'local';
37
37
  this.triggerLabel = 'Choose a time';
38
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.}}';
39
+ this.unreadableMessage = '{mode, select, single {Enter a time such as {example}.} other {Enter times such as {example}, separated by commas.}}';
40
40
  this.draft = '';
41
41
  this.handleTextCommit = (event) => {
42
42
  if (this.mode !== 'multiple') {
@@ -182,10 +182,13 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
182
182
  }
183
183
  validate() {
184
184
  const held = this.held;
185
- const shape = this.seconds ? 'HH:mm:ss' : 'HH:mm';
186
185
  if (held.length > 0 && !held.every((one) => this.isTime(one))) {
187
186
  return (this.badInputMessage ||
188
- formatWords(this, 'unreadableMessage', { mode: this.mode ?? 'single', shape }));
187
+ formatWords(this, 'unreadableMessage', {
188
+ example: this.written(this.seconds ? '14:30:15' : '14:30'),
189
+ mode: this.mode ?? 'single',
190
+ shape: this.seconds ? 'HH:mm:ss' : 'HH:mm',
191
+ }));
189
192
  }
190
193
  const rules = { disableTimes: this.disableTimes, max: this.max, min: this.min };
191
194
  const faults = held.map((one) => timeFault(one, rules));
@@ -131,9 +131,14 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
131
131
  * **Every sentence a control raises in the browser's bubble is a property**,
132
132
  * named for the validity flag it reports, so a form in another language
133
133
  * speaks it. Unset is the sentence registered for the page's language, then
134
- * the English. `{shape}` is how a time is written, `HH:mm` or `HH:mm:ss`, and
135
- * `{mode}` is `single`, `range` or `multiple`, so each can have its own
136
- * sentence.
134
+ * the English. `{example}` is a time written as the picker writes one,
135
+ * `2:30 PM` in `en-US` and `14:30` in `de-DE`, `{shape}` is `HH:mm` or
136
+ * `HH:mm:ss`, and `{mode}` is `single`, `range` or `multiple`, so each can
137
+ * have its own sentence.
138
+ *
139
+ * **The default names a time as the picker shows one.** It said "Enter a
140
+ * time as HH:mm." to a reader whose list showed `2:30 PM`, and a pattern
141
+ * token is words for a developer. Time Input says the same.
137
142
  */
138
143
  badInputMessage: string;
139
144
  /**
@@ -14,7 +14,7 @@ import '../../option/root/index.js';
14
14
  let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerFeedback(MixinControllerValue(HelixFormElement)) {
15
15
  constructor() {
16
16
  super(...arguments);
17
- this.badInputMessage = '{mode, select, single {Enter a time as {shape}.} other {Enter times as {shape}, separated by commas.}}';
17
+ this.badInputMessage = '{mode, select, single {Enter a time such as {example}.} other {Enter times such as {example}, separated by commas.}}';
18
18
  this.disableTimes = '';
19
19
  this.emptyText = 'No times to choose from';
20
20
  this.hint = '';
@@ -300,6 +300,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
300
300
  return [
301
301
  { badInput: true },
302
302
  formatWords(this, 'badInputMessage', {
303
+ example: writeClock(this.seconds ? '14:30:15' : '14:30', this.clock, this.seconds),
303
304
  mode: this.mode ?? 'single',
304
305
  shape: this.seconds ? 'HH:mm:ss' : 'HH:mm',
305
306
  }),