@lekoala/date-picker 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,18 @@
1
1
  import { autoUpdate, reposition } from "@lekoala/floating";
2
- import { getDefaultControlClasses, resolveControlClasses, sameControlClasses } from "./control-classes.js";
2
+ import {
3
+ getDefaultControlClasses,
4
+ normalizeControlClasses,
5
+ resolveControlClasses,
6
+ setDefaultControlClasses,
7
+ } from "./control-classes.js";
3
8
  import { compareDates, isDate, monthKey, todayISO } from "./date.js";
4
9
  import { DateCalendarElement } from "./date-calendar.js";
5
10
  import { DateFieldController } from "./date-field.js";
6
11
  import { DateRangeController, normalizeRange } from "./date-range.js";
7
12
  import { createDateAdapter, formatLongDate, resolveLocale } from "./intl.js";
8
- import { getDefaultMessages } from "./messages.js";
13
+ import { getDefaultMessages, setDefaultMessages, subscribeDefaultMessages } from "./messages.js";
9
14
  import { compareTimes, isTime } from "./time.js";
15
+ import { upgradeProperties } from "./upgrade-properties.js";
10
16
 
11
17
  let uid = 0;
12
18
 
@@ -16,6 +22,26 @@ const DRAG_THRESHOLD = 4;
16
22
  export class DatePickerElement extends HTMLElement {
17
23
  static observedAttributes = ["value", "locale", "min", "max", "open-on-focus", "month-format"];
18
24
 
25
+ /** @public @param {Partial<import("./control-classes.js").ControlClasses>} classes */
26
+ static setDefaultControlClasses(classes) {
27
+ setDefaultControlClasses(classes);
28
+ }
29
+
30
+ /** @public */
31
+ static getDefaultControlClasses() {
32
+ return getDefaultControlClasses();
33
+ }
34
+
35
+ /** @public @param {Partial<typeof import("./messages.js").DEFAULT_MESSAGES>} messages */
36
+ static setDefaultMessages(messages) {
37
+ setDefaultMessages(messages);
38
+ }
39
+
40
+ /** @public */
41
+ static getDefaultMessages() {
42
+ return getDefaultMessages();
43
+ }
44
+
19
45
  constructor() {
20
46
  super();
21
47
  this._id = `date-picker-${++uid}`;
@@ -63,8 +89,12 @@ export class DatePickerElement extends HTMLElement {
63
89
  this._stopTracking = null;
64
90
  this._open = false;
65
91
  this._suppressFocusOpen = false;
66
- this._messages = getDefaultMessages();
67
- this._controlClasses = getDefaultControlClasses();
92
+ /** @type {typeof import("./messages.js").DEFAULT_MESSAGES | null} */
93
+ this._messages = null;
94
+ /** @type {Partial<import("./control-classes.js").ControlClasses> | null} */
95
+ this._controlClasses = null;
96
+ /** @type {(() => void) | null} */
97
+ this._stopDefaultMessages = null;
68
98
  /** @type {any} */
69
99
  this._source = null;
70
100
  /** @type {any} */
@@ -83,11 +113,29 @@ export class DatePickerElement extends HTMLElement {
83
113
 
84
114
  connectedCallback() {
85
115
  if (this._connected) return;
86
- if (this.hasAttribute("range")) {
87
- this._connectRange();
88
- return;
116
+ upgradeProperties(this, [
117
+ "locale",
118
+ "min",
119
+ "max",
120
+ "monthFormat",
121
+ "openOnFocus",
122
+ "coordinateSpace",
123
+ "controlClasses",
124
+ "messages",
125
+ "source",
126
+ "dateState",
127
+ "renderDay",
128
+ "isDateDisabled",
129
+ ]);
130
+ if (this.hasAttribute("range")) this._connectRange();
131
+ else this._connectSingle();
132
+ // The range setter requires the controller created by _connectRange().
133
+ if (this._connected) {
134
+ upgradeProperties(this, ["value", "range"]);
135
+ this._stopDefaultMessages = subscribeDefaultMessages(() => {
136
+ if (this._messages === null) this._refreshMessages();
137
+ });
89
138
  }
90
- this._connectSingle();
91
139
  }
92
140
 
93
141
  _connectSingle() {
@@ -101,7 +149,7 @@ export class DatePickerElement extends HTMLElement {
101
149
  this._connected = true;
102
150
  this._input = input;
103
151
  this._discoverTimeFields();
104
- this._field = new DateFieldController(input, { messages: this._messages, locale: this.locale });
152
+ this._field = new DateFieldController(input, { messages: this.messages, locale: this.locale });
105
153
  this._field.onAttributesChanged = () => this._syncInputState();
106
154
  this._field.confirm = (date, options) => this._confirmDate(date, options);
107
155
  this._setupFormValue();
@@ -115,7 +163,7 @@ export class DatePickerElement extends HTMLElement {
115
163
  this.getAttribute("value") || (isDate(input.value) ? input.value : adapter.parse(input.value));
116
164
  if (initial && isDate(initial)) this._setValue(initial, { emit: false, format: true });
117
165
  else if (!input.value) this._setValue("", { emit: false, format: false });
118
- else input.setCustomValidity(this._messages.invalidDate);
166
+ else input.setCustomValidity(this.messages.invalidDate);
119
167
 
120
168
  // Native form reset restores control defaultValues, then _restoreDefault()
121
169
  // recommits the canonical value from them. Never redefine an author-provided
@@ -171,8 +219,8 @@ export class DatePickerElement extends HTMLElement {
171
219
  this._connected = true;
172
220
  this._input = null;
173
221
  this._fields = [
174
- new DateFieldController(startInput, { messages: this._messages, locale: this.locale }),
175
- new DateFieldController(endInput, { messages: this._messages, locale: this.locale }),
222
+ new DateFieldController(startInput, { messages: this.messages, locale: this.locale }),
223
+ new DateFieldController(endInput, { messages: this.messages, locale: this.locale }),
176
224
  ];
177
225
  this._range = new DateRangeController();
178
226
  this._rangeOriginalDescribedBy = [
@@ -195,8 +243,8 @@ export class DatePickerElement extends HTMLElement {
195
243
  const initialEnd = isDate(endInput.value) ? endInput.value : adapter.parse(endInput.value);
196
244
  this._setRange(initialStart || "", initialEnd || "", { emit: false });
197
245
  if (startInput.value.trim() && !isDate(initialStart))
198
- startInput.setCustomValidity(this._messages.invalidDate);
199
- if (endInput.value.trim() && !isDate(initialEnd)) endInput.setCustomValidity(this._messages.invalidDate);
246
+ startInput.setCustomValidity(this.messages.invalidDate);
247
+ if (endInput.value.trim() && !isDate(initialEnd)) endInput.setCustomValidity(this.messages.invalidDate);
200
248
 
201
249
  if (this._fields[0].hidden) this._fields[0].hidden.defaultValue = this._range.start;
202
250
  if (this._fields[1].hidden) this._fields[1].hidden.defaultValue = this._range.end;
@@ -209,6 +257,8 @@ export class DatePickerElement extends HTMLElement {
209
257
  }
210
258
 
211
259
  disconnectedCallback() {
260
+ this._stopDefaultMessages?.();
261
+ this._stopDefaultMessages = null;
212
262
  this._connected = false;
213
263
  this.hide(false);
214
264
  this._controller?.abort();
@@ -356,34 +406,42 @@ export class DatePickerElement extends HTMLElement {
356
406
  }
357
407
 
358
408
  /**
359
- * Author classes for header controls; merged with global defaults.
409
+ * Author classes for header controls; omitted roles follow current global defaults.
360
410
  * @public
361
411
  * @returns {import("./control-classes.js").ControlClasses}
362
412
  */
363
413
  get controlClasses() {
364
- return { ...this._controlClasses };
414
+ return resolveControlClasses(this._controlClasses);
365
415
  }
366
416
 
367
417
  /** @param {Partial<import("./control-classes.js").ControlClasses> | null} value */
368
418
  set controlClasses(value) {
369
- const classes = resolveControlClasses(value);
370
- if (sameControlClasses(classes, this._controlClasses)) return;
371
- this._controlClasses = classes;
419
+ this._controlClasses = normalizeControlClasses(value);
372
420
  if (this._calendar) this._calendar.controlClasses = this._controlClasses;
373
421
  }
374
422
 
375
- /** @public */
423
+ /** @public @returns {typeof import("./messages.js").DEFAULT_MESSAGES} */
376
424
  get messages() {
377
- return this._messages;
425
+ return this._messages ?? getDefaultMessages();
378
426
  }
379
427
 
428
+ /** @param {Partial<typeof import("./messages.js").DEFAULT_MESSAGES> | null} value */
380
429
  set messages(value) {
381
- this._messages = { ...getDefaultMessages(), ...(value || {}) };
430
+ this._messages = value == null ? null : { ...getDefaultMessages(), ...value };
382
431
  if (this._calendar) this._calendar.messages = this._messages;
383
- for (const field of this._fields || []) field.setMessages(this._messages);
384
- this._field?.setMessages(this._messages);
432
+ this._refreshMessages();
433
+ }
434
+
435
+ _refreshMessages() {
436
+ const messages = this.messages;
437
+ for (const field of this._fields || []) field.setMessages(messages);
438
+ this._field?.setMessages(messages);
439
+ this._panel?.setAttribute("aria-label", messages.calendar);
440
+ const field = this._field ?? this._fields?.[0];
441
+ if (this._formatHint && field)
442
+ this._formatHint.textContent = `${messages.formatHint}: ${field.placeholder}`;
385
443
  this._refreshButtonLabel();
386
- void this.validate();
444
+ if (this._connected) void this.validate();
387
445
  }
388
446
 
389
447
  /** @public @returns {any} */
@@ -541,7 +599,7 @@ export class DatePickerElement extends HTMLElement {
541
599
  panel.className = "dp-picker-panel";
542
600
  panel.setAttribute("popover", "manual");
543
601
  panel.setAttribute("role", "dialog");
544
- panel.setAttribute("aria-label", this._messages.calendar);
602
+ panel.setAttribute("aria-label", this.messages.calendar);
545
603
 
546
604
  const calendar = new DateCalendarElement();
547
605
  calendar.selection = "none";
@@ -1184,7 +1242,7 @@ export class DatePickerElement extends HTMLElement {
1184
1242
  calendar.renderDay = this._renderDay;
1185
1243
  calendar.isDateDisabled = this._isDateDisabled;
1186
1244
  if (!this._rangeMode() && this._value) calendar.value = this._value;
1187
- if (panel) panel.setAttribute("aria-label", this._messages.calendar);
1245
+ if (panel) panel.setAttribute("aria-label", this.messages.calendar);
1188
1246
  }
1189
1247
 
1190
1248
  _refreshLocale() {
@@ -1193,7 +1251,7 @@ export class DatePickerElement extends HTMLElement {
1193
1251
  if (this._rangeMode() && this._fields) {
1194
1252
  this._fields[0].setLocale(this.locale);
1195
1253
  this._fields[1].setLocale(this.locale);
1196
- hint.textContent = `${this._messages.formatHint}: ${this._fields[0].placeholder}`;
1254
+ hint.textContent = `${this.messages.formatHint}: ${this._fields[0].placeholder}`;
1197
1255
  for (const [index, field] of this._fields.entries()) {
1198
1256
  if (!field.input.hasAttribute("placeholder") || this._generatedPlaceholder) {
1199
1257
  field.input.placeholder = field.placeholder;
@@ -1208,7 +1266,7 @@ export class DatePickerElement extends HTMLElement {
1208
1266
  const field = this._field;
1209
1267
  if (!field) return;
1210
1268
  field.setLocale(this.locale);
1211
- hint.textContent = `${this._messages.formatHint}: ${field.placeholder}`;
1269
+ hint.textContent = `${this.messages.formatHint}: ${field.placeholder}`;
1212
1270
  if (!field.input.hasAttribute("placeholder") || this._generatedPlaceholder) {
1213
1271
  field.input.placeholder = field.placeholder;
1214
1272
  this._generatedPlaceholder = true;
@@ -1224,7 +1282,7 @@ export class DatePickerElement extends HTMLElement {
1224
1282
  this._refreshRangeButtonLabel();
1225
1283
  return;
1226
1284
  }
1227
- const prefix = this._value ? this._messages.changeDate : this._messages.chooseDate;
1285
+ const prefix = this._value ? this.messages.changeDate : this.messages.chooseDate;
1228
1286
  const suffix = this._value ? `, ${formatLongDate(this._value, this.locale)}` : "";
1229
1287
  single.setAttribute("aria-label", `${prefix}${suffix}`);
1230
1288
  }
@@ -1236,9 +1294,9 @@ export class DatePickerElement extends HTMLElement {
1236
1294
  const { start, end } = this._range;
1237
1295
  for (const { button, endpoint } of this._buttons) {
1238
1296
  const bound = endpoint === "end" ? end : start;
1239
- const prefix = bound ? this._messages.changeDate : this._messages.chooseDate;
1297
+ const prefix = bound ? this.messages.changeDate : this.messages.chooseDate;
1240
1298
  const suffix = bound ? `, ${formatLongDate(bound, this.locale)}` : "";
1241
- const boundLabel = endpoint === "end" ? this._messages.rangeEnd : this._messages.rangeStart;
1299
+ const boundLabel = endpoint === "end" ? this.messages.rangeEnd : this.messages.rangeStart;
1242
1300
  button.setAttribute("aria-label", `${prefix}${suffix}, ${boundLabel}`);
1243
1301
  }
1244
1302
  }
@@ -1294,7 +1352,7 @@ export class DatePickerElement extends HTMLElement {
1294
1352
  }
1295
1353
  const confirmed = await calendar.ensureDate(date);
1296
1354
  const state = calendar.getDateState(date);
1297
- if (!confirmed || state.disabled) return { ok: false, message: this._messages.unavailableDate };
1355
+ if (!confirmed || state.disabled) return { ok: false, message: this.messages.unavailableDate };
1298
1356
  return { ok: true };
1299
1357
  }
1300
1358
 
@@ -1425,7 +1483,7 @@ export class DatePickerElement extends HTMLElement {
1425
1483
  for (const bound of bounds) {
1426
1484
  const index = bound === "end" ? 1 : 0;
1427
1485
  const input = this._fields[index].input;
1428
- const message = bound === "end" ? this._messages.rangeOrderEnd : this._messages.rangeOrderStart;
1486
+ const message = bound === "end" ? this.messages.rangeOrderEnd : this.messages.rangeOrderStart;
1429
1487
  const shouldHaveOrder = inverted && bound === which;
1430
1488
  if (shouldHaveOrder) {
1431
1489
  input.setCustomValidity(message);
@@ -1461,7 +1519,7 @@ export class DatePickerElement extends HTMLElement {
1461
1519
  for (const [index, bound] of bounds.entries()) {
1462
1520
  const input = inputs[index];
1463
1521
  if (!input) continue;
1464
- const message = bound === "end" ? this._messages.rangeOrderEnd : this._messages.rangeOrderStart;
1522
+ const message = bound === "end" ? this.messages.rangeOrderEnd : this.messages.rangeOrderStart;
1465
1523
  const shouldHaveOrder = inverted && bound === which;
1466
1524
  if (shouldHaveOrder) {
1467
1525
  input.setCustomValidity(message);
package/src/messages.js CHANGED
@@ -28,4 +28,16 @@ export function getDefaultMessages() {
28
28
  /** @param {Partial<typeof DEFAULT_MESSAGES>} messages */
29
29
  export function setDefaultMessages(messages) {
30
30
  defaults = { ...defaults, ...messages };
31
+ for (const listener of listeners) listener();
32
+ }
33
+
34
+ /** @type {Set<() => void>} */
35
+ const listeners = new Set();
36
+
37
+ /** @param {() => void} listener */
38
+ export function subscribeDefaultMessages(listener) {
39
+ listeners.add(listener);
40
+ return () => {
41
+ listeners.delete(listener);
42
+ };
31
43
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Replay properties assigned before custom-element definition through their setters.
3
+ * Call only once the element is ready to accept the listed properties.
4
+ * @param {HTMLElement} element
5
+ * @param {string[]} properties
6
+ */
7
+ export function upgradeProperties(element, properties) {
8
+ for (const property of properties) {
9
+ if (!Object.hasOwn(element, property)) continue;
10
+ const value = Reflect.get(element, property);
11
+ Reflect.deleteProperty(element, property);
12
+ Reflect.set(element, property, value);
13
+ }
14
+ }