@fusion.dev/helix-headless 0.159.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.
@@ -1,4 +1,4 @@
1
- import { type CalendarView, type DateCalendar, HelixFormElement, type MonthCalendar } from '@fusion.dev/helix-core';
1
+ import { type CalendarView, type DateCalendar, HelixFormElement, type MonthCalendar, type PickerFailure } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessDatePicker_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinPickerFeedbackInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & typeof HelixFormElement;
4
4
  /**
@@ -15,9 +15,11 @@ declare const HelixHeadlessDatePicker_base: (new (...args: any[]) => import("@fu
15
15
  * **Dates are `YYYY-MM-DD` strings, and every calculation is in UTC**, because
16
16
  * a local-time `Date` shifts across a time zone or daylight-saving boundary.
17
17
  *
18
- * **The days are the Gregorian calendar's**, written in `calendar`: the
19
- * locale's own where its months are the Gregorian months, as `th-TH` writes
20
- * 2569 for 2026, and `gregory` where they are not, as for `fa-IR`.
18
+ * **The months are the ones `calendar` counts**: the Gregorian months for the
19
+ * locale's own calendar where its months are the Gregorian months, as `th-TH`
20
+ * writes 2569 for 2026, and `gregory` where they are not, as for `fa-IR`
21
+ * with no `calendar`. Name a calendar with months of its own, such as
22
+ * `persian`, and every view counts its days, months, and years from `Intl`.
21
23
  *
22
24
  * `month` sets which month the grid shows, separately from `value`, so paging
23
25
  * selects nothing and you can control the view. When unset, it follows the
@@ -25,7 +27,14 @@ declare const HelixHeadlessDatePicker_base: (new (...args: any[]) => import("@fu
25
27
  *
26
28
  * `mode` sets how many dates are held. Unset holds one, `range` holds two and
27
29
  * marks the dates between them, and `multiple` toggles each date. All three
28
- * read and write the same comma-separated `value`.
30
+ * read and write the same comma-separated `value`. With a `mode`, the grid
31
+ * carries `aria-multiselectable`. While a range holds one end, the grid draws
32
+ * the range to the day under the pointer or the focus, marking those cells
33
+ * `data-range-preview`.
34
+ *
35
+ * The chosen date, range, or count of dates is the grid's description, in a
36
+ * region screen readers announce when it changes, as React Aria's calendars
37
+ * do. `selection-label` words it.
29
38
  *
30
39
  * `max-view` makes the heading a button that steps back to the months of the
31
40
  * year, and with `year` to a page of years, as Zag's date picker does. The
@@ -58,7 +67,7 @@ declare const HelixHeadlessDatePicker_base: (new (...args: any[]) => import("@fu
58
67
  * @event {Event} input - Fired after the value changes.
59
68
  *
60
69
  * @csspart cell - One column of a week row. A range is painted on the cells.
61
- * @csspart cell-range-end - The cell of a range's last day, beside `cell`.
70
+ * @csspart cell-range-end - The cell of a range's last day, beside `cell`, or of the day a waiting range is drawn to.
62
71
  * @csspart cell-range-middle - A cell between a range's ends, beside `cell`.
63
72
  * @csspart cell-range-start - The cell of a range's first day, beside `cell`.
64
73
  * @csspart day - The shape inside a cell. A single chosen date is painted on it.
@@ -84,12 +93,17 @@ declare const HelixHeadlessDatePicker_base: (new (...args: any[]) => import("@fu
84
93
  * @csspart periods - The grid of months or years.
85
94
  * @csspart previous - The control that moves to the previous month.
86
95
  * @csspart previous-mark - The glyph on the previous month control, where none is slotted.
96
+ * @csspart selection - The chosen date or range in words, which screen readers hear when it changes. Drawn for screen readers only.
87
97
  * @csspart week - One row of days.
88
98
  * @csspart weekday - A column header naming a day of the week. It shows the short name and says the full one.
89
99
  *
90
100
  * @cssproperty --date-picker-column-count - How many month columns are drawn, capped at two. `--date-picker-columns` overrides it.
91
101
  *
92
- * @cssstate invalid - While `invalid` is set or `feedback` says what is wrong.
102
+ * @cssstate blank - While no date is chosen.
103
+ * @cssstate invalid - While `invalid` is set, `feedback` says what is wrong, the picker holds a date outside `min` and `max` or one it bars, or it is `required`, holds no date, and the reader has answered once or a form has refused it.
104
+ * @cssstate month-view - While the months of a year are shown, where `max-view` is set.
105
+ * @cssstate range-pending - While a `range` holds its first end and waits for the second.
106
+ * @cssstate year-view - While a page of years is shown, where `max-view` is `year`.
93
107
  */
94
108
  export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_base {
95
109
  /**
@@ -116,14 +130,16 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
116
130
  * `ja-JP-u-ca-japanese`, and `gregory` for every other, such as `persian`
117
131
  * for `fa-IR`.
118
132
  *
119
- * `persian`, `hebrew`, `islamic-umalqura`, `islamic-civil`, `ethiopic`,
120
- * `coptic`, and `indian` draw a calendar with months of its own, as React
121
- * Aria's calendars do: each grid is one of that calendar's months, the
122
- * Hebrew leap month and Ethiopia's five-day thirteenth month included, and
123
- * Previous, Next and the page keys move by those months. `month` and
133
+ * `persian`, `hebrew`, `islamic`, `islamic-umalqura`, `islamic-civil`,
134
+ * `islamic-tbla`, `islamic-rgsa`, `ethiopic`, `ethioaa`, `coptic`,
135
+ * `indian`, `chinese`, and `dangi` draw a calendar with months of its own,
136
+ * as React Aria's calendars do: each grid is one of that calendar's months,
137
+ * the Hebrew and Chinese leap months and Ethiopia's five-day thirteenth
138
+ * month included, and Previous, Next and the page keys move by those
139
+ * months. With `max-view`, the months view shows that calendar's year, of
140
+ * twelve or thirteen months, and the years view its years. `month` and
124
141
  * `helix-navigate` then name a month by its first day, `YYYY-MM-DD`. The
125
- * heading stays text, as the months and years views are drawn in Gregorian
126
- * months only. The value is `YYYY-MM-DD` in every calendar.
142
+ * value is `YYYY-MM-DD` in every calendar.
127
143
  */
128
144
  calendar?: DateCalendar | MonthCalendar;
129
145
  /**
@@ -207,6 +223,8 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
207
223
  mode?: 'range' | 'multiple';
208
224
  /** The month on show, whoever set it, and `undefined` until somebody has. */
209
225
  private monthState;
226
+ /** Whether the month on show is one the reader paged to, which `showChosen` forgets. */
227
+ private browsed;
210
228
  /**
211
229
  * The month shown, as `YYYY-MM`, or as its first day, `YYYY-MM-DD`, under a
212
230
  * `calendar` with months of its own. When it is not set, it follows
@@ -284,6 +302,21 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
284
302
  private choosing;
285
303
  /** The range's ends, when there are two and the mode asks for them. */
286
304
  private get range();
305
+ /**
306
+ * The day a reader points at or moves to while a range holds one end, which
307
+ * the grid draws the range to before it is chosen.
308
+ */
309
+ private previewed;
310
+ /** Whether a range holds its first end and waits for the second. */
311
+ private get pending();
312
+ /**
313
+ * The range the grid draws: the chosen one, or, while one end waits for
314
+ * the other, the run to the day under the pointer or the focus.
315
+ */
316
+ private get drawnRange();
317
+ /** Draws a waiting range to the day the pointer is on. */
318
+ private preview;
319
+ private readonly handleGridLeave;
287
320
  private isSelected;
288
321
  /** The month being shown, resolved from `month`, then `value`, then today. */
289
322
  private get shown();
@@ -292,8 +325,6 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
292
325
  * months of its own, and the Gregorian months otherwise.
293
326
  */
294
327
  private get system();
295
- /** Whether `calendar` names a calendar with months of its own. */
296
- private get ownMonths();
297
328
  /**
298
329
  * The month to open on when nothing has declared one.
299
330
  *
@@ -407,6 +438,16 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
407
438
  * plain `focus()` on it did nothing and focus stayed on the trigger.
408
439
  */
409
440
  focus(options?: FocusOptions): void;
441
+ /**
442
+ * Shows the days again, on the month of the chosen date, with the tab stop
443
+ * on that date, forgetting the month and the day the reader browsed to.
444
+ *
445
+ * Date Input calls it each time its panel opens, which is where the APG
446
+ * date picker dialog puts focus. Measured before this: from August 4, two
447
+ * presses of Page Down, Escape, and Down again opened on October 4.
448
+ * A `month` you set stays until the reader pages away from it.
449
+ */
450
+ showChosen(): void;
410
451
  connectedCallback(): void;
411
452
  disconnectedCallback(): void;
412
453
  /**
@@ -434,6 +475,23 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
434
475
  * Stops the form from submitting while no date is chosen.
435
476
  */
436
477
  required: boolean;
478
+ /**
479
+ * What a screen reader hears about the chosen dates: the grid's
480
+ * description, said again whenever the dates change. `{kind}` is `date`,
481
+ * `start` while a range holds one end, `range`, or `dates` for `multiple`.
482
+ * `{start}` and `{end}` are dates written in `locale`, and `{count}` is how
483
+ * many dates are chosen.
484
+ */
485
+ selectionLabel: string;
486
+ /**
487
+ * `selectionLabel` for the dates chosen now, or nothing while there are none.
488
+ *
489
+ * React Aria's calendars describe the grid by the chosen date or range and
490
+ * announce it when it changes. Measured before this: a range of seven days
491
+ * was seven cells with `aria-selected` in a grid that said nothing of a
492
+ * range, and choosing its end was heard as one more selected cell.
493
+ */
494
+ private get selectionWords();
437
495
  /**
438
496
  * How far apart the selectable dates are, in days.
439
497
  *
@@ -505,6 +563,13 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
505
563
  * browser points its bubble at.
506
564
  */
507
565
  private reportRequired;
566
+ /**
567
+ * What is wrong with the answer, as validity flags and a message, or nothing.
568
+ * `reportRequired` hands it to the form, and `failure` to the mark.
569
+ */
570
+ private get problem();
571
+ /** A missing date waits for the reader; a refused one is marked at once. */
572
+ protected get failure(): PickerFailure | undefined;
508
573
  protected willUpdate(changed: PropertyValues): void;
509
574
  /**
510
575
  * Reports validity after render, because the bubble needs somewhere to point.
@@ -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 { addDays, addMonths, valueAsDate as asDate, barredBy, calendarLocale, customElement, formatWords, dateAsValue as fromDate, HelixFormElement, HelixNavigateEvent, handedLocale, hostAria, inlineStep, commaList as list, MixinControllerValue, MixinPickerFeedback, MONTH_CALENDARS, monthOf, monthOut, monthSystem, periodCells, periodKey, reportValues, requestChange, todayIn, weekendOf, weekStartOf, words, wordsLocale, YEARS_SHOWN, yearOut, yearPageOf, } from '@fusion.dev/helix-core';
7
+ import { addDays, addMonths, valueAsDate as asDate, barredBy, calendarLocale, customElement, formatWords, dateAsValue as fromDate, HelixFormElement, HelixNavigateEvent, handedLocale, hostAria, inlineStep, commaList as list, MixinControllerValue, MixinPickerFeedback, monthOut, monthSystem, periodCells, periodKey, reportValues, requestChange, todayIn, weekendOf, weekStartOf, words, wordsLocale, YEARS_SHOWN, yearOut, yearPageOf, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft, caretRight } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -34,6 +34,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
34
34
  this.max = '';
35
35
  this.min = '';
36
36
  this.monthState = undefined;
37
+ this.browsed = false;
37
38
  this.months = 1;
38
39
  this.monthViewLabel = '{heading}. Choose a month';
39
40
  this.nextLabel = 'Next month';
@@ -45,8 +46,12 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
45
46
  this.rangeOverflowMessage = 'Choose a date on or before {max}.';
46
47
  this.focused = '';
47
48
  this.choosing = false;
49
+ this.previewed = '';
50
+ this.handleGridLeave = () => {
51
+ this.previewed = '';
52
+ };
48
53
  this.handleKeydown = (event) => {
49
- if (this.view !== 'day' && !this.ownMonths) {
54
+ if (this.view !== 'day') {
50
55
  this.handlePeriodKeydown(event);
51
56
  return;
52
57
  }
@@ -80,7 +85,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
80
85
  }
81
86
  else {
82
87
  const step = event.key === 'PageUp' ? -1 : 1;
83
- this.moveTo(this.system.add(from, event.shiftKey ? step * 12 : step));
88
+ this.moveTo(event.shiftKey ? this.system.addYears(from, step) : this.system.add(from, step));
84
89
  }
85
90
  event.preventDefault();
86
91
  };
@@ -91,8 +96,9 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
91
96
  return;
92
97
  }
93
98
  if (this.view === 'day') {
94
- this.pageYear = this.shown.getUTCFullYear();
95
- this.periodFocus = this.shown.getUTCMonth();
99
+ const { month, year } = this.system.parts(this.shown);
100
+ this.pageYear = year;
101
+ this.periodFocus = month - 1;
96
102
  this.zoom('month');
97
103
  }
98
104
  else if (this.view === 'month' && this.maxView === 'year') {
@@ -103,6 +109,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
103
109
  this.rangeUnderflowMessage = 'Choose a date on or after {min}.';
104
110
  this.readOnly = false;
105
111
  this.required = false;
112
+ this.selectionLabel = '{kind, select, date {Selected date: {start}} start {Range starts {start}. Choose the day it ends.} range {Selected range: {start} to {end}} other {{count, plural, one {# date selected} other {# dates selected}}}}';
106
113
  this.step = 1;
107
114
  this.stepMismatchMessage = 'Choose one of the dates the calendar offers.';
108
115
  this.timeZone = '';
@@ -120,6 +127,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
120
127
  }
121
128
  set month(next) {
122
129
  const previous = this.month;
130
+ this.browsed = false;
123
131
  this.monthState = next;
124
132
  this.requestUpdate('month', previous);
125
133
  }
@@ -143,6 +151,30 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
143
151
  const [first, second] = [dates[0], dates[1]];
144
152
  return first <= second ? [first, second] : [second, first];
145
153
  }
154
+ get pending() {
155
+ return this.mode === 'range' && this.selected.length === 1;
156
+ }
157
+ get drawnRange() {
158
+ const range = this.range;
159
+ if (range) {
160
+ return { ends: range, preview: false };
161
+ }
162
+ const end = this.previewed || this.focused;
163
+ const start = this.selected[0];
164
+ if (!this.pending ||
165
+ start === undefined ||
166
+ end === '' ||
167
+ end === start ||
168
+ asDate(end) === null) {
169
+ return null;
170
+ }
171
+ return { ends: start <= end ? [start, end] : [end, start], preview: true };
172
+ }
173
+ preview(iso) {
174
+ if (this.pending && iso !== this.previewed) {
175
+ this.previewed = iso;
176
+ }
177
+ }
146
178
  isSelected(iso) {
147
179
  const range = this.range;
148
180
  if (range) {
@@ -161,9 +193,6 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
161
193
  get system() {
162
194
  return monthSystem(this.calendar);
163
195
  }
164
- get ownMonths() {
165
- return MONTH_CALENDARS.includes(this.calendar ?? '');
166
- }
167
196
  get derivedMonth() {
168
197
  const from = asDate(this.selected[0] ?? '') ?? asDate(todayIn(this.timeZone)) ?? new Date();
169
198
  return this.system.start(from);
@@ -226,6 +255,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
226
255
  requestChange({
227
256
  apply: (month) => {
228
257
  const previous = this.month;
258
+ this.browsed = true;
229
259
  this.monthState = month;
230
260
  this.requestUpdate('month', previous);
231
261
  },
@@ -237,6 +267,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
237
267
  moveTo(wanted) {
238
268
  const date = this.clamp(wanted);
239
269
  this.focused = fromDate(date);
270
+ this.previewed = '';
240
271
  this.show(date);
241
272
  this.updateComplete.then(() => {
242
273
  this.renderRoot.querySelector('[part~="cell"][tabindex="0"]')?.focus();
@@ -262,23 +293,25 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
262
293
  next = [iso];
263
294
  }
264
295
  this.focused = iso;
296
+ this.previewed = '';
265
297
  this.choosing = true;
266
298
  this.requestValue(next.join(','));
267
299
  }
268
300
  get periodYear() {
269
- return this.pageYear ?? this.shown.getUTCFullYear();
301
+ return this.pageYear ?? this.system.parts(this.shown).year;
270
302
  }
271
303
  get periodStop() {
272
- if (this.periodFocus !== undefined) {
273
- return this.periodFocus;
304
+ if (this.view === 'year') {
305
+ return this.periodFocus ?? this.periodYear;
274
306
  }
275
- return this.view === 'year' ? this.periodYear : this.shown.getUTCMonth();
307
+ const index = this.periodFocus ?? this.system.parts(this.shown).month - 1;
308
+ return Math.min(index, this.system.months(this.periodYear) - 1);
276
309
  }
277
310
  monthOut(first) {
278
- return monthOut(first, this.min, this.max);
311
+ return monthOut(first, this.min, this.max, this.system);
279
312
  }
280
313
  yearOut(year) {
281
- return yearOut(year, this.min, this.max);
314
+ return yearOut(year, this.min, this.max, this.system);
282
315
  }
283
316
  zoom(view) {
284
317
  this.view = view;
@@ -293,8 +326,8 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
293
326
  return;
294
327
  }
295
328
  this.pageYear = at;
296
- const month = this.shown.getUTCMonth();
297
- const open = Array.from({ length: 12 }, (_, index) => index).filter((index) => !this.monthOut(firstOf(at, index)));
329
+ const month = this.system.parts(this.shown).month - 1;
330
+ const open = Array.from({ length: this.system.months(at) }, (_, index) => index).filter((index) => !this.monthOut(this.system.from(at, index + 1, 1)));
298
331
  this.periodFocus = open.includes(month)
299
332
  ? month
300
333
  : ([...open].sort((left, right) => Math.abs(left - month) - Math.abs(right - month))[0] ??
@@ -302,12 +335,12 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
302
335
  this.zoom('month');
303
336
  return;
304
337
  }
305
- const first = firstOf(this.periodYear, at);
338
+ const first = this.system.from(this.periodYear, at + 1, 1);
306
339
  if (this.monthOut(first)) {
307
340
  return;
308
341
  }
309
342
  this.show(first);
310
- if (this.month !== monthOf(first)) {
343
+ if (this.month !== this.system.key(first)) {
311
344
  return;
312
345
  }
313
346
  this.pageYear = undefined;
@@ -344,7 +377,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
344
377
  this.choosePeriod(stop);
345
378
  return;
346
379
  }
347
- const moved = periodKey(this.view === 'year' ? 'year' : 'month', stop, this.periodYear, event.key, inlineStep(this, event.key));
380
+ const moved = periodKey(this.view === 'year' ? 'year' : 'month', stop, this.periodYear, event.key, inlineStep(this, event.key), (year) => this.system.months(year));
348
381
  if (moved === null) {
349
382
  return;
350
383
  }
@@ -354,10 +387,26 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
354
387
  void this.updateComplete.then(() => this.focus());
355
388
  }
356
389
  focus(options) {
390
+ if (this.isUpdatePending) {
391
+ void this.updateComplete.then(() => this.focus(options));
392
+ return;
393
+ }
357
394
  this.renderRoot
358
395
  ?.querySelector('[part~="cell"][tabindex="0"], [part~="period"][tabindex="0"]')
359
396
  ?.focus(options);
360
397
  }
398
+ showChosen() {
399
+ this.focused = '';
400
+ this.pageYear = undefined;
401
+ this.periodFocus = undefined;
402
+ this.view = 'day';
403
+ if (this.browsed) {
404
+ this.browsed = false;
405
+ const previous = this.month;
406
+ this.monthState = undefined;
407
+ this.requestUpdate('month', previous);
408
+ }
409
+ }
361
410
  connectedCallback() {
362
411
  super.connectedCallback();
363
412
  this.addEventListener('keydown', this.handleKeydown);
@@ -372,6 +421,28 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
372
421
  }
373
422
  return super.requestValue(next);
374
423
  }
424
+ get selectionWords() {
425
+ const dates = this.selected.map((one) => asDate(one));
426
+ const first = dates[0];
427
+ if (!first || dates.some((one) => one === null)) {
428
+ return '';
429
+ }
430
+ const { long } = this.formats;
431
+ const range = this.range;
432
+ const kind = range
433
+ ? 'range'
434
+ : this.pending
435
+ ? 'start'
436
+ : this.mode === 'multiple'
437
+ ? 'dates'
438
+ : 'date';
439
+ return formatWords(this, 'selectionLabel', {
440
+ count: dates.length,
441
+ end: range ? long.format(asDate(range[1])) : '',
442
+ kind,
443
+ start: long.format(range ? asDate(range[0]) : first),
444
+ });
445
+ }
375
446
  spoken(iso) {
376
447
  const date = asDate(iso);
377
448
  return date ? this.formats.long.format(date) : iso;
@@ -399,24 +470,38 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
399
470
  : ['stepMismatch', this.stepMismatchMessage];
400
471
  }
401
472
  reportRequired() {
402
- const anchor = this.partNamed('grid') ?? undefined;
403
- if (this.required && this.value === '') {
404
- this.internals.setValidity({ valueMissing: true }, this.valueMissingMessage, anchor);
473
+ const problem = this.problem;
474
+ if (problem) {
475
+ this.internals.setValidity(problem[0], problem[1], this.partNamed('grid') ?? undefined);
405
476
  return;
406
477
  }
478
+ this.internals.setValidity({});
479
+ }
480
+ get problem() {
481
+ if (this.required && this.value === '') {
482
+ return [{ valueMissing: true }, this.valueMissingMessage];
483
+ }
407
484
  const held = this.selected;
408
485
  if (held.length > 0 && held.some((one) => asDate(one) === null)) {
409
- this.internals.setValidity({ badInput: true }, formatWords(this, 'badInputMessage', { mode: this.mode ?? 'single' }), anchor);
410
- return;
486
+ return [
487
+ { badInput: true },
488
+ formatWords(this, 'badInputMessage', { mode: this.mode ?? 'single' }),
489
+ ];
411
490
  }
412
491
  for (const one of held) {
413
492
  const fault = this.fault(one);
414
493
  if (fault) {
415
- this.internals.setValidity({ [fault[0]]: true }, fault[1], anchor);
416
- return;
494
+ return [{ [fault[0]]: true }, fault[1]];
417
495
  }
418
496
  }
419
- this.internals.setValidity({});
497
+ return undefined;
498
+ }
499
+ get failure() {
500
+ const problem = this.problem;
501
+ if (!problem) {
502
+ return undefined;
503
+ }
504
+ return problem[0].valueMissing ? 'missing' : 'refused';
420
505
  }
421
506
  willUpdate(changed) {
422
507
  super.willUpdate(changed);
@@ -431,6 +516,10 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
431
516
  role: 'group',
432
517
  });
433
518
  this.internals.ariaDisabled = this.isDisabled ? 'true' : null;
519
+ this.toggleState('blank', this.value === '');
520
+ this.toggleState('range-pending', this.pending);
521
+ this.toggleState('month-view', this.view === 'month');
522
+ this.toggleState('year-view', this.view === 'year');
434
523
  this.report();
435
524
  }
436
525
  updated(changed) {
@@ -448,9 +537,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
448
537
  return Math.max(1, Math.floor(this.months) || 1);
449
538
  }
450
539
  get formats() {
451
- const language = this.ownMonths
452
- ? `${new Intl.Locale(this.language || new Intl.DateTimeFormat().resolvedOptions().locale, { calendar: this.calendar ?? 'gregory' })}`
453
- : calendarLocale(this.language, this.calendar);
540
+ const language = calendarLocale(this.language, this.calendar);
454
541
  if (this.cachedFormats?.locale === language) {
455
542
  return this.cachedFormats;
456
543
  }
@@ -489,7 +576,9 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
489
576
  const weeks = this.weeksOf(month);
490
577
  const anchor = fromDate(this.anchor);
491
578
  const today = todayIn(this.timeZone);
492
- const range = this.range;
579
+ const drawn = this.drawnRange;
580
+ const range = drawn?.ends ?? null;
581
+ const preview = drawn?.preview === true;
493
582
  const weekend = this.weekend;
494
583
  const first = index === 0;
495
584
  const last = index === this.monthCount - 1;
@@ -526,8 +615,11 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
526
615
  part="grid"
527
616
  role="grid"
528
617
  aria-label=${heading}
618
+ aria-describedby=${this.selectionWords === '' ? nothing : 'selection'}
619
+ aria-multiselectable=${this.mode === undefined ? nothing : 'true'}
529
620
  aria-readonly=${this.readOnly ? 'true' : nothing}
530
621
  data-unset=${this.value === '' ? '' : nothing}
622
+ @pointerleave=${this.handleGridLeave}
531
623
  >
532
624
  <div part="week" role="row">
533
625
  ${weeks[0]?.map((date) => html `<span part="weekday" role="columnheader" aria-label=${weekdayName.format(date)}
@@ -553,11 +645,13 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
553
645
  data-outside=${outside ? '' : nothing}
554
646
  data-range-end=${end ? '' : nothing}
555
647
  data-range-middle=${middle ? '' : nothing}
648
+ data-range-preview=${preview && (start || middle || end) ? '' : nothing}
556
649
  data-range-start=${start ? '' : nothing}
557
650
  data-today=${iso === today ? '' : nothing}
558
651
  data-weekend=${weekend.includes(date.getUTCDay()) ? '' : nothing}
559
652
  tabindex=${!outside && iso === anchor && !this.isDisabled ? '0' : '-1'}
560
653
  @click=${outside ? nothing : () => this.select(date)}
654
+ @pointerenter=${outside ? nothing : () => this.preview(iso)}
561
655
  ><span
562
656
  part="day${!outside && selected && !middle ? ' day-selected' : ''}${iso === today ? ' day-today' : ''}${outside ? ' day-outside' : ''}${disabled ? ' day-disabled' : ''}${weekend.includes(date.getUTCDay()) ? ' day-weekend' : ''}"
563
657
  >${this.renderDay(date, outside)}</span
@@ -573,7 +667,6 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
573
667
  }
574
668
  renderHeadingText(text, label) {
575
669
  const steps = this.maxView !== undefined &&
576
- !this.ownMonths &&
577
670
  (this.view === 'day' || (this.view === 'month' && this.maxView === 'year'));
578
671
  if (!steps) {
579
672
  return text;
@@ -591,14 +684,15 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
591
684
  const years = this.view === 'year';
592
685
  const page = years ? yearPageOf(this.periodYear) : this.periodYear;
593
686
  const heading = years
594
- ? `${yearName.format(firstOf(page, 0))} – ${yearName.format(firstOf(page + YEARS_SHOWN - 1, 0))}`
595
- : yearName.format(firstOf(page, 0));
687
+ ? `${yearName.format(this.system.from(page, 1, 1))} – ${yearName.format(this.system.from(page + YEARS_SHOWN - 1, 1, 1))}`
688
+ : yearName.format(this.system.from(page, 1, 1));
596
689
  const stop = this.periodStop;
597
690
  const cells = periodCells({
598
691
  max: this.max,
599
692
  min: this.min,
600
693
  page: this.periodYear,
601
694
  shown: this.shown,
695
+ system: this.system,
602
696
  today: todayIn(this.timeZone),
603
697
  view: years ? 'year' : 'month',
604
698
  }).map(({ at, chosen, current, first, out }) => {
@@ -615,7 +709,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
615
709
  >${words}</span
616
710
  >`;
617
711
  });
618
- const rows = [0, 3, 6, 9].map((from) => html `<div part="period-row" role="row">${cells.slice(from, from + 3)}</div>`);
712
+ const rows = Array.from({ length: Math.ceil(cells.length / 3) }, (_, row) => row * 3).map((from) => html `<div part="period-row" role="row">${cells.slice(from, from + 3)}</div>`);
619
713
  return html `<div part="month">
620
714
  <div part="header">
621
715
  <button
@@ -644,7 +738,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
644
738
  </div>`;
645
739
  }
646
740
  render() {
647
- if (this.view !== 'day' && !this.ownMonths) {
741
+ if (this.view !== 'day') {
648
742
  return html `<slot name="header"></slot>
649
743
  <div part="months" style="--date-picker-column-count: 1">${this.renderPeriods()}</div>
650
744
  <slot name="footer"></slot>${this.renderPickerFeedback()}`;
@@ -655,6 +749,9 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
655
749
  <div part="months" style="--date-picker-column-count: ${Math.min(count, 2)}">
656
750
  ${Array.from({ length: count }, (_, index) => this.renderMonth(this.system.add(shown, index), index))}
657
751
  </div>
752
+ <span part="selection" id="selection" data-spoken aria-live="polite"
753
+ >${this.selectionWords}</span
754
+ >
658
755
  <slot name="footer"></slot>${this.renderPickerFeedback()}`;
659
756
  }
660
757
  get valueAsDate() {
@@ -755,6 +852,9 @@ __decorate([
755
852
  __decorate([
756
853
  state()
757
854
  ], HelixHeadlessDatePicker.prototype, "focused", void 0);
855
+ __decorate([
856
+ state()
857
+ ], HelixHeadlessDatePicker.prototype, "previewed", void 0);
758
858
  __decorate([
759
859
  state()
760
860
  ], HelixHeadlessDatePicker.prototype, "pageYear", void 0);
@@ -771,6 +871,10 @@ __decorate([
771
871
  __decorate([
772
872
  property({ reflect: true, type: Boolean })
773
873
  ], HelixHeadlessDatePicker.prototype, "required", void 0);
874
+ __decorate([
875
+ words,
876
+ property({ attribute: 'selection-label' })
877
+ ], HelixHeadlessDatePicker.prototype, "selectionLabel", void 0);
774
878
  __decorate([
775
879
  property({ type: Number })
776
880
  ], HelixHeadlessDatePicker.prototype, "step", void 0);
@@ -92,6 +92,7 @@ registerElementEvents({
92
92
  'helix-before-change',
93
93
  'helix-before-close',
94
94
  'helix-before-open',
95
+ 'helix-clear',
95
96
  'input',
96
97
  'toggle',
97
98
  ],
@@ -268,6 +269,7 @@ registerElementEvents({
268
269
  'helix-before-change',
269
270
  'helix-before-close',
270
271
  'helix-before-open',
272
+ 'helix-clear',
271
273
  'input',
272
274
  'toggle',
273
275
  ],
@@ -3,6 +3,7 @@ export const FIELD = {
3
3
  day: (value) => html `<span part="day">${value}</span>`,
4
4
  dayPeriod: (value) => html `<span part="day-period">${value}</span>`,
5
5
  era: (value) => html `<span part="era">${value}</span>`,
6
+ fractionalSecond: (value) => html `<span part="fractional-second">${value}</span>`,
6
7
  hour: (value) => html `<span part="hour">${value}</span>`,
7
8
  minute: (value) => html `<span part="minute">${value}</span>`,
8
9
  month: (value) => html `<span part="month">${value}</span>`,
@@ -5,7 +5,9 @@
5
5
  export interface Asked {
6
6
  dateStyle?: 'full' | 'long' | 'medium' | 'short' | undefined;
7
7
  day?: 'numeric' | '2-digit' | undefined;
8
+ dayPeriod?: 'long' | 'narrow' | 'short' | undefined;
8
9
  era?: 'long' | 'narrow' | 'short' | undefined;
10
+ fractionalSecondDigits?: 1 | 2 | 3 | undefined;
9
11
  hour?: 'numeric' | '2-digit' | undefined;
10
12
  hourCycle?: 'h11' | 'h12' | 'h23' | 'h24' | undefined;
11
13
  minute?: 'numeric' | '2-digit' | undefined;
@@ -13,7 +15,7 @@ export interface Asked {
13
15
  second?: 'numeric' | '2-digit' | undefined;
14
16
  timeStyle?: 'full' | 'long' | 'medium' | 'short' | undefined;
15
17
  timeZone?: string | undefined;
16
- timeZoneName?: 'long' | 'short' | undefined;
18
+ timeZoneName?: 'long' | 'longGeneric' | 'longOffset' | 'short' | 'shortGeneric' | 'shortOffset' | undefined;
17
19
  weekday?: 'long' | 'short' | 'narrow' | undefined;
18
20
  year?: 'numeric' | '2-digit' | undefined;
19
21
  }
@@ -1,5 +1,8 @@
1
1
  import { FIELD } from './fields.js';
2
- const FIELDS = Object.keys(FIELD);
2
+ const FIELDS = [
3
+ ...Object.keys(FIELD).filter((one) => one !== 'fractionalSecond'),
4
+ 'fractionalSecondDigits',
5
+ ];
3
6
  export const formatOptions = (asked, dateOnly = false) => {
4
7
  const options = {
5
8
  hourCycle: asked.hourCycle,