@fusion.dev/helix-headless 0.2.0 → 0.4.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 (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -9,7 +9,6 @@ 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
- /** Keys the grid answers to, per the APG date grid. */
13
12
  const KEYS = new Set([
14
13
  'ArrowDown',
15
14
  'ArrowLeft',
@@ -22,14 +21,6 @@ const KEYS = new Set([
22
21
  'PageUp',
23
22
  ' ',
24
23
  ]);
25
- /**
26
- * Dates are held as `YYYY-MM-DD` and computed in UTC.
27
- *
28
- * A local-time `Date` shifts across a timezone or a daylight-saving boundary,
29
- * so the same calendar renders a different grid depending on where it is read.
30
- * Parsing and formatting both in UTC makes the string the source of truth and
31
- * the arithmetic total.
32
- */
33
24
  const toUtc = (iso) => {
34
25
  const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
35
26
  if (!parts) {
@@ -39,200 +30,45 @@ const toUtc = (iso) => {
39
30
  return Number.isNaN(date.getTime()) ? null : date;
40
31
  };
41
32
  const toIso = (date) => date.toISOString().slice(0, 10);
42
- /**
43
- * Today, in the reader's own timezone.
44
- *
45
- * **`toISOString` converts to UTC first, which makes "today" a different day
46
- * for most of the planet.** Anywhere west of UTC, from late afternoon onward,
47
- * `toIso(new Date())` names tomorrow. The parts are read locally instead and
48
- * pasted together, which is the only way to get a calendar date out of a `Date`
49
- * without a conversion happening on the way.
50
- */
51
33
  const localToday = () => {
52
34
  const now = new Date();
53
35
  const month = `${now.getMonth() + 1}`.padStart(2, '0');
54
36
  const day = `${now.getDate()}`.padStart(2, '0');
55
37
  return `${now.getFullYear()}-${month}-${day}`;
56
38
  };
57
- /** Splits a comma-separated attribute, tolerating spaces and empty entries. */
58
39
  const list = (value) => value
59
40
  .split(',')
60
41
  .map((entry) => entry.trim())
61
42
  .filter(Boolean);
62
43
  const addDays = (date, days) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days));
63
- /** Whether two dates fall in the same month of the same year. */
64
44
  const sameMonth = (left, right) => left.getUTCFullYear() === right.getUTCFullYear() && left.getUTCMonth() === right.getUTCMonth();
65
45
  const addMonths = (date, months) => {
66
46
  const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
67
- // Clamped, so the last of a long month lands on the last of a short one
68
- // instead of rolling into the month after it.
69
47
  const last = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
70
48
  return new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth(), Math.min(date.getUTCDate(), last)));
71
49
  };
72
- /**
73
- * @summary Shows a month, and takes a date from it.
74
- *
75
- * @description
76
- * A grid of days, per the APG date grid: the whole grid is one tab stop, arrows
77
- * move by day and week, `PageUp` and `PageDown` move by month, and `Home` and
78
- * `End` go to the ends of the week. Only the focused cell is in the tab
79
- * sequence.
80
- *
81
- * Roving `tabindex` is written on the cells, which is free here because they
82
- * are rendered into this shadow root rather than being markup a consumer wrote.
83
- *
84
- * Dates are `YYYY-MM-DD` strings and every calculation is in UTC. A local-time
85
- * `Date` shifts across a timezone or a daylight-saving boundary, so the string
86
- * is what the component holds and the arithmetic never leaves UTC.
87
- *
88
- * `month` is what the grid shows, separately from `value`, so paging through
89
- * months does not select anything and a consumer can drive the view. Left
90
- * unset, it follows the first selected date, and then the current month.
91
- *
92
- * `mode` decides how many dates are held. Absent takes one. `range` takes two
93
- * and marks everything between them, starting over on the third click rather
94
- * than replacing an end at random. `multiple` toggles, so clicking a chosen
95
- * date takes it out. All three read and write the same comma-separated
96
- * `value`.
97
- *
98
- * `locale` is worth setting where the output is compared across environments.
99
- * Left unset, month and day names come from the runtime's own locale, and a
100
- * server and a browser need not agree on what that is.
101
- *
102
- * A control whose value changes dispatches `helix-before-change`:
103
- * `Input`, `Select`, `Listbox`, `Radio.Group` and `Toggle.Group` all do, and a
104
- * calendar submitting a date is the same kind of thing. `helix-select` stays
105
- * what it is elsewhere, which is a row or an item being chosen in something
106
- * that reports no value at all, such as `Menu` or `Tree`.
107
- *
108
- * Paging is `helix-navigate` and not either of those, because it chooses
109
- * nothing: the view moves and the answer does not.
110
- *
111
- * The properties below are written by this component and read by whoever draws
112
- * it. A consumer sets none of them: they carry a number this element computed
113
- * and CSS cannot, which is what lets a stylesheet on this tier draw the thing
114
- * without measuring anything itself.
115
- *
116
- * @since 0.1.0
117
- * @status experimental
118
- * @category forms
119
- * @aliases Calendar, Date grid, Month view
120
- * @documentation https://helix.fusion.dev/components/date-picker
121
- *
122
- * @slot next-icon - The glyph on the control that moves forward a month, usually a caret.
123
- * @slot previous-icon - The glyph on the control that moves back a month, usually a caret.
124
- *
125
- * @event {Event} change - Announced once the value has moved.
126
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the dates change, and cancelable.
127
- * @event {HelixNavigateEvent} helix-navigate - Fired before the month on show changes, and cancelable.
128
- * @event {Event} input - Announced once the value has moved.
129
- *
130
- * @csspart cell - One column of a row, which is what a range paints.
131
- * @csspart day - The shape inside a cell, which is what a single date paints.
132
- * @csspart grid - The grid of days.
133
- * @csspart header - The month name and its controls.
134
- * @csspart heading - The month and year being shown.
135
- * @csspart month - One month, its header and its grid.
136
- * @csspart months - The run of months.
137
- * @csspart next - The control that moves to the next month.
138
- * @csspart next-mark - The glyph on the next month control, where none is slotted.
139
- * @csspart previous - The control that moves to the previous month.
140
- * @csspart previous-mark - The glyph on the previous month control, where none is slotted.
141
- * @csspart week - One row of days.
142
- * @csspart weekday - A column header naming a day of the week.
143
- *
144
- * @cssproperty --date-picker-column-count - How many month columns are drawn, capped at two. `--date-picker-columns` overrides it.
145
- */
146
50
  let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControllerValue(HelixFormElement) {
147
51
  constructor() {
148
52
  super(...arguments);
149
- /**
150
- * Month to open on, where nothing declared one.
151
- */
152
53
  this.defaultMonth = '';
153
- /**
154
- * Dates that cannot be chosen, comma separated, as `YYYY-MM-DD`.
155
- */
156
54
  this.disableDates = '';
157
- /**
158
- * Days of the week that cannot be chosen, comma separated, `0` to `6`.
159
- */
160
55
  this.disableDays = '';
161
- /**
162
- * First column of the week, `0` for Sunday through `6` for Saturday.
163
- */
164
56
  this.firstDayOfWeek = 0;
165
- /**
166
- * Whether the current selection fails validation.
167
- */
168
57
  this.invalid = false;
169
- /**
170
- * Accessible name for the grid.
171
- */
172
58
  this.label = 'Date Picker';
173
- /**
174
- * Locale for month and day names.
175
- *
176
- * Absent uses the runtime's own.
177
- */
178
59
  this.locale = '';
179
- /**
180
- * Latest selectable date, as `YYYY-MM-DD`.
181
- */
182
60
  this.max = '';
183
- /**
184
- * Earliest selectable date, as `YYYY-MM-DD`.
185
- */
186
61
  this.min = '';
187
- /** The month on show, whoever set it, and `undefined` until somebody has. */
188
62
  this.monthState = undefined;
189
- /**
190
- * How many months to show, side by side.
191
- */
192
63
  this.months = 1;
193
- /**
194
- * Accessible name for the control that pages forward.
195
- */
196
64
  this.nextLabel = 'Next month';
197
- /**
198
- * Accessible name for the control that pages back.
199
- *
200
- * A property because a calendar rendering its own month names from `locale`
201
- * would otherwise still announce its controls in English. `attribute:`
202
- * spelled out, since Lit lowercases a camelCase name rather than dash-casing
203
- * it.
204
- */
205
65
  this.previousLabel = 'Previous month';
206
- /**
207
- * Whether the control refuses changes while still submitting its value.
208
- *
209
- * **Enforced by refusing every request in one place**, which is the seam this
210
- * component already has: `requestValue` is what every path here comes through,
211
- * and gating it once is what stops the next call site from being written
212
- * without the check.
213
- *
214
- * Not `disabled`, which is the confusion worth naming: a disabled control
215
- * submits nothing and leaves the tab order, and a read-only one submits its
216
- * value and stays reachable, which is what a form showing a value somebody
217
- * else set needs.
218
- */
219
66
  this.readOnly = false;
220
- /**
221
- * The date the grid's tab stop is on.
222
- *
223
- * Absent until the grid is used.
224
- */
225
67
  this.focused = '';
226
68
  this.handleKeydown = (event) => {
227
69
  if (!KEYS.has(event.key) || this.disabled) {
228
70
  return;
229
71
  }
230
- /* Grid keys, from the grid.
231
- *
232
- * The listener is on the host, which holds the previous and next buttons as
233
- * well, so without this check Enter and Space pressed on one of those read
234
- * as "choose the focused day" and are then `preventDefault()`ed: the
235
- * button's own activation is canceled and the month never moves. */
236
72
  const path = event.composedPath();
237
73
  const grid = this.renderRoot?.querySelector('[part="grid"]');
238
74
  if (!grid || !path.includes(grid)) {
@@ -261,45 +97,15 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
261
97
  this.moveTo(addDays(from, 6 - ((from.getUTCDay() - this.firstDayOfWeek + 7) % 7)));
262
98
  }
263
99
  else {
264
- // Shift pages by a year, which is the same move twelve months over.
265
100
  const step = event.key === 'PageUp' ? -1 : 1;
266
101
  this.moveTo(addMonths(from, event.shiftKey ? step * 12 : step));
267
102
  }
268
103
  event.preventDefault();
269
104
  };
270
- /**
271
- * Whether the form refuses to submit while nothing is chosen.
272
- */
273
105
  this.required = false;
274
- /**
275
- * How far apart the selectable dates are, in days.
276
- *
277
- * `<input type="date" step>`'s own unit, counted from `min` where there is
278
- * one and from 1 January 1970 where there is not, which is the platform's own
279
- * base. A booking calendar that only takes Mondays is `step="7"` with a `min`
280
- * on a Monday, which `disable-dates` cannot express: it takes a list, and a
281
- * year of Mondays is not a list anybody writes.
282
- *
283
- * One is every date and needs no rule, which is why anything below one is
284
- * treated as one rather than as an error: a step of zero would block every
285
- * date including the one already chosen.
286
- */
287
106
  this.step = 1;
288
107
  }
289
- /**
290
- * The month on show, as `YYYY-MM`. Follows `value`, then the current month.
291
- *
292
- * Separate from `value` on purpose, so paging selects nothing and a consumer
293
- * can drive the view. Reading gives the month on show; writing sets it, and
294
- * `default-month` seeds one the calendar keeps until somebody does.
295
- */
296
108
  get month() {
297
- /* What is on show, which is what the tag says it gives.
298
- *
299
- * `show()` compares the requested month against this before moving, so a
300
- * getter returning `''` while a month is on display leaves that guard dead:
301
- * the first arrow key writes `monthState` and from then on the grid stops
302
- * following `value`. */
303
109
  return this.monthState ?? (this.defaultMonth || toIso(this.derivedMonth).slice(0, 7));
304
110
  }
305
111
  set month(next) {
@@ -307,11 +113,9 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
307
113
  this.monthState = next;
308
114
  this.requestUpdate('month', previous);
309
115
  }
310
- /** The selected dates, in order. */
311
116
  get selected() {
312
117
  return list(this.value);
313
118
  }
314
- /** The range's ends, when there are two and the mode asks for them. */
315
119
  get range() {
316
120
  const dates = this.selected;
317
121
  if (this.mode !== 'range' || dates.length < 2) {
@@ -327,7 +131,6 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
327
131
  }
328
132
  return this.selected.includes(iso);
329
133
  }
330
- /** The month being shown, resolved from `month`, then `value`, then today. */
331
134
  get shown() {
332
135
  const explicit = /^(\d{4})-(\d{2})$/.exec(this.monthState ?? this.defaultMonth);
333
136
  if (explicit) {
@@ -335,30 +138,10 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
335
138
  }
336
139
  return this.derivedMonth;
337
140
  }
338
- /**
339
- * The month to open on when nothing has declared one.
340
- *
341
- * Split out from `shown` so `month` can report what is on display without the
342
- * two calling each other: `month` reading `shown` and `shown` reading `month`
343
- * is a stack overflow.
344
- */
345
141
  get derivedMonth() {
346
142
  const from = toUtc(this.selected[0] ?? '') ?? new Date();
347
143
  return new Date(Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), 1));
348
144
  }
349
- /**
350
- * The date the tab stop sits on, which is where the arrows move from.
351
- *
352
- * It has to be a date the grid actually renders, so the month is compared
353
- * with its year. Comparing the month alone puts the stop on the same date a
354
- * year away, which is rendered nowhere, and the grid is left with no tab stop
355
- * at all and no way into it from the keyboard.
356
- *
357
- * Each candidate is tested in turn rather than the first one winning
358
- * outright. Where the last date focused is in some other month, the selected
359
- * date still holds the stop, and only with neither in view does it fall to
360
- * the first of the month.
361
- */
362
145
  get anchor() {
363
146
  const shown = this.shown;
364
147
  for (const candidate of [toUtc(this.focused), toUtc(this.selected[0] ?? '')]) {
@@ -368,20 +151,11 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
368
151
  }
369
152
  return shown;
370
153
  }
371
- /** Six weeks of dates, so a grid does not change height between months. */
372
154
  weeksOf(month) {
373
155
  const offset = (month.getUTCDay() - this.firstDayOfWeek + 7) % 7;
374
156
  const start = addDays(month, -offset);
375
157
  return Array.from({ length: 6 }, (_, week) => Array.from({ length: 7 }, (_, day) => addDays(start, week * 7 + day)));
376
158
  }
377
- /**
378
- * Whether a date is barred, by bound, by weekday, or by name.
379
- *
380
- * `barred`, not `disabled`: `HelixFormElement.disabled` is a boolean about the
381
- * whole control, and a method and a flag cannot share a name. "Disabled" on a
382
- * control means one thing everywhere else in the library, so the method is the
383
- * one that moves.
384
- */
385
159
  barred(date) {
386
160
  const iso = toIso(date);
387
161
  if (this.min !== '' && iso < this.min) {
@@ -393,9 +167,6 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
393
167
  if (list(this.disableDays).includes(String(date.getUTCDay()))) {
394
168
  return true;
395
169
  }
396
- /* Counted from `min` where there is one and from the epoch where there is
397
- not, which is what the platform does with a `step` and no `min`. Whole
398
- days, because both ends are midnight UTC and neither carries a time. */
399
170
  const step = Math.max(1, Math.floor(this.step) || 1);
400
171
  if (step > 1) {
401
172
  const base = this.min === '' ? 0 : (asDate(this.min)?.getTime() ?? 0);
@@ -406,14 +177,6 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
406
177
  }
407
178
  return list(this.disableDates).includes(iso);
408
179
  }
409
- /**
410
- * Moves the view, announcing it first.
411
- *
412
- * `helix-navigate` and not `helix-change`: paging chooses nothing, and a
413
- * consumer driving the view has to hear that it moved without being told the
414
- * answer did. Refusing the event is how they take the decision, and nothing
415
- * moves.
416
- */
417
180
  show(date) {
418
181
  if (this.disabled) {
419
182
  return;
@@ -440,16 +203,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
440
203
  this.renderRoot.querySelector('[part="cell"][tabindex="0"]')?.focus();
441
204
  });
442
205
  }
443
- /**
444
- * Adds a date to the selection, as the mode allows.
445
- *
446
- * `range` starts over once it holds two, so a third click begins a new range
447
- * rather than extending or replacing an end at random. `multiple` toggles, so
448
- * clicking a chosen date takes it out.
449
- */
450
206
  select(date) {
451
- /* Refused at the source rather than at each cell, so every path in is
452
- covered by one check. */
453
207
  if (this.disabled) {
454
208
  return;
455
209
  }
@@ -468,13 +222,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
468
222
  else {
469
223
  next = [iso];
470
224
  }
471
- /* Focus moves whatever happens, because the reader pressed there and the
472
- roving tab stop is about where they are rather than about what is
473
- chosen. The selection is the part that can be refused. */
474
225
  this.focused = iso;
475
- /* Announced before it is written, so a listener calling `preventDefault()`
476
- refuses a date that has not been chosen yet. `requestChange` is the one
477
- path every stateful change in the library goes through. */
478
226
  this.requestValue(next.join(','));
479
227
  }
480
228
  connectedCallback() {
@@ -485,36 +233,18 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
485
233
  super.disconnectedCallback();
486
234
  this.removeEventListener('keydown', this.handleKeydown);
487
235
  }
488
- /** Refuses every change while read-only. See `readOnly`. */
489
236
  requestValue(next) {
490
237
  if (this.readOnly) {
491
238
  return false;
492
239
  }
493
240
  return super.requestValue(next);
494
241
  }
495
- /**
496
- * Reports "required and empty", or clears it.
497
- *
498
- * Written out rather than taken from `MixinField`, which is where the seven
499
- * form controls get this. That mixin brings a label, a hint and a feedback
500
- * region with it, and this is a grid rather than a field: it is composed
501
- * *into* the field that has those, so taking the mixin would give one control
502
- * two of each. What is shared is five lines, and the anchor is what the
503
- * browser points its bubble at.
504
- */
505
242
  reportRequired() {
506
243
  const anchor = this.renderRoot?.querySelector('[part="grid"]') ?? undefined;
507
244
  if (this.required && this.value === '') {
508
245
  this.internals.setValidity({ valueMissing: true }, 'Choose a date.', anchor);
509
246
  return;
510
247
  }
511
- /* **A value the calendar cannot read is bad input.** The grid draws only
512
- what it can parse, so `value="not-a-date"` would otherwise select
513
- nothing, report `:valid` and hand the string to the form.
514
-
515
- `asDate` rather than the shape alone, which is the half `DateInput`'s own
516
- regex still misses: `2026-02-30` matches `YYYY-MM-DD` and is not a day,
517
- and `valueAsDate` reads the parts back after constructing to say so. */
518
248
  const held = this.selected;
519
249
  if (held.length > 0 && held.some((one) => asDate(one) === null)) {
520
250
  this.internals.setValidity({ badInput: true }, this.mode === undefined
@@ -527,46 +257,14 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
527
257
  willUpdate(changed) {
528
258
  super.willUpdate(changed);
529
259
  hostAria(this.internals, { label: this.label, role: 'group' });
530
- /* Announced as off, not merely inert. Removing it from the accessibility
531
- tree would take the dates with it; this says the control exists and
532
- cannot be used, which is the same call `HelixFormElement` documents. */
533
- /* `isDisabled` and not `disabled`, which is the whole reason the getter
534
- exists: a calendar inside a disabled fieldset is disabled and its own
535
- property says nothing about it. Writing `disabled` here also overwrote
536
- the answer `HelixFormElement.willUpdate` had just computed. */
537
260
  this.internals.ariaDisabled = this.isDisabled ? 'true' : null;
538
261
  this.internals.ariaInvalid = this.invalid ? 'true' : null;
539
262
  this.report();
540
263
  }
541
- /**
542
- * Reports validity after render, because the bubble needs somewhere to point.
543
- *
544
- * `updated` and not `willUpdate`: the anchor is a rendered part, and on the
545
- * first pass through `willUpdate` there is no grid yet, so the browser would
546
- * put the message in the corner of the viewport rather than on the calendar.
547
- */
548
264
  updated(changed) {
549
265
  super.updated(changed);
550
266
  this.reportRequired();
551
267
  }
552
- /**
553
- * Reports the dates to the form, one entry each.
554
- *
555
- * `FormData` rather than the comma-joined string the property holds, because
556
- * `mode="multiple"` is genuinely several answers and a server reading
557
- * `dates=2026-08-04,2026-08-06` has to know to split it on a convention this
558
- * library invented. One entry per date is what every other multi-valued
559
- * control on the platform sends, and `range` sends its two the same way, in
560
- * order, so start and end arrive as the pair they already are.
561
- *
562
- * **Nothing at all without a `name`, and that is the mechanism rather than a
563
- * detail.** It is what lets a calendar sit inside something that owns the
564
- * form value, which is how `DatePicker` will hold one. The guard has to be
565
- * explicit here in a way it does not for `Listbox`: a control reporting a
566
- * plain string is skipped by the form when it has no name, but a `FormData`
567
- * carries its own names and its entries are appended whatever the element is
568
- * called, so an unnamed calendar would submit empty-named dates.
569
- */
570
268
  report() {
571
269
  const dates = this.selected;
572
270
  if (this.name === '' || this.disabled || dates.length === 0) {
@@ -579,14 +277,6 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
579
277
  }
580
278
  this.internals.setFormValue(data);
581
279
  }
582
- /**
583
- * How many months are shown, clamped once.
584
- *
585
- * One getter for both readers. Clamping at the call site while the last month
586
- * is identified from the raw `months` renders nothing for a zero, a negative
587
- * or a non-numeric value, and takes the forward control with it, since
588
- * `index === this.months - 1` is never true for `NaN`.
589
- */
590
280
  get monthCount() {
591
281
  return Math.max(1, Math.floor(this.months) || 1);
592
282
  }
@@ -604,19 +294,6 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
604
294
  this.cachedFormats = built;
605
295
  return built;
606
296
  }
607
- /**
608
- * The caret saying which way the month moves, behind a seam.
609
- *
610
- * Slot fallback, so a consumer replaces it by slotting; and a method, so the
611
- * tier above replaces it by overriding. Those answer different people, which
612
- * is the same pairing `Select` records on its indicator.
613
- *
614
- * **One of each is rendered at most**, whatever `months` is set to: the
615
- * controls are drawn on the first month and the last rather than once per
616
- * month, so a duplicate `<slot name="previous-icon">` cannot arise. A second
617
- * slot of the same name is assigned nothing, which would have left one of two
618
- * calendars with no mark and no error.
619
- */
620
297
  renderPreviousMark() {
621
298
  return html `<slot name="previous-icon">
622
299
  <svg part="previous-mark" fill="currentColor" viewBox=${caretLeft.viewBox} aria-hidden="true">
@@ -635,14 +312,8 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
635
312
  const { full, heading, weekday } = this.formats;
636
313
  const weeks = this.weeksOf(month);
637
314
  const anchor = toIso(this.anchor);
638
- /* Today, marked with `aria-current="date"` in the accessibility tree and on
639
- the cell, so a consumer can add a mark from CSS. Computed once per month
640
- rather than per cell. */
641
315
  const today = localToday();
642
316
  const range = this.range;
643
- // One set of controls across the whole run: back on the first month,
644
- // forward on the last. A pair per month would offer six ways to make the
645
- // same move and leave the reader to work out which.
646
317
  const first = index === 0;
647
318
  const last = index === this.monthCount - 1;
648
319
  return html `<div part="month">
@@ -683,17 +354,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
683
354
  const iso = toIso(date);
684
355
  const disabled = this.barred(date);
685
356
  const selected = this.isSelected(iso);
686
- /* A day from a neighboring month is filler, so the grid is a
687
- rectangle, and nothing else. It takes no click and shows no
688
- selection: at `months="2"` the same date is a trailing cell in
689
- one grid and a real cell in the next, so marking both puts one
690
- answer on the page twice and runs the range band through two
691
- rows. The band stops at the last day the grid owns and picks up
692
- in the next, which is where those dates actually are. */
693
357
  const outside = !sameMonth(date, month);
694
- // The cell is the column track and the day is the shape centered
695
- // in it. A range paints the track, so it runs continuously
696
- // between days and stops at the ends of a row on its own.
697
358
  return html `<span
698
359
  part="cell"
699
360
  role="gridcell"
@@ -718,35 +379,16 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
718
379
  render() {
719
380
  const shown = this.shown;
720
381
  const count = this.monthCount;
721
- /* The column count, written out for the tier above.
722
- Data rather than styling, the same arrangement Slider uses for its
723
- fraction: only this element knows how many months there are, and a
724
- stylesheet cannot count children into a `repeat()`. Capped at two, and a
725
- consumer wanting more sets `--date-picker-columns`. */
726
382
  return html `<div part="months" style="--date-picker-column-count: ${Math.min(count, 2)}">
727
383
  ${Array.from({ length: count }, (_, index) => this.renderMonth(addMonths(shown, index), index))}
728
384
  </div>`;
729
385
  }
730
- /**
731
- * The value as a `Date`, at midnight UTC, or `null` while there is no date.
732
- *
733
- * The platform's own pair on `<input type="date">`, and this control holds the
734
- * same `YYYY-MM-DD` string, so a consumer moving from one to the other reads
735
- * the same two properties. UTC because the platform chose UTC: a date that is
736
- * the same date everywhere must not become a different one by crossing a
737
- * timezone, and midnight local would do exactly that for anybody west of
738
- * Greenwich.
739
- *
740
- * `null` while the value is half typed, which is most of the keystrokes a
741
- * reader makes in one of these.
742
- */
743
386
  get valueAsDate() {
744
387
  return asDate(this.value);
745
388
  }
746
389
  set valueAsDate(date) {
747
390
  this.value = fromDate(date);
748
391
  }
749
- /** The value as milliseconds since the epoch, or `NaN` while there is no date. */
750
392
  get valueAsNumber() {
751
393
  return asDate(this.value)?.getTime() ?? Number.NaN;
752
394
  }