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