@lekoala/date-picker 0.1.1 → 0.2.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/README.md +27 -4
- package/custom-elements.json +44 -0
- package/dist/date-picker.css +111 -12
- package/dist/date-picker.js +162 -13
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +125 -26
- package/dist/types/date-calendar.d.ts +3 -0
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +24 -0
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/intl.d.ts +9 -0
- package/dist/types/intl.d.ts.map +1 -1
- package/dist/types/time.d.ts +8 -0
- package/dist/types/time.d.ts.map +1 -0
- package/docs/API.md +47 -0
- package/docs/DECISIONS.md +8 -1
- package/docs/ROADMAP.md +3 -2
- package/docs/USE_CASES.md +14 -1
- package/package.json +2 -2
- package/src/date-calendar.js +42 -7
- package/src/date-picker.css +111 -12
- package/src/date-picker.js +108 -5
- package/src/index.js +1 -0
- package/src/intl.js +35 -0
- package/src/time.js +39 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Small, native-first date picker and inline calendar primitives.
|
|
4
4
|
|
|
5
|
-
> v0.
|
|
5
|
+
> v0.2: the interaction contract, event names, public styling tokens and the component API are frozen — see [docs/DECISIONS.md](docs/DECISIONS.md). Internal DOM and class names are `.dp-*` implementation details and stay private.
|
|
6
6
|
|
|
7
7
|
This project fills the gap between a normal editable date field and a full scheduling calendar. A plain date input remains the right answer for simple CMS/editing forms. This component becomes useful when a date has context: availability, disabled days, annotations, remote state, start/end relationships, or a mini calendar that navigates another view.
|
|
8
8
|
|
|
@@ -77,6 +77,24 @@ const cleanup = linkDateRange(startPicker, endPicker);
|
|
|
77
77
|
|
|
78
78
|
The relationship applies `end >= start` and `start <= end` as effective bounds. Neither picker knows about its sibling.
|
|
79
79
|
|
|
80
|
+
## Date + time
|
|
81
|
+
|
|
82
|
+
A date pairs with one or two native time companions. The picker owns the civil date; the time inputs own their civil time values:
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<date-picker>
|
|
86
|
+
<input name="appointment[date]">
|
|
87
|
+
<input type="time" data-time-start name="appointment[time]">
|
|
88
|
+
</date-picker>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
appointment[date] = 2026-09-10
|
|
93
|
+
appointment[time] = 09:30
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The application/server combines these civil values and applies the relevant timezone when a datetime/instant is needed. The picker never constructs a combined datetime value.
|
|
97
|
+
|
|
80
98
|
## Availability and per-day metadata
|
|
81
99
|
|
|
82
100
|
The calendar accepts a source function or `{ load() }` object with the same abortable shape used elsewhere in LeKoala components:
|
|
@@ -194,7 +212,7 @@ The default ESM exports point at `src/`, matching the native-first development s
|
|
|
194
212
|
|
|
195
213
|
## Scope
|
|
196
214
|
|
|
197
|
-
Included in v0.
|
|
215
|
+
Included in v0.2:
|
|
198
216
|
|
|
199
217
|
- inline single-date selection;
|
|
200
218
|
- mini-calendar/navigation-only use;
|
|
@@ -205,15 +223,20 @@ Included in v0.1:
|
|
|
205
223
|
- presentation-only range band (`highlightedRange`);
|
|
206
224
|
- shared-calendar two-field range picker (`<date-picker range>`);
|
|
207
225
|
- linked start/end pickers (`linkDateRange`);
|
|
226
|
+
- native time companions (one or two `input[type=time]` beside the date);
|
|
227
|
+
- `Intl` month/year control order + `month-format="short"`;
|
|
228
|
+
- RTL through the inherited `dir` (no `isRTL` option);
|
|
229
|
+
- readable keyboard focus on disabled/selected days;
|
|
230
|
+
- compact-consumer CSS tokens (`--dp-header-control-size`, `--dp-day-radius`, …);
|
|
208
231
|
- async per-day state with cancellation;
|
|
209
232
|
- custom day decorations that cannot replace the accessible cell;
|
|
210
233
|
- fixed six-row presentation as an opt-in;
|
|
211
234
|
- ISO week numbers;
|
|
212
235
|
- keyboard and focus contracts.
|
|
213
236
|
|
|
214
|
-
Deliberately out of scope for v0.
|
|
237
|
+
Deliberately out of scope for v0.2:
|
|
215
238
|
|
|
216
|
-
- time
|
|
239
|
+
- custom time-picker UI and combined datetime values (only native `input[type=time]` companions are supported);
|
|
217
240
|
- timezones;
|
|
218
241
|
- in-place single-calendar range *selection interaction* (use the two-field picker);
|
|
219
242
|
- multi-date selection;
|
package/custom-elements.json
CHANGED
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "selection"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "month-format"
|
|
40
43
|
}
|
|
41
44
|
],
|
|
42
45
|
"members": [
|
|
@@ -72,6 +75,10 @@
|
|
|
72
75
|
"kind": "getter",
|
|
73
76
|
"name": "selection"
|
|
74
77
|
},
|
|
78
|
+
{
|
|
79
|
+
"kind": "getter",
|
|
80
|
+
"name": "monthFormat"
|
|
81
|
+
},
|
|
75
82
|
{
|
|
76
83
|
"kind": "getter",
|
|
77
84
|
"name": "highlightedRange"
|
|
@@ -190,6 +197,21 @@
|
|
|
190
197
|
},
|
|
191
198
|
{
|
|
192
199
|
"name": "--dp-gap"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "--dp-header-control-size"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "--dp-header-block-gap"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "--dp-nav-gap"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "--dp-year-inline-size"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "--dp-day-radius"
|
|
193
215
|
}
|
|
194
216
|
]
|
|
195
217
|
}
|
|
@@ -228,6 +250,9 @@
|
|
|
228
250
|
},
|
|
229
251
|
{
|
|
230
252
|
"name": "open-on-focus"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"name": "month-format"
|
|
231
256
|
}
|
|
232
257
|
],
|
|
233
258
|
"members": [
|
|
@@ -251,6 +276,10 @@
|
|
|
251
276
|
"kind": "getter",
|
|
252
277
|
"name": "locale"
|
|
253
278
|
},
|
|
279
|
+
{
|
|
280
|
+
"kind": "getter",
|
|
281
|
+
"name": "monthFormat"
|
|
282
|
+
},
|
|
254
283
|
{
|
|
255
284
|
"kind": "getter",
|
|
256
285
|
"name": "min"
|
|
@@ -347,6 +376,21 @@
|
|
|
347
376
|
},
|
|
348
377
|
{
|
|
349
378
|
"name": "--dp-gap"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"name": "--dp-header-control-size"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"name": "--dp-header-block-gap"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"name": "--dp-nav-gap"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"name": "--dp-year-inline-size"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"name": "--dp-day-radius"
|
|
350
394
|
}
|
|
351
395
|
]
|
|
352
396
|
}
|
package/dist/date-picker.css
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
--dp-radius: 0.5rem;
|
|
10
10
|
--dp-day-size: 2.75rem;
|
|
11
11
|
--dp-gap: 0.25rem;
|
|
12
|
+
--dp-header-control-size: 2.25rem;
|
|
13
|
+
--dp-header-block-gap: 0.4rem;
|
|
14
|
+
--dp-nav-gap: calc(var(--dp-gap) * 2.5);
|
|
15
|
+
--dp-year-inline-size: 5.5rem;
|
|
16
|
+
--dp-day-radius: calc(var(--dp-radius) * 0.75);
|
|
12
17
|
color: var(--dp-fg);
|
|
13
18
|
font: inherit;
|
|
14
19
|
}
|
|
@@ -39,22 +44,46 @@ date-calendar {
|
|
|
39
44
|
.dp-calendar-header {
|
|
40
45
|
display: grid;
|
|
41
46
|
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
|
42
|
-
|
|
47
|
+
/* Stretch (not center): once a UA natural height exceeds
|
|
48
|
+
--dp-header-control-size, the row grows to the tallest control and the
|
|
49
|
+
others stretch to match, so month/year/nav stay equal. */
|
|
50
|
+
align-items: stretch;
|
|
43
51
|
gap: var(--dp-gap);
|
|
44
|
-
margin-block-end:
|
|
52
|
+
margin-block-end: var(--dp-header-block-gap);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Locale year-first order (Intl-derived, direction-independent): the flexible
|
|
56
|
+
track stays under the month select so the fixed-width year input never
|
|
57
|
+
stretches. Visual mirroring itself is left to the inherited `dir`. */
|
|
58
|
+
date-calendar[data-year-first] .dp-calendar-header {
|
|
59
|
+
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
.dp-calendar-header .dp-prev {
|
|
48
|
-
margin-inline-start:
|
|
63
|
+
margin-inline-start: var(--dp-nav-gap);
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
.dp-calendar-header :where(button, select, input) {
|
|
52
|
-
min-block-size:
|
|
67
|
+
min-block-size: var(--dp-header-control-size);
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
/* Shared vertical metrics: UA default padding/line-height differ per control
|
|
70
|
+
(up to ~13px apart), which only shows once --dp-header-control-size drops
|
|
71
|
+
below content size. */
|
|
72
|
+
padding-block: 0.15rem;
|
|
53
73
|
border: 1px solid var(--dp-border);
|
|
54
74
|
border-radius: calc(var(--dp-radius) * 0.75);
|
|
55
75
|
background: var(--dp-bg);
|
|
56
76
|
color: var(--dp-fg);
|
|
57
77
|
font: inherit;
|
|
78
|
+
line-height: 1.2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* The select menulist carries UA-internal leading that author padding only
|
|
82
|
+
adds to (and it ignores line-height), so it gets zero author padding.
|
|
83
|
+
Equal outer heights come from the header grid's stretch alignment: the row
|
|
84
|
+
sizes to the tallest control and the others stretch to match. */
|
|
85
|
+
.dp-calendar-header select {
|
|
86
|
+
padding-block: 0;
|
|
58
87
|
}
|
|
59
88
|
|
|
60
89
|
.dp-calendar-header select,
|
|
@@ -65,16 +94,20 @@ date-calendar {
|
|
|
65
94
|
|
|
66
95
|
.dp-month-select {
|
|
67
96
|
inline-size: 100%;
|
|
97
|
+
/* Long month names in a narrow track (compact consumers, long locales)
|
|
98
|
+
would paint under the native caret: truncate with an ellipsis instead. */
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
text-overflow: ellipsis;
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
.dp-year-input {
|
|
71
|
-
inline-size:
|
|
104
|
+
inline-size: var(--dp-year-inline-size);
|
|
72
105
|
}
|
|
73
106
|
|
|
74
107
|
.dp-nav {
|
|
75
108
|
display: grid;
|
|
76
109
|
place-items: center;
|
|
77
|
-
inline-size:
|
|
110
|
+
inline-size: var(--dp-header-control-size);
|
|
78
111
|
padding: 0;
|
|
79
112
|
font-size: 1.35rem;
|
|
80
113
|
line-height: 1;
|
|
@@ -88,6 +121,14 @@ date-calendar {
|
|
|
88
121
|
pointer-events: none;
|
|
89
122
|
}
|
|
90
123
|
|
|
124
|
+
/* Inherited RTL mirrors the header layout via the grid; mirror the nav glyphs
|
|
125
|
+
so previous/next keep their visual sense. Actions stay chronological: no
|
|
126
|
+
button swap, no keyboard remapping. */
|
|
127
|
+
[dir="rtl"] .dp-nav svg,
|
|
128
|
+
html[dir="rtl"] .dp-nav svg {
|
|
129
|
+
transform: scaleX(-1);
|
|
130
|
+
}
|
|
131
|
+
|
|
91
132
|
.dp-calendar-header :where(button, select, input):disabled,
|
|
92
133
|
.dp-picker-button:disabled {
|
|
93
134
|
cursor: not-allowed;
|
|
@@ -133,7 +174,7 @@ date-calendar {
|
|
|
133
174
|
.dp-day {
|
|
134
175
|
position: relative;
|
|
135
176
|
border: 1px solid transparent;
|
|
136
|
-
border-radius:
|
|
177
|
+
border-radius: var(--dp-day-radius);
|
|
137
178
|
cursor: pointer;
|
|
138
179
|
outline: none;
|
|
139
180
|
vertical-align: middle;
|
|
@@ -142,10 +183,21 @@ date-calendar {
|
|
|
142
183
|
|
|
143
184
|
.dp-day:focus-visible {
|
|
144
185
|
outline: 2px solid var(--dp-accent);
|
|
145
|
-
outline-offset:
|
|
186
|
+
outline-offset: 2px;
|
|
187
|
+
/* Separation gap between the cell fill and the ring: invisible on plain
|
|
188
|
+
days, but keeps the ring readable on accent-filled days (selected, range
|
|
189
|
+
endpoints) where an accent-on-accent outline would disappear. */
|
|
190
|
+
box-shadow: 0 0 0 2px var(--dp-bg);
|
|
146
191
|
z-index: 1;
|
|
147
192
|
}
|
|
148
193
|
|
|
194
|
+
/* Base tint for days outside the displayed month. It stays before the
|
|
195
|
+
selection states so selected/range endpoints keep their accent-contrast
|
|
196
|
+
text on the accent fill (equal specificity, source order decides). */
|
|
197
|
+
.dp-day[data-outside-month="true"] {
|
|
198
|
+
color: var(--dp-muted);
|
|
199
|
+
}
|
|
200
|
+
|
|
149
201
|
.dp-day[data-selected="true"] {
|
|
150
202
|
background: var(--dp-accent);
|
|
151
203
|
color: var(--dp-accent-fg);
|
|
@@ -155,10 +207,6 @@ date-calendar {
|
|
|
155
207
|
border-color: var(--dp-accent);
|
|
156
208
|
}
|
|
157
209
|
|
|
158
|
-
.dp-day[data-outside-month="true"] {
|
|
159
|
-
color: var(--dp-muted);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
210
|
.dp-day[data-disabled="true"] {
|
|
163
211
|
cursor: not-allowed;
|
|
164
212
|
opacity: 0.42;
|
|
@@ -201,6 +249,17 @@ date-calendar {
|
|
|
201
249
|
font-variant-numeric: tabular-nums;
|
|
202
250
|
}
|
|
203
251
|
|
|
252
|
+
/* A focused disabled day stays discoverable by keyboard: restore full opacity
|
|
253
|
+
so the focus outline renders at full strength, and mark the number with the
|
|
254
|
+
muted tone (same language as outside-month days) instead of the wash-out. */
|
|
255
|
+
.dp-day[data-disabled="true"]:focus-visible {
|
|
256
|
+
opacity: 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.dp-day[data-disabled="true"]:focus-visible .dp-day-number {
|
|
260
|
+
color: var(--dp-muted);
|
|
261
|
+
}
|
|
262
|
+
|
|
204
263
|
.dp-day-extra {
|
|
205
264
|
position: absolute;
|
|
206
265
|
inset-inline: 2px;
|
|
@@ -235,16 +294,56 @@ date-picker > input:not([type="hidden"]) {
|
|
|
235
294
|
min-inline-size: 0;
|
|
236
295
|
}
|
|
237
296
|
|
|
297
|
+
/* Optional native time companions share the date row; authored inputs stay
|
|
298
|
+
consumer-styled, the picker only reserves their grid cells. Time tracks are
|
|
299
|
+
flexible (never `auto`) so a wide UA time control cannot squeeze the date. */
|
|
300
|
+
date-picker:has(> [data-time-start], > [data-time-end]) {
|
|
301
|
+
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
date-picker:has(> [data-time-start]):has(> [data-time-end]) {
|
|
305
|
+
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) minmax(0, 1fr);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
date-picker > input[data-time-start] {
|
|
309
|
+
grid-column: 3;
|
|
310
|
+
grid-row: 1;
|
|
311
|
+
min-inline-size: 0;
|
|
312
|
+
inline-size: 100%;
|
|
313
|
+
margin-inline-start: var(--dp-gap);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
date-picker > input[data-time-end] {
|
|
317
|
+
grid-column: 4;
|
|
318
|
+
grid-row: 1;
|
|
319
|
+
min-inline-size: 0;
|
|
320
|
+
inline-size: 100%;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* From/to read as one joined control: square the inner corners, but only
|
|
324
|
+
when both companions are present so a lone time keeps its own radius. */
|
|
325
|
+
date-picker:has(> [data-time-start]):has(> [data-time-end]) > input[data-time-start] {
|
|
326
|
+
border-start-end-radius: 0;
|
|
327
|
+
border-end-end-radius: 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
date-picker:has(> [data-time-start]):has(> [data-time-end]) > input[data-time-end] {
|
|
331
|
+
border-start-start-radius: 0;
|
|
332
|
+
border-end-start-radius: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
238
335
|
date-picker[range] > input:not([type="hidden"]) {
|
|
239
336
|
grid-column: 1;
|
|
240
337
|
}
|
|
241
338
|
|
|
242
339
|
date-picker[range] > input[data-range-start] {
|
|
243
340
|
grid-row: 1;
|
|
341
|
+
border-end-start-radius: 0;
|
|
244
342
|
}
|
|
245
343
|
|
|
246
344
|
date-picker[range] > input[data-range-end] {
|
|
247
345
|
grid-row: 2;
|
|
346
|
+
border-start-start-radius: 0;
|
|
248
347
|
}
|
|
249
348
|
|
|
250
349
|
.dp-picker-button {
|
package/dist/date-picker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*** @lekoala/date-picker v0.
|
|
1
|
+
/*** @lekoala/date-picker v0.2.0 - https://github.com/lekoala/date-picker ***/
|
|
2
2
|
(() => {
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -387,6 +387,32 @@
|
|
|
387
387
|
timeZone: "UTC"
|
|
388
388
|
}).format(toIntlDate(`${value.slice(0, 7)}-15`));
|
|
389
389
|
}
|
|
390
|
+
function monthYearOrder(locale = "") {
|
|
391
|
+
const resolved = resolveLocale(locale);
|
|
392
|
+
try {
|
|
393
|
+
const parts = new Intl.DateTimeFormat(resolved, {
|
|
394
|
+
calendar: "gregory",
|
|
395
|
+
year: "numeric",
|
|
396
|
+
month: "long",
|
|
397
|
+
timeZone: "UTC"
|
|
398
|
+
}).formatToParts(toIntlDate("2026-09-15"));
|
|
399
|
+
let monthIndex = -1;
|
|
400
|
+
let yearIndex = -1;
|
|
401
|
+
parts.forEach((part, index) => {
|
|
402
|
+
const type = part.type;
|
|
403
|
+
if (type === "month") {
|
|
404
|
+
if (monthIndex < 0)
|
|
405
|
+
monthIndex = index;
|
|
406
|
+
} else if (type === "year" || type === "relatedYear" || type === "yearName") {
|
|
407
|
+
if (yearIndex < 0)
|
|
408
|
+
yearIndex = index;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
if (monthIndex >= 0 && yearIndex >= 0)
|
|
412
|
+
return yearIndex < monthIndex ? ["year", "month"] : ["month", "year"];
|
|
413
|
+
} catch {}
|
|
414
|
+
return ["month", "year"];
|
|
415
|
+
}
|
|
390
416
|
function monthNames(locale = "", style = "long") {
|
|
391
417
|
const resolved = resolveLocale(locale);
|
|
392
418
|
const formatter = new Intl.DateTimeFormat(resolved, { calendar: "gregory", month: style, timeZone: "UTC" });
|
|
@@ -567,7 +593,8 @@
|
|
|
567
593
|
"locale",
|
|
568
594
|
"fixed-weeks",
|
|
569
595
|
"show-week-numbers",
|
|
570
|
-
"selection"
|
|
596
|
+
"selection",
|
|
597
|
+
"month-format"
|
|
571
598
|
];
|
|
572
599
|
constructor() {
|
|
573
600
|
super();
|
|
@@ -689,6 +716,15 @@
|
|
|
689
716
|
set selection(value) {
|
|
690
717
|
this.setAttribute("selection", value === "none" ? "none" : "single");
|
|
691
718
|
}
|
|
719
|
+
get monthFormat() {
|
|
720
|
+
return this.getAttribute("month-format") === "short" ? "short" : "long";
|
|
721
|
+
}
|
|
722
|
+
set monthFormat(value) {
|
|
723
|
+
if (value === "short")
|
|
724
|
+
this.setAttribute("month-format", "short");
|
|
725
|
+
else
|
|
726
|
+
this.removeAttribute("month-format");
|
|
727
|
+
}
|
|
692
728
|
get highlightedRange() {
|
|
693
729
|
return { ...this._highlightedRange };
|
|
694
730
|
}
|
|
@@ -1109,7 +1145,7 @@
|
|
|
1109
1145
|
const display = this.display;
|
|
1110
1146
|
const displayYear = yearOf(display);
|
|
1111
1147
|
const displayMonth = Number(display.slice(5, 7));
|
|
1112
|
-
const names = monthNames(locale,
|
|
1148
|
+
const names = monthNames(locale, this.monthFormat);
|
|
1113
1149
|
const weekdayShort = weekdayNames(locale, this.firstDay, "short");
|
|
1114
1150
|
const weekdayLong = weekdayNames(locale, this.firstDay, "long");
|
|
1115
1151
|
const weeks = this._weeks();
|
|
@@ -1154,14 +1190,18 @@
|
|
|
1154
1190
|
}).join("");
|
|
1155
1191
|
const focusedElement = document.activeElement;
|
|
1156
1192
|
const focusKey = focusedElement instanceof HTMLElement && this.contains(focusedElement) ? this._focusTargetKey(focusedElement) : "";
|
|
1193
|
+
const yearFirst = monthYearOrder(locale)[0] === "year";
|
|
1194
|
+
this.toggleAttribute("data-year-first", yearFirst);
|
|
1195
|
+
const monthControl = `<label class="dp-visually-hidden" for="${this._id}-month">${escapeHtml(this._messages.month)}</label>
|
|
1196
|
+
<select id="${this._id}-month" class="dp-month-select" aria-label="${escapeHtml(this._messages.month)}">${monthOptions}</select>`;
|
|
1197
|
+
const yearControl = `<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this._messages.year)}</label>
|
|
1198
|
+
<input id="${this._id}-year" class="dp-year-input" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">`;
|
|
1157
1199
|
this._rendering = true;
|
|
1158
1200
|
this.innerHTML = `
|
|
1159
1201
|
<div class="dp-calendar-shell">
|
|
1160
1202
|
<div class="dp-calendar-header">
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this._messages.year)}</label>
|
|
1164
|
-
<input id="${this._id}-year" class="dp-year-input" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">
|
|
1203
|
+
${yearFirst ? yearControl : monthControl}
|
|
1204
|
+
${yearFirst ? monthControl : yearControl}
|
|
1165
1205
|
<button type="button" class="dp-nav dp-prev" data-calendar-action="previous" aria-label="${escapeHtml(this._messages.previousMonth)}"${prevDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m10 4-4 4 4 4"/></svg></button>
|
|
1166
1206
|
<button type="button" class="dp-nav dp-next" data-calendar-action="next" aria-label="${escapeHtml(this._messages.nextMonth)}"${nextDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 4 4 4-4 4"/></svg></button>
|
|
1167
1207
|
</div>
|
|
@@ -1174,7 +1214,11 @@
|
|
|
1174
1214
|
this._rendering = false;
|
|
1175
1215
|
if (focusKey) {
|
|
1176
1216
|
queueMicrotask(() => {
|
|
1177
|
-
|
|
1217
|
+
let selector = focusKey;
|
|
1218
|
+
if (selector.startsWith(".dp-day[data-date=")) {
|
|
1219
|
+
selector = `.dp-day[data-date="${CSS.escape(this._model.focused)}"]`;
|
|
1220
|
+
}
|
|
1221
|
+
const target = this.querySelector(selector);
|
|
1178
1222
|
if (target instanceof HTMLElement)
|
|
1179
1223
|
target.focus();
|
|
1180
1224
|
});
|
|
@@ -1697,11 +1741,40 @@
|
|
|
1697
1741
|
}
|
|
1698
1742
|
}
|
|
1699
1743
|
|
|
1744
|
+
// src/time.js
|
|
1745
|
+
var TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/;
|
|
1746
|
+
function parseTimeParts(value) {
|
|
1747
|
+
const match = TIME_RE.exec(String(value || ""));
|
|
1748
|
+
if (!match)
|
|
1749
|
+
return null;
|
|
1750
|
+
const hour = Number(match[1]);
|
|
1751
|
+
const minute = Number(match[2]);
|
|
1752
|
+
const second = match[3] === undefined ? 0 : Number(match[3]);
|
|
1753
|
+
const millisecond = match[4] === undefined ? 0 : Number(match[4].padEnd(3, "0"));
|
|
1754
|
+
if (hour > 23 || minute > 59 || second > 59 || millisecond > 999)
|
|
1755
|
+
return null;
|
|
1756
|
+
return { hour, minute, second, millisecond };
|
|
1757
|
+
}
|
|
1758
|
+
function isTime(value) {
|
|
1759
|
+
return parseTimeParts(value) !== null;
|
|
1760
|
+
}
|
|
1761
|
+
function toMillis(parts) {
|
|
1762
|
+
return ((parts.hour * 60 + parts.minute) * 60 + parts.second) * 1000 + parts.millisecond;
|
|
1763
|
+
}
|
|
1764
|
+
function compareTimes(a, b) {
|
|
1765
|
+
const left = parseTimeParts(a);
|
|
1766
|
+
const right = parseTimeParts(b);
|
|
1767
|
+
if (!left || !right)
|
|
1768
|
+
throw new TypeError("compareTimes() expects HH:MM values");
|
|
1769
|
+
const diff = toMillis(left) - toMillis(right);
|
|
1770
|
+
return diff === 0 ? 0 : diff < 0 ? -1 : 1;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1700
1773
|
// src/date-picker.js
|
|
1701
1774
|
var uid2 = 0;
|
|
1702
1775
|
|
|
1703
1776
|
class DatePickerElement extends HTMLElement {
|
|
1704
|
-
static observedAttributes = ["value", "locale", "min", "max", "open-on-focus"];
|
|
1777
|
+
static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
|
|
1705
1778
|
constructor() {
|
|
1706
1779
|
super();
|
|
1707
1780
|
this._id = `date-picker-${++uid2}`;
|
|
@@ -1713,6 +1786,8 @@
|
|
|
1713
1786
|
this._fields = null;
|
|
1714
1787
|
this._range = null;
|
|
1715
1788
|
this._orderTags = new Set;
|
|
1789
|
+
this._timeFields = [null, null];
|
|
1790
|
+
this._timeOrderOwned = new Set;
|
|
1716
1791
|
this._lastFocusEndpoint = "";
|
|
1717
1792
|
this._rangeCommitId = 0;
|
|
1718
1793
|
this._pendingIntents = new Map;
|
|
@@ -1746,13 +1821,14 @@
|
|
|
1746
1821
|
this._connectSingle();
|
|
1747
1822
|
}
|
|
1748
1823
|
_connectSingle() {
|
|
1749
|
-
const input = this.querySelector(":scope > input:not([type=hidden])");
|
|
1824
|
+
const input = this.querySelector(":scope > input:not([type=hidden]):not([type=time]):not([data-time-start]):not([data-time-end])");
|
|
1750
1825
|
if (!(input instanceof HTMLInputElement)) {
|
|
1751
1826
|
console.warn("<date-picker> expects a direct child text input");
|
|
1752
1827
|
return;
|
|
1753
1828
|
}
|
|
1754
1829
|
this._connected = true;
|
|
1755
1830
|
this._input = input;
|
|
1831
|
+
this._discoverTimeFields();
|
|
1756
1832
|
this._field = new DateFieldController(input, { messages: this._messages, locale: this.locale });
|
|
1757
1833
|
this._field.onAttributesChanged = () => this._syncInputState();
|
|
1758
1834
|
this._field.confirm = (date) => this._confirmDate(date);
|
|
@@ -1775,7 +1851,31 @@
|
|
|
1775
1851
|
input.defaultValue = input.value;
|
|
1776
1852
|
}
|
|
1777
1853
|
}
|
|
1854
|
+
_discoverTimeFields() {
|
|
1855
|
+
this._timeFields = [null, null];
|
|
1856
|
+
const markers = ["data-time-start", "data-time-end"];
|
|
1857
|
+
for (const [index, marker] of markers.entries()) {
|
|
1858
|
+
const matches = this.querySelectorAll(`:scope > input[${marker}]`);
|
|
1859
|
+
if (matches.length > 1) {
|
|
1860
|
+
console.warn(`<date-picker> expects at most one direct child input[${marker}]; ignoring extras`);
|
|
1861
|
+
}
|
|
1862
|
+
const candidate = matches[0];
|
|
1863
|
+
if (candidate == null)
|
|
1864
|
+
continue;
|
|
1865
|
+
if (!(candidate instanceof HTMLInputElement) || candidate.type !== "time") {
|
|
1866
|
+
console.warn(`<date-picker> input[${marker}] must be an <input type="time">; ignoring it`);
|
|
1867
|
+
continue;
|
|
1868
|
+
}
|
|
1869
|
+
this._timeFields[index] = candidate;
|
|
1870
|
+
}
|
|
1871
|
+
if (this.querySelector(":scope > input[type=time]:not([data-time-start]):not([data-time-end])")) {
|
|
1872
|
+
console.warn("<date-picker> time inputs need [data-time-start] or [data-time-end]; ignoring it");
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1778
1875
|
_connectRange() {
|
|
1876
|
+
if (this.querySelector(":scope > input[data-time-start], :scope > input[data-time-end]")) {
|
|
1877
|
+
console.warn("<date-picker range> time companions are not supported yet; leaving them native");
|
|
1878
|
+
}
|
|
1779
1879
|
const startInput = this.querySelector(":scope > input[data-range-start]");
|
|
1780
1880
|
const endInput = this.querySelector(":scope > input[data-range-end]");
|
|
1781
1881
|
if (!(startInput instanceof HTMLInputElement) || !(endInput instanceof HTMLInputElement)) {
|
|
@@ -1827,6 +1927,8 @@
|
|
|
1827
1927
|
this.hide(false);
|
|
1828
1928
|
this._controller?.abort();
|
|
1829
1929
|
this._controller = null;
|
|
1930
|
+
this._clearTimeOrderValidity();
|
|
1931
|
+
this._timeFields = [null, null];
|
|
1830
1932
|
if (this._rangeMode()) {
|
|
1831
1933
|
for (const field of this._fields || [])
|
|
1832
1934
|
this._teardownField(field);
|
|
@@ -1911,6 +2013,15 @@
|
|
|
1911
2013
|
else
|
|
1912
2014
|
this.removeAttribute("locale");
|
|
1913
2015
|
}
|
|
2016
|
+
get monthFormat() {
|
|
2017
|
+
return this.getAttribute("month-format") === "short" ? "short" : "long";
|
|
2018
|
+
}
|
|
2019
|
+
set monthFormat(value) {
|
|
2020
|
+
if (value === "short")
|
|
2021
|
+
this.setAttribute("month-format", "short");
|
|
2022
|
+
else
|
|
2023
|
+
this.removeAttribute("month-format");
|
|
2024
|
+
}
|
|
1914
2025
|
get min() {
|
|
1915
2026
|
const value = this.getAttribute("min") || "";
|
|
1916
2027
|
return isDate(value) ? value : "";
|
|
@@ -2107,6 +2218,13 @@
|
|
|
2107
2218
|
input.addEventListener("keydown", (event) => {
|
|
2108
2219
|
this._onFieldKeyDown("", event);
|
|
2109
2220
|
}, { signal });
|
|
2221
|
+
for (const [index, timeInput] of this._timeFields.entries()) {
|
|
2222
|
+
if (!timeInput)
|
|
2223
|
+
continue;
|
|
2224
|
+
const endpoint = index === 1 ? "end" : "start";
|
|
2225
|
+
timeInput.addEventListener("input", () => this._revalidateTimeOrder(endpoint), { signal });
|
|
2226
|
+
timeInput.addEventListener("change", () => this._revalidateTimeOrder(endpoint), { signal });
|
|
2227
|
+
}
|
|
2110
2228
|
button.addEventListener("click", (event) => {
|
|
2111
2229
|
if (this._open) {
|
|
2112
2230
|
if (event.detail === 0) {
|
|
@@ -2299,6 +2417,7 @@
|
|
|
2299
2417
|
calendar.locale = this.locale;
|
|
2300
2418
|
calendar.min = this.min;
|
|
2301
2419
|
calendar.max = this.max;
|
|
2420
|
+
calendar.monthFormat = this.monthFormat;
|
|
2302
2421
|
calendar.messages = this._messages;
|
|
2303
2422
|
calendar.source = this._source;
|
|
2304
2423
|
calendar.dateState = this._dateState;
|
|
@@ -2524,6 +2643,33 @@
|
|
|
2524
2643
|
}
|
|
2525
2644
|
}
|
|
2526
2645
|
}
|
|
2646
|
+
_revalidateTimeOrder(which) {
|
|
2647
|
+
const [startInput, endInput] = this._timeFields;
|
|
2648
|
+
const bounds = ["start", "end"];
|
|
2649
|
+
const inputs = [startInput, endInput];
|
|
2650
|
+
const startValue = startInput && !startInput.disabled ? startInput.value.trim() : "";
|
|
2651
|
+
const endValue = endInput && !endInput.disabled ? endInput.value.trim() : "";
|
|
2652
|
+
const inverted = Boolean(startInput && endInput && startValue && endValue) && isTime(startValue) && isTime(endValue) && compareTimes(endValue, startValue) < 0;
|
|
2653
|
+
for (const [index, bound] of bounds.entries()) {
|
|
2654
|
+
const input = inputs[index];
|
|
2655
|
+
if (!input)
|
|
2656
|
+
continue;
|
|
2657
|
+
const message = bound === "end" ? this._messages.rangeOrderEnd : this._messages.rangeOrderStart;
|
|
2658
|
+
const shouldHaveOrder = inverted && bound === which;
|
|
2659
|
+
if (shouldHaveOrder) {
|
|
2660
|
+
input.setCustomValidity(message);
|
|
2661
|
+
this._timeOrderOwned.add(input);
|
|
2662
|
+
} else if (this._timeOrderOwned.has(input)) {
|
|
2663
|
+
input.setCustomValidity("");
|
|
2664
|
+
this._timeOrderOwned.delete(input);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
_clearTimeOrderValidity() {
|
|
2669
|
+
for (const input of this._timeOrderOwned)
|
|
2670
|
+
input.setCustomValidity("");
|
|
2671
|
+
this._timeOrderOwned.clear();
|
|
2672
|
+
}
|
|
2527
2673
|
_restoreDefault() {
|
|
2528
2674
|
const field = this._field;
|
|
2529
2675
|
if (!field)
|
|
@@ -2608,10 +2754,13 @@
|
|
|
2608
2754
|
return true;
|
|
2609
2755
|
if (!input.value.trim()) {
|
|
2610
2756
|
input.setCustomValidity("");
|
|
2611
|
-
|
|
2757
|
+
} else {
|
|
2758
|
+
await this._commitText(false, { force: true });
|
|
2612
2759
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2760
|
+
this._revalidateTimeOrder("start");
|
|
2761
|
+
this._revalidateTimeOrder("end");
|
|
2762
|
+
const timesValid = this._timeFields.every((timeInput) => timeInput?.checkValidity() ?? true);
|
|
2763
|
+
return input.checkValidity() && timesValid;
|
|
2615
2764
|
}
|
|
2616
2765
|
show(options = {}) {
|
|
2617
2766
|
const panel = this._panel;
|