@fusion.dev/helix-headless 0.1.0 → 0.3.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.
- package/custom-elements.json +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- 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
|
}
|