@johly/bits-ui 2.18.11 → 2.18.12

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.
@@ -15,15 +15,30 @@ export const dateFieldAttrs = createBitsAttrs({
15
15
  component: "date-field",
16
16
  parts: ["input", "label", "segment"],
17
17
  });
18
+ /**
19
+ * Whether the year segment holds a year that is still being typed. The segment
20
+ * is zero-padded, so "0202" is what typing "202" towards "2024" looks like.
21
+ */
22
+ function isPartialYear(year) {
23
+ return year !== null && Number.parseInt(year) < 1000;
24
+ }
25
+ /**
26
+ * Days in the typed month. Until the year is fully typed it could still become
27
+ * a leap year, so a leap year is assumed.
28
+ */
29
+ function getDaysInSegmentMonth(dateRef, year, month) {
30
+ const typedYear = year && !isPartialYear(year) ? Number.parseInt(year) : 2000;
31
+ return getDaysInMonth(dateRef.set({ year: typedYear, month: Number.parseInt(month) }));
32
+ }
18
33
  const SEGMENT_CONFIGS = {
19
34
  day: {
20
35
  min: 1,
21
36
  max: (root) => {
22
- const segmentMonthValue = root.segmentValues.month;
37
+ const { year, month } = root.segmentValues;
38
+ if (!month)
39
+ return 31;
23
40
  const placeholder = root.value.current ?? root.placeholder.current;
24
- return segmentMonthValue
25
- ? getDaysInMonth(placeholder.set({ month: Number.parseInt(segmentMonthValue) }))
26
- : getDaysInMonth(placeholder);
41
+ return getDaysInSegmentMonth(placeholder, year, month);
27
42
  },
28
43
  cycle: 1,
29
44
  padZero: true,
@@ -265,6 +280,19 @@ export class DateFieldRootState {
265
280
  this.states.minute.updating = null;
266
281
  this.states.dayPeriod.updating = null;
267
282
  }
283
+ /** Once the year is fully typed, clamp a day it doesn't have (29 February). */
284
+ #clampDayToYear(segments, year, dateRef) {
285
+ if (year === null || isPartialYear(year))
286
+ return;
287
+ if (segments.month === null || segments.day === null)
288
+ return;
289
+ const daysInMonth = getDaysInSegmentMonth(dateRef, year, segments.month);
290
+ if (Number.parseInt(segments.day) <= daysInMonth)
291
+ return;
292
+ segments.day = `${daysInMonth}`;
293
+ if (this.states.day.updating)
294
+ this.states.day.updating = segments.day;
295
+ }
268
296
  setValue(value) {
269
297
  this.value.current = value;
270
298
  }
@@ -439,8 +467,7 @@ export class DateFieldRootState {
439
467
  const next = castCb(pVal);
440
468
  this.states.month.updating = next;
441
469
  if (next !== null && prev.day !== null) {
442
- const date = dateRef.set({ month: Number.parseInt(next) });
443
- const daysInMonth = getDaysInMonth(toDate(date));
470
+ const daysInMonth = getDaysInSegmentMonth(dateRef, prev.year, next);
444
471
  const prevDay = Number.parseInt(prev.day);
445
472
  if (prevDay > daysInMonth) {
446
473
  prev.day = `${daysInMonth}`;
@@ -491,6 +518,7 @@ export class DateFieldRootState {
491
518
  else if (part === "year") {
492
519
  const next = castCb(pVal);
493
520
  this.states.year.updating = next;
521
+ this.#clampDayToYear(prev, next, dateRef);
494
522
  newSegmentValues = { ...prev, [part]: next };
495
523
  }
496
524
  else if (part === "day") {
@@ -509,8 +537,7 @@ export class DateFieldRootState {
509
537
  const next = castCb(pVal);
510
538
  if (part === "month" && next !== null && prev.day !== null) {
511
539
  this.states.month.updating = next;
512
- const date = dateRef.set({ month: Number.parseInt(next) });
513
- const daysInMonth = getDaysInMonth(toDate(date));
540
+ const daysInMonth = getDaysInSegmentMonth(dateRef, prev.year, next);
514
541
  if (Number.parseInt(prev.day) > daysInMonth) {
515
542
  prev.day = `${daysInMonth}`;
516
543
  }
@@ -519,6 +546,7 @@ export class DateFieldRootState {
519
546
  else if (part === "year") {
520
547
  const next = castCb(pVal);
521
548
  this.states.year.updating = next;
549
+ this.#clampDayToYear(prev, next, dateRef);
522
550
  newSegmentValues = { ...prev, [part]: next };
523
551
  }
524
552
  else if (part === "day") {
@@ -531,6 +559,12 @@ export class DateFieldRootState {
531
559
  }
532
560
  }
533
561
  this.segmentValues = newSegmentValues;
562
+ // Committing a year mid-typing would clamp the day to it, turning
563
+ // 29 February into the 28th before "2024" is complete, so the value
564
+ // is left alone until the year is.
565
+ if (part === "year" && "year" in newSegmentValues && isPartialYear(newSegmentValues.year)) {
566
+ return;
567
+ }
534
568
  if (areAllSegmentsFilled(newSegmentValues, this.#fieldNode)) {
535
569
  this.setValue(getValueFromSegments({
536
570
  segmentObj: newSegmentValues,
@@ -1,6 +1,6 @@
1
1
  import { styleToString } from "svelte-toolbelt";
2
2
  import { getPlaceholder } from "../placeholders.js";
3
- import { hasTime, isZonedDateTime } from "../utils.js";
3
+ import { getDaysInMonth, hasTime, isZonedDateTime } from "../utils.js";
4
4
  import { ALL_SEGMENT_PARTS, DATE_SEGMENT_PARTS, EDITABLE_SEGMENT_PARTS, EDITABLE_TIME_SEGMENT_PARTS, } from "./parts.js";
5
5
  import { getSegments } from "./segments.js";
6
6
  import { isBrowser, isNull, isNumberString } from "../../is.js";
@@ -25,6 +25,25 @@ export function initializeSegmentValues(granularity) {
25
25
  });
26
26
  return Object.fromEntries(initialParts);
27
27
  }
28
+ /**
29
+ * The date to format a segment value with. A day can be typed before its month,
30
+ * so it may not exist in `dateRef`'s month; setting it there would constrain it
31
+ * (31 would read as 30), so it is shown in the first month long enough instead.
32
+ */
33
+ function getPartDate(dateRef, part, value) {
34
+ if (part !== "day")
35
+ return dateRef.set({ [part]: value });
36
+ const day = Number.parseInt(value);
37
+ if (day <= getDaysInMonth(dateRef))
38
+ return dateRef.set({ day });
39
+ const monthsInYear = dateRef.calendar.getMonthsInYear(dateRef);
40
+ for (let month = 1; month <= monthsInYear; month++) {
41
+ const candidate = dateRef.set({ month, day: 1 });
42
+ if (day <= getDaysInMonth(candidate))
43
+ return candidate.set({ day });
44
+ }
45
+ return dateRef.set({ day });
46
+ }
28
47
  function createContentObj(props) {
29
48
  const { segmentValues, formatter, locale, dateRef } = props;
30
49
  const content = Object.keys(segmentValues).reduce((obj, part) => {
@@ -53,7 +72,7 @@ function createContentObj(props) {
53
72
  return "0";
54
73
  }
55
74
  else if (!isNull(value) && !isNull(intValue)) {
56
- const formatted = formatter.part(dateRef.set({ [part]: value }), part, {
75
+ const formatted = formatter.part(getPartDate(dateRef, part, value), part, {
57
76
  hourCycle: props.hourCycle === 24 ? "h23" : undefined,
58
77
  });
59
78
  /**
@@ -116,7 +135,7 @@ function createContentObj(props) {
116
135
  return "0";
117
136
  }
118
137
  else if (!isNull(value)) {
119
- const formatted = formatter.part(dateRef.set({ [part]: value }), part);
138
+ const formatted = formatter.part(getPartDate(dateRef, part, value), part);
120
139
  if (part === "year") {
121
140
  return `${value}`;
122
141
  }
@@ -238,9 +257,23 @@ function getUsedSegments(fieldNode) {
238
257
  });
239
258
  return usedSegments;
240
259
  }
260
+ /**
261
+ * The order the segments are applied in when building a date from them.
262
+ *
263
+ * Setting a part on a `DateValue` constrains the result immediately, so the day
264
+ * has to be applied after the year and month it belongs to. Applying it in the
265
+ * order the segments are displayed clamps the day to the number of days in the
266
+ * `dateRef`'s month in day-first locales (e.g. `31/08` entered in `en-GB` with a
267
+ * `dateRef` in November would resolve to the 30th).
268
+ */
269
+ const SEGMENT_APPLY_ORDER = ["year", "month", "day", "hour", "minute", "second", "dayPeriod"];
270
+ function getSegmentApplyOrder(part) {
271
+ const index = SEGMENT_APPLY_ORDER.indexOf(part);
272
+ return index === -1 ? SEGMENT_APPLY_ORDER.length : index;
273
+ }
241
274
  export function getValueFromSegments(props) {
242
275
  const { segmentObj, fieldNode, dateRef } = props;
243
- const usedSegments = getUsedSegments(fieldNode);
276
+ const usedSegments = getUsedSegments(fieldNode).sort((a, b) => getSegmentApplyOrder(a) - getSegmentApplyOrder(b));
244
277
  let date = dateRef;
245
278
  for (const part of usedSegments) {
246
279
  if ("hour" in segmentObj) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johly/bits-ui",
3
- "version": "2.18.11",
3
+ "version": "2.18.12",
4
4
  "license": "MIT",
5
5
  "repository": "github:johanohly/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",