@allxsmith/bestax-bulma 5.26.2 → 5.26.3

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.
Files changed (58) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/extras.css +1 -1
  4. package/dist/extras.css.map +1 -1
  5. package/dist/index.cjs +453 -303
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +453 -303
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/elements/Progress.d.ts +6 -1
  10. package/dist/types/form/Control.d.ts +14 -2
  11. package/dist/types/form/DateInput.d.ts +13 -3
  12. package/dist/types/form/DateTimeInput.d.ts +13 -3
  13. package/dist/types/form/Input.d.ts +16 -4
  14. package/dist/types/form/InputBase.d.ts +5 -2
  15. package/dist/types/form/Select.d.ts +8 -2
  16. package/dist/types/form/TextArea.d.ts +4 -2
  17. package/dist/types/form/TextAreaBase.d.ts +12 -3
  18. package/dist/types/form/TimeInput.d.ts +13 -3
  19. package/dist/types/form/TimeInputBase.d.ts +5 -1
  20. package/dist/types/form/_pickerInternals/Calendar.d.ts +10 -1
  21. package/dist/types/form/_pickerInternals/PickerPopover.d.ts +6 -0
  22. package/dist/types/helpers/Theme.d.ts +13 -4
  23. package/dist/types/helpers/portal.d.ts +2 -1
  24. package/dist/types/helpers/useAnchoredPosition.d.ts +6 -0
  25. package/dist/types/helpers/useFocusTrap.d.ts +2 -1
  26. package/dist/types/helpers/useOtherClasses.d.ts +13 -1
  27. package/dist/types-cjs/elements/Progress.d.ts +6 -1
  28. package/dist/types-cjs/form/Control.d.ts +14 -2
  29. package/dist/types-cjs/form/DateInput.d.ts +13 -3
  30. package/dist/types-cjs/form/DateTimeInput.d.ts +13 -3
  31. package/dist/types-cjs/form/Input.d.ts +16 -4
  32. package/dist/types-cjs/form/InputBase.d.ts +5 -2
  33. package/dist/types-cjs/form/Select.d.ts +8 -2
  34. package/dist/types-cjs/form/TextArea.d.ts +4 -2
  35. package/dist/types-cjs/form/TextAreaBase.d.ts +12 -3
  36. package/dist/types-cjs/form/TimeInput.d.ts +13 -3
  37. package/dist/types-cjs/form/TimeInputBase.d.ts +5 -1
  38. package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +10 -1
  39. package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +6 -0
  40. package/dist/types-cjs/helpers/Theme.d.ts +13 -4
  41. package/dist/types-cjs/helpers/portal.d.ts +2 -1
  42. package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +6 -0
  43. package/dist/types-cjs/helpers/useFocusTrap.d.ts +2 -1
  44. package/dist/types-cjs/helpers/useOtherClasses.d.ts +13 -1
  45. package/dist/versions/bestax-no-dark-mode.css +1 -1
  46. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  47. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  48. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  49. package/dist/versions/bestax-no-helpers.css +1 -1
  50. package/dist/versions/bestax-no-helpers.css.map +1 -1
  51. package/dist/versions/bestax-prefixed.css +1 -1
  52. package/dist/versions/bestax-prefixed.css.map +1 -1
  53. package/package.json +1 -1
  54. package/src/scss/elements/_index.scss +2 -0
  55. package/src/scss/elements/_progress.scss +17 -0
  56. package/src/scss/elements/_skeleton.scss +21 -0
  57. package/src/scss/form/_picker-popover.scss +16 -0
  58. package/src/scss/form/_timeinput.scss +7 -2
package/dist/index.esm.js CHANGED
@@ -2939,7 +2939,8 @@ function resolvePortalContainer(container) {
2939
2939
  * Tab order and focus follow the DOM too. Content portaled from inside a
2940
2940
  * focus trap is no longer inside the trapped element, so the trap neither
2941
2941
  * reaches it nor holds focus there: render nested overlays inside the
2942
- * trapped element, or give them a trap of their own.
2942
+ * trapped element, or give them a trap of their own. To trap portaled
2943
+ * content, put `useFocusTrap`'s ref on the element inside the `Portal`.
2943
2944
  *
2944
2945
  * @function
2945
2946
  * @param {PortalProps} props - Props for the Portal component.
@@ -5195,7 +5196,8 @@ const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
5195
5196
  * behaviour, and a modal also has to block the page behind it, which `Modal`
5196
5197
  * does. Content the container renders through a portal lives elsewhere in
5197
5198
  * the DOM, so it is outside the trap: render nested overlays inside the
5198
- * container, or give them a trap of their own.
5199
+ * container, or give them a trap of their own. To trap content a `Portal`
5200
+ * renders, pass a ref to the element inside the `Portal` as `containerRef`.
5199
5201
  *
5200
5202
  * The container has to be in the DOM when the trap turns on. The trap waits
5201
5203
  * for hydration, so a container that only appears once the page has
@@ -5312,6 +5314,15 @@ function useFocusTrap(containerRef, options = {}) {
5312
5314
  const useClientLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
5313
5315
 
5314
5316
  // INTERNAL: deliberately not exported from src/index.ts.
5317
+ /**
5318
+ * A `styledGap` for a panel whose stylesheet puts the gap in a margin on the
5319
+ * side facing the anchor, negative on a portaled panel above it, so the
5320
+ * larger magnitude is the gap. In pixels.
5321
+ */
5322
+ function marginGap(panel) {
5323
+ const { marginTop, marginBottom } = getComputedStyle(panel);
5324
+ return Math.max(Math.abs(parseFloat(marginTop)) || 0, Math.abs(parseFloat(marginBottom)) || 0);
5325
+ }
5315
5326
  /**
5316
5327
  * The corner that keeps a panel of this size in the viewport, preferring
5317
5328
  * below the anchor and lined up with its left edge.
@@ -5536,15 +5547,6 @@ function nearestDialog(event) {
5536
5547
  return role === 'dialog' || role === 'alertdialog';
5537
5548
  }) ?? null);
5538
5549
  }
5539
- /**
5540
- * The gap the stylesheet puts between the panel and the trigger, in pixels.
5541
- * It is a margin on whichever side faces the trigger, and a negative one on
5542
- * a portaled panel above it, so the larger magnitude is the gap.
5543
- */
5544
- function styledGap(panel) {
5545
- const { marginTop, marginBottom } = getComputedStyle(panel);
5546
- return Math.max(Math.abs(parseFloat(marginTop)) || 0, Math.abs(parseFloat(marginBottom)) || 0);
5547
- }
5548
5550
  /**
5549
5551
  * The `Popover` component opens a panel of interactive content, such as a filter form or a set of share options, anchored to the element that toggles it.
5550
5552
  *
@@ -5596,7 +5598,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
5596
5598
  // The gap comes from `--bulma-popover-offset` in the stylesheet, so the
5597
5599
  // coordinates leave it out and `auto` reads it off the panel.
5598
5600
  offset: 0,
5599
- styledGap,
5601
+ styledGap: marginGap,
5600
5602
  });
5601
5603
  // The trigger renders first inside the wrapper. Read when needed rather
5602
5604
  // than held, so a trigger that remounted is still the one found.
@@ -11060,7 +11062,7 @@ const TextAreaBase = forwardRef(({ color, size, isRounded, isStatic, isHovered,
11060
11062
  TextAreaBase.displayName = 'TextAreaBase';
11061
11063
 
11062
11064
  /**
11063
- * The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static/read-only state, hover/focus/loading states, and all Bulma helper props.
11065
+ * The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static state, hover/focus/loading states, and all Bulma helper props.
11064
11066
  *
11065
11067
  * @function
11066
11068
  * @param {InputProps} props - Props for Input.
@@ -11163,7 +11165,7 @@ fieldClassName, controlClassName,
11163
11165
  Select.displayName = 'Select';
11164
11166
 
11165
11167
  /**
11166
- * The `TextArea` component provides a Bulma-styled multi-line text input, supporting color, size, rounded corners, static/read-only state, hover/focus/loading states, fixed size, and all Bulma helper props.
11168
+ * The `TextArea` component provides a Bulma-styled multi-line text input, supporting color, size, hover/focus/loading states, fixed size, and all Bulma helper props.
11167
11169
  *
11168
11170
  * @function
11169
11171
  * @param {TextAreaProps} props - Props for TextArea.
@@ -11249,6 +11251,208 @@ const mergeLabels = (overrides) => ({
11249
11251
  ...(overrides ?? {}),
11250
11252
  });
11251
11253
 
11254
+ function isSameDay(a, b) {
11255
+ return (a.getFullYear() === b.getFullYear() &&
11256
+ a.getMonth() === b.getMonth() &&
11257
+ a.getDate() === b.getDate());
11258
+ }
11259
+ function isSameMonth(a, b) {
11260
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
11261
+ }
11262
+ function isBefore(a, b) {
11263
+ return a.getTime() < b.getTime();
11264
+ }
11265
+ function isAfter(a, b) {
11266
+ return a.getTime() > b.getTime();
11267
+ }
11268
+ function isWithin(d, min, max) {
11269
+ if (min && isBefore(d, min))
11270
+ return false;
11271
+ if (max && isAfter(d, max))
11272
+ return false;
11273
+ return true;
11274
+ }
11275
+ // The boundaries build on `makeDate` (below), so a date in years 0–99 keeps
11276
+ // its year.
11277
+ function startOfDay(d) {
11278
+ return makeDate(d.getFullYear(), d.getMonth(), d.getDate());
11279
+ }
11280
+ function endOfDay(d) {
11281
+ const r = makeDate(d.getFullYear(), d.getMonth(), d.getDate());
11282
+ r.setHours(23, 59, 59, 999);
11283
+ return r;
11284
+ }
11285
+ function startOfMonth(d) {
11286
+ return makeDate(d.getFullYear(), d.getMonth());
11287
+ }
11288
+ /**
11289
+ * Local midnight on `year`-`month`-`day` (`month` from 0), like the
11290
+ * `Date(y, m, d)` constructor except that years 0–99 stay as given rather than
11291
+ * turning into 1900–1999.
11292
+ */
11293
+ function makeDate(year, month = 0, day = 1) {
11294
+ const d = new Date(2000, 0, 1);
11295
+ d.setFullYear(year, month, day);
11296
+ return d;
11297
+ }
11298
+ /**
11299
+ * First instant of the day, month or year containing `d`. Works by setters on
11300
+ * a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
11301
+ * as 1900–1999: segmented typing passes through such years on the way to a
11302
+ * four-digit one.
11303
+ */
11304
+ function startOfPeriod(d, granularity) {
11305
+ const r = new Date(d);
11306
+ if (granularity === 'year')
11307
+ r.setMonth(0, 1);
11308
+ else if (granularity === 'month')
11309
+ r.setDate(1);
11310
+ r.setHours(0, 0, 0, 0);
11311
+ return r;
11312
+ }
11313
+ /** Last instant of the day, month or year containing `d`. */
11314
+ function endOfPeriod(d, granularity) {
11315
+ const r = new Date(d);
11316
+ if (granularity === 'year')
11317
+ r.setMonth(11, 31);
11318
+ // Day 0 of the next month is this month's last day.
11319
+ else if (granularity === 'month')
11320
+ r.setMonth(r.getMonth() + 1, 0);
11321
+ r.setHours(23, 59, 59, 999);
11322
+ return r;
11323
+ }
11324
+ /**
11325
+ * Whether the day `d` falls outside `[min, max]` or is blocked by
11326
+ * `shouldDisableDate` / `unselectableDates`. This is the day grid's rule for a
11327
+ * disabled cell.
11328
+ */
11329
+ function isDayUnselectable(d, c) {
11330
+ if (!isWithin(d, c.min, c.max))
11331
+ return true;
11332
+ if (c.shouldDisableDate?.(d))
11333
+ return true;
11334
+ if (c.unselectableDates?.some(u => isSameDay(u, d)))
11335
+ return true;
11336
+ return false;
11337
+ }
11338
+ /**
11339
+ * Whether every day of the month or year containing `d` is unselectable, so
11340
+ * the period as a whole can't be picked. Walks the period's days and stops at
11341
+ * the first selectable one. `min` and `max` count whole days here, so a `min`
11342
+ * of 3pm on a month's last day leaves that day, and so its month, selectable.
11343
+ */
11344
+ function isPeriodUnselectable(d, granularity, c) {
11345
+ const days = {
11346
+ ...c,
11347
+ min: c.min && startOfDay(c.min),
11348
+ max: c.max && endOfDay(c.max),
11349
+ };
11350
+ const start = startOfPeriod(d, granularity);
11351
+ const end = endOfPeriod(d, granularity).getTime();
11352
+ // A period wholly outside the bounds needs no walk.
11353
+ if (days.min && end < days.min.getTime())
11354
+ return true;
11355
+ if (days.max && start.getTime() > days.max.getTime())
11356
+ return true;
11357
+ for (let day = start; day.getTime() <= end; day = addDays(day, 1)) {
11358
+ if (!isDayUnselectable(day, days))
11359
+ return false;
11360
+ }
11361
+ return true;
11362
+ }
11363
+ function addDays(d, n) {
11364
+ const r = new Date(d);
11365
+ r.setDate(r.getDate() + n);
11366
+ return r;
11367
+ }
11368
+ function addMonths(d, n) {
11369
+ const r = new Date(d);
11370
+ const day = r.getDate();
11371
+ r.setDate(1);
11372
+ r.setMonth(r.getMonth() + n);
11373
+ // Clamp day to last day of new month if original day is out of range.
11374
+ const lastDay = makeDate(r.getFullYear(), r.getMonth() + 1, 0).getDate();
11375
+ r.setDate(Math.min(day, lastDay));
11376
+ return r;
11377
+ }
11378
+ function addYears(d, n) {
11379
+ return addMonths(d, n * 12);
11380
+ }
11381
+ function setTimeOfDay(d, parts) {
11382
+ const r = new Date(d);
11383
+ if (parts.hours !== undefined)
11384
+ r.setHours(parts.hours);
11385
+ if (parts.minutes !== undefined)
11386
+ r.setMinutes(parts.minutes);
11387
+ if (parts.seconds !== undefined)
11388
+ r.setSeconds(parts.seconds);
11389
+ return r;
11390
+ }
11391
+ function clampDate(d, min, max) {
11392
+ if (min && isBefore(d, min))
11393
+ return new Date(min);
11394
+ if (max && isAfter(d, max))
11395
+ return new Date(max);
11396
+ return new Date(d);
11397
+ }
11398
+ /**
11399
+ * Snap a Date's time-of-day to the nearest grid defined by the given
11400
+ * increment steps. Used by the "Now" button in pickers configured with
11401
+ * non-1 hour / minute / second steps so the committed time always lands on
11402
+ * a slot that exists on the wheel. Overflows roll up: e.g.
11403
+ * 13:58 with step=15 → 14:00. Seconds are zeroed when `enableSeconds` is
11404
+ * false, regardless of step.
11405
+ */
11406
+ function snapTimeToIncrement(d, opts = {}) {
11407
+ const { incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, enableSeconds = false, } = opts;
11408
+ const r = new Date(d);
11409
+ let h = r.getHours();
11410
+ let m = r.getMinutes();
11411
+ let s = enableSeconds ? r.getSeconds() : 0;
11412
+ if (enableSeconds && incrementSeconds > 1) {
11413
+ s = Math.round(s / incrementSeconds) * incrementSeconds;
11414
+ if (s >= 60) {
11415
+ s -= 60;
11416
+ m += 1;
11417
+ }
11418
+ }
11419
+ if (incrementMinutes > 1) {
11420
+ m = Math.round(m / incrementMinutes) * incrementMinutes;
11421
+ if (m >= 60) {
11422
+ m -= 60;
11423
+ h += 1;
11424
+ }
11425
+ }
11426
+ if (incrementHours > 1) {
11427
+ h = Math.round(h / incrementHours) * incrementHours;
11428
+ // setHours handles 24+ overflow naturally (rolls forward into next day).
11429
+ }
11430
+ r.setHours(h, m, s, 0);
11431
+ return r;
11432
+ }
11433
+ /**
11434
+ * Build a 6-week × 7-day grid (42 cells) anchored on the month containing
11435
+ * `monthAnchor`. The first cell is the start of the week containing the first
11436
+ * of the month, where the week starts on `firstDayOfWeek`.
11437
+ */
11438
+ function buildMonthGrid(monthAnchor, firstDayOfWeek = 0) {
11439
+ const monthStart = startOfMonth(monthAnchor);
11440
+ const firstWeekday = monthStart.getDay();
11441
+ const offset = (firstWeekday - firstDayOfWeek + 7) % 7;
11442
+ const gridStart = addDays(monthStart, -offset);
11443
+ const today = startOfDay(new Date());
11444
+ const cells = [];
11445
+ for (let i = 0; i < 42; i++) {
11446
+ const date = addDays(gridStart, i);
11447
+ cells.push({
11448
+ date,
11449
+ inCurrentMonth: date.getMonth() === monthAnchor.getMonth(),
11450
+ isToday: isSameDay(date, today),
11451
+ });
11452
+ }
11453
+ return cells;
11454
+ }
11455
+
11252
11456
  const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
11253
11457
  const DEFAULT_MONTH_FORMAT = 'YYYY-MM';
11254
11458
  const DEFAULT_YEAR_FORMAT = 'YYYY';
@@ -11367,7 +11571,8 @@ _locale) {
11367
11571
  const hours = parts.hours ?? 0;
11368
11572
  const minutes = parts.minutes ?? 0;
11369
11573
  const seconds = parts.seconds ?? 0;
11370
- const d = new Date(year, month, day, hours, minutes, seconds, 0);
11574
+ const d = makeDate(year, month, day);
11575
+ d.setHours(hours, minutes, seconds, 0);
11371
11576
  if (d.getFullYear() !== year ||
11372
11577
  d.getMonth() !== month ||
11373
11578
  d.getDate() !== day) {
@@ -11533,204 +11738,11 @@ function getMonthNames(locale, length = 'long') {
11533
11738
  return out;
11534
11739
  }
11535
11740
 
11536
- function isSameDay(a, b) {
11537
- return (a.getFullYear() === b.getFullYear() &&
11538
- a.getMonth() === b.getMonth() &&
11539
- a.getDate() === b.getDate());
11540
- }
11541
- function isSameMonth(a, b) {
11542
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
11543
- }
11544
- function isBefore(a, b) {
11545
- return a.getTime() < b.getTime();
11546
- }
11547
- function isAfter(a, b) {
11548
- return a.getTime() > b.getTime();
11549
- }
11550
- function isWithin(d, min, max) {
11551
- if (min && isBefore(d, min))
11552
- return false;
11553
- if (max && isAfter(d, max))
11554
- return false;
11555
- return true;
11556
- }
11557
- function startOfDay(d) {
11558
- return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
11559
- }
11560
- function endOfDay(d) {
11561
- return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
11562
- }
11563
- function startOfMonth(d) {
11564
- return new Date(d.getFullYear(), d.getMonth(), 1, 0, 0, 0, 0);
11565
- }
11566
- /**
11567
- * Local midnight on `year`-`month`-`day` (`month` from 0), like the
11568
- * `Date(y, m, d)` constructor except that years 0–99 stay as given rather than
11569
- * turning into 1900–1999.
11570
- */
11571
- function makeDate(year, month = 0, day = 1) {
11572
- const d = new Date(2000, 0, 1);
11573
- d.setFullYear(year, month, day);
11574
- return d;
11575
- }
11576
- /**
11577
- * First instant of the day, month or year containing `d`. Works by setters on
11578
- * a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
11579
- * as 1900–1999: segmented typing passes through such years on the way to a
11580
- * four-digit one.
11581
- */
11582
- function startOfPeriod(d, granularity) {
11583
- const r = new Date(d);
11584
- if (granularity === 'year')
11585
- r.setMonth(0, 1);
11586
- else if (granularity === 'month')
11587
- r.setDate(1);
11588
- r.setHours(0, 0, 0, 0);
11589
- return r;
11590
- }
11591
- /** Last instant of the day, month or year containing `d`. */
11592
- function endOfPeriod(d, granularity) {
11593
- const r = new Date(d);
11594
- if (granularity === 'year')
11595
- r.setMonth(11, 31);
11596
- // Day 0 of the next month is this month's last day.
11597
- else if (granularity === 'month')
11598
- r.setMonth(r.getMonth() + 1, 0);
11599
- r.setHours(23, 59, 59, 999);
11600
- return r;
11601
- }
11602
- /**
11603
- * Whether the day `d` falls outside `[min, max]` or is blocked by
11604
- * `shouldDisableDate` / `unselectableDates`. This is the day grid's rule for a
11605
- * disabled cell.
11606
- */
11607
- function isDayUnselectable(d, c) {
11608
- if (!isWithin(d, c.min, c.max))
11609
- return true;
11610
- if (c.shouldDisableDate?.(d))
11611
- return true;
11612
- if (c.unselectableDates?.some(u => isSameDay(u, d)))
11613
- return true;
11614
- return false;
11615
- }
11616
- /**
11617
- * Whether every day of the month or year containing `d` is unselectable, so
11618
- * the period as a whole can't be picked. Walks the period's days and stops at
11619
- * the first selectable one. `min` and `max` count whole days here, so a `min`
11620
- * of 3pm on a month's last day leaves that day, and so its month, selectable.
11621
- */
11622
- function isPeriodUnselectable(d, granularity, c) {
11623
- const days = {
11624
- ...c,
11625
- min: c.min && startOfDay(c.min),
11626
- max: c.max && endOfDay(c.max),
11627
- };
11628
- const start = startOfPeriod(d, granularity);
11629
- const end = endOfPeriod(d, granularity).getTime();
11630
- // A period wholly outside the bounds needs no walk.
11631
- if (days.min && end < days.min.getTime())
11632
- return true;
11633
- if (days.max && start.getTime() > days.max.getTime())
11634
- return true;
11635
- for (let day = start; day.getTime() <= end; day = addDays(day, 1)) {
11636
- if (!isDayUnselectable(day, days))
11637
- return false;
11638
- }
11639
- return true;
11640
- }
11641
- function addDays(d, n) {
11642
- const r = new Date(d);
11643
- r.setDate(r.getDate() + n);
11644
- return r;
11645
- }
11646
- function addMonths(d, n) {
11647
- const r = new Date(d);
11648
- const day = r.getDate();
11649
- r.setDate(1);
11650
- r.setMonth(r.getMonth() + n);
11651
- // Clamp day to last day of new month if original day is out of range.
11652
- const lastDay = new Date(r.getFullYear(), r.getMonth() + 1, 0).getDate();
11653
- r.setDate(Math.min(day, lastDay));
11654
- return r;
11655
- }
11656
- function addYears(d, n) {
11657
- return addMonths(d, n * 12);
11658
- }
11659
- function setTimeOfDay(d, parts) {
11660
- const r = new Date(d);
11661
- if (parts.hours !== undefined)
11662
- r.setHours(parts.hours);
11663
- if (parts.minutes !== undefined)
11664
- r.setMinutes(parts.minutes);
11665
- if (parts.seconds !== undefined)
11666
- r.setSeconds(parts.seconds);
11667
- return r;
11668
- }
11669
- function clampDate(d, min, max) {
11670
- if (min && isBefore(d, min))
11671
- return new Date(min);
11672
- if (max && isAfter(d, max))
11673
- return new Date(max);
11674
- return new Date(d);
11675
- }
11676
- /**
11677
- * Snap a Date's time-of-day to the nearest grid defined by the given
11678
- * increment steps. Used by the "Now" button in pickers configured with
11679
- * non-1 hour / minute / second steps so the committed time always lands on
11680
- * a slot that exists on the wheel. Overflows roll up: e.g.
11681
- * 13:58 with step=15 → 14:00. Seconds are zeroed when `enableSeconds` is
11682
- * false, regardless of step.
11683
- */
11684
- function snapTimeToIncrement(d, opts = {}) {
11685
- const { incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, enableSeconds = false, } = opts;
11686
- const r = new Date(d);
11687
- let h = r.getHours();
11688
- let m = r.getMinutes();
11689
- let s = enableSeconds ? r.getSeconds() : 0;
11690
- if (enableSeconds && incrementSeconds > 1) {
11691
- s = Math.round(s / incrementSeconds) * incrementSeconds;
11692
- if (s >= 60) {
11693
- s -= 60;
11694
- m += 1;
11695
- }
11696
- }
11697
- if (incrementMinutes > 1) {
11698
- m = Math.round(m / incrementMinutes) * incrementMinutes;
11699
- if (m >= 60) {
11700
- m -= 60;
11701
- h += 1;
11702
- }
11703
- }
11704
- if (incrementHours > 1) {
11705
- h = Math.round(h / incrementHours) * incrementHours;
11706
- // setHours handles 24+ overflow naturally (rolls forward into next day).
11707
- }
11708
- r.setHours(h, m, s, 0);
11709
- return r;
11710
- }
11711
11741
  /**
11712
- * Build a 6-week × 7-day grid (42 cells) anchored on the month containing
11713
- * `monthAnchor`. The first cell is the start of the week containing the first
11714
- * of the month, where the week starts on `firstDayOfWeek`.
11742
+ * Selects the calendar's focused cell, the one tab stop of whichever grid or
11743
+ * year list is on show, for a popover to put focus on as it opens.
11715
11744
  */
11716
- function buildMonthGrid(monthAnchor, firstDayOfWeek = 0) {
11717
- const monthStart = startOfMonth(monthAnchor);
11718
- const firstWeekday = monthStart.getDay();
11719
- const offset = (firstWeekday - firstDayOfWeek + 7) % 7;
11720
- const gridStart = addDays(monthStart, -offset);
11721
- const today = startOfDay(new Date());
11722
- const cells = [];
11723
- for (let i = 0; i < 42; i++) {
11724
- const date = addDays(gridStart, i);
11725
- cells.push({
11726
- date,
11727
- inCurrentMonth: date.getMonth() === monthAnchor.getMonth(),
11728
- isToday: isSameDay(date, today),
11729
- });
11730
- }
11731
- return cells;
11732
- }
11733
-
11745
+ const CALENDAR_FOCUSED_CELL = '[data-focused="true"], [data-focused-year="true"]';
11734
11746
  const BASE_VIEW = {
11735
11747
  day: 'days',
11736
11748
  month: 'months',
@@ -11776,20 +11788,32 @@ function monthStep(key, col) {
11776
11788
  }
11777
11789
  }
11778
11790
  /**
11779
- * The month nearest `month` that `disabled` leaves enabled, `month` itself
11780
- * when it is. At equal distance the later month wins. With every month
11781
- * disabled it stays `month`, as there is nothing to reach.
11791
+ * Whether DOM focus should follow a grid's tab stop as it moves: focus is in
11792
+ * the grid, or fell to <body> while the grid held it last, as it does when
11793
+ * the focused cell leaves the page or becomes disabled. Focus is read in the
11794
+ * grid's own tree, so this holds inside a shadow root too.
11795
+ */
11796
+ function focusFollows(grid, heldLast) {
11797
+ const active = getActiveElementInTree(grid);
11798
+ if (grid.contains(active))
11799
+ return true;
11800
+ return heldLast && active === grid.ownerDocument.body;
11801
+ }
11802
+ /**
11803
+ * The cell nearest index `i` that `disabled` leaves enabled, `i` itself when
11804
+ * it is. At equal distance the later cell wins. With every cell disabled it
11805
+ * stays `i`, as there is nothing to reach.
11782
11806
  */
11783
- function nearestEnabledMonth(month, disabled) {
11784
- if (!disabled[month])
11785
- return month;
11786
- for (let d = 1; d < 12; d++) {
11787
- if (month + d < 12 && !disabled[month + d])
11788
- return month + d;
11789
- if (month - d >= 0 && !disabled[month - d])
11790
- return month - d;
11807
+ function nearestEnabled(i, disabled) {
11808
+ if (!disabled[i])
11809
+ return i;
11810
+ for (let d = 1; d < disabled.length; d++) {
11811
+ if (i + d < disabled.length && !disabled[i + d])
11812
+ return i + d;
11813
+ if (i - d >= 0 && !disabled[i - d])
11814
+ return i - d;
11791
11815
  }
11792
- return month;
11816
+ return i;
11793
11817
  }
11794
11818
  /**
11795
11819
  * The year list's keyboard when it is the selection surface: arrows move a
@@ -11861,6 +11885,33 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11861
11885
  shouldDisableDate,
11862
11886
  unselectableDates,
11863
11887
  }), [min, max, shouldDisableDate, unselectableDates]);
11888
+ // The day grid's one tab stop. A disabled button can't take focus, so when
11889
+ // the focused day is disabled the stop moves to the nearest enabled day of
11890
+ // the month on show, and focusing it makes it the focused day. A nearby
11891
+ // month's day is left out, as focusing it would turn the grid to its month.
11892
+ const tabStopDay = useMemo(() => {
11893
+ const days = cells.filter(c => c.inCurrentMonth).map(c => c.date);
11894
+ const i = nearestEnabled(focusedDate.getDate() - 1, days.map(isDateUnselectable));
11895
+ return days[i];
11896
+ }, [cells, focusedDate, isDateUnselectable]);
11897
+ // Whether the grid on show holds focus, or held it last before focus went
11898
+ // nowhere: the cell that had it left the page or became disabled. A blur
11899
+ // that takes focus out of the grid, to nowhere included, is the user
11900
+ // leaving, so it clears this. A render that removes or disables the focused
11901
+ // cell sends no blur here: browsers fire none, or fire it inside React's
11902
+ // commit, where React delivers no events.
11903
+ const gridHeldFocusRef = useRef(false);
11904
+ const gridFocusHandlers = {
11905
+ onFocus: () => {
11906
+ gridHeldFocusRef.current = true;
11907
+ },
11908
+ onBlur: (e) => {
11909
+ const next = e.relatedTarget;
11910
+ if (!next || !e.currentTarget.contains(next)) {
11911
+ gridHeldFocusRef.current = false;
11912
+ }
11913
+ },
11914
+ };
11864
11915
  // Step from `next` by `direction` days until we hit a selectable date,
11865
11916
  // skipping past dates blocked by min/max, shouldDisableDate, or
11866
11917
  // unselectableDates. If the whole searched range is disabled, focus stays
@@ -12024,6 +12075,16 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12024
12075
  // Only the selection surface disables years; as navigation every listed
12025
12076
  // year can be visited.
12026
12077
  const disabledYears = useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
12078
+ // The year list's one tab stop, after the month grid's: when the focused
12079
+ // year is disabled the stop moves to the nearest enabled year in the list,
12080
+ // and focusing it makes it the focused year. A year outside the list keeps
12081
+ // the stop, which then marks no option.
12082
+ const tabStopYear = useMemo(() => {
12083
+ const i = yearList.indexOf(focusedYear);
12084
+ if (i < 0)
12085
+ return focusedYear;
12086
+ return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
12087
+ }, [yearList, focusedYear, disabledYears]);
12027
12088
  // Which of the focused year's months have no selectable day.
12028
12089
  const disabledMonths = useMemo(() => granularity === 'month'
12029
12090
  ? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
@@ -12031,7 +12092,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12031
12092
  // The month grid's one tab stop. A disabled button can't take focus, so
12032
12093
  // when the focused month is disabled the stop moves to the nearest enabled
12033
12094
  // month, and focusing it makes it the focused month.
12034
- const tabStopMonth = nearestEnabledMonth(focusedDate.getMonth(), disabledMonths);
12095
+ const tabStopMonth = nearestEnabled(focusedDate.getMonth(), disabledMonths);
12035
12096
  const handleYearSelect = useCallback((year) => {
12036
12097
  const next = new Date(focusedDate);
12037
12098
  next.setFullYear(year);
@@ -12073,24 +12134,37 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12073
12134
  e.preventDefault();
12074
12135
  moveYearFocus(step[0], step[1]);
12075
12136
  }, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
12137
+ // The day or month grid takes focus as the popover opens on it, or comes
12138
+ // back to it from the year list.
12076
12139
  useEffect(() => {
12077
- if (view !== 'days')
12140
+ if (!autoFocusCell)
12078
12141
  return;
12079
- if (!autoFocusCell || !gridRef.current)
12142
+ const grid = view === 'days'
12143
+ ? gridRef.current
12144
+ : view === 'months'
12145
+ ? monthGridRef.current
12146
+ : null;
12147
+ grid?.querySelector('[data-focused="true"]')?.focus();
12148
+ }, [autoFocusCell, view]);
12149
+ // From then on focus follows the tab stop only while the grid has it, or
12150
+ // has just lost it, so focus on a header button or a time wheel stays put.
12151
+ const tabStopDayTime = tabStopDay.getTime();
12152
+ useEffect(() => {
12153
+ const grid = gridRef.current;
12154
+ if (view !== 'days' || !grid)
12080
12155
  return;
12081
- const target = gridRef.current.querySelector('[data-focused="true"]');
12082
- target?.focus();
12083
- }, [autoFocusCell, focusedDate, view]);
12084
- // The month grid takes focus when the popover asks for it, and keeps it on
12085
- // the focused month while the keyboard moves through the grid.
12156
+ if (!focusFollows(grid, gridHeldFocusRef.current))
12157
+ return;
12158
+ grid.querySelector('[data-focused="true"]')?.focus();
12159
+ }, [tabStopDayTime, view]);
12086
12160
  useEffect(() => {
12087
12161
  const grid = monthGridRef.current;
12088
12162
  if (view !== 'months' || !grid)
12089
12163
  return;
12090
- if (!autoFocusCell && !grid.contains(document.activeElement))
12164
+ if (!focusFollows(grid, gridHeldFocusRef.current))
12091
12165
  return;
12092
12166
  grid.querySelector('[data-focused="true"]')?.focus();
12093
- }, [autoFocusCell, focusedDate, view]);
12167
+ }, [focusedYear, tabStopMonth, view]);
12094
12168
  // When the year view opens as navigation, scroll the focused year into
12095
12169
  // view.
12096
12170
  useEffect(() => {
@@ -12124,15 +12198,15 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12124
12198
  const list = yearGridRef.current;
12125
12199
  if (!isYearGranularity || !list)
12126
12200
  return;
12127
- if (!list.contains(document.activeElement))
12201
+ if (!list.contains(getActiveElementInTree(list)))
12128
12202
  return;
12129
12203
  list.querySelector('[data-focused-year="true"]')?.focus();
12130
12204
  }, [isYearGranularity, focusedYear]);
12131
12205
  const navArrow = (d) => (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
12132
- return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, children: cells.map(cell => {
12206
+ return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
12133
12207
  const disabled = isDateUnselectable(cell.date);
12134
12208
  const isSelected = !!value && isSameDay(value, cell.date);
12135
- const isFocused = isSameDay(cell.date, focusedDate);
12209
+ const isFocused = isSameDay(cell.date, tabStopDay);
12136
12210
  const otherMonth = !cell.inCurrentMonth;
12137
12211
  const cellClass = prefixedClassNames(classPrefix, 'dateinput-cell', {
12138
12212
  'is-selected': isSelected,
@@ -12141,13 +12215,20 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12141
12215
  'is-other-month': otherMonth,
12142
12216
  });
12143
12217
  const display = !otherMonth || nearbyMonthDays;
12144
- return (jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onClick: () => {
12218
+ return (jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
12219
+ // Reached by Tab while the focused day is disabled, or
12220
+ // by pointer: keys move on from here. A nearby month's
12221
+ // day waits for its click, which turns the grid.
12222
+ if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
12223
+ onFocusedDateChange(cell.date);
12224
+ }
12225
+ }, onClick: () => {
12145
12226
  if (disabled)
12146
12227
  return;
12147
12228
  onFocusedDateChange(cell.date);
12148
12229
  onSelect(cell.date);
12149
12230
  }, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
12150
- }) })] })), view === 'months' && (jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, children: MONTH_ROWS.map(row => (jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
12231
+ }) })] })), view === 'months' && (jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
12151
12232
  const date = makeDate(focusedYear, month);
12152
12233
  const disabled = disabledMonths[month];
12153
12234
  const isSelected = !!value && isSameMonth(value, date);
@@ -12169,7 +12250,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12169
12250
  onSelect(date);
12170
12251
  }, children: monthCellNames[month] }, month));
12171
12252
  }) }, row[0]))) })), view === 'years' && (jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
12172
- const isFocused = year === focusedYear;
12253
+ const isFocused = year === tabStopYear;
12173
12254
  const isToday = year === todayYear;
12174
12255
  // As navigation the focused year is the current one. As the
12175
12256
  // selection surface the value's year is selected and focus roams.
@@ -12182,7 +12263,13 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12182
12263
  'is-today': isToday,
12183
12264
  'is-disabled': disabled,
12184
12265
  });
12185
- return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: isFocused ? 0 : -1, disabled: disabled, className: cellCls, onClick: () => isYearGranularity
12266
+ return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: isFocused ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
12267
+ // Reached by Tab while the focused year is disabled, or by
12268
+ // pointer: keys move on from here.
12269
+ if (isYearGranularity && year !== focusedYear) {
12270
+ onFocusedDateChange(clampDate(makeDate(year), min, max));
12271
+ }
12272
+ }, onClick: () => isYearGranularity
12186
12273
  ? handleYearPick(year)
12187
12274
  : handleYearSelect(year), children: year }, year));
12188
12275
  }) }))] }));
@@ -12190,12 +12277,26 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12190
12277
  Calendar.displayName = 'Calendar';
12191
12278
 
12192
12279
  const isBrowser = typeof window !== 'undefined';
12193
- const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'bottom-left', appendToBody = false, className, trapFocus = true, closeOnClickOutside = true, closeOnEscape = true, ariaLabel, ariaLabelledBy, role = 'dialog', id, restoreFocusRef, }) => {
12280
+ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'bottom-left', appendToBody = false, className, trapFocus = true, closeOnClickOutside = true, closeOnEscape = true, ariaLabel, ariaLabelledBy, role = 'dialog', id, restoreFocusRef, initialFocusSelector, }) => {
12194
12281
  const panelRef = useRef(null);
12282
+ // Read as the trap turns on, once the panel's content has rendered.
12283
+ const initialFocusRef = useMemo(() => initialFocusSelector === undefined
12284
+ ? undefined
12285
+ : {
12286
+ get current() {
12287
+ return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
12288
+ },
12289
+ }, [initialFocusSelector]);
12195
12290
  const resolved = useAnchoredPosition(anchorRef, panelRef, {
12196
12291
  active: isOpen,
12197
12292
  position,
12198
12293
  fixed: appendToBody,
12294
+ // A portaled panel takes its gap from `--bulma-picker-popover-offset` as
12295
+ // a margin, so its coordinates leave the gap out and `auto` reads it off
12296
+ // the panel. An in-place panel's gap is in its `top` or `bottom`, which
12297
+ // `auto` cannot read, so it keeps the hook's default estimate.
12298
+ offset: appendToBody ? 0 : undefined,
12299
+ styledGap: marginGap,
12199
12300
  });
12200
12301
  useEffect(() => {
12201
12302
  if (!isOpen || !closeOnClickOutside || !isBrowser)
@@ -12229,6 +12330,7 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12229
12330
  }, [isOpen, closeOnEscape, onClose]);
12230
12331
  useFocusTrap(panelRef, {
12231
12332
  active: isOpen && trapFocus,
12333
+ initialFocusRef,
12232
12334
  restoreFocus: restoreFocusRef ?? true,
12233
12335
  });
12234
12336
  const panelClass = usePrefixedClassNames('picker-popover', {
@@ -12408,8 +12510,11 @@ function buildSegmentMap(format) {
12408
12510
  // ---------------------------------------------------------------------------
12409
12511
  const wrap$1 = (n, max) => ((n % max) + max) % max;
12410
12512
  const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
12411
- /** Number of days in the month of `d` (handles leap Februarys). */
12412
- const daysInMonth = (d) => new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
12513
+ /**
12514
+ * Number of days in the month of `d` (handles leap Februarys). Built on
12515
+ * `makeDate`, so a year below 100 keeps its own leap rule.
12516
+ */
12517
+ const daysInMonth = (d) => makeDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
12413
12518
  /**
12414
12519
  * Returns a new Date with the given segment incremented (`delta` = +1 or -1).
12415
12520
  * Wraps at segment boundaries: year is unbounded, month 0↔11 (in place, no
@@ -13017,16 +13122,19 @@ const toIsoValue = (d, granularity) => {
13017
13122
  const month = `${year}-${pad2$1(d.getMonth() + 1)}`;
13018
13123
  return granularity === 'month' ? month : `${month}-${pad2$1(d.getDate())}`;
13019
13124
  };
13020
- /** Read a native `type="date"` or `type="month"` value back into a Date. */
13125
+ /**
13126
+ * Read a native `type="date"` or `type="month"` value back into a Date. HTML
13127
+ * allows a year of four or more digits, as `toIsoValue` writes one past 9999.
13128
+ */
13021
13129
  const fromIsoValue = (s, granularity) => {
13022
13130
  if (granularity === 'month') {
13023
- const m = /^(\d{4})-(\d{2})$/.exec(s);
13131
+ const m = /^(\d{4,})-(\d{2})$/.exec(s);
13024
13132
  return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
13025
13133
  }
13026
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
13134
+ const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
13027
13135
  if (!m)
13028
13136
  return null;
13029
- return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
13137
+ return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
13030
13138
  };
13031
13139
  const DEFAULT_FORMATS = {
13032
13140
  day: DEFAULT_DATE_FORMAT,
@@ -13090,16 +13198,20 @@ const DateInputBase = forwardRef((props, ref) => {
13090
13198
  });
13091
13199
  const containerClass = usePrefixedClassNames('dateinput-container');
13092
13200
  const triggerClass = usePrefixedClassNames('dateinput-trigger');
13201
+ // What the latest request asked for, so a second request in the same
13202
+ // event sees it before React renders. The callbacks fire here rather than
13203
+ // in a state updater, which StrictMode calls twice, and after the state
13204
+ // is set, so a request a callback makes is the one that lands last.
13205
+ const requestedOpenRef = useRef(false);
13093
13206
  const setOpen = useCallback((next) => {
13094
- setOpenState(prev => {
13095
- if (prev === next)
13096
- return prev;
13097
- if (next)
13098
- onOpen?.();
13099
- else
13100
- onClose?.();
13101
- return next;
13102
- });
13207
+ if (requestedOpenRef.current === next)
13208
+ return;
13209
+ requestedOpenRef.current = next;
13210
+ setOpenState(next);
13211
+ if (next)
13212
+ onOpen?.();
13213
+ else
13214
+ onClose?.();
13103
13215
  }, [onOpen, onClose]);
13104
13216
  // Sync displayed text with value when value changes externally.
13105
13217
  useEffect(() => {
@@ -13244,14 +13356,14 @@ const DateInputBase = forwardRef((props, ref) => {
13244
13356
  : t.chooseDate;
13245
13357
  const calendar = (jsx(Calendar
13246
13358
  // A new granularity starts the calendar over on its own view.
13247
- , { granularity: granularity, value: value, focusedDate: focusedDate, onSelect: handleSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size, id: popoverId, autoFocusCell: open, labels: labels }, granularity));
13359
+ , { granularity: granularity, value: value, focusedDate: focusedDate, onSelect: handleSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size, id: `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
13248
13360
  if (inline) {
13249
13361
  return (jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [calendar, name && (jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoValue(value, granularity) : '', required: required }))] }));
13250
13362
  }
13251
13363
  return (jsxs("div", { ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), ...cleanRest, children: [jsx("input", { ref: combinedRef, type: "text", role: "combobox", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": popoverId, autoComplete: "off", className: inputClass, value: text, placeholder: placeholder, disabled: disabled, readOnly: inputReadOnlyAttr, name: name, form: form, required: required, id: id, ...inputHandlers }), popover && triggerIcon && (jsx("button", { type: "button", className: triggerClass, onClick: () => {
13252
13364
  if (canOpen)
13253
13365
  setOpen(!open);
13254
- }, disabled: !canOpen, "aria-label": chooseLabel, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: handlePopoverClose, anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: chooseLabel, id: popoverId, restoreFocusRef: inputRef, children: calendar }))] }));
13366
+ }, disabled: !canOpen, "aria-label": chooseLabel, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: handlePopoverClose, anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: chooseLabel, id: popoverId, restoreFocusRef: inputRef, initialFocusSelector: CALENDAR_FOCUSED_CELL, children: calendar }))] }));
13255
13367
  });
13256
13368
  DateInputBase.displayName = 'DateInputBase';
13257
13369
 
@@ -14224,19 +14336,23 @@ const TimeInputBase = forwardRef((props, ref) => {
14224
14336
  const mobileFooterClass = usePrefixedClassNames('timeinput-footer-mobile');
14225
14337
  const mobileFooterResetClass = usePrefixedClassNames('timeinput-footer-reset');
14226
14338
  const mobileFooterDoneClass = usePrefixedClassNames('timeinput-footer-done');
14339
+ // What the latest request asked for, so a second request in the same
14340
+ // event sees it before React renders. The callbacks fire here rather than
14341
+ // in a state updater, which StrictMode calls twice, and after the state
14342
+ // is set, so a request a callback makes is the one that lands last.
14343
+ const requestedOpenRef = useRef(false);
14227
14344
  const setOpen = useCallback((next) => {
14228
- setOpenState(prev => {
14229
- if (prev === next)
14230
- return prev;
14231
- if (next) {
14232
- valueAtOpenRef.current = value;
14233
- onOpen?.();
14234
- }
14235
- else {
14236
- onClose?.();
14237
- }
14238
- return next;
14239
- });
14345
+ if (requestedOpenRef.current === next)
14346
+ return;
14347
+ requestedOpenRef.current = next;
14348
+ setOpenState(next);
14349
+ if (next) {
14350
+ valueAtOpenRef.current = value;
14351
+ onOpen?.();
14352
+ }
14353
+ else {
14354
+ onClose?.();
14355
+ }
14240
14356
  }, [onOpen, onClose, value]);
14241
14357
  useEffect(() => {
14242
14358
  setText(value ? formatTime(value, defaultFormat, locale) : '');
@@ -14392,8 +14508,9 @@ const TimeInput = forwardRef(({ label, labelSize, labelProps, horizontal, iconLe
14392
14508
  });
14393
14509
  TimeInput.displayName = 'TimeInput';
14394
14510
 
14511
+ // The year is padded to four digits, as `datetime-local` requires.
14395
14512
  const toIsoDateTime = (d, withSeconds) => {
14396
- const yyyy = d.getFullYear();
14513
+ const yyyy = String(d.getFullYear()).padStart(4, '0');
14397
14514
  const mo = String(d.getMonth() + 1).padStart(2, '0');
14398
14515
  const dd = String(d.getDate()).padStart(2, '0');
14399
14516
  const hh = String(d.getHours()).padStart(2, '0');
@@ -14406,10 +14523,13 @@ const toIsoDateTime = (d, withSeconds) => {
14406
14523
  const fromIsoDateTime = (s) => {
14407
14524
  // The HTML datetime-local value may carry fractional seconds (the spec
14408
14525
  // allows them and some engines normalize to `:ss.sss`); accept and drop.
14409
- const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?$/.exec(s);
14526
+ // Its year is four or more digits, as `toIsoDateTime` writes one past 9999.
14527
+ const m = /^(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?$/.exec(s);
14410
14528
  if (!m)
14411
14529
  return null;
14412
- return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), m[6] ? Number(m[6]) : 0, 0);
14530
+ const d = makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
14531
+ d.setHours(Number(m[4]), Number(m[5]), m[6] ? Number(m[6]) : 0, 0);
14532
+ return d;
14413
14533
  };
14414
14534
  /**
14415
14535
  * Raw DateTimeInput — combined input + calendar + time spinner without
@@ -14496,19 +14616,23 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14496
14616
  const footerResetClass = usePrefixedClassNames('datetimeinput-footer-reset');
14497
14617
  const footerDoneClass = usePrefixedClassNames('datetimeinput-footer-done');
14498
14618
  const footerTimePillClass = usePrefixedClassNames('datetimeinput-footer-time-pill');
14619
+ // What the latest request asked for, so a second request in the same event
14620
+ // sees it before React renders. The callbacks fire here rather than in a
14621
+ // state updater, which StrictMode calls twice, and after the state is set,
14622
+ // so a request a callback makes is the one that lands last.
14623
+ const requestedOpenRef = useRef(false);
14499
14624
  const setOpen = useCallback((next) => {
14500
- setOpenState(prev => {
14501
- if (prev === next)
14502
- return prev;
14503
- if (next) {
14504
- valueAtOpenRef.current = value;
14505
- onOpen?.();
14506
- }
14507
- else {
14508
- onClose?.();
14509
- }
14510
- return next;
14511
- });
14625
+ if (requestedOpenRef.current === next)
14626
+ return;
14627
+ requestedOpenRef.current = next;
14628
+ setOpenState(next);
14629
+ if (next) {
14630
+ valueAtOpenRef.current = value;
14631
+ onOpen?.();
14632
+ }
14633
+ else {
14634
+ onClose?.();
14635
+ }
14512
14636
  }, [onOpen, onClose, value]);
14513
14637
  useEffect(() => {
14514
14638
  setText(value ? formatDateTime(value, defaultFormat, locale) : '');
@@ -14638,7 +14762,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14638
14762
  return (jsxs("div", { ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), ...cleanRest, children: [jsx("input", { ref: combinedRef, type: "text", role: "combobox", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": popoverId, autoComplete: "off", className: inputClass, value: text, placeholder: placeholder, disabled: disabled, readOnly: inputReadOnlyAttr, name: name, form: form, required: required, id: id, ...inputHandlers }), popover && triggerIcon && (jsx("button", { type: "button", className: triggerClass, onClick: () => {
14639
14763
  if (canOpen)
14640
14764
  setOpen(!open);
14641
- }, disabled: !canOpen, "aria-label": t.chooseDateTime, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: () => setOpen(false), anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: t.chooseDateTime, id: popoverId, restoreFocusRef: inputRef, children: panel }))] }));
14765
+ }, disabled: !canOpen, "aria-label": t.chooseDateTime, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: () => setOpen(false), anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: t.chooseDateTime, id: popoverId, restoreFocusRef: inputRef, initialFocusSelector: CALENDAR_FOCUSED_CELL, children: panel }))] }));
14642
14766
  });
14643
14767
  DateTimeInputBase.displayName = 'DateTimeInputBase';
14644
14768
 
@@ -15620,31 +15744,57 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
15620
15744
  // rewrites the shared element in place rather than removing and recreating
15621
15745
  // it between the cleanup and the next run.
15622
15746
  useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
15623
- // Toggle Bulma's light/dark scheme by writing the `data-theme` attribute on
15624
- // the document root (<html>). This is always global, even on a scoped Theme.
15625
- // `'system'` removes the attribute so Bulma follows the OS preference.
15747
+ // Bulma names its scheme attribute after the class prefix a stylesheet was
15748
+ // built with (`data-<prefix>theme`), so the prefixed builds read
15749
+ // `data-bestax-theme` and never `data-theme`.
15750
+ const classPrefix = useClassPrefix();
15751
+ // Toggle Bulma's light/dark scheme by writing the theme attribute on the
15752
+ // document root (<html>). This is always global, even on a scoped Theme.
15753
+ // Under a class prefix the prefixed attribute is written as well as
15754
+ // `data-theme`, so whichever build is loaded sees it; with no prefix only
15755
+ // `data-theme` is. `'system'` removes them so Bulma follows the OS
15756
+ // preference.
15626
15757
  useEffect(() => {
15627
15758
  if (colorMode === undefined) {
15628
15759
  return;
15629
15760
  }
15630
15761
  const root = document.documentElement;
15631
- const previous = root.getAttribute('data-theme');
15632
- if (colorMode === 'system') {
15633
- root.removeAttribute('data-theme');
15634
- }
15635
- else {
15636
- root.setAttribute('data-theme', colorMode);
15637
- }
15638
- // Restore the previous value when colorMode changes or the component unmounts.
15639
- return () => {
15640
- if (previous === null) {
15641
- root.removeAttribute('data-theme');
15762
+ const names = classPrefix
15763
+ ? ['data-theme', `data-${classPrefix}theme`]
15764
+ : ['data-theme'];
15765
+ const previous = names.map(name => root.getAttribute(name));
15766
+ for (const name of names) {
15767
+ if (colorMode === 'system') {
15768
+ root.removeAttribute(name);
15769
+ continue;
15642
15770
  }
15643
- else {
15644
- root.setAttribute('data-theme', previous);
15771
+ try {
15772
+ root.setAttribute(name, colorMode);
15773
+ }
15774
+ catch {
15775
+ // A class prefix can hold characters an attribute name cannot.
15776
+ // `data-theme` is still written, as it was before the prefixed
15777
+ // attribute existed, but a sheet built with that prefix keeps its
15778
+ // scheme, so say so.
15779
+ warnOnce('Theme:color-mode-prefix', `[bestax-bulma] <Theme colorMode="${colorMode}">: the class ` +
15780
+ `prefix "${classPrefix}" makes ${name} an invalid attribute ` +
15781
+ 'name, so only data-theme is written, and a stylesheet built ' +
15782
+ 'with that prefix keeps its scheme.');
15645
15783
  }
15784
+ }
15785
+ // Restore the previous values when colorMode changes or the component unmounts.
15786
+ return () => {
15787
+ names.forEach((name, i) => {
15788
+ const value = previous[i];
15789
+ if (value === null) {
15790
+ root.removeAttribute(name);
15791
+ }
15792
+ else {
15793
+ root.setAttribute(name, value);
15794
+ }
15795
+ });
15646
15796
  };
15647
- }, [colorMode]);
15797
+ }, [colorMode, classPrefix]);
15648
15798
  // For local injection (when isRoot is false), prepare style object for CSS vars
15649
15799
  const style = useMemo(() => {
15650
15800
  if (isRoot) {