@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
|
@@ -12,286 +12,57 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
|
12
12
|
import { html } from 'lit/static-html.js';
|
|
13
13
|
import '../../date-picker/index.js';
|
|
14
14
|
import '../../tag/index.js';
|
|
15
|
-
/** Whether a string is a date the rest of this component can reason about. */
|
|
16
15
|
const isIso = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
17
|
-
|
|
18
|
-
* The dates in a value, which may hold more than one.
|
|
19
|
-
*
|
|
20
|
-
* A range is two and a multiple is any number, comma separated, which is what
|
|
21
|
-
* the calendar announces and what a form field can carry as one string. Empty
|
|
22
|
-
* entries are dropped so a trailing comma while typing is not a date.
|
|
23
|
-
*/
|
|
24
|
-
const dates = (value) =>
|
|
25
|
-
/* The en dash and the spaced hyphen both mean "to", because a range is shown
|
|
26
|
-
that way and what is shown is what gets typed back. An ISO date holds
|
|
27
|
-
hyphens of its own, so only a spaced one separates: `2026-08-04-2026-08-11`
|
|
28
|
-
is not a thing anybody writes, and treating a bare hyphen as a separator
|
|
29
|
-
would take `2026-08-04` apart into three numbers. */
|
|
30
|
-
value
|
|
16
|
+
const dates = (value) => value
|
|
31
17
|
.replace(/\s+[–-]\s+/g, ',')
|
|
32
18
|
.split(',')
|
|
33
19
|
.map((one) => one.trim())
|
|
34
20
|
.filter(Boolean);
|
|
35
|
-
/**
|
|
36
|
-
* @summary A date, typed or picked from a calendar.
|
|
37
|
-
*
|
|
38
|
-
* @description
|
|
39
|
-
* **The calendar is `helix-date-picker`, not a second one written here.** It
|
|
40
|
-
* already knows about months, ranges, disabled days, the first day of the week
|
|
41
|
-
* and every keyboard rule a grid owes a reader, and a picker that reimplemented
|
|
42
|
-
* a tenth of that would be a worse grid that also had to be maintained. This
|
|
43
|
-
* element is the field, the overlay, and the translation between them.
|
|
44
|
-
*
|
|
45
|
-
* **Typed as well as picked, and the typing is the reason it is not
|
|
46
|
-
* `<input type="date">`.** The native control gives a free picker and a free
|
|
47
|
-
* locale and takes almost all styling with it, which is a fair trade until a
|
|
48
|
-
* product needs the calendar to match everything around it. What is kept from
|
|
49
|
-
* the native one is the part worth keeping: a real text field, so a reader who
|
|
50
|
-
* knows the date types it and never opens anything.
|
|
51
|
-
*
|
|
52
|
-
* **The value is `YYYY-MM-DD`, always.** That is what a form submits, what
|
|
53
|
-
* `min` and `max` compare against, and what the calendar speaks. What the field
|
|
54
|
-
* *shows* is the same string: a locale-formatted display would have to be parsed
|
|
55
|
-
* back on every keystroke, and half-typed input does not survive a round trip
|
|
56
|
-
* through a formatter. The same reasoning the sandbox records for its two
|
|
57
|
-
* syntaxes, which is that a document being edited must not be a rendering of
|
|
58
|
-
* something else.
|
|
59
|
-
*
|
|
60
|
-
* **Opening does not commit.** The calendar announces a date, this element takes
|
|
61
|
-
* it, and the overlay shuts, because a single date has nothing more to say. A
|
|
62
|
-
* consumer who owns `value` answers the announcement the way they answer any
|
|
63
|
-
* other control here.
|
|
64
|
-
*
|
|
65
|
-
* @since 0.1.0
|
|
66
|
-
* @status experimental
|
|
67
|
-
* @category forms
|
|
68
|
-
* @aliases DatePicker, DateField, InputDate
|
|
69
|
-
* @documentation https://helix.fusion.dev/components/date-input
|
|
70
|
-
*
|
|
71
|
-
* @slot feedback - What is wrong with it.
|
|
72
|
-
* @slot hint - Help under the field.
|
|
73
|
-
* @slot trigger - The mark on the field saying a calendar opens.
|
|
74
|
-
*
|
|
75
|
-
* @event {Event} change - Announced once the value has moved.
|
|
76
|
-
* @event {HelixBeforeChangeEvent} helix-before-change - Fired before the date changes. Cancelable: refusing it takes the decision and nothing moves.
|
|
77
|
-
* @event {HelixBeforeCloseEvent} helix-before-close - Fired before it closes. Cancelable: refusing it takes the decision and nothing moves.
|
|
78
|
-
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before the calendar opens. Cancelable: refusing it takes the decision and nothing moves.
|
|
79
|
-
* @event {Event} input - Announced once the value has moved.
|
|
80
|
-
* @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
|
|
81
|
-
*
|
|
82
|
-
* @csspart control - The text input.
|
|
83
|
-
* @csspart date - One held date, as a tag, in `multiple`.
|
|
84
|
-
* @csspart date-dismiss - The remove button, when `dismissible`.
|
|
85
|
-
* @csspart date-dismiss-mark - The glyph on the dismiss control, where none is slotted.
|
|
86
|
-
* @csspart date-prefix - The category before the label.
|
|
87
|
-
* @csspart feedback - What is wrong with it.
|
|
88
|
-
* @csspart field - The box holding the text and the trigger.
|
|
89
|
-
* @csspart hint - Help under the field, while nothing is wrong.
|
|
90
|
-
* @csspart label - The question, over the field.
|
|
91
|
-
* @csspart picker - The date picker inside it.
|
|
92
|
-
* @csspart picker-cell - One column of a row, which is what a range paints.
|
|
93
|
-
* @csspart picker-day - The shape inside a cell, which is what a single date paints.
|
|
94
|
-
* @csspart picker-grid - The grid of days.
|
|
95
|
-
* @csspart picker-header - The month name and its controls.
|
|
96
|
-
* @csspart picker-heading - The month and year being shown.
|
|
97
|
-
* @csspart picker-month - One month, its header and its grid.
|
|
98
|
-
* @csspart picker-months - The run of months.
|
|
99
|
-
* @csspart picker-next - The control that moves to the next month.
|
|
100
|
-
* @csspart picker-next-mark - The glyph on the next month control.
|
|
101
|
-
* @csspart picker-previous - The control that moves to the previous month.
|
|
102
|
-
* @csspart picker-previous-mark - The glyph on the previous month control.
|
|
103
|
-
* @csspart picker-week - One row of days.
|
|
104
|
-
* @csspart picker-weekday - A column header naming a day of the week.
|
|
105
|
-
* @csspart rest - How many dates are held beyond the ones shown.
|
|
106
|
-
* @csspart surface - The floating panel.
|
|
107
|
-
* @csspart trigger - The control that opens the calendar.
|
|
108
|
-
* @csspart trigger-icon - The mark on the button that opens the calendar.
|
|
109
|
-
*/
|
|
110
21
|
let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControllerOpen(MixinControllerValue(MixinField(HelixFormElement))) {
|
|
111
22
|
constructor() {
|
|
112
23
|
super(...arguments);
|
|
113
|
-
/* A consumer's `aria-describedby` names ids in their own tree and the control
|
|
114
|
-
is in this shadow root, so the reference has to be carried down as an
|
|
115
|
-
element rather than copied as an id. See `AriaForwardController`: measured
|
|
116
|
-
in Chrome's own tree, the host held the description and the control the
|
|
117
|
-
reader is actually on was exposed without it. */
|
|
118
24
|
this.aria = new AriaForwardController(this, {
|
|
119
25
|
target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
|
|
120
26
|
});
|
|
121
|
-
/**
|
|
122
|
-
* What the browser may autofill here, as one of the platform's own tokens.
|
|
123
|
-
*
|
|
124
|
-
* Empty leaves the attribute off, which is the browser's default and is what
|
|
125
|
-
* lets autofill work at all: hardcoding `off` here means a birthday or a
|
|
126
|
-
* postal code can never be filled in for a reader.
|
|
127
|
-
*/
|
|
128
27
|
this.autocomplete = '';
|
|
129
|
-
/**
|
|
130
|
-
* How a date is written in the field: `short` through `full`, or ISO.
|
|
131
|
-
*
|
|
132
|
-
* **The field renders when it is idle and holds the value when it is being
|
|
133
|
-
* edited.** A written date is a rendering, and a document being edited must
|
|
134
|
-
* not be a rendering of something else: `Intl.DateTimeFormat` formats and does
|
|
135
|
-
* not parse, so `4 August 2026` cannot be typed back, and writing a parser per
|
|
136
|
-
* locale is how a field ends up rejecting what it just displayed.
|
|
137
|
-
*
|
|
138
|
-
* The way out is not to choose between the two but to separate them in time.
|
|
139
|
-
* Idle, the field shows `4 August 2026`, which is what a date is for reading.
|
|
140
|
-
* Focused, it shows `2026-08-04`, which is what a date is for typing, and it
|
|
141
|
-
* goes back on the way out. Nothing parses a rendering, and nothing is
|
|
142
|
-
* read-only, which is what an earlier version of this settled for.
|
|
143
|
-
*
|
|
144
|
-
* A range is the exception that needs no swap: the dash between its two dates
|
|
145
|
-
* is a separator this element understands, so that much reads back either way.
|
|
146
|
-
*
|
|
147
|
-
* `iso` turns the rendering off, for a field whose reader is entering data
|
|
148
|
-
* rather than reading it, or where the ISO form is itself the point.
|
|
149
|
-
*
|
|
150
|
-
* The value is always `YYYY-MM-DD` regardless, because that is what a form
|
|
151
|
-
* submits and what `min` and `max` compare against.
|
|
152
|
-
*/
|
|
153
28
|
this.dateStyle = 'long';
|
|
154
|
-
/**
|
|
155
|
-
* Dates that cannot be chosen, as a comma-separated `YYYY-MM-DD` list.
|
|
156
|
-
*/
|
|
157
29
|
this.disableDates = '';
|
|
158
|
-
/**
|
|
159
|
-
* Days of the week that cannot be chosen, `0` for Sunday through `6`.
|
|
160
|
-
*/
|
|
161
30
|
this.disableDays = '';
|
|
162
|
-
/**
|
|
163
|
-
* First column of the week in the calendar, `0` for Sunday through `6`.
|
|
164
|
-
*/
|
|
165
31
|
this.firstDayOfWeek = 0;
|
|
166
|
-
/**
|
|
167
|
-
* Language tag the calendar names its months and days in.
|
|
168
|
-
*/
|
|
169
32
|
this.locale = '';
|
|
170
|
-
/**
|
|
171
|
-
* Latest date that can be chosen, as `YYYY-MM-DD`.
|
|
172
|
-
*/
|
|
173
33
|
this.max = '';
|
|
174
|
-
/**
|
|
175
|
-
* How many dates are shown as tags before the rest are counted, in `multiple`.
|
|
176
|
-
*
|
|
177
|
-
* `0` is all of them, and is the default: a cap that guesses is worse than no
|
|
178
|
-
* cap, because the number that fits depends on the width of the field and the
|
|
179
|
-
* length of what is in it, neither of which this element knows. What a cap is
|
|
180
|
-
* genuinely for is a field in a row or a table cell, where a set of twelve
|
|
181
|
-
* dates would push everything below it down a line at a time.
|
|
182
|
-
*
|
|
183
|
-
* The remainder is counted rather than hidden. `+3` is a fact a reader can
|
|
184
|
-
* act on, where a row that silently stops is a field that appears to have lost
|
|
185
|
-
* what was put in it, and the count is what tells them there is more to see.
|
|
186
|
-
*/
|
|
187
34
|
this.maxTags = 0;
|
|
188
|
-
/**
|
|
189
|
-
* Earliest date that can be chosen, as `YYYY-MM-DD`.
|
|
190
|
-
*/
|
|
191
35
|
this.min = '';
|
|
192
|
-
/**
|
|
193
|
-
* How many months the calendar shows, side by side.
|
|
194
|
-
*
|
|
195
|
-
* Worth more than one mainly for a range, where picking a start in March and
|
|
196
|
-
* an end in April otherwise means paging between them and losing sight of the
|
|
197
|
-
* first. The panel is sized by its content, so this widens it.
|
|
198
|
-
*/
|
|
199
36
|
this.months = 1;
|
|
200
|
-
/**
|
|
201
|
-
* What the field says before a date is in it.
|
|
202
|
-
*/
|
|
203
37
|
this.placeholder = 'YYYY-MM-DD';
|
|
204
|
-
/**
|
|
205
|
-
* Whether the control refuses changes while still submitting its value.
|
|
206
|
-
*
|
|
207
|
-
* **Enforced by refusing every request in one place**, which is the seam this
|
|
208
|
-
* component already has: `requestValue` is what every path here comes through,
|
|
209
|
-
* and gating it once is what stops the next call site from being written
|
|
210
|
-
* without the check.
|
|
211
|
-
*
|
|
212
|
-
* Not `disabled`, which is the confusion worth naming: a disabled control
|
|
213
|
-
* submits nothing and leaves the tab order, and a read-only one submits its
|
|
214
|
-
* value and stays reachable, which is what a form showing a value somebody
|
|
215
|
-
* else set needs.
|
|
216
|
-
*/
|
|
217
38
|
this.readOnly = false;
|
|
218
|
-
/**
|
|
219
|
-
* How far apart the selectable dates are, in days.
|
|
220
|
-
*
|
|
221
|
-
* `<input type="date" step>`'s own unit, handed to the calendar this renders
|
|
222
|
-
* so the field and the grid agree about which dates exist. Counted from `min`
|
|
223
|
-
* where there is one and from 1 January 1970 where there is not, which is the
|
|
224
|
-
* platform's base.
|
|
225
|
-
*/
|
|
226
39
|
this.step = 1;
|
|
227
|
-
/**
|
|
228
|
-
* The overlay, placed against the field rather than the trigger.
|
|
229
|
-
*
|
|
230
|
-
* Against the field because that is the box a reader sees as the control: a
|
|
231
|
-
* calendar hung off a small button at one end of a wide field reads as
|
|
232
|
-
* belonging to the button. `Select` anchors to its trigger for the opposite
|
|
233
|
-
* reason, its trigger being the whole control.
|
|
234
|
-
*/
|
|
235
40
|
this.panel = new PopoverController(this, {
|
|
236
41
|
anchor: () => this.renderRoot?.querySelector('[part="field"]'),
|
|
237
|
-
/* The surface *and* the trigger.
|
|
238
|
-
*
|
|
239
|
-
* A control is not always one element: the button that opens this panel is
|
|
240
|
-
* part of it, and a press on it is not a press outside. Scoped to the
|
|
241
|
-
* surface alone, `pointerdown` on the trigger closes the panel and the
|
|
242
|
-
* trigger's own `toggleOpen` reopens it on the `click` that follows, so the
|
|
243
|
-
* panel can never be closed by the button that opened it.
|
|
244
|
-
* `DismissController` documents this failure and takes a list for it. */
|
|
245
42
|
boundary: () => [
|
|
246
43
|
this.renderRoot?.querySelector('[part="surface"]'),
|
|
247
44
|
this.renderRoot?.querySelector('[part="trigger"]'),
|
|
248
45
|
],
|
|
249
|
-
/* Wider than the four every other panel here takes. Those sit against a
|
|
250
|
-
control the same width as themselves and read as an extension of it; this
|
|
251
|
-
one is a calendar of its own size hanging off a field, so the gap is what
|
|
252
|
-
says the two are separate things rather than one control that grew. */
|
|
253
46
|
offset: () => 8,
|
|
254
47
|
onDismiss: () => this.requestOpen(false),
|
|
255
48
|
placement: 'bottom-start',
|
|
256
49
|
surface: () => this.renderRoot?.querySelector('[part="surface"]'),
|
|
257
50
|
});
|
|
258
|
-
/**
|
|
259
|
-
* Whether the caret is in the field, which is when it holds the value.
|
|
260
|
-
*/
|
|
261
51
|
this.editing = false;
|
|
262
|
-
/**
|
|
263
|
-
* What is being typed but not yet held, for `multiple` only.
|
|
264
|
-
*
|
|
265
|
-
* The other modes type into the value directly, because one date and a range
|
|
266
|
-
* are both a whole answer at the moment they are complete. A multiple is not:
|
|
267
|
-
* it is a set being added to, so the field needs somewhere to hold a date
|
|
268
|
-
* before it joins the set and somewhere to leave a half-typed one that is not
|
|
269
|
-
* a date yet.
|
|
270
|
-
*/
|
|
271
52
|
this.draft = '';
|
|
272
|
-
/** Relayed under this element's name, so a consumer hears one control. */
|
|
273
53
|
this.handleInput = (event) => {
|
|
274
54
|
event.stopPropagation();
|
|
275
|
-
// Typing is editing, whatever the focus did. The field has to hold the value
|
|
276
|
-
// while it is being changed, or a keystroke lands in a rendering.
|
|
277
55
|
this.editing = true;
|
|
278
56
|
const typed = event.target.value;
|
|
279
57
|
if (this.mode === 'multiple') {
|
|
280
58
|
this.draft = typed;
|
|
281
59
|
return;
|
|
282
60
|
}
|
|
283
|
-
/* Stored as commas once every date in it is whole, and kept exactly as typed
|
|
284
|
-
until then. A form should submit `2026-08-04,2026-08-11` however the dash
|
|
285
|
-
was written, and the dash is a reading of that rather than a second value
|
|
286
|
-
shape to support. Half-typed text stays verbatim, for the reason the class
|
|
287
|
-
gives about a document being edited: normalizing `2026-08-0` would take
|
|
288
|
-
the caret away mid-date. */
|
|
289
61
|
const held = dates(typed);
|
|
290
62
|
const whole = held.length > 0 && held.every(isIso);
|
|
291
63
|
this.requestValue(whole ? held.join(',') : typed);
|
|
292
64
|
this.restore();
|
|
293
65
|
};
|
|
294
|
-
/** Adds what was typed to the set, or takes the last one back out. */
|
|
295
66
|
this.handleDraftKey = (event) => {
|
|
296
67
|
if (this.mode !== 'multiple') {
|
|
297
68
|
return;
|
|
@@ -305,13 +76,9 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
305
76
|
this.draft = '';
|
|
306
77
|
}
|
|
307
78
|
else if (event.key === 'Backspace' && this.draft === '' && held.length > 0) {
|
|
308
|
-
/* The convention every tag field has: an empty field and a backspace takes
|
|
309
|
-
the last one back, so a mistake costs one key rather than a hunt for a
|
|
310
|
-
small control. */
|
|
311
79
|
this.requestValue(held.slice(0, -1).join(','));
|
|
312
80
|
}
|
|
313
81
|
};
|
|
314
|
-
/** Takes one date back out of the set. */
|
|
315
82
|
this.removeDate = (date) => {
|
|
316
83
|
this.requestValue(dates(this.value)
|
|
317
84
|
.filter((one) => one !== date)
|
|
@@ -322,16 +89,11 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
322
89
|
const { value } = event.detail;
|
|
323
90
|
this.requestValue(value);
|
|
324
91
|
this.restore();
|
|
325
|
-
/* A single date has nothing more to say, so the panel gets out of the way.
|
|
326
|
-
A range or a multiple has: the reader is still choosing, and shutting the
|
|
327
|
-
calendar under them after the first date is taking the control away
|
|
328
|
-
mid-answer. */
|
|
329
92
|
if (this.mode === undefined) {
|
|
330
93
|
this.requestOpen(false);
|
|
331
94
|
this.updateComplete.then(() => this.returnFocus());
|
|
332
95
|
}
|
|
333
96
|
};
|
|
334
|
-
/** Escape shuts the calendar without moving the date. */
|
|
335
97
|
this.handleKeydown = (event) => {
|
|
336
98
|
if (event.key === 'Escape' && this.open) {
|
|
337
99
|
event.stopPropagation();
|
|
@@ -339,64 +101,12 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
339
101
|
this.returnFocus();
|
|
340
102
|
}
|
|
341
103
|
};
|
|
342
|
-
/**
|
|
343
|
-
* Accessible name for the control that opens the calendar.
|
|
344
|
-
*/
|
|
345
104
|
this.triggerLabel = 'Choose a date';
|
|
346
|
-
/**
|
|
347
|
-
* Puts the caret back in the field without the field reopening the calendar.
|
|
348
|
-
*
|
|
349
|
-
* Focus opens the calendar, so handing focus back after a pick reopens the
|
|
350
|
-
* thing that was just answered. `relatedTarget` was the guard and it is not
|
|
351
|
-
* enough on its own: it catches a hand-off from the trigger, and it does not
|
|
352
|
-
* catch this, because the cell that had focus is gone by the time the caret
|
|
353
|
-
* moves and the document has fallen back to something outside this element.
|
|
354
|
-
*
|
|
355
|
-
* **Reasoned rather than measured, and worth saying which.** In a focused
|
|
356
|
-
* document: the reader presses a day, the cell has focus, the panel is hidden,
|
|
357
|
-
* the browser moves focus off the removed cell to the body, and the caret is
|
|
358
|
-
* then put back here with `relatedTarget` pointing at the body, which is
|
|
359
|
-
* outside this element and reads as an arrival. Every step is ordinary browser
|
|
360
|
-
* behavior and none of it can be exercised here: `focus()` dispatches no
|
|
361
|
-
* event in an unfocused document, which is what both the test runtime and the
|
|
362
|
-
* available browser pane are. A spec for this passes with the guard removed,
|
|
363
|
-
* so there is no spec: it would cover nothing while looking like proof.
|
|
364
|
-
*
|
|
365
|
-
* A flag rather than a better guess, because this element is the one moving
|
|
366
|
-
* the focus and is the only thing that can know so. Cleared synchronously,
|
|
367
|
-
* since `focus()` dispatches before it returns.
|
|
368
|
-
*/
|
|
369
105
|
this.returning = false;
|
|
370
106
|
this.handleBlur = () => {
|
|
371
107
|
this.editing = false;
|
|
372
108
|
};
|
|
373
|
-
/*
|
|
374
|
-
* Focusing the field opens the calendar.
|
|
375
|
-
*
|
|
376
|
-
* A reader who reaches this control is choosing a date, and making them find
|
|
377
|
-
* the trigger afterwards is a second action for something they have already
|
|
378
|
-
* asked for. Typing still works with it up, because the calendar follows the
|
|
379
|
-
* field rather than the other way round.
|
|
380
|
-
*
|
|
381
|
-
* **Only where focus arrived from outside this element**, which is the whole
|
|
382
|
-
* of the difficulty: picking a date shuts the panel and puts the caret back in
|
|
383
|
-
* the field, so an unguarded version reopens the calendar the reader just
|
|
384
|
-
* answered. `relatedTarget` is where focus came from, so a hand-off from the
|
|
385
|
-
* calendar or the trigger is left alone and only a genuine arrival opens
|
|
386
|
-
* anything.
|
|
387
|
-
*
|
|
388
|
-
* On the input rather than on the field, too. Focus does not bubble, so a
|
|
389
|
-
* listener here covers the text and not the trigger, which is what keeps
|
|
390
|
-
* pressing the trigger from opening the panel on focus and then shutting it
|
|
391
|
-
* again on the click.
|
|
392
|
-
*/
|
|
393
109
|
this.handleFocus = (event) => {
|
|
394
|
-
/* Not while the caret is being handed back. Picking a date returns focus to
|
|
395
|
-
the field, and treating that as editing puts the field into the shape it
|
|
396
|
-
takes for typing: a reader picks 4 August 2026 from the calendar and the
|
|
397
|
-
field answers `2026-08-04`. Focus is a good enough signal for editing
|
|
398
|
-
everywhere except the one moment this element moved the focus itself, and
|
|
399
|
-
it already knows when that is. */
|
|
400
110
|
this.editing = !this.returning;
|
|
401
111
|
const from = event.relatedTarget;
|
|
402
112
|
if (this.open || this.isDisabled || this.returning) {
|
|
@@ -414,26 +124,14 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
414
124
|
}
|
|
415
125
|
return this.renderRoot.querySelector('[part="control"]');
|
|
416
126
|
}
|
|
417
|
-
/**
|
|
418
|
-
* What the calendar should show, which is only ever a whole date.
|
|
419
|
-
*
|
|
420
|
-
* Half-typed input reaches here on every keystroke, and `2026-08-1` is not a
|
|
421
|
-
* date: handing it to the calendar would move the month under a reader
|
|
422
|
-
* mid-word. So the grid follows the field only once the field says something
|
|
423
|
-
* complete.
|
|
424
|
-
*/
|
|
425
127
|
get picked() {
|
|
426
128
|
const held = dates(this.value);
|
|
427
129
|
return held.length > 0 && held.every(isIso) ? held.join(',') : '';
|
|
428
130
|
}
|
|
429
|
-
/** One date, written the way `dateStyle` asks for. */
|
|
430
131
|
written(date) {
|
|
431
132
|
if (this.dateStyle === 'iso' || !isIso(date)) {
|
|
432
133
|
return date;
|
|
433
134
|
}
|
|
434
|
-
/* UTC, because the dates are UTC throughout: a date parsed in a zone behind
|
|
435
|
-
Greenwich renders as the day before, which is the classic off-by-one that
|
|
436
|
-
makes a calendar disagree with the field beside it. */
|
|
437
135
|
const at = new Date(`${date}T00:00:00Z`);
|
|
438
136
|
return Number.isNaN(at.getTime())
|
|
439
137
|
? date
|
|
@@ -442,50 +140,24 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
442
140
|
timeZone: 'UTC',
|
|
443
141
|
}).format(at);
|
|
444
142
|
}
|
|
445
|
-
/*
|
|
446
|
-
* What the field shows, which is the value in every mode but one.
|
|
447
|
-
*
|
|
448
|
-
* A range reads `2026-08-04 – 2026-08-11`, because two dates separated by a
|
|
449
|
-
* comma is a list and a range is a span: the comma says "and these", the dash
|
|
450
|
-
* says "through". It is the one place this component shows something other
|
|
451
|
-
* than the value it holds, and it is safe because the dash reads back: `dates`
|
|
452
|
-
* takes a spaced dash as a separator, so what is shown is what can be typed.
|
|
453
|
-
*
|
|
454
|
-
* `multiple` shows nothing here at all. Its dates are tags beside the input
|
|
455
|
-
* and the input is a draft.
|
|
456
|
-
*/
|
|
457
143
|
get display() {
|
|
458
144
|
if (this.mode === 'multiple') {
|
|
459
145
|
return this.draft;
|
|
460
146
|
}
|
|
461
147
|
const held = dates(this.value);
|
|
462
|
-
/* Being edited, the field is the value. The rendering is what it shows when
|
|
463
|
-
nobody is in it, so there is never a moment where what is on screen is
|
|
464
|
-
something this element could not read back. */
|
|
465
148
|
if (this.editing || this.dateStyle === 'iso') {
|
|
466
149
|
return this.mode === 'range' && held.length === 2 ? held.join(' – ') : this.value;
|
|
467
150
|
}
|
|
468
151
|
const written = held.map((one) => this.written(one));
|
|
469
152
|
return this.mode === 'range' ? written.join(' – ') : written.join(', ');
|
|
470
153
|
}
|
|
471
|
-
/** The dates that get a tag, which is all of them until `maxTags` says not. */
|
|
472
154
|
get shownDates() {
|
|
473
155
|
const held = dates(this.value);
|
|
474
156
|
return this.maxTags > 0 ? held.slice(0, this.maxTags) : held;
|
|
475
157
|
}
|
|
476
|
-
/** How many are held beyond the ones shown. */
|
|
477
158
|
get restCount() {
|
|
478
159
|
return Math.max(0, dates(this.value).length - this.shownDates.length);
|
|
479
160
|
}
|
|
480
|
-
/**
|
|
481
|
-
* Puts the field back where the answer differs from what was typed.
|
|
482
|
-
*
|
|
483
|
-
* `requestValue` is the mixin's, and it announces and decides; this is the
|
|
484
|
-
* other half, which `Input` needs for the same reason: the native control has
|
|
485
|
-
* already changed, so wherever the answer does not follow it, it has to be
|
|
486
|
-
* restored. A refusal and a consumer who owns `value` and has not given a new
|
|
487
|
-
* one are the same thing from here.
|
|
488
|
-
*/
|
|
489
161
|
restore() {
|
|
490
162
|
const control = this.control;
|
|
491
163
|
if (control && control.value !== this.display) {
|
|
@@ -493,21 +165,12 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
493
165
|
}
|
|
494
166
|
this.sync();
|
|
495
167
|
}
|
|
496
|
-
/** Reports the value and the validity the host stands behind. */
|
|
497
168
|
sync() {
|
|
498
169
|
this.internals.setFormValue(this.value);
|
|
499
170
|
const control = this.control;
|
|
500
171
|
if (!control) {
|
|
501
172
|
return;
|
|
502
173
|
}
|
|
503
|
-
/* Bounds are checked here rather than left to the input, which is `text`
|
|
504
|
-
and has no idea what a date is: `min` and `max` on a text input do
|
|
505
|
-
nothing, and `type="date"` is what was given up to own the calendar.
|
|
506
|
-
String comparison is enough because ISO dates sort lexically, which is
|
|
507
|
-
the whole reason the value is that shape. */
|
|
508
|
-
/* Every date in the value, not the value itself: a range is two of them and
|
|
509
|
-
a multiple is any number, so a string comparison against `min` would be
|
|
510
|
-
comparing a bound to a list. */
|
|
511
174
|
const held = dates(this.value);
|
|
512
175
|
let message = '';
|
|
513
176
|
if (held.length > 0 && !held.every(isIso)) {
|
|
@@ -533,7 +196,6 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
533
196
|
}
|
|
534
197
|
this.refreshFeedback();
|
|
535
198
|
}
|
|
536
|
-
/** Refuses every change while read-only. See `readOnly`. */
|
|
537
199
|
requestValue(next) {
|
|
538
200
|
if (this.readOnly) {
|
|
539
201
|
return false;
|
|
@@ -542,24 +204,13 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
542
204
|
}
|
|
543
205
|
firstUpdated(changed) {
|
|
544
206
|
super.firstUpdated(changed);
|
|
545
|
-
// Whatever the markup arrived with, so a form knows before anything is typed.
|
|
546
207
|
this.sync();
|
|
547
208
|
}
|
|
548
209
|
updated(changed) {
|
|
549
210
|
super.updated(changed);
|
|
550
|
-
// Every update, not only the ones that name `value`: see the note on
|
|
551
|
-
// `ColorInput.updated`.
|
|
552
211
|
this.sync();
|
|
553
|
-
/* Driven from here rather than from the toggle, because `open` is a property
|
|
554
|
-
a consumer may set directly and the panel has to follow either way. The
|
|
555
|
-
controller is idempotent, so arriving twice for the same open costs
|
|
556
|
-
nothing. Select drives it the same way, and without this the surface stays
|
|
557
|
-
`display: none`: a `popover` element is hidden until something shows it,
|
|
558
|
-
so the calendar rendered its forty-two cells into a box of nothing. */
|
|
559
212
|
if (this.open) {
|
|
560
213
|
this.panel.show();
|
|
561
|
-
// Only on the update that opened it, or every keystroke while it is up
|
|
562
|
-
// would steal focus back into the grid.
|
|
563
214
|
if (changed.has('open')) {
|
|
564
215
|
this.renderRoot?.querySelector('[part="picker"]')?.focus();
|
|
565
216
|
}
|
|
@@ -573,19 +224,9 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
573
224
|
this.control?.focus();
|
|
574
225
|
this.returning = false;
|
|
575
226
|
}
|
|
576
|
-
/** Puts the caret in the field, the way a label pointing at one would. */
|
|
577
227
|
focus(options) {
|
|
578
228
|
this.control?.focus(options);
|
|
579
229
|
}
|
|
580
|
-
/**
|
|
581
|
-
* The calendar the field opens, behind a seam the styled tier can move.
|
|
582
|
-
*
|
|
583
|
-
* A method rather than a fragment inline, so a tier above can replace, wrap
|
|
584
|
-
* or omit it without reimplementing the whole template. The tag is
|
|
585
|
-
* interpolated rather than written, so `tier` resolves it: this renders the
|
|
586
|
-
* headless element, and a styled subclass that sets `tier` renders the styled
|
|
587
|
-
* one from the same template.
|
|
588
|
-
*/
|
|
589
230
|
renderPicker() {
|
|
590
231
|
return html `<${this.tag('date-picker')}
|
|
591
232
|
part="picker"
|
|
@@ -609,15 +250,6 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
609
250
|
@input=${this.stopInner}
|
|
610
251
|
></${this.tag('date-picker')}>`;
|
|
611
252
|
}
|
|
612
|
-
/**
|
|
613
|
-
* One chosen date, as a tag, behind a seam the styled tier can move.
|
|
614
|
-
*
|
|
615
|
-
* A method rather than a fragment inline, so a tier above can replace, wrap
|
|
616
|
-
* or omit it without reimplementing the whole template. The tag is
|
|
617
|
-
* interpolated rather than written, so `tier` resolves it: this renders the
|
|
618
|
-
* headless element, and a styled subclass that sets `tier` renders the styled
|
|
619
|
-
* one from the same template.
|
|
620
|
-
*/
|
|
621
253
|
renderTag(date) {
|
|
622
254
|
return html `<${this.tag('tag')}
|
|
623
255
|
part="date"
|
|
@@ -632,21 +264,6 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
632
264
|
>${this.written(date)}</${this.tag('tag')}
|
|
633
265
|
>`;
|
|
634
266
|
}
|
|
635
|
-
/**
|
|
636
|
-
* The mark on the button that opens the calendar, behind a seam.
|
|
637
|
-
*
|
|
638
|
-
* Slot fallback, so a consumer replaces it by slotting; and a method, so the
|
|
639
|
-
* tier above replaces it by overriding.
|
|
640
|
-
*
|
|
641
|
-
* **A named import rather than `<helix-icon>`**, which loses on three counts.
|
|
642
|
-
* `helix-icon` indexes the whole icon map at runtime, `icons[this.name]`, and
|
|
643
|
-
* no bundler can shake a runtime lookup, so every consumer of any component
|
|
644
|
-
* that renders one ships all 50kB of path data; a named import ships one
|
|
645
|
-
* path. The import is checked when the package compiles, where a `name`
|
|
646
|
-
* string is checked only by a later name lookup. And `helix-icon` is not
|
|
647
|
-
* registered on the headless tier at all, which registers `helix-headless-icon`,
|
|
648
|
-
* so a headless consumer gets an inert unknown element and no mark whatsoever.
|
|
649
|
-
*/
|
|
650
267
|
renderTrigger() {
|
|
651
268
|
return html `<slot name="trigger"><svg aria-hidden="true" fill="currentColor" part="trigger-icon" viewBox=${calendarBlank.viewBox}>
|
|
652
269
|
${unsafeSVG(calendarBlank.content)}
|
|
@@ -696,26 +313,12 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinControlle
|
|
|
696
313
|
</div>
|
|
697
314
|
${this.renderHint()}${this.renderFeedback()}`;
|
|
698
315
|
}
|
|
699
|
-
/**
|
|
700
|
-
* The value as a `Date`, at midnight UTC, or `null` while there is no date.
|
|
701
|
-
*
|
|
702
|
-
* The platform's own pair on `<input type="date">`, and this control holds the
|
|
703
|
-
* same `YYYY-MM-DD` string, so a consumer moving from one to the other reads
|
|
704
|
-
* the same two properties. UTC because the platform chose UTC: a date that is
|
|
705
|
-
* the same date everywhere must not become a different one by crossing a
|
|
706
|
-
* timezone, and midnight local would do exactly that for anybody west of
|
|
707
|
-
* Greenwich.
|
|
708
|
-
*
|
|
709
|
-
* `null` while the value is half typed, which is most of the keystrokes a
|
|
710
|
-
* reader makes in one of these.
|
|
711
|
-
*/
|
|
712
316
|
get valueAsDate() {
|
|
713
317
|
return asDate(this.value);
|
|
714
318
|
}
|
|
715
319
|
set valueAsDate(date) {
|
|
716
320
|
this.value = fromDate(date);
|
|
717
321
|
}
|
|
718
|
-
/** The value as milliseconds since the epoch, or `NaN` while there is no date. */
|
|
719
322
|
get valueAsNumber() {
|
|
720
323
|
return asDate(this.value)?.getTime() ?? Number.NaN;
|
|
721
324
|
}
|