@allxsmith/bestax-bulma 5.26.2 → 5.26.4

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 (82) 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 +992 -580
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +992 -580
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/elements/Progress.d.ts +6 -1
  10. package/dist/types/form/Checkboxes.d.ts +7 -0
  11. package/dist/types/form/Control.d.ts +14 -2
  12. package/dist/types/form/DateInput.d.ts +28 -3
  13. package/dist/types/form/DateInputBase.d.ts +17 -3
  14. package/dist/types/form/DateTimeInput.d.ts +28 -3
  15. package/dist/types/form/DateTimeInputBase.d.ts +17 -3
  16. package/dist/types/form/File.d.ts +6 -0
  17. package/dist/types/form/FormContext.d.ts +39 -7
  18. package/dist/types/form/Input.d.ts +21 -4
  19. package/dist/types/form/InputBase.d.ts +5 -2
  20. package/dist/types/form/Radios.d.ts +7 -0
  21. package/dist/types/form/Rate.d.ts +7 -0
  22. package/dist/types/form/Select.d.ts +13 -2
  23. package/dist/types/form/Slider.d.ts +7 -0
  24. package/dist/types/form/Taginput.d.ts +6 -0
  25. package/dist/types/form/TextArea.d.ts +9 -2
  26. package/dist/types/form/TextAreaBase.d.ts +12 -3
  27. package/dist/types/form/TimeInput.d.ts +28 -3
  28. package/dist/types/form/TimeInputBase.d.ts +21 -4
  29. package/dist/types/form/_pickerInternals/Calendar.d.ts +17 -2
  30. package/dist/types/form/_pickerInternals/PickerPopover.d.ts +6 -0
  31. package/dist/types/form/_pickerInternals/dateUtils.d.ts +17 -0
  32. package/dist/types/form/controlLoading.d.ts +4 -0
  33. package/dist/types/helpers/Theme.d.ts +13 -4
  34. package/dist/types/helpers/portal.d.ts +2 -1
  35. package/dist/types/helpers/useAnchoredPosition.d.ts +10 -1
  36. package/dist/types/helpers/useFocusTrap.d.ts +13 -5
  37. package/dist/types/helpers/useOtherClasses.d.ts +13 -1
  38. package/dist/types-cjs/elements/Progress.d.ts +6 -1
  39. package/dist/types-cjs/form/Checkboxes.d.ts +7 -0
  40. package/dist/types-cjs/form/Control.d.ts +14 -2
  41. package/dist/types-cjs/form/DateInput.d.ts +28 -3
  42. package/dist/types-cjs/form/DateInputBase.d.ts +17 -3
  43. package/dist/types-cjs/form/DateTimeInput.d.ts +28 -3
  44. package/dist/types-cjs/form/DateTimeInputBase.d.ts +17 -3
  45. package/dist/types-cjs/form/File.d.ts +6 -0
  46. package/dist/types-cjs/form/FormContext.d.ts +39 -7
  47. package/dist/types-cjs/form/Input.d.ts +21 -4
  48. package/dist/types-cjs/form/InputBase.d.ts +5 -2
  49. package/dist/types-cjs/form/Radios.d.ts +7 -0
  50. package/dist/types-cjs/form/Rate.d.ts +7 -0
  51. package/dist/types-cjs/form/Select.d.ts +13 -2
  52. package/dist/types-cjs/form/Slider.d.ts +7 -0
  53. package/dist/types-cjs/form/Taginput.d.ts +6 -0
  54. package/dist/types-cjs/form/TextArea.d.ts +9 -2
  55. package/dist/types-cjs/form/TextAreaBase.d.ts +12 -3
  56. package/dist/types-cjs/form/TimeInput.d.ts +28 -3
  57. package/dist/types-cjs/form/TimeInputBase.d.ts +21 -4
  58. package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +17 -2
  59. package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +6 -0
  60. package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +17 -0
  61. package/dist/types-cjs/form/controlLoading.d.ts +4 -0
  62. package/dist/types-cjs/helpers/Theme.d.ts +13 -4
  63. package/dist/types-cjs/helpers/portal.d.ts +2 -1
  64. package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +10 -1
  65. package/dist/types-cjs/helpers/useFocusTrap.d.ts +13 -5
  66. package/dist/types-cjs/helpers/useOtherClasses.d.ts +13 -1
  67. package/dist/versions/bestax-no-dark-mode.css +1 -1
  68. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  69. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  70. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  71. package/dist/versions/bestax-no-helpers.css +1 -1
  72. package/dist/versions/bestax-no-helpers.css.map +1 -1
  73. package/dist/versions/bestax-prefixed.css +1 -1
  74. package/dist/versions/bestax-prefixed.css.map +1 -1
  75. package/package.json +1 -1
  76. package/src/scss/elements/_index.scss +2 -0
  77. package/src/scss/elements/_progress.scss +17 -0
  78. package/src/scss/elements/_skeleton.scss +21 -0
  79. package/src/scss/form/_datetimeinput.scss +9 -0
  80. package/src/scss/form/_picker-popover.scss +23 -4
  81. package/src/scss/form/_rate.scss +3 -1
  82. package/src/scss/form/_timeinput.scss +12 -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,12 +5198,20 @@ 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
- *
5202
- * The container has to be in the DOM when the trap turns on. The trap waits
5203
- * for hydration, so a container that only appears once the page has
5204
- * hydrated, the way portaled content does, still has it attach in the commit
5205
- * where the container appears.
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`.
5203
+ *
5204
+ * The trap looks at `containerRef` again each time the component calling it
5205
+ * renders. A container that mounts after the trap turns on, such as a panel
5206
+ * waiting for a `Portal` target held in state, has it attach once that
5207
+ * component renders with the container in the DOM. When the container leaves
5208
+ * or is replaced, the trap lets go of it as it would turning off, handing
5209
+ * focus back, and attaches to the next one.
5210
+ * A child that mounts the container in a render of its own (from its own
5211
+ * state, or as a `Suspense` boundary resolves) goes unseen until the
5212
+ * component calling the hook renders again. The trap waits for hydration, so
5213
+ * that component renders again when the page hydrates, and portaled content,
5214
+ * which first appears then, is found.
5206
5215
  *
5207
5216
  * @function useFocusTrap
5208
5217
  * @param containerRef - Ref to the element focus stays inside.
@@ -5222,12 +5231,18 @@ function useFocusTrap(containerRef, options = {}) {
5222
5231
  // Portaled content has no server-rendered counterpart, so a portaled
5223
5232
  // container only exists from the commit after hydration.
5224
5233
  const hydrated = useIsHydrated();
5234
+ // The container the trap is attached to, or `null` while it has none.
5235
+ const heldRef = React.useRef(null);
5236
+ // Bumped to run the trap again when the container in the ref changes.
5237
+ const [refChanges, setRefChanges] = React.useState(0);
5225
5238
  React.useEffect(() => {
5239
+ heldRef.current = null;
5226
5240
  if (!active || !hydrated)
5227
5241
  return undefined;
5228
5242
  const container = containerRef.current;
5229
5243
  if (!container)
5230
5244
  return undefined;
5245
+ heldRef.current = container;
5231
5246
  const doc = container.ownerDocument;
5232
5247
  const opener = focusedElement(doc);
5233
5248
  // `initialFocusRef` counts only when focusing it puts focus inside. Focus
@@ -5302,7 +5317,28 @@ function useFocusTrap(containerRef, options = {}) {
5302
5317
  target.focus();
5303
5318
  }
5304
5319
  };
5305
- }, [active, hydrated, containerRef, initialFocusRef, restoreFocus]);
5320
+ }, [
5321
+ active,
5322
+ hydrated,
5323
+ containerRef,
5324
+ initialFocusRef,
5325
+ restoreFocus,
5326
+ refChanges,
5327
+ ]);
5328
+ // Nothing says when a ref is set, so after every render, a ref that no
5329
+ // longer matches what the trap holds (a container that mounted, left or was
5330
+ // replaced) runs it again. Effects run in order, so whenever the effect
5331
+ // above ran in this commit, it read the ref just before this does, and a
5332
+ // mismatch means the ref changed in a render it didn't run for. The render
5333
+ // a bump asks for runs it, so the two match and the trap settles. An empty
5334
+ // ref matches a trap holding nothing, so a container that never mounts
5335
+ // costs no render.
5336
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
5337
+ React.useEffect(() => {
5338
+ if (active && hydrated && containerRef.current !== heldRef.current) {
5339
+ setRefChanges(n => n + 1);
5340
+ }
5341
+ });
5306
5342
  }
5307
5343
 
5308
5344
  // INTERNAL: deliberately not exported from src/index.ts.
@@ -5314,6 +5350,15 @@ function useFocusTrap(containerRef, options = {}) {
5314
5350
  const useClientLayoutEffect = typeof document === 'undefined' ? React.useEffect : React.useLayoutEffect;
5315
5351
 
5316
5352
  // INTERNAL: deliberately not exported from src/index.ts.
5353
+ /**
5354
+ * A `styledGap` for a panel whose stylesheet puts the gap in a margin on the
5355
+ * side facing the anchor, negative on a portaled panel above it, so the
5356
+ * larger magnitude is the gap. In pixels.
5357
+ */
5358
+ function marginGap(panel) {
5359
+ const { marginTop, marginBottom } = getComputedStyle(panel);
5360
+ return Math.max(Math.abs(parseFloat(marginTop)) || 0, Math.abs(parseFloat(marginBottom)) || 0);
5361
+ }
5317
5362
  /**
5318
5363
  * The corner that keeps a panel of this size in the viewport, preferring
5319
5364
  * below the anchor and lined up with its left edge.
@@ -5538,15 +5583,6 @@ function nearestDialog(event) {
5538
5583
  return role === 'dialog' || role === 'alertdialog';
5539
5584
  }) ?? null);
5540
5585
  }
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
5586
  /**
5551
5587
  * 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
5588
  *
@@ -5598,7 +5634,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
5598
5634
  // The gap comes from `--bulma-popover-offset` in the stylesheet, so the
5599
5635
  // coordinates leave it out and `auto` reads it off the panel.
5600
5636
  offset: 0,
5601
- styledGap,
5637
+ styledGap: marginGap,
5602
5638
  });
5603
5639
  // The trigger renders first inside the wrapper. Read when needed rather
5604
5640
  // than held, so a trigger that remounted is still the one found.
@@ -8735,6 +8771,56 @@ const useInsideControl = () => React.useContext(ControlContext);
8735
8771
  const FieldProvider = FieldContext.Provider;
8736
8772
  /** Provider for Control context — used internally by Control component. */
8737
8773
  const ControlProvider = ControlContext.Provider;
8774
+ // Each Field-shaping prop, with what takes its place on a `Field` the caller
8775
+ // wraps around the `Control`. `message` needs nothing there, because the
8776
+ // wrapper still renders it inside `Field > Control`. The other Field-level
8777
+ // props (`labelSize`, `labelProps`, `messageColor`) change nothing unless a
8778
+ // `label` or `message` renders, so on their own they keep no `Field`.
8779
+ // `bare-control.test.tsx` holds this list to what each wrapper hands its
8780
+ // `Field` and to which props change its markup, so a new Field-level prop
8781
+ // fails there until it is sorted into one group or the other.
8782
+ const FIELD_SHAPING = [
8783
+ ['label', 'label'],
8784
+ ['message', undefined],
8785
+ ['horizontal', 'horizontal'],
8786
+ ['fieldClassName', 'className (for fieldClassName)'],
8787
+ ];
8788
+ /** `a`, `a and b`, `a, b and c`. */
8789
+ const listOf = (items) => items.length > 1
8790
+ ? `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`
8791
+ : items[0];
8792
+ /**
8793
+ * Whether a convenience form wrapper renders a `Field` of its own (#905).
8794
+ * Never inside an outer `Field`. Inside a `Control` with no `Field` around
8795
+ * it, a `Field` of its own would put a `.field` inside the `.control`, where
8796
+ * Bulma's control styles break (its icon rules are sibling selectors on the
8797
+ * input). So it renders one there only when a prop that shapes the `Field`'s
8798
+ * markup is set (`label`, `message`, `horizontal`, `fieldClassName`), keeping
8799
+ * the markup it always had, and warns in development that the fix is a
8800
+ * `Field` around that `Control`. A prop counts when it is truthy, the same
8801
+ * test that decides whether it changes the `Field`'s output.
8802
+ * Internal; not part of the public API.
8803
+ */
8804
+ const rendersOwnField = (component, options) => {
8805
+ if (options.insideField)
8806
+ return false;
8807
+ if (!options.insideControl)
8808
+ return true;
8809
+ const held = FIELD_SHAPING.filter(([prop]) => options[prop]);
8810
+ if (held.length === 0)
8811
+ return false;
8812
+ const names = held.map(([prop]) => prop);
8813
+ const moved = held
8814
+ .map(([, onField]) => onField)
8815
+ .filter((onField) => onField !== undefined);
8816
+ warnOnce(`${component}:Field-in-bare-Control:${names.join('+')}`, `[bestax-bulma] <${component} ${names.join(' ')}> inside a <Control> ` +
8817
+ `with no <Field> around it renders its own <Field> for ` +
8818
+ `${names.length > 1 ? 'those props' : 'that prop'}, which puts a ` +
8819
+ `.field inside the .control, a nesting Bulma's styles are not ` +
8820
+ `written for. Wrap the <Control> in a <Field> instead` +
8821
+ (moved.length > 0 ? `, and set ${listOf(moved)} on that <Field>.` : '.'));
8822
+ return true;
8823
+ };
8738
8824
  const FieldLabelIdContext = React.createContext(undefined);
8739
8825
  /**
8740
8826
  * The id a labeled Field wants its single composed control to adopt (#495).
@@ -8865,6 +8951,18 @@ const Checkbox = React.forwardRef(({ color, size, className, children, textColor
8865
8951
  });
8866
8952
  Checkbox.displayName = 'Checkbox';
8867
8953
 
8954
+ // Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
8955
+ // picker bases read it so their right-side launcher gives way to that
8956
+ // spinner, which sits at the same edge, whether the `Control` is the one a
8957
+ // convenience input renders or one the caller wrapped around the input.
8958
+ // Kept apart from `FormContext`, whose contexts only report whether a `Field`
8959
+ // or `Control` is present. Internal; not part of the public API.
8960
+ const ControlLoadingContext = React.createContext(false);
8961
+ /** Provider for the loading state, used internally by `Control`. */
8962
+ const ControlLoadingProvider = ControlLoadingContext.Provider;
8963
+ /** Whether the nearest enclosing `Control` is loading; `false` outside one. */
8964
+ const useControlLoading = () => React.useContext(ControlLoadingContext);
8965
+
8868
8966
  const allowedColors = [...validColors, 'inherit', 'current'];
8869
8967
  /**
8870
8968
  * The `Control` component is a Bulma-styled wrapper for form controls (`Input`, `Select`, `TextArea`, etc.), supporting icons (left/right), loading state, expansion, size, and Bulma helper props for layout and color.
@@ -8924,7 +9022,7 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
8924
9022
  });
8925
9023
  const controlClass = classNames(mainClass, bulmaHelperClasses, className);
8926
9024
  // --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
8927
- return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [children, leftIconValue &&
9025
+ return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsxRuntime.jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
8928
9026
  (isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
8929
9027
  (isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
8930
9028
  });
@@ -9149,10 +9247,18 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, }) {
9149
9247
  const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
9150
9248
  const insideField = useInsideField();
9151
9249
  const insideControl = useInsideControl();
9250
+ const ownField = rendersOwnField('Checkboxes', {
9251
+ insideField,
9252
+ insideControl,
9253
+ label,
9254
+ message,
9255
+ horizontal,
9256
+ fieldClassName,
9257
+ });
9152
9258
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
9153
9259
  label,
9154
9260
  labelProps,
9155
- rendersLabel: !insideField,
9261
+ rendersLabel: ownField,
9156
9262
  });
9157
9263
  const { bulmaHelperClasses, rest } = useBulmaClasses({
9158
9264
  ...props,
@@ -9181,7 +9287,7 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
9181
9287
  if (!insideControl) {
9182
9288
  content = jsxRuntime.jsx(Control, { children: content });
9183
9289
  }
9184
- if (!insideField) {
9290
+ if (ownField) {
9185
9291
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9186
9292
  }
9187
9293
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -9200,11 +9306,20 @@ const File = React.forwardRef(({
9200
9306
  // Field props
9201
9307
  label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
9202
9308
  const insideField = useInsideField();
9309
+ const insideControl = useInsideControl();
9310
+ const ownField = rendersOwnField('File', {
9311
+ insideField,
9312
+ insideControl,
9313
+ label,
9314
+ message,
9315
+ horizontal,
9316
+ fieldClassName,
9317
+ });
9203
9318
  const { controlId, fieldLabelProps } = useAutoLabelId({
9204
9319
  label,
9205
9320
  id: props.id,
9206
9321
  labelProps,
9207
- rendersLabel: !insideField,
9322
+ rendersLabel: ownField,
9208
9323
  });
9209
9324
  const { classPrefix } = useConfig();
9210
9325
  const { bulmaHelperClasses, rest } = useBulmaClasses({
@@ -9236,7 +9351,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
9236
9351
  });
9237
9352
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
9238
9353
  const fileElement = (jsxRuntime.jsx("div", { className: fileClass, children: jsxRuntime.jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsxRuntime.jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxRuntime.jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsxRuntime.jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
9239
- if (!insideField) {
9354
+ if (ownField) {
9240
9355
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
9241
9356
  }
9242
9357
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [fileElement, messageEl] }));
@@ -9369,10 +9484,18 @@ Radio.displayName = 'Radio';
9369
9484
  const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
9370
9485
  const insideField = useInsideField();
9371
9486
  const insideControl = useInsideControl();
9487
+ const ownField = rendersOwnField('Radios', {
9488
+ insideField,
9489
+ insideControl,
9490
+ label,
9491
+ message,
9492
+ horizontal,
9493
+ fieldClassName,
9494
+ });
9372
9495
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
9373
9496
  label,
9374
9497
  labelProps,
9375
- rendersLabel: !insideField,
9498
+ rendersLabel: ownField,
9376
9499
  });
9377
9500
  const { bulmaHelperClasses, rest } = useBulmaClasses({
9378
9501
  ...props,
@@ -9404,7 +9527,7 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
9404
9527
  if (!insideControl) {
9405
9528
  content = jsxRuntime.jsx(Control, { children: content });
9406
9529
  }
9407
- if (!insideField) {
9530
+ if (ownField) {
9408
9531
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9409
9532
  }
9410
9533
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -9584,6 +9707,14 @@ const Slider = React.forwardRef((props, ref) => {
9584
9707
  range: _range, value: _value, defaultValue: _defaultValue, onChange: _onChange, ariaLabel: _ariaLabel, minDistance: _minDistance, nameLow: _nameLow, nameHigh: _nameHigh, ...restProps } = props;
9585
9708
  const insideField = useInsideField();
9586
9709
  const insideControl = useInsideControl();
9710
+ const ownField = rendersOwnField('Slider', {
9711
+ insideField,
9712
+ insideControl,
9713
+ label,
9714
+ message,
9715
+ horizontal,
9716
+ fieldClassName,
9717
+ });
9587
9718
  const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
9588
9719
  // In range mode the low thumb carries `rest` (and any user id), so the
9589
9720
  // label targets it; each thumb keeps its own aria-label for AT.
@@ -9591,7 +9722,7 @@ const Slider = React.forwardRef((props, ref) => {
9591
9722
  label,
9592
9723
  id: restProps.id,
9593
9724
  labelProps,
9594
- rendersLabel: !insideField,
9725
+ rendersLabel: ownField,
9595
9726
  });
9596
9727
  // Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
9597
9728
  const tooltipMode = tooltip ?? (showOutput ? 'auto' : 'hidden');
@@ -9930,7 +10061,7 @@ const Slider = React.forwardRef((props, ref) => {
9930
10061
  if (!insideControl) {
9931
10062
  content = jsxRuntime.jsx(Control, { children: content });
9932
10063
  }
9933
- if (!insideField) {
10064
+ if (ownField) {
9934
10065
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9935
10066
  }
9936
10067
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -10160,10 +10291,18 @@ function getFillPercent(iconIndex, value) {
10160
10291
  const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
10161
10292
  const insideField = useInsideField();
10162
10293
  const insideControl = useInsideControl();
10294
+ const ownField = rendersOwnField('Rate', {
10295
+ insideField,
10296
+ insideControl,
10297
+ label,
10298
+ message,
10299
+ horizontal,
10300
+ fieldClassName,
10301
+ });
10163
10302
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
10164
10303
  label,
10165
10304
  labelProps,
10166
- rendersLabel: !insideField,
10305
+ rendersLabel: ownField,
10167
10306
  });
10168
10307
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10169
10308
  const [internalValue, setInternalValue] = React.useState(defaultValue);
@@ -10366,7 +10505,7 @@ const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, messa
10366
10505
  if (!insideControl) {
10367
10506
  content = jsxRuntime.jsx(Control, { children: content });
10368
10507
  }
10369
- if (!insideField) {
10508
+ if (ownField) {
10370
10509
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
10371
10510
  }
10372
10511
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -10647,6 +10786,15 @@ const Taginput = React.forwardRef(({
10647
10786
  // Field props
10648
10787
  label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = [], data = [], placeholder, field = 'label', allowNew = true, allowDuplicates = false, openOnFocus = false, removeOnKeys = true, confirmKeys = ['Enter', ','], closable = true, attached = false, maxTags, maxlength, disabled = false, readonly = false, rounded = false, ellipsis = false, hasCounter = true, onPasteSeparators = [','], beforeAdding, createTag, keepFirst = false, keepOpen = true, loading = false, ariaCloseLabel, icon, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, tagColor, size, onChange, onAdd, onRemove, onTyping, tagTemplate, name, form, id, className, ...props }, ref) => {
10649
10788
  const insideField = useInsideField();
10789
+ const insideControl = useInsideControl();
10790
+ const ownField = rendersOwnField('Taginput', {
10791
+ insideField,
10792
+ insideControl,
10793
+ label,
10794
+ message,
10795
+ horizontal,
10796
+ fieldClassName,
10797
+ });
10650
10798
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10651
10799
  const { classPrefix } = useConfig();
10652
10800
  const defaultIconLibrary = useIconLibrary();
@@ -10668,7 +10816,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10668
10816
  label,
10669
10817
  id,
10670
10818
  labelProps,
10671
- rendersLabel: !insideField && !isMaxReached,
10819
+ rendersLabel: ownField && !isMaxReached,
10672
10820
  });
10673
10821
  // Get display value from tag
10674
10822
  const getDisplayValue = (tag) => {
@@ -10940,7 +11088,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10940
11088
  'is-active': index === highlightedIndex,
10941
11089
  }), onClick: () => addTag(item), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: item }, index))) }) })), showCounter && jsxRuntime.jsx("small", { className: counterClasses, children: counterText }), name &&
10942
11090
  tags.map((tag, i) => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
10943
- if (!insideField) {
11091
+ if (ownField) {
10944
11092
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
10945
11093
  }
10946
11094
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [taginputElement, messageEl] }));
@@ -11062,7 +11210,7 @@ const TextAreaBase = React.forwardRef(({ color, size, isRounded, isStatic, isHov
11062
11210
  TextAreaBase.displayName = 'TextAreaBase';
11063
11211
 
11064
11212
  /**
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.
11213
+ * 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
11214
  *
11067
11215
  * @function
11068
11216
  * @param {InputProps} props - Props for Input.
@@ -11093,11 +11241,19 @@ fieldClassName, controlClassName,
11093
11241
  ...inputProps }, ref) => {
11094
11242
  const insideField = useInsideField();
11095
11243
  const insideControl = useInsideControl();
11244
+ const ownField = rendersOwnField('Input', {
11245
+ insideField,
11246
+ insideControl,
11247
+ label,
11248
+ message,
11249
+ horizontal,
11250
+ fieldClassName,
11251
+ });
11096
11252
  const { controlId, fieldLabelProps } = useAutoLabelId({
11097
11253
  label,
11098
11254
  id: inputProps.id,
11099
11255
  labelProps,
11100
- rendersLabel: !insideField,
11256
+ rendersLabel: ownField,
11101
11257
  });
11102
11258
  const helpClass = usePrefixedClassNames('help', {
11103
11259
  [`is-${messageColor}`]: !!messageColor,
@@ -11107,7 +11263,7 @@ fieldClassName, controlClassName,
11107
11263
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11108
11264
  }
11109
11265
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
11110
- if (!insideField) {
11266
+ if (ownField) {
11111
11267
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11112
11268
  }
11113
11269
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -11143,11 +11299,19 @@ fieldClassName, controlClassName,
11143
11299
  ...selectProps }, ref) => {
11144
11300
  const insideField = useInsideField();
11145
11301
  const insideControl = useInsideControl();
11302
+ const ownField = rendersOwnField('Select', {
11303
+ insideField,
11304
+ insideControl,
11305
+ label,
11306
+ message,
11307
+ horizontal,
11308
+ fieldClassName,
11309
+ });
11146
11310
  const { controlId, fieldLabelProps } = useAutoLabelId({
11147
11311
  label,
11148
11312
  id: selectProps.id,
11149
11313
  labelProps,
11150
- rendersLabel: !insideField,
11314
+ rendersLabel: ownField,
11151
11315
  });
11152
11316
  const helpClass = usePrefixedClassNames('help', {
11153
11317
  [`is-${messageColor}`]: !!messageColor,
@@ -11157,7 +11321,7 @@ fieldClassName, controlClassName,
11157
11321
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11158
11322
  }
11159
11323
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
11160
- if (!insideField) {
11324
+ if (ownField) {
11161
11325
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11162
11326
  }
11163
11327
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -11165,7 +11329,7 @@ fieldClassName, controlClassName,
11165
11329
  Select.displayName = 'Select';
11166
11330
 
11167
11331
  /**
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.
11332
+ * 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
11333
  *
11170
11334
  * @function
11171
11335
  * @param {TextAreaProps} props - Props for TextArea.
@@ -11194,11 +11358,19 @@ fieldClassName, controlClassName,
11194
11358
  ...textAreaProps }, ref) => {
11195
11359
  const insideField = useInsideField();
11196
11360
  const insideControl = useInsideControl();
11361
+ const ownField = rendersOwnField('TextArea', {
11362
+ insideField,
11363
+ insideControl,
11364
+ label,
11365
+ message,
11366
+ horizontal,
11367
+ fieldClassName,
11368
+ });
11197
11369
  const { controlId, fieldLabelProps } = useAutoLabelId({
11198
11370
  label,
11199
11371
  id: textAreaProps.id,
11200
11372
  labelProps,
11201
- rendersLabel: !insideField,
11373
+ rendersLabel: ownField,
11202
11374
  });
11203
11375
  const helpClass = usePrefixedClassNames('help', {
11204
11376
  [`is-${messageColor}`]: !!messageColor,
@@ -11208,7 +11380,7 @@ fieldClassName, controlClassName,
11208
11380
  content = (jsxRuntime.jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
11209
11381
  }
11210
11382
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
11211
- if (!insideField) {
11383
+ if (ownField) {
11212
11384
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11213
11385
  }
11214
11386
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -11251,355 +11423,90 @@ const mergeLabels = (overrides) => ({
11251
11423
  ...(overrides ?? {}),
11252
11424
  });
11253
11425
 
11254
- const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
11255
- const DEFAULT_MONTH_FORMAT = 'YYYY-MM';
11256
- const DEFAULT_YEAR_FORMAT = 'YYYY';
11257
- const DEFAULT_TIME_FORMAT_24 = 'HH:mm';
11258
- const DEFAULT_TIME_FORMAT_12 = 'hh:mm A';
11259
- const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm';
11260
- const pad = (n, width = 2) => String(n).padStart(width, '0');
11261
- const TOKEN_RE = /YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g;
11262
- function formatToken(d, token) {
11263
- switch (token) {
11264
- case 'YYYY':
11265
- // Zero-pad to a fixed 4 chars so the rendered width matches the
11266
- // segment-map width during digit-by-digit keyboard entry (and to match
11267
- // ISO 8601). Years >9999 keep their natural width.
11268
- return pad(d.getFullYear(), 4);
11269
- case 'YY':
11270
- return pad(d.getFullYear() % 100);
11271
- case 'MM':
11272
- return pad(d.getMonth() + 1);
11273
- case 'M':
11274
- return String(d.getMonth() + 1);
11275
- case 'DD':
11276
- return pad(d.getDate());
11277
- case 'D':
11278
- return String(d.getDate());
11279
- case 'HH':
11280
- return pad(d.getHours());
11281
- case 'H':
11282
- return String(d.getHours());
11283
- case 'hh': {
11284
- const h12 = d.getHours() % 12 || 12;
11285
- return pad(h12);
11286
- }
11287
- case 'h': {
11288
- const h12 = d.getHours() % 12 || 12;
11289
- return String(h12);
11290
- }
11291
- case 'mm':
11292
- return pad(d.getMinutes());
11293
- case 'm':
11294
- return String(d.getMinutes());
11295
- case 'ss':
11296
- return pad(d.getSeconds());
11297
- case 's':
11298
- return String(d.getSeconds());
11299
- case 'A':
11300
- return d.getHours() < 12 ? 'AM' : 'PM';
11301
- case 'a':
11302
- return d.getHours() < 12 ? 'am' : 'pm';
11303
- default:
11304
- return token;
11305
- }
11426
+ function isSameDay(a, b) {
11427
+ return (a.getFullYear() === b.getFullYear() &&
11428
+ a.getMonth() === b.getMonth() &&
11429
+ a.getDate() === b.getDate());
11306
11430
  }
11307
- function formatTokenString(d, fmt) {
11308
- return fmt.replace(TOKEN_RE, t => formatToken(d, t));
11431
+ function isSameMonth(a, b) {
11432
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
11309
11433
  }
11310
- function formatWithIntl(d, options, locale) {
11311
- return new Intl.DateTimeFormat(locale, options).format(d);
11434
+ function isBefore(a, b) {
11435
+ return a.getTime() < b.getTime();
11312
11436
  }
11313
- function formatDate(d, fmt, locale) {
11314
- const f = fmt ?? DEFAULT_DATE_FORMAT;
11315
- if (typeof f === 'string')
11316
- return formatTokenString(d, f);
11317
- return formatWithIntl(d, f, locale);
11437
+ function isAfter(a, b) {
11438
+ return a.getTime() > b.getTime();
11318
11439
  }
11319
- function formatTime(d, fmt, locale) {
11320
- const f = fmt ?? DEFAULT_TIME_FORMAT_24;
11321
- if (typeof f === 'string')
11322
- return formatTokenString(d, f);
11323
- return formatWithIntl(d, f, locale);
11440
+ function isWithin(d, min, max) {
11441
+ if (min && isBefore(d, min))
11442
+ return false;
11443
+ if (max && isAfter(d, max))
11444
+ return false;
11445
+ return true;
11324
11446
  }
11325
- function formatDateTime(d, fmt, locale) {
11326
- const f = fmt ?? DEFAULT_DATETIME_FORMAT;
11327
- if (typeof f === 'string')
11328
- return formatTokenString(d, f);
11329
- return formatWithIntl(d, f, locale);
11447
+ // The boundaries build on `makeDate` (below), so a date in years 0–99 keeps
11448
+ // its year.
11449
+ function startOfDay(d) {
11450
+ return makeDate(d.getFullYear(), d.getMonth(), d.getDate());
11451
+ }
11452
+ function endOfDay(d) {
11453
+ const r = makeDate(d.getFullYear(), d.getMonth(), d.getDate());
11454
+ r.setHours(23, 59, 59, 999);
11455
+ return r;
11456
+ }
11457
+ function startOfMonth(d) {
11458
+ return makeDate(d.getFullYear(), d.getMonth());
11330
11459
  }
11331
11460
  /**
11332
- * Derive the hour cycle (`'12'` or `'24'`) a display format will render, from
11333
- * its first hour token: `h`/`hh` → 12-hour with meridiem, `H`/`HH` → 24-hour.
11334
- *
11335
- * Returns `null` when the cycle can't be read from the format — it's an
11336
- * `Intl.DateTimeFormat` options object (no token string), `undefined`, or has
11337
- * no hour token — so callers fall back to the raw `hourFormat` prop. Scans with
11338
- * the same {@link TOKEN_RE} grammar the input renders with, so the derived cycle
11339
- * always agrees with what the field actually displays.
11461
+ * Local midnight on `year`-`month`-`day` (`month` from 0), like the
11462
+ * `Date(y, m, d)` constructor except that years 0–99 stay as given rather than
11463
+ * turning into 1900–1999.
11340
11464
  */
11341
- function hourCycleFromFormat(fmt) {
11342
- if (typeof fmt !== 'string')
11343
- return null;
11344
- for (const [token] of fmt.matchAll(TOKEN_RE)) {
11345
- if (token === 'H' || token === 'HH')
11346
- return '24';
11347
- if (token === 'h' || token === 'hh')
11348
- return '12';
11349
- }
11350
- return null;
11465
+ function makeDate(year, month = 0, day = 1) {
11466
+ const d = new Date(2000, 0, 1);
11467
+ d.setFullYear(year, month, day);
11468
+ return d;
11351
11469
  }
11352
11470
  /**
11353
- * Parse a date string against a token format. Returns null on mismatch.
11354
- * For Intl-options formats parsing is the consumer's responsibility — pass
11355
- * a custom `parse` prop on the picker.
11471
+ * The earliest year the pickers accept. HTML's date, month and datetime-local
11472
+ * inputs hold no year below 1, so the calendar, typing and parsing stop there
11473
+ * too.
11356
11474
  */
11357
- function parseDate(s, fmt,
11358
- // locale arg accepted for symmetry; locale-aware parsing is not supported.
11359
- _locale) {
11360
- if (!s)
11361
- return null;
11362
- const format = fmt ?? DEFAULT_DATE_FORMAT;
11363
- const parts = parseTokens(s, format);
11364
- if (!parts)
11365
- return null;
11366
- const year = parts.year ?? new Date().getFullYear();
11367
- const month = (parts.month ?? 1) - 1;
11368
- const day = parts.day ?? 1;
11369
- const hours = parts.hours ?? 0;
11370
- const minutes = parts.minutes ?? 0;
11371
- const seconds = parts.seconds ?? 0;
11372
- const d = new Date(year, month, day, hours, minutes, seconds, 0);
11373
- if (d.getFullYear() !== year ||
11374
- d.getMonth() !== month ||
11375
- d.getDate() !== day) {
11376
- return null;
11377
- }
11378
- return d;
11475
+ const FIRST_YEAR = 1;
11476
+ /**
11477
+ * The lower bound a picker applies: `min`, raised to local midnight on
11478
+ * 1 January of {@link FIRST_YEAR} when it is earlier or absent. A `min` in
11479
+ * range comes back as the same object.
11480
+ */
11481
+ function floorMin(min) {
11482
+ const floor = makeDate(FIRST_YEAR);
11483
+ return min && min.getTime() >= floor.getTime() ? min : floor;
11379
11484
  }
11380
- function parseTime(s, fmt) {
11381
- if (!s)
11382
- return null;
11383
- const format = fmt ?? DEFAULT_TIME_FORMAT_24;
11384
- const parts = parseTokens(s, format);
11385
- if (!parts)
11386
- return null;
11387
- const today = new Date();
11388
- return new Date(today.getFullYear(), today.getMonth(), today.getDate(), parts.hours ?? 0, parts.minutes ?? 0, parts.seconds ?? 0, 0);
11485
+ /**
11486
+ * First instant of the day, month or year containing `d`. Works by setters on
11487
+ * a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
11488
+ * as 1900–1999: segmented typing passes through such years on the way to a
11489
+ * four-digit one.
11490
+ */
11491
+ function startOfPeriod(d, granularity) {
11492
+ const r = new Date(d);
11493
+ if (granularity === 'year')
11494
+ r.setMonth(0, 1);
11495
+ else if (granularity === 'month')
11496
+ r.setDate(1);
11497
+ r.setHours(0, 0, 0, 0);
11498
+ return r;
11389
11499
  }
11390
- function escapeRegExp(s) {
11391
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11392
- }
11393
- function parseTokens(input, fmt) {
11394
- const tokenOrder = [];
11395
- let regex = '';
11396
- let i = 0;
11397
- while (i < fmt.length) {
11398
- const four = fmt.slice(i, i + 4);
11399
- const two = fmt.slice(i, i + 2);
11400
- const one = fmt[i];
11401
- let token = null;
11402
- if (four === 'YYYY')
11403
- token = 'YYYY';
11404
- else if (two === 'YY' ||
11405
- two === 'MM' ||
11406
- two === 'DD' ||
11407
- two === 'HH' ||
11408
- two === 'hh' ||
11409
- two === 'mm' ||
11410
- two === 'ss')
11411
- token = two;
11412
- else if (one === 'M' ||
11413
- one === 'D' ||
11414
- one === 'H' ||
11415
- one === 'h' ||
11416
- one === 'm' ||
11417
- one === 's' ||
11418
- one === 'A' ||
11419
- one === 'a')
11420
- token = one;
11421
- if (token) {
11422
- tokenOrder.push(token);
11423
- switch (token) {
11424
- case 'YYYY':
11425
- regex += '(\\d{4})';
11426
- break;
11427
- case 'YY':
11428
- regex += '(\\d{2})';
11429
- break;
11430
- case 'MM':
11431
- case 'DD':
11432
- case 'HH':
11433
- case 'hh':
11434
- case 'mm':
11435
- case 'ss':
11436
- regex += '(\\d{1,2})';
11437
- break;
11438
- case 'M':
11439
- case 'D':
11440
- case 'H':
11441
- case 'h':
11442
- case 'm':
11443
- case 's':
11444
- regex += '(\\d{1,2})';
11445
- break;
11446
- case 'A':
11447
- regex += '(AM|PM)';
11448
- break;
11449
- case 'a':
11450
- regex += '(am|pm)';
11451
- break;
11452
- }
11453
- i += token.length;
11454
- }
11455
- else {
11456
- regex += escapeRegExp(one);
11457
- i += 1;
11458
- }
11459
- }
11460
- const m = new RegExp('^' + regex + '$').exec(input);
11461
- if (!m)
11462
- return null;
11463
- const out = {};
11464
- tokenOrder.forEach((token, idx) => {
11465
- const v = m[idx + 1];
11466
- switch (token) {
11467
- case 'YYYY':
11468
- out.year = Number(v);
11469
- break;
11470
- case 'YY':
11471
- out.year = 2000 + Number(v);
11472
- break;
11473
- case 'MM':
11474
- case 'M':
11475
- out.month = Number(v);
11476
- break;
11477
- case 'DD':
11478
- case 'D':
11479
- out.day = Number(v);
11480
- break;
11481
- case 'HH':
11482
- case 'H':
11483
- out.hours = Number(v);
11484
- break;
11485
- case 'hh':
11486
- case 'h':
11487
- out.hours = Number(v);
11488
- break;
11489
- case 'mm':
11490
- case 'm':
11491
- out.minutes = Number(v);
11492
- break;
11493
- case 'ss':
11494
- case 's':
11495
- out.seconds = Number(v);
11496
- break;
11497
- case 'A':
11498
- out.meridiem = v.toLowerCase() === 'am' ? 'am' : 'pm';
11499
- break;
11500
- case 'a':
11501
- out.meridiem = v === 'am' ? 'am' : 'pm';
11502
- break;
11503
- }
11504
- });
11505
- if (out.meridiem !== undefined && out.hours !== undefined) {
11506
- if (out.meridiem === 'pm' && out.hours < 12)
11507
- out.hours += 12;
11508
- if (out.meridiem === 'am' && out.hours === 12)
11509
- out.hours = 0;
11510
- }
11511
- return out;
11512
- }
11513
- /**
11514
- * Locale-aware day name labels in calendar order (Sunday → Saturday).
11515
- * Caller rotates by `firstDayOfWeek`.
11516
- */
11517
- function getDayNames(locale, length = 'short') {
11518
- const fmt = new Intl.DateTimeFormat(locale, { weekday: length });
11519
- // 2021-01-03 was a Sunday; use it as the anchor.
11520
- const sunday = new Date(2021, 0, 3);
11521
- const out = [];
11522
- for (let i = 0; i < 7; i++) {
11523
- const d = new Date(sunday);
11524
- d.setDate(sunday.getDate() + i);
11525
- out.push(fmt.format(d));
11526
- }
11527
- return out;
11528
- }
11529
- function getMonthNames(locale, length = 'long') {
11530
- const fmt = new Intl.DateTimeFormat(locale, { month: length });
11531
- const out = [];
11532
- for (let i = 0; i < 12; i++) {
11533
- out.push(fmt.format(new Date(2021, i, 1)));
11534
- }
11535
- return out;
11536
- }
11537
-
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;
11500
+ /** Last instant of the day, month or year containing `d`. */
11501
+ function endOfPeriod(d, granularity) {
11502
+ const r = new Date(d);
11503
+ if (granularity === 'year')
11504
+ r.setMonth(11, 31);
11505
+ // Day 0 of the next month is this month's last day.
11506
+ else if (granularity === 'month')
11507
+ r.setMonth(r.getMonth() + 1, 0);
11508
+ r.setHours(23, 59, 59, 999);
11509
+ return r;
11603
11510
  }
11604
11511
  /**
11605
11512
  * Whether the day `d` falls outside `[min, max]` or is blocked by
@@ -11651,7 +11558,7 @@ function addMonths(d, n) {
11651
11558
  r.setDate(1);
11652
11559
  r.setMonth(r.getMonth() + n);
11653
11560
  // 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();
11561
+ const lastDay = makeDate(r.getFullYear(), r.getMonth() + 1, 0).getDate();
11655
11562
  r.setDate(Math.min(day, lastDay));
11656
11563
  return r;
11657
11564
  }
@@ -11668,12 +11575,14 @@ function setTimeOfDay(d, parts) {
11668
11575
  r.setSeconds(parts.seconds);
11669
11576
  return r;
11670
11577
  }
11578
+ /**
11579
+ * A copy of `d` moved into `[min, max]`. When `min` is after `max` nothing is
11580
+ * in range and `min` wins, so clamping twice gives the same date and a
11581
+ * picker's focus stays off the years before 1 under a `max` before then.
11582
+ */
11671
11583
  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);
11584
+ const capped = max && isAfter(d, max) ? max : d;
11585
+ return new Date(min && isBefore(capped, min) ? min : capped);
11677
11586
  }
11678
11587
  /**
11679
11588
  * Snap a Date's time-of-day to the nearest grid defined by the given
@@ -11695,44 +11604,337 @@ function snapTimeToIncrement(d, opts = {}) {
11695
11604
  s -= 60;
11696
11605
  m += 1;
11697
11606
  }
11698
- }
11699
- if (incrementMinutes > 1) {
11700
- m = Math.round(m / incrementMinutes) * incrementMinutes;
11701
- if (m >= 60) {
11702
- m -= 60;
11703
- h += 1;
11607
+ }
11608
+ if (incrementMinutes > 1) {
11609
+ m = Math.round(m / incrementMinutes) * incrementMinutes;
11610
+ if (m >= 60) {
11611
+ m -= 60;
11612
+ h += 1;
11613
+ }
11614
+ }
11615
+ if (incrementHours > 1) {
11616
+ h = Math.round(h / incrementHours) * incrementHours;
11617
+ // setHours handles 24+ overflow naturally (rolls forward into next day).
11618
+ }
11619
+ r.setHours(h, m, s, 0);
11620
+ return r;
11621
+ }
11622
+ /**
11623
+ * Build a 6-week × 7-day grid (42 cells) anchored on the month containing
11624
+ * `monthAnchor`. The first cell is the start of the week containing the first
11625
+ * of the month, where the week starts on `firstDayOfWeek`.
11626
+ */
11627
+ function buildMonthGrid(monthAnchor, firstDayOfWeek = 0) {
11628
+ const monthStart = startOfMonth(monthAnchor);
11629
+ const firstWeekday = monthStart.getDay();
11630
+ const offset = (firstWeekday - firstDayOfWeek + 7) % 7;
11631
+ const gridStart = addDays(monthStart, -offset);
11632
+ const today = startOfDay(new Date());
11633
+ const cells = [];
11634
+ for (let i = 0; i < 42; i++) {
11635
+ const date = addDays(gridStart, i);
11636
+ cells.push({
11637
+ date,
11638
+ inCurrentMonth: date.getMonth() === monthAnchor.getMonth(),
11639
+ isToday: isSameDay(date, today),
11640
+ });
11641
+ }
11642
+ return cells;
11643
+ }
11644
+
11645
+ const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
11646
+ const DEFAULT_MONTH_FORMAT = 'YYYY-MM';
11647
+ const DEFAULT_YEAR_FORMAT = 'YYYY';
11648
+ const DEFAULT_TIME_FORMAT_24 = 'HH:mm';
11649
+ const DEFAULT_TIME_FORMAT_12 = 'hh:mm A';
11650
+ const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm';
11651
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
11652
+ const TOKEN_RE = /YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g;
11653
+ function formatToken(d, token) {
11654
+ switch (token) {
11655
+ case 'YYYY':
11656
+ // Zero-pad to a fixed 4 chars so the rendered width matches the
11657
+ // segment-map width during digit-by-digit keyboard entry (and to match
11658
+ // ISO 8601), so year 19 shows as `0019`. Years >9999 keep their natural
11659
+ // width. The pickers commit no year below 1, so a minus sign reaches
11660
+ // this only from a `value` passed in.
11661
+ return pad(d.getFullYear(), 4);
11662
+ case 'YY':
11663
+ return pad(d.getFullYear() % 100);
11664
+ case 'MM':
11665
+ return pad(d.getMonth() + 1);
11666
+ case 'M':
11667
+ return String(d.getMonth() + 1);
11668
+ case 'DD':
11669
+ return pad(d.getDate());
11670
+ case 'D':
11671
+ return String(d.getDate());
11672
+ case 'HH':
11673
+ return pad(d.getHours());
11674
+ case 'H':
11675
+ return String(d.getHours());
11676
+ case 'hh': {
11677
+ const h12 = d.getHours() % 12 || 12;
11678
+ return pad(h12);
11679
+ }
11680
+ case 'h': {
11681
+ const h12 = d.getHours() % 12 || 12;
11682
+ return String(h12);
11683
+ }
11684
+ case 'mm':
11685
+ return pad(d.getMinutes());
11686
+ case 'm':
11687
+ return String(d.getMinutes());
11688
+ case 'ss':
11689
+ return pad(d.getSeconds());
11690
+ case 's':
11691
+ return String(d.getSeconds());
11692
+ case 'A':
11693
+ return d.getHours() < 12 ? 'AM' : 'PM';
11694
+ case 'a':
11695
+ return d.getHours() < 12 ? 'am' : 'pm';
11696
+ default:
11697
+ return token;
11698
+ }
11699
+ }
11700
+ function formatTokenString(d, fmt) {
11701
+ return fmt.replace(TOKEN_RE, t => formatToken(d, t));
11702
+ }
11703
+ function formatWithIntl(d, options, locale) {
11704
+ return new Intl.DateTimeFormat(locale, options).format(d);
11705
+ }
11706
+ function formatDate(d, fmt, locale) {
11707
+ const f = fmt ?? DEFAULT_DATE_FORMAT;
11708
+ if (typeof f === 'string')
11709
+ return formatTokenString(d, f);
11710
+ return formatWithIntl(d, f, locale);
11711
+ }
11712
+ function formatTime(d, fmt, locale) {
11713
+ const f = fmt ?? DEFAULT_TIME_FORMAT_24;
11714
+ if (typeof f === 'string')
11715
+ return formatTokenString(d, f);
11716
+ return formatWithIntl(d, f, locale);
11717
+ }
11718
+ function formatDateTime(d, fmt, locale) {
11719
+ const f = fmt ?? DEFAULT_DATETIME_FORMAT;
11720
+ if (typeof f === 'string')
11721
+ return formatTokenString(d, f);
11722
+ return formatWithIntl(d, f, locale);
11723
+ }
11724
+ /**
11725
+ * Derive the hour cycle (`'12'` or `'24'`) a display format will render, from
11726
+ * its first hour token: `h`/`hh` → 12-hour with meridiem, `H`/`HH` → 24-hour.
11727
+ *
11728
+ * Returns `null` when the cycle can't be read from the format — it's an
11729
+ * `Intl.DateTimeFormat` options object (no token string), `undefined`, or has
11730
+ * no hour token — so callers fall back to the raw `hourFormat` prop. Scans with
11731
+ * the same {@link TOKEN_RE} grammar the input renders with, so the derived cycle
11732
+ * always agrees with what the field actually displays.
11733
+ */
11734
+ function hourCycleFromFormat(fmt) {
11735
+ if (typeof fmt !== 'string')
11736
+ return null;
11737
+ for (const [token] of fmt.matchAll(TOKEN_RE)) {
11738
+ if (token === 'H' || token === 'HH')
11739
+ return '24';
11740
+ if (token === 'h' || token === 'hh')
11741
+ return '12';
11742
+ }
11743
+ return null;
11744
+ }
11745
+ /**
11746
+ * Parse a date string against a token format. Returns null on mismatch.
11747
+ * For Intl-options formats parsing is the consumer's responsibility — pass
11748
+ * a custom `parse` prop on the picker.
11749
+ */
11750
+ function parseDate(s, fmt,
11751
+ // locale arg accepted for symmetry; locale-aware parsing is not supported.
11752
+ _locale) {
11753
+ if (!s)
11754
+ return null;
11755
+ const format = fmt ?? DEFAULT_DATE_FORMAT;
11756
+ const parts = parseTokens(s, format);
11757
+ if (!parts)
11758
+ return null;
11759
+ const year = parts.year ?? new Date().getFullYear();
11760
+ const month = (parts.month ?? 1) - 1;
11761
+ const day = parts.day ?? 1;
11762
+ const hours = parts.hours ?? 0;
11763
+ const minutes = parts.minutes ?? 0;
11764
+ const seconds = parts.seconds ?? 0;
11765
+ const d = makeDate(year, month, day);
11766
+ d.setHours(hours, minutes, seconds, 0);
11767
+ if (d.getFullYear() !== year ||
11768
+ d.getMonth() !== month ||
11769
+ d.getDate() !== day) {
11770
+ return null;
11771
+ }
11772
+ return d;
11773
+ }
11774
+ function parseTime(s, fmt) {
11775
+ if (!s)
11776
+ return null;
11777
+ const format = fmt ?? DEFAULT_TIME_FORMAT_24;
11778
+ const parts = parseTokens(s, format);
11779
+ if (!parts)
11780
+ return null;
11781
+ const today = new Date();
11782
+ return new Date(today.getFullYear(), today.getMonth(), today.getDate(), parts.hours ?? 0, parts.minutes ?? 0, parts.seconds ?? 0, 0);
11783
+ }
11784
+ function escapeRegExp(s) {
11785
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11786
+ }
11787
+ function parseTokens(input, fmt) {
11788
+ const tokenOrder = [];
11789
+ let regex = '';
11790
+ let i = 0;
11791
+ while (i < fmt.length) {
11792
+ const four = fmt.slice(i, i + 4);
11793
+ const two = fmt.slice(i, i + 2);
11794
+ const one = fmt[i];
11795
+ let token = null;
11796
+ if (four === 'YYYY')
11797
+ token = 'YYYY';
11798
+ else if (two === 'YY' ||
11799
+ two === 'MM' ||
11800
+ two === 'DD' ||
11801
+ two === 'HH' ||
11802
+ two === 'hh' ||
11803
+ two === 'mm' ||
11804
+ two === 'ss')
11805
+ token = two;
11806
+ else if (one === 'M' ||
11807
+ one === 'D' ||
11808
+ one === 'H' ||
11809
+ one === 'h' ||
11810
+ one === 'm' ||
11811
+ one === 's' ||
11812
+ one === 'A' ||
11813
+ one === 'a')
11814
+ token = one;
11815
+ if (token) {
11816
+ tokenOrder.push(token);
11817
+ switch (token) {
11818
+ case 'YYYY':
11819
+ regex += '(\\d{4})';
11820
+ break;
11821
+ case 'YY':
11822
+ regex += '(\\d{2})';
11823
+ break;
11824
+ case 'MM':
11825
+ case 'DD':
11826
+ case 'HH':
11827
+ case 'hh':
11828
+ case 'mm':
11829
+ case 'ss':
11830
+ regex += '(\\d{1,2})';
11831
+ break;
11832
+ case 'M':
11833
+ case 'D':
11834
+ case 'H':
11835
+ case 'h':
11836
+ case 'm':
11837
+ case 's':
11838
+ regex += '(\\d{1,2})';
11839
+ break;
11840
+ case 'A':
11841
+ regex += '(AM|PM)';
11842
+ break;
11843
+ case 'a':
11844
+ regex += '(am|pm)';
11845
+ break;
11846
+ }
11847
+ i += token.length;
11848
+ }
11849
+ else {
11850
+ regex += escapeRegExp(one);
11851
+ i += 1;
11704
11852
  }
11705
11853
  }
11706
- if (incrementHours > 1) {
11707
- h = Math.round(h / incrementHours) * incrementHours;
11708
- // setHours handles 24+ overflow naturally (rolls forward into next day).
11854
+ const m = new RegExp('^' + regex + '$').exec(input);
11855
+ if (!m)
11856
+ return null;
11857
+ const out = {};
11858
+ tokenOrder.forEach((token, idx) => {
11859
+ const v = m[idx + 1];
11860
+ switch (token) {
11861
+ case 'YYYY':
11862
+ out.year = Number(v);
11863
+ break;
11864
+ case 'YY':
11865
+ out.year = 2000 + Number(v);
11866
+ break;
11867
+ case 'MM':
11868
+ case 'M':
11869
+ out.month = Number(v);
11870
+ break;
11871
+ case 'DD':
11872
+ case 'D':
11873
+ out.day = Number(v);
11874
+ break;
11875
+ case 'HH':
11876
+ case 'H':
11877
+ out.hours = Number(v);
11878
+ break;
11879
+ case 'hh':
11880
+ case 'h':
11881
+ out.hours = Number(v);
11882
+ break;
11883
+ case 'mm':
11884
+ case 'm':
11885
+ out.minutes = Number(v);
11886
+ break;
11887
+ case 'ss':
11888
+ case 's':
11889
+ out.seconds = Number(v);
11890
+ break;
11891
+ case 'A':
11892
+ out.meridiem = v.toLowerCase() === 'am' ? 'am' : 'pm';
11893
+ break;
11894
+ case 'a':
11895
+ out.meridiem = v === 'am' ? 'am' : 'pm';
11896
+ break;
11897
+ }
11898
+ });
11899
+ if (out.meridiem !== undefined && out.hours !== undefined) {
11900
+ if (out.meridiem === 'pm' && out.hours < 12)
11901
+ out.hours += 12;
11902
+ if (out.meridiem === 'am' && out.hours === 12)
11903
+ out.hours = 0;
11709
11904
  }
11710
- r.setHours(h, m, s, 0);
11711
- return r;
11905
+ return out;
11712
11906
  }
11713
11907
  /**
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`.
11908
+ * Locale-aware day name labels in calendar order (Sunday → Saturday).
11909
+ * Caller rotates by `firstDayOfWeek`.
11717
11910
  */
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
- });
11911
+ function getDayNames(locale, length = 'short') {
11912
+ const fmt = new Intl.DateTimeFormat(locale, { weekday: length });
11913
+ // 2021-01-03 was a Sunday; use it as the anchor.
11914
+ const sunday = new Date(2021, 0, 3);
11915
+ const out = [];
11916
+ for (let i = 0; i < 7; i++) {
11917
+ const d = new Date(sunday);
11918
+ d.setDate(sunday.getDate() + i);
11919
+ out.push(fmt.format(d));
11732
11920
  }
11733
- return cells;
11921
+ return out;
11922
+ }
11923
+ function getMonthNames(locale, length = 'long') {
11924
+ const fmt = new Intl.DateTimeFormat(locale, { month: length });
11925
+ const out = [];
11926
+ for (let i = 0; i < 12; i++) {
11927
+ out.push(fmt.format(new Date(2021, i, 1)));
11928
+ }
11929
+ return out;
11734
11930
  }
11735
11931
 
11932
+ /**
11933
+ * Selects the calendar's focused cell, the one tab stop of whichever grid or
11934
+ * year list is on show, for a popover to put focus on as it opens. It runs
11935
+ * against the whole panel, so it matches only inside the grid or the list.
11936
+ */
11937
+ const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
11736
11938
  const BASE_VIEW = {
11737
11939
  day: 'days',
11738
11940
  month: 'months',
@@ -11778,20 +11980,32 @@ function monthStep(key, col) {
11778
11980
  }
11779
11981
  }
11780
11982
  /**
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.
11983
+ * Whether DOM focus should follow a grid's tab stop as it moves: focus is in
11984
+ * the grid, or fell to <body> while the grid held it last, as it does when
11985
+ * the focused cell leaves the page or becomes disabled. Focus is read in the
11986
+ * grid's own tree, so this holds inside a shadow root too.
11784
11987
  */
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;
11988
+ function focusFollows(grid, heldLast) {
11989
+ const active = getActiveElementInTree(grid);
11990
+ if (grid.contains(active))
11991
+ return true;
11992
+ return heldLast && active === grid.ownerDocument.body;
11993
+ }
11994
+ /**
11995
+ * The cell nearest index `i` that `disabled` leaves enabled, `i` itself when
11996
+ * it is. At equal distance the later cell wins. With every cell disabled it
11997
+ * stays `i`, as there is nothing to reach.
11998
+ */
11999
+ function nearestEnabled(i, disabled) {
12000
+ if (!disabled[i])
12001
+ return i;
12002
+ for (let d = 1; d < disabled.length; d++) {
12003
+ if (i + d < disabled.length && !disabled[i + d])
12004
+ return i + d;
12005
+ if (i - d >= 0 && !disabled[i - d])
12006
+ return i - d;
11793
12007
  }
11794
- return month;
12008
+ return i;
11795
12009
  }
11796
12010
  /**
11797
12011
  * The year list's keyboard when it is the selection surface: arrows move a
@@ -11817,7 +12031,7 @@ function yearStep(key, focusedYear, years) {
11817
12031
  return null;
11818
12032
  }
11819
12033
  }
11820
- const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
12034
+ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minProp, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
11821
12035
  const gridRef = React.useRef(null);
11822
12036
  const monthGridRef = React.useRef(null);
11823
12037
  const yearGridRef = React.useRef(null);
@@ -11827,6 +12041,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11827
12041
  const isDayGranularity = granularity === 'day';
11828
12042
  const isYearGranularity = granularity === 'year';
11829
12043
  const [view, setView] = React.useState(baseView);
12044
+ // Nothing before year 1 can be picked or reached, as in HTML's date inputs.
12045
+ const min = React.useMemo(() => floorMin(minProp), [minProp]);
11830
12046
  const computedDayNames = React.useMemo(() => {
11831
12047
  if (dayNames && dayNames.length === 7)
11832
12048
  return dayNames;
@@ -11863,6 +12079,33 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11863
12079
  shouldDisableDate,
11864
12080
  unselectableDates,
11865
12081
  }), [min, max, shouldDisableDate, unselectableDates]);
12082
+ // The day grid's one tab stop. A disabled button can't take focus, so when
12083
+ // the focused day is disabled the stop moves to the nearest enabled day of
12084
+ // the month on show, and focusing it makes it the focused day. A nearby
12085
+ // month's day is left out, as focusing it would turn the grid to its month.
12086
+ const tabStopDay = React.useMemo(() => {
12087
+ const days = cells.filter(c => c.inCurrentMonth).map(c => c.date);
12088
+ const i = nearestEnabled(focusedDate.getDate() - 1, days.map(isDateUnselectable));
12089
+ return days[i];
12090
+ }, [cells, focusedDate, isDateUnselectable]);
12091
+ // Whether the grid on show holds focus, or held it last before focus went
12092
+ // nowhere: the cell that had it left the page or became disabled. A blur
12093
+ // that takes focus out of the grid, to nowhere included, is the user
12094
+ // leaving, so it clears this. A render that removes or disables the focused
12095
+ // cell sends no blur here: browsers fire none, or fire it inside React's
12096
+ // commit, where React delivers no events.
12097
+ const gridHeldFocusRef = React.useRef(false);
12098
+ const gridFocusHandlers = {
12099
+ onFocus: () => {
12100
+ gridHeldFocusRef.current = true;
12101
+ },
12102
+ onBlur: (e) => {
12103
+ const next = e.relatedTarget;
12104
+ if (!next || !e.currentTarget.contains(next)) {
12105
+ gridHeldFocusRef.current = false;
12106
+ }
12107
+ },
12108
+ };
11866
12109
  // Step from `next` by `direction` days until we hit a selectable date,
11867
12110
  // skipping past dates blocked by min/max, shouldDisableDate, or
11868
12111
  // unselectableDates. If the whole searched range is disabled, focus stays
@@ -11870,7 +12113,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11870
12113
  const moveFocus = React.useCallback((next, direction) => {
11871
12114
  let candidate = next;
11872
12115
  for (let i = 0; i < 366; i++) {
11873
- if (min && candidate.getTime() < startOfDay(min).getTime())
12116
+ if (candidate.getTime() < startOfDay(min).getTime())
11874
12117
  return;
11875
12118
  if (max && candidate.getTime() > startOfDay(max).getTime())
11876
12119
  return;
@@ -11936,7 +12179,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11936
12179
  let candidate = next;
11937
12180
  for (let i = 0; i < 120; i++) {
11938
12181
  const month = startOfMonth(candidate).getTime();
11939
- if (min && month < startOfMonth(min).getTime())
12182
+ if (month < startOfMonth(min).getTime())
11940
12183
  return;
11941
12184
  if (max && month > startOfMonth(max).getTime())
11942
12185
  return;
@@ -11984,25 +12227,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11984
12227
  const today = new Date();
11985
12228
  const todayYear = today.getFullYear();
11986
12229
  // The header steps a month at a time over the day grid and a year at a
11987
- // time over the month grid, kept inside min/max.
11988
- const prevMonthAnchor = addMonths(focusedDate, -1);
11989
- const nextMonthAnchor = addMonths(focusedDate, 1);
12230
+ // time over the month grid, kept inside min/max. A step is off once the
12231
+ // month or year on show holds the bound or lies past it, as picking a year
12232
+ // from the list can leave it.
11990
12233
  const prevAnchor = isDayGranularity
11991
- ? prevMonthAnchor
12234
+ ? addMonths(focusedDate, -1)
11992
12235
  : clampDate(addYears(focusedDate, -1), min, max);
11993
12236
  const nextAnchor = isDayGranularity
11994
- ? nextMonthAnchor
12237
+ ? addMonths(focusedDate, 1)
11995
12238
  : clampDate(addYears(focusedDate, 1), min, max);
12239
+ const focusedMonth = startOfMonth(focusedDate).getTime();
11996
12240
  const prevDisabled = isDayGranularity
11997
- ? !!(min &&
11998
- prevMonthAnchor.getTime() < startOfDay(min).getTime() &&
11999
- isSameMonth(focusedDate, min))
12000
- : !!(min && focusedYear <= min.getFullYear());
12241
+ ? focusedMonth <= startOfMonth(min).getTime()
12242
+ : focusedYear <= min.getFullYear();
12001
12243
  const nextDisabled = isDayGranularity
12002
- ? !!(max &&
12003
- nextMonthAnchor.getTime() > startOfDay(max).getTime() &&
12004
- isSameMonth(focusedDate, max))
12005
- : !!(max && focusedYear >= max.getFullYear());
12244
+ ? !!max && focusedMonth >= startOfMonth(max).getTime()
12245
+ : !!max && focusedYear >= max.getFullYear();
12006
12246
  const labelId = id ? `${id}-label` : undefined;
12007
12247
  const monthLabel = isDayGranularity
12008
12248
  ? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
@@ -12013,7 +12253,9 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12013
12253
  const [mountYear] = React.useState(focusedYear);
12014
12254
  const windowYear = isYearGranularity ? mountYear : focusedYear;
12015
12255
  const yearList = React.useMemo(() => {
12016
- const minYear = min ? min.getFullYear() : windowYear - 100;
12256
+ const minYear = minProp
12257
+ ? min.getFullYear()
12258
+ : Math.max(FIRST_YEAR, windowYear - 100);
12017
12259
  const maxYear = max ? max.getFullYear() : windowYear + 100;
12018
12260
  const [lo, hi] = yearsRange ?? [minYear, maxYear];
12019
12261
  const start = Math.max(lo, minYear);
@@ -12022,10 +12264,25 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12022
12264
  for (let y = start; y <= end; y++)
12023
12265
  out.push(y);
12024
12266
  return out;
12025
- }, [windowYear, min, max, yearsRange]);
12267
+ }, [windowYear, minProp, min, max, yearsRange]);
12026
12268
  // Only the selection surface disables years; as navigation every listed
12027
12269
  // year can be visited.
12028
12270
  const disabledYears = React.useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
12271
+ // The year list's one tab stop, after the month grid's: when the focused
12272
+ // year is disabled the stop moves to the nearest enabled year in the list,
12273
+ // and focusing it makes it the focused year. As the selection surface a
12274
+ // year outside a non-empty list starts from the end it fell past, so the
12275
+ // list keeps a stop. As navigation the stop marks the current year, so a
12276
+ // year outside the list marks no option.
12277
+ const tabStopYear = React.useMemo(() => {
12278
+ let i = yearList.indexOf(focusedYear);
12279
+ if (i < 0) {
12280
+ if (!isYearGranularity || yearList.length === 0)
12281
+ return focusedYear;
12282
+ i = focusedYear < yearList[0] ? 0 : yearList.length - 1;
12283
+ }
12284
+ return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
12285
+ }, [yearList, focusedYear, disabledYears, isYearGranularity]);
12029
12286
  // Which of the focused year's months have no selectable day.
12030
12287
  const disabledMonths = React.useMemo(() => granularity === 'month'
12031
12288
  ? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
@@ -12033,7 +12290,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12033
12290
  // The month grid's one tab stop. A disabled button can't take focus, so
12034
12291
  // when the focused month is disabled the stop moves to the nearest enabled
12035
12292
  // month, and focusing it makes it the focused month.
12036
- const tabStopMonth = nearestEnabledMonth(focusedDate.getMonth(), disabledMonths);
12293
+ const tabStopMonth = nearestEnabled(focusedDate.getMonth(), disabledMonths);
12037
12294
  const handleYearSelect = React.useCallback((year) => {
12038
12295
  const next = new Date(focusedDate);
12039
12296
  next.setFullYear(year);
@@ -12075,24 +12332,37 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12075
12332
  e.preventDefault();
12076
12333
  moveYearFocus(step[0], step[1]);
12077
12334
  }, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
12335
+ // The day or month grid takes focus as the popover opens on it, or comes
12336
+ // back to it from the year list.
12337
+ React.useEffect(() => {
12338
+ if (!autoFocusCell)
12339
+ return;
12340
+ const grid = view === 'days'
12341
+ ? gridRef.current
12342
+ : view === 'months'
12343
+ ? monthGridRef.current
12344
+ : null;
12345
+ grid?.querySelector('[data-focused="true"]')?.focus();
12346
+ }, [autoFocusCell, view]);
12347
+ // From then on focus follows the tab stop only while the grid has it, or
12348
+ // has just lost it, so focus on a header button or a time wheel stays put.
12349
+ const tabStopDayTime = tabStopDay.getTime();
12078
12350
  React.useEffect(() => {
12079
- if (view !== 'days')
12351
+ const grid = gridRef.current;
12352
+ if (view !== 'days' || !grid)
12080
12353
  return;
12081
- if (!autoFocusCell || !gridRef.current)
12354
+ if (!focusFollows(grid, gridHeldFocusRef.current))
12082
12355
  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.
12356
+ grid.querySelector('[data-focused="true"]')?.focus();
12357
+ }, [tabStopDayTime, view]);
12088
12358
  React.useEffect(() => {
12089
12359
  const grid = monthGridRef.current;
12090
12360
  if (view !== 'months' || !grid)
12091
12361
  return;
12092
- if (!autoFocusCell && !grid.contains(document.activeElement))
12362
+ if (!focusFollows(grid, gridHeldFocusRef.current))
12093
12363
  return;
12094
12364
  grid.querySelector('[data-focused="true"]')?.focus();
12095
- }, [autoFocusCell, focusedDate, view]);
12365
+ }, [focusedYear, tabStopMonth, view]);
12096
12366
  // When the year view opens as navigation, scroll the focused year into
12097
12367
  // view.
12098
12368
  React.useEffect(() => {
@@ -12126,15 +12396,15 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12126
12396
  const list = yearGridRef.current;
12127
12397
  if (!isYearGranularity || !list)
12128
12398
  return;
12129
- if (!list.contains(document.activeElement))
12399
+ if (!list.contains(getActiveElementInTree(list)))
12130
12400
  return;
12131
12401
  list.querySelector('[data-focused-year="true"]')?.focus();
12132
12402
  }, [isYearGranularity, focusedYear]);
12133
12403
  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 => {
12404
+ 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
12405
  const disabled = isDateUnselectable(cell.date);
12136
12406
  const isSelected = !!value && isSameDay(value, cell.date);
12137
- const isFocused = isSameDay(cell.date, focusedDate);
12407
+ const isFocused = isSameDay(cell.date, tabStopDay);
12138
12408
  const otherMonth = !cell.inCurrentMonth;
12139
12409
  const cellClass = prefixedClassNames(classPrefix, 'dateinput-cell', {
12140
12410
  'is-selected': isSelected,
@@ -12143,13 +12413,20 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12143
12413
  'is-other-month': otherMonth,
12144
12414
  });
12145
12415
  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: () => {
12416
+ 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: () => {
12417
+ // Reached by Tab while the focused day is disabled, or
12418
+ // by pointer: keys move on from here. A nearby month's
12419
+ // day waits for its click, which turns the grid.
12420
+ if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
12421
+ onFocusedDateChange(cell.date);
12422
+ }
12423
+ }, onClick: () => {
12147
12424
  if (disabled)
12148
12425
  return;
12149
12426
  onFocusedDateChange(cell.date);
12150
12427
  onSelect(cell.date);
12151
12428
  }, 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 => {
12429
+ }) })] })), 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
12430
  const date = makeDate(focusedYear, month);
12154
12431
  const disabled = disabledMonths[month];
12155
12432
  const isSelected = !!value && isSameMonth(value, date);
@@ -12171,7 +12448,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12171
12448
  onSelect(date);
12172
12449
  }, children: monthCellNames[month] }, month));
12173
12450
  }) }, 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;
12451
+ const isFocused = year === tabStopYear;
12175
12452
  const isToday = year === todayYear;
12176
12453
  // As navigation the focused year is the current one. As the
12177
12454
  // selection surface the value's year is selected and focus roams.
@@ -12184,7 +12461,13 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12184
12461
  'is-today': isToday,
12185
12462
  'is-disabled': disabled,
12186
12463
  });
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
12464
+ 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: () => {
12465
+ // Reached by Tab while the focused year is disabled, or by
12466
+ // pointer: keys move on from here.
12467
+ if (isYearGranularity && year !== focusedYear) {
12468
+ onFocusedDateChange(clampDate(makeDate(year), min, max));
12469
+ }
12470
+ }, onClick: () => isYearGranularity
12188
12471
  ? handleYearPick(year)
12189
12472
  : handleYearSelect(year), children: year }, year));
12190
12473
  }) }))] }));
@@ -12192,12 +12475,25 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12192
12475
  Calendar.displayName = 'Calendar';
12193
12476
 
12194
12477
  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, }) => {
12478
+ 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
12479
  const panelRef = React.useRef(null);
12480
+ // Read as the trap turns on, once the panel's content has rendered.
12481
+ const initialFocusRef = React.useMemo(() => initialFocusSelector === undefined
12482
+ ? undefined
12483
+ : {
12484
+ get current() {
12485
+ return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
12486
+ },
12487
+ }, [initialFocusSelector]);
12197
12488
  const resolved = useAnchoredPosition(anchorRef, panelRef, {
12198
12489
  active: isOpen,
12199
12490
  position,
12200
12491
  fixed: appendToBody,
12492
+ // The gap comes from `--bulma-picker-popover-offset` as a margin, in place
12493
+ // or portaled, so the coordinates leave it out and `auto` reads it off
12494
+ // the panel.
12495
+ offset: 0,
12496
+ styledGap: marginGap,
12201
12497
  });
12202
12498
  React.useEffect(() => {
12203
12499
  if (!isOpen || !closeOnClickOutside || !isBrowser)
@@ -12231,6 +12527,7 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12231
12527
  }, [isOpen, closeOnEscape, onClose]);
12232
12528
  useFocusTrap(panelRef, {
12233
12529
  active: isOpen && trapFocus,
12530
+ initialFocusRef,
12234
12531
  restoreFocus: restoreFocusRef ?? true,
12235
12532
  });
12236
12533
  const panelClass = usePrefixedClassNames('picker-popover', {
@@ -12410,8 +12707,11 @@ function buildSegmentMap(format) {
12410
12707
  // ---------------------------------------------------------------------------
12411
12708
  const wrap$1 = (n, max) => ((n % max) + max) % max;
12412
12709
  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();
12710
+ /**
12711
+ * Number of days in the month of `d` (handles leap Februarys). Built on
12712
+ * `makeDate`, so a year below 100 keeps its own leap rule.
12713
+ */
12714
+ const daysInMonth = (d) => makeDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
12415
12715
  /**
12416
12716
  * Returns a new Date with the given segment incremented (`delta` = +1 or -1).
12417
12717
  * Wraps at segment boundaries: year is unbounded, month 0↔11 (in place, no
@@ -12638,33 +12938,35 @@ function useSegmentedEntry(params) {
12638
12938
  // within the same event tick.
12639
12939
  const typedDigitsRef = React.useRef('');
12640
12940
  // The text focus seeded an empty picker with, until the user edits it or
12641
- // leaves. While the text still matches it, nothing has been typed.
12941
+ // leaves.
12642
12942
  const seedRef = React.useRef(null);
12643
- // Whether the input's current focus is the one a closing popover handed
12644
- // back, until focus leaves the picker.
12645
- const handedBackRef = React.useRef(false);
12943
+ // Whether the text is still what the picker put there, so nothing in it was
12944
+ // typed: the value's display, or an empty picker's seed until an edit drops
12945
+ // it. Against a value it compares text alone, so typing that ends back on
12946
+ // the display counts as untouched.
12947
+ const isUntouched = React.useCallback((s) => value
12948
+ ? s === formatFn(value, format, locale)
12949
+ : seedRef.current !== null && s === seedRef.current, [value, formatFn, format, locale]);
12646
12950
  // A seed the popover never turned into a value closes with it, wherever
12647
12951
  // focus goes, so a dismissed empty field stays empty.
12648
12952
  //
12649
12953
  // As the popover closes, its focus trap hands focus back to the input. That
12650
12954
  // focus is not the user arriving: it must not open the popover again under
12651
- // `openOnFocus`, or seed an empty picker that leaving would then commit.
12652
- // `closingRef` is set in the layout phase of the commit that closes it and
12653
- // cleared by that commit's effects, and React runs a commit's effect
12654
- // cleanups, the trap's among them, before any of its effects, so a focus
12655
- // in between is the one handed back.
12955
+ // `openOnFocus`, or seed an empty picker. `closingRef` is set in the layout
12956
+ // phase of the commit that closes it and cleared by that commit's effects,
12957
+ // and React runs a commit's effect cleanups, the trap's among them, before
12958
+ // any of its effects, so a focus in between is the one handed back.
12656
12959
  const closingRef = React.useRef(false);
12657
12960
  const wasOpenRef = React.useRef(isOpen);
12658
12961
  React.useLayoutEffect(() => {
12659
12962
  if (wasOpenRef.current && !isOpen) {
12660
12963
  closingRef.current = true;
12661
- if (!value && seedRef.current !== null && text === seedRef.current) {
12964
+ if (!value && isUntouched(text))
12662
12965
  setText('');
12663
- }
12664
12966
  seedRef.current = null;
12665
12967
  }
12666
12968
  wasOpenRef.current = isOpen;
12667
- }, [isOpen, value, text, setText]);
12969
+ }, [isOpen, value, isUntouched, text, setText]);
12668
12970
  React.useEffect(() => {
12669
12971
  closingRef.current = false;
12670
12972
  }, [isOpen]);
@@ -12742,13 +13044,18 @@ function useSegmentedEntry(params) {
12742
13044
  }
12743
13045
  setActiveSegmentIdx(null);
12744
13046
  typedDigitsRef.current = '';
12745
- seedRef.current = null;
12746
- const handedBack = handedBackRef.current;
12747
- handedBackRef.current = false;
12748
- // Leaving after a dismiss commits only what was typed since. Text that
12749
- // still shows the value wasn't, and re-parsing it could drop what the
12750
- // format leaves out, such as seconds.
12751
- if (handedBack && value && text === formatFn(value, format, locale)) {
13047
+ // Leaving commits only what was typed. Re-parsing the value's display
13048
+ // could drop what the format leaves out, such as seconds, and a seed
13049
+ // nobody typed over is not the user's date.
13050
+ const untouched = isUntouched(text);
13051
+ // An open popover keeps an unused seed until it closes, so one portaled
13052
+ // outside the picker, whose focus reads as leaving, keeps it as one
13053
+ // inside does.
13054
+ if (!isOpen)
13055
+ seedRef.current = null;
13056
+ if (untouched) {
13057
+ if (!value && !isOpen)
13058
+ setText('');
12752
13059
  onBlur?.(e);
12753
13060
  return;
12754
13061
  }
@@ -12762,6 +13069,8 @@ function useSegmentedEntry(params) {
12762
13069
  onBlur?.(e);
12763
13070
  }, [
12764
13071
  containerRef,
13072
+ isUntouched,
13073
+ isOpen,
12765
13074
  tryParse,
12766
13075
  text,
12767
13076
  isAllowed,
@@ -12775,7 +13084,6 @@ function useSegmentedEntry(params) {
12775
13084
  ]);
12776
13085
  const handleFocus = React.useCallback((e) => {
12777
13086
  const handedBack = closingRef.current;
12778
- handedBackRef.current = handedBack;
12779
13087
  if (!handedBack && openOnFocus && popover && !disabled && !readOnly) {
12780
13088
  setOpen(true);
12781
13089
  }
@@ -12943,12 +13251,20 @@ function useSegmentedEntry(params) {
12943
13251
  }
12944
13252
  if (e.key === 'Enter') {
12945
13253
  e.preventDefault();
12946
- const parsed = tryParse(text);
12947
- if (parsed && isAllowed(parsed)) {
12948
- commitValue(parsed);
13254
+ // Untouched text has nothing to commit, as on leaving, but Enter
13255
+ // still confirms what the field shows.
13256
+ if (isUntouched(text)) {
12949
13257
  if (closeOnSelect)
12950
13258
  setOpen(false);
12951
13259
  }
13260
+ else {
13261
+ const parsed = tryParse(text);
13262
+ if (parsed && isAllowed(parsed)) {
13263
+ commitValue(parsed);
13264
+ if (closeOnSelect)
13265
+ setOpen(false);
13266
+ }
13267
+ }
12952
13268
  }
12953
13269
  onKeyDown?.(e);
12954
13270
  }, [
@@ -12962,6 +13278,7 @@ function useSegmentedEntry(params) {
12962
13278
  setOpen,
12963
13279
  closeOnSelect,
12964
13280
  popover,
13281
+ isUntouched,
12965
13282
  tryParse,
12966
13283
  text,
12967
13284
  isAllowed,
@@ -13010,25 +13327,31 @@ const pad2$1 = (n) => String(n).padStart(2, '0');
13010
13327
  * The value as the native input and the hidden form input carry it:
13011
13328
  * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
13012
13329
  * `<input type="month">` use. The year is padded to four digits, as those
13013
- * inputs require and as the `YYYY` token displays it.
13330
+ * inputs require and as the `YYYY` token displays it. HTML has no such shape
13331
+ * for a year before 1, so a date then is empty, as those inputs would make it.
13014
13332
  */
13015
13333
  const toIsoValue = (d, granularity) => {
13334
+ if (d.getFullYear() < FIRST_YEAR)
13335
+ return '';
13016
13336
  const year = String(d.getFullYear()).padStart(4, '0');
13017
13337
  if (granularity === 'year')
13018
13338
  return year;
13019
13339
  const month = `${year}-${pad2$1(d.getMonth() + 1)}`;
13020
13340
  return granularity === 'month' ? month : `${month}-${pad2$1(d.getDate())}`;
13021
13341
  };
13022
- /** Read a native `type="date"` or `type="month"` value back into a Date. */
13342
+ /**
13343
+ * Read a native `type="date"` or `type="month"` value back into a Date. HTML
13344
+ * allows a year of four or more digits, as `toIsoValue` writes one past 9999.
13345
+ */
13023
13346
  const fromIsoValue = (s, granularity) => {
13024
13347
  if (granularity === 'month') {
13025
- const m = /^(\d{4})-(\d{2})$/.exec(s);
13348
+ const m = /^(\d{4,})-(\d{2})$/.exec(s);
13026
13349
  return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
13027
13350
  }
13028
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
13351
+ const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
13029
13352
  if (!m)
13030
13353
  return null;
13031
- return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
13354
+ return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
13032
13355
  };
13033
13356
  const DEFAULT_FORMATS = {
13034
13357
  day: DEFAULT_DATE_FORMAT,
@@ -13050,14 +13373,19 @@ const SKIPPED_SEGMENTS = {
13050
13373
  * @returns {JSX.Element}
13051
13374
  */
13052
13375
  const DateInputBase = React.forwardRef((props, ref) => {
13053
- const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, granularity = 'day', format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon = true, triggerIconName = 'chevron-down', labels, ...rest } = props;
13376
+ const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, granularity = 'day', format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', labels, ...rest } = props;
13377
+ // The launcher gives way to the loading spinner of a Control this sits in.
13378
+ const controlLoading = useControlLoading();
13379
+ const triggerIcon = triggerIconProp ?? !controlLoading;
13054
13380
  const t = mergeLabels(labels);
13055
13381
  const isDayGranularity = granularity === 'day';
13056
13382
  const resolvedFormat = format ?? DEFAULT_FORMATS[granularity];
13057
13383
  const isControlled = controlledValue !== undefined;
13058
13384
  const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
13059
13385
  const value = isControlled ? (controlledValue ?? null) : internalValue;
13060
- const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), min, max),
13386
+ // Nothing before year 1 is in range, as in HTML's date inputs.
13387
+ const lowerBound = React.useMemo(() => floorMin(min), [min]);
13388
+ const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
13061
13389
  // intentionally only on mount: keep focusedDate stable until value/open change
13062
13390
  // eslint-disable-next-line react-hooks/exhaustive-deps
13063
13391
  []);
@@ -13065,8 +13393,8 @@ const DateInputBase = React.forwardRef((props, ref) => {
13065
13393
  // Re-clamp focusedDate if min/max change after mount so the focused cell
13066
13394
  // never disappears outside the displayable range.
13067
13395
  React.useEffect(() => {
13068
- setFocusedDate(prev => clampDate(prev, min, max));
13069
- }, [min, max]);
13396
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
13397
+ }, [lowerBound, max]);
13070
13398
  const [open, setOpenState] = React.useState(false);
13071
13399
  const [text, setText] = React.useState(value ? formatDate(value, resolvedFormat, locale) : '');
13072
13400
  const inputRef = React.useRef(null);
@@ -13092,16 +13420,20 @@ const DateInputBase = React.forwardRef((props, ref) => {
13092
13420
  });
13093
13421
  const containerClass = usePrefixedClassNames('dateinput-container');
13094
13422
  const triggerClass = usePrefixedClassNames('dateinput-trigger');
13423
+ // What the latest request asked for, so a second request in the same
13424
+ // event sees it before React renders. The callbacks fire here rather than
13425
+ // in a state updater, which StrictMode calls twice, and after the state
13426
+ // is set, so a request a callback makes is the one that lands last.
13427
+ const requestedOpenRef = React.useRef(false);
13095
13428
  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
- });
13429
+ if (requestedOpenRef.current === next)
13430
+ return;
13431
+ requestedOpenRef.current = next;
13432
+ setOpenState(next);
13433
+ if (next)
13434
+ onOpen?.();
13435
+ else
13436
+ onClose?.();
13105
13437
  }, [onOpen, onClose]);
13106
13438
  // Sync displayed text with value when value changes externally.
13107
13439
  React.useEffect(() => {
@@ -13120,7 +13452,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
13120
13452
  }, [isControlled, onChange, isDayGranularity, granularity]);
13121
13453
  // At month or year granularity the bounds widen to the periods holding
13122
13454
  // them, so the month or year containing `min` stays reachable.
13123
- const periodMin = React.useMemo(() => (min && !isDayGranularity ? startOfPeriod(min, granularity) : min), [min, isDayGranularity, granularity]);
13455
+ const periodMin = React.useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
13124
13456
  const periodMax = React.useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
13125
13457
  const handleSelect = React.useCallback((d) => {
13126
13458
  if (!isWithin(d, periodMin, periodMax))
@@ -13171,7 +13503,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
13171
13503
  ]);
13172
13504
  // Typing, and the re-parse on blur or Enter, that lands in the period the
13173
13505
  // value already holds commits nothing, so a `value` elsewhere in its month
13174
- // or year isn't rewritten by focus passing through the field.
13506
+ // or year changes only when the user picks or types a different period.
13175
13507
  const commitTyped = React.useCallback((next) => {
13176
13508
  if (next &&
13177
13509
  value &&
@@ -13237,7 +13569,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
13237
13569
  ? fromIsoValue(e.target.value, granularity)
13238
13570
  : null;
13239
13571
  commitValue(parsed);
13240
- }, min: min ? toIsoValue(min, granularity) : undefined, max: max ? toIsoValue(max, granularity) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
13572
+ }, min: min ? toIsoValue(lowerBound, granularity) : undefined, max: max ? toIsoValue(max, granularity) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
13241
13573
  }
13242
13574
  const chooseLabel = granularity === 'month'
13243
13575
  ? t.chooseMonth
@@ -13246,14 +13578,18 @@ const DateInputBase = React.forwardRef((props, ref) => {
13246
13578
  : t.chooseDate;
13247
13579
  const calendar = (jsxRuntime.jsx(Calendar
13248
13580
  // 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));
13581
+ , { 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,
13582
+ // The popover panel takes `popoverId`, so the calendar inside it
13583
+ // takes one of its own. Inline there is no panel, and the calendar
13584
+ // keeps the id it released with.
13585
+ id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
13250
13586
  if (inline) {
13251
13587
  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
13588
  }
13253
13589
  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
13590
  if (canOpen)
13255
13591
  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 }))] }));
13592
+ }, 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
13593
  });
13258
13594
  DateInputBase.displayName = 'DateInputBase';
13259
13595
 
@@ -13277,26 +13613,34 @@ DateInputBase.displayName = 'DateInputBase';
13277
13613
  const DateInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
13278
13614
  const insideField = useInsideField();
13279
13615
  const insideControl = useInsideControl();
13616
+ const ownField = rendersOwnField('DateInput', {
13617
+ insideField,
13618
+ insideControl,
13619
+ label,
13620
+ message,
13621
+ horizontal,
13622
+ fieldClassName,
13623
+ });
13280
13624
  const { controlId, fieldLabelProps } = useAutoLabelId({
13281
13625
  label,
13282
13626
  id: baseProps.id,
13283
13627
  labelProps,
13284
13628
  // Inline mode renders a bare calendar with no input to label.
13285
- rendersLabel: !insideField && !baseProps.inline,
13629
+ rendersLabel: ownField && !baseProps.inline,
13286
13630
  });
13287
13631
  const helpClass = usePrefixedClassNames('help', {
13288
13632
  [`is-${messageColor}`]: !!messageColor,
13289
13633
  });
13290
- // The right-side launcher is on by default; suppress it while the Control
13291
- // shows its loading spinner (also on the right) unless explicitly set.
13292
- let content = (jsxRuntime.jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
13634
+ // The base hides its launcher while the Control it sits in is loading,
13635
+ // whether that is the one rendered below or an enclosing one.
13636
+ let content = (jsxRuntime.jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps }));
13293
13637
  // Inline mode renders a bare calendar with no input, so the Control's
13294
13638
  // icon-left container has nothing to anchor to. Skip the Control wrap.
13295
13639
  if (!insideControl && !baseProps.inline) {
13296
13640
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
13297
13641
  }
13298
13642
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
13299
- if (!insideField) {
13643
+ if (ownField) {
13300
13644
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
13301
13645
  }
13302
13646
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -14170,7 +14514,10 @@ const fromIsoTime = (s) => {
14170
14514
  * @returns {JSX.Element}
14171
14515
  */
14172
14516
  const TimeInputBase = React.forwardRef((props, ref) => {
14173
- const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, hourFormat = '24', enableSeconds = false, incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon = true, triggerIconName = 'chevron-down', labels, audioTick = false, haptics = false, ...rest } = props;
14517
+ const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, hourFormat = '24', enableSeconds = false, incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', labels, audioTick = false, haptics = false, ...rest } = props;
14518
+ // The launcher gives way to the loading spinner of a Control this sits in.
14519
+ const controlLoading = useControlLoading();
14520
+ const triggerIcon = triggerIconProp ?? !controlLoading;
14174
14521
  // Platform-appropriate feedback routing: when `haptics` is opted in and
14175
14522
  // the runtime has no Vibration API (notably iOS Safari), enable the
14176
14523
  // audio thunk so the user still gets *some* tactile cue. When vibrate
@@ -14226,23 +14573,29 @@ const TimeInputBase = React.forwardRef((props, ref) => {
14226
14573
  const mobileFooterClass = usePrefixedClassNames('timeinput-footer-mobile');
14227
14574
  const mobileFooterResetClass = usePrefixedClassNames('timeinput-footer-reset');
14228
14575
  const mobileFooterDoneClass = usePrefixedClassNames('timeinput-footer-done');
14576
+ // What the latest request asked for, so a second request in the same
14577
+ // event sees it before React renders. The callbacks fire here rather than
14578
+ // in a state updater, which StrictMode calls twice, and after the state
14579
+ // is set, so a request a callback makes is the one that lands last.
14580
+ const requestedOpenRef = React.useRef(false);
14229
14581
  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
- });
14582
+ if (requestedOpenRef.current === next)
14583
+ return;
14584
+ requestedOpenRef.current = next;
14585
+ setOpenState(next);
14586
+ if (next) {
14587
+ valueAtOpenRef.current = value;
14588
+ onOpen?.();
14589
+ }
14590
+ else {
14591
+ onClose?.();
14592
+ }
14242
14593
  }, [onOpen, onClose, value]);
14243
14594
  React.useEffect(() => {
14244
14595
  setText(value ? formatTime(value, defaultFormat, locale) : '');
14245
14596
  }, [value, defaultFormat, locale]);
14597
+ // A value's date before year 1 is out of range, as in the date pickers.
14598
+ const lowerBound = React.useMemo(() => floorMin(min), [min]);
14246
14599
  const commitValue = React.useCallback((next) => {
14247
14600
  if (!isControlled)
14248
14601
  setInternalValue(next);
@@ -14251,10 +14604,10 @@ const TimeInputBase = React.forwardRef((props, ref) => {
14251
14604
  const handleSpinnerChange = React.useCallback((parts) => {
14252
14605
  const base = value ?? new Date();
14253
14606
  const next = setTimeOfDay(base, parts);
14254
- if (!isWithin(next, min, max))
14607
+ if (!isWithin(next, lowerBound, max))
14255
14608
  return;
14256
14609
  commitValue(next);
14257
- }, [value, min, max, commitValue]);
14610
+ }, [value, lowerBound, max, commitValue]);
14258
14611
  const tryParse = React.useCallback((s) => {
14259
14612
  const trimmed = s.trim();
14260
14613
  if (!trimmed)
@@ -14286,7 +14639,7 @@ const TimeInputBase = React.forwardRef((props, ref) => {
14286
14639
  setText,
14287
14640
  makeBaseDate,
14288
14641
  locale,
14289
- min,
14642
+ min: lowerBound,
14290
14643
  max,
14291
14644
  isBlocked: unselectableTimes,
14292
14645
  disabled,
@@ -14368,34 +14721,47 @@ TimeInputBase.displayName = 'TimeInputBase';
14368
14721
  const TimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
14369
14722
  const insideField = useInsideField();
14370
14723
  const insideControl = useInsideControl();
14724
+ const ownField = rendersOwnField('TimeInput', {
14725
+ insideField,
14726
+ insideControl,
14727
+ label,
14728
+ message,
14729
+ horizontal,
14730
+ fieldClassName,
14731
+ });
14371
14732
  const { controlId, fieldLabelProps } = useAutoLabelId({
14372
14733
  label,
14373
14734
  id: baseProps.id,
14374
14735
  labelProps,
14375
14736
  // Inline mode renders a bare panel with no input to label.
14376
- rendersLabel: !insideField && !baseProps.inline,
14737
+ rendersLabel: ownField && !baseProps.inline,
14377
14738
  });
14378
14739
  const helpClass = usePrefixedClassNames('help', {
14379
14740
  [`is-${messageColor}`]: !!messageColor,
14380
14741
  });
14381
- // The right-side launcher is on by default; suppress it while the Control
14382
- // shows its loading spinner (also on the right) unless explicitly set.
14383
- let content = (jsxRuntime.jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
14742
+ // The base hides its launcher while the Control it sits in is loading,
14743
+ // whether that is the one rendered below or an enclosing one.
14744
+ let content = (jsxRuntime.jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps }));
14384
14745
  // Inline mode renders a bare panel with no input, so the Control's
14385
14746
  // icon-left container has nothing to anchor to. Skip the Control wrap.
14386
14747
  if (!insideControl && !baseProps.inline) {
14387
14748
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
14388
14749
  }
14389
14750
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14390
- if (!insideField) {
14751
+ if (ownField) {
14391
14752
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14392
14753
  }
14393
14754
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
14394
14755
  });
14395
14756
  TimeInput.displayName = 'TimeInput';
14396
14757
 
14758
+ // The year is padded to four digits, as `datetime-local` requires. HTML has
14759
+ // no such shape for a year before 1, so a date then is empty, as that input
14760
+ // would make it.
14397
14761
  const toIsoDateTime = (d, withSeconds) => {
14398
- const yyyy = d.getFullYear();
14762
+ if (d.getFullYear() < FIRST_YEAR)
14763
+ return '';
14764
+ const yyyy = String(d.getFullYear()).padStart(4, '0');
14399
14765
  const mo = String(d.getMonth() + 1).padStart(2, '0');
14400
14766
  const dd = String(d.getDate()).padStart(2, '0');
14401
14767
  const hh = String(d.getHours()).padStart(2, '0');
@@ -14408,10 +14774,13 @@ const toIsoDateTime = (d, withSeconds) => {
14408
14774
  const fromIsoDateTime = (s) => {
14409
14775
  // The HTML datetime-local value may carry fractional seconds (the spec
14410
14776
  // 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);
14777
+ // Its year is four or more digits, as `toIsoDateTime` writes one past 9999.
14778
+ const m = /^(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?$/.exec(s);
14412
14779
  if (!m)
14413
14780
  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);
14781
+ const d = makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
14782
+ d.setHours(Number(m[4]), Number(m[5]), m[6] ? Number(m[6]) : 0, 0);
14783
+ return d;
14415
14784
  };
14416
14785
  /**
14417
14786
  * Raw DateTimeInput — combined input + calendar + time spinner without
@@ -14424,7 +14793,10 @@ const fromIsoDateTime = (s) => {
14424
14793
  const DateTimeInputBase = React.forwardRef((props, ref) => {
14425
14794
  const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, hourFormat = '24', enableSeconds = false, incrementHours = 1,
14426
14795
  // Step between visible wheel values; 1 gives the iOS-style every-minute wheel.
14427
- incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon = true, triggerIconName = 'chevron-down', audioTick = false, haptics = false, labels, ...rest } = props;
14796
+ incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', audioTick = false, haptics = false, labels, ...rest } = props;
14797
+ // The launcher gives way to the loading spinner of a Control this sits in.
14798
+ const controlLoading = useControlLoading();
14799
+ const triggerIcon = triggerIconProp ?? !controlLoading;
14428
14800
  const t = mergeLabels(labels);
14429
14801
  // Platform-appropriate feedback routing for the time wheel: on iOS (no
14430
14802
  // navigator.vibrate) the audio thunk fills in; on Android the real haptic
@@ -14434,14 +14806,16 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14434
14806
  const isControlled = controlledValue !== undefined;
14435
14807
  const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
14436
14808
  const value = isControlled ? (controlledValue ?? null) : internalValue;
14437
- const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), min, max),
14809
+ // Nothing before year 1 is in range, as in HTML's date inputs.
14810
+ const lowerBound = React.useMemo(() => floorMin(min), [min]);
14811
+ const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
14438
14812
  // eslint-disable-next-line react-hooks/exhaustive-deps
14439
14813
  []);
14440
14814
  const [focusedDate, setFocusedDate] = React.useState(initialFocused);
14441
14815
  // Re-clamp focusedDate when min/max change so the focused cell stays valid.
14442
14816
  React.useEffect(() => {
14443
- setFocusedDate(prev => clampDate(prev, min, max));
14444
- }, [min, max]);
14817
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
14818
+ }, [lowerBound, max]);
14445
14819
  const defaultFormat = format ??
14446
14820
  (hourFormat === '12'
14447
14821
  ? enableSeconds
@@ -14498,19 +14872,23 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14498
14872
  const footerResetClass = usePrefixedClassNames('datetimeinput-footer-reset');
14499
14873
  const footerDoneClass = usePrefixedClassNames('datetimeinput-footer-done');
14500
14874
  const footerTimePillClass = usePrefixedClassNames('datetimeinput-footer-time-pill');
14875
+ // What the latest request asked for, so a second request in the same event
14876
+ // sees it before React renders. The callbacks fire here rather than in a
14877
+ // state updater, which StrictMode calls twice, and after the state is set,
14878
+ // so a request a callback makes is the one that lands last.
14879
+ const requestedOpenRef = React.useRef(false);
14501
14880
  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
- });
14881
+ if (requestedOpenRef.current === next)
14882
+ return;
14883
+ requestedOpenRef.current = next;
14884
+ setOpenState(next);
14885
+ if (next) {
14886
+ valueAtOpenRef.current = value;
14887
+ onOpen?.();
14888
+ }
14889
+ else {
14890
+ onClose?.();
14891
+ }
14514
14892
  }, [onOpen, onClose, value]);
14515
14893
  React.useEffect(() => {
14516
14894
  setText(value ? formatDateTime(value, defaultFormat, locale) : '');
@@ -14534,18 +14912,18 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14534
14912
  minutes: value?.getMinutes() ?? 0,
14535
14913
  seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
14536
14914
  });
14537
- if (!isWithin(merged, min, max))
14915
+ if (!isWithin(merged, lowerBound, max))
14538
14916
  return;
14539
14917
  commitValue(merged);
14540
14918
  setFocusedDate(d);
14541
- }, [value, enableSeconds, min, max, commitValue]);
14919
+ }, [value, enableSeconds, lowerBound, max, commitValue]);
14542
14920
  const handleTimeChange = React.useCallback((parts) => {
14543
14921
  const base = value ?? focusedDate;
14544
14922
  const next = setTimeOfDay(base, parts);
14545
- if (!isWithin(next, min, max))
14923
+ if (!isWithin(next, lowerBound, max))
14546
14924
  return;
14547
14925
  commitValue(next);
14548
- }, [value, focusedDate, min, max, commitValue]);
14926
+ }, [value, focusedDate, lowerBound, max, commitValue]);
14549
14927
  const tryParse = React.useCallback((s) => {
14550
14928
  const trimmed = s.trim();
14551
14929
  if (!trimmed)
@@ -14580,7 +14958,7 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14580
14958
  setText,
14581
14959
  makeBaseDate,
14582
14960
  locale,
14583
- min,
14961
+ min: lowerBound,
14584
14962
  max,
14585
14963
  isBlocked,
14586
14964
  disabled,
@@ -14613,7 +14991,7 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14613
14991
  ? fromIsoDateTime(e.target.value)
14614
14992
  : null;
14615
14993
  commitValue(parsed);
14616
- }, min: min ? toIsoDateTime(min, enableSeconds) : undefined, max: max ? toIsoDateTime(max, enableSeconds) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
14994
+ }, min: min ? toIsoDateTime(lowerBound, enableSeconds) : undefined, max: max ? toIsoDateTime(max, enableSeconds) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
14617
14995
  }
14618
14996
  const spinnerValue = {
14619
14997
  hours: value?.getHours() ?? 0,
@@ -14640,7 +15018,7 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14640
15018
  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
15019
  if (canOpen)
14642
15020
  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 }))] }));
15021
+ }, 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
15022
  });
14645
15023
  DateTimeInputBase.displayName = 'DateTimeInputBase';
14646
15024
 
@@ -14664,26 +15042,34 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
14664
15042
  const DateTimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
14665
15043
  const insideField = useInsideField();
14666
15044
  const insideControl = useInsideControl();
15045
+ const ownField = rendersOwnField('DateTimeInput', {
15046
+ insideField,
15047
+ insideControl,
15048
+ label,
15049
+ message,
15050
+ horizontal,
15051
+ fieldClassName,
15052
+ });
14667
15053
  const { controlId, fieldLabelProps } = useAutoLabelId({
14668
15054
  label,
14669
15055
  id: baseProps.id,
14670
15056
  labelProps,
14671
15057
  // Inline mode renders a bare picker with no input to label.
14672
- rendersLabel: !insideField && !baseProps.inline,
15058
+ rendersLabel: ownField && !baseProps.inline,
14673
15059
  });
14674
15060
  const helpClass = usePrefixedClassNames('help', {
14675
15061
  [`is-${messageColor}`]: !!messageColor,
14676
15062
  });
14677
- // The right-side launcher is on by default; suppress it while the Control
14678
- // shows its loading spinner (also on the right) unless explicitly set.
14679
- let content = (jsxRuntime.jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
15063
+ // The base hides its launcher while the Control it sits in is loading,
15064
+ // whether that is the one rendered below or an enclosing one.
15065
+ let content = (jsxRuntime.jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps }));
14680
15066
  // Inline mode renders a bare picker with no input, so the Control's
14681
15067
  // icon-left container has nothing to anchor to. Skip the Control wrap.
14682
15068
  if (!insideControl && !baseProps.inline) {
14683
15069
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
14684
15070
  }
14685
15071
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14686
- if (!insideField) {
15072
+ if (ownField) {
14687
15073
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14688
15074
  }
14689
15075
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -15622,31 +16008,57 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
15622
16008
  // rewrites the shared element in place rather than removing and recreating
15623
16009
  // it between the cleanup and the next run.
15624
16010
  React.useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
15625
- // Toggle Bulma's light/dark scheme by writing the `data-theme` attribute on
15626
- // the document root (<html>). This is always global, even on a scoped Theme.
15627
- // `'system'` removes the attribute so Bulma follows the OS preference.
16011
+ // Bulma names its scheme attribute after the class prefix a stylesheet was
16012
+ // built with (`data-<prefix>theme`), so the prefixed builds read
16013
+ // `data-bestax-theme` and never `data-theme`.
16014
+ const classPrefix = useClassPrefix();
16015
+ // Toggle Bulma's light/dark scheme by writing the theme attribute on the
16016
+ // document root (<html>). This is always global, even on a scoped Theme.
16017
+ // Under a class prefix the prefixed attribute is written as well as
16018
+ // `data-theme`, so whichever build is loaded sees it; with no prefix only
16019
+ // `data-theme` is. `'system'` removes them so Bulma follows the OS
16020
+ // preference.
15628
16021
  React.useEffect(() => {
15629
16022
  if (colorMode === undefined) {
15630
16023
  return;
15631
16024
  }
15632
16025
  const root = document.documentElement;
15633
- const previous = root.getAttribute('data-theme');
15634
- if (colorMode === 'system') {
15635
- root.removeAttribute('data-theme');
15636
- }
15637
- else {
15638
- root.setAttribute('data-theme', colorMode);
15639
- }
15640
- // Restore the previous value when colorMode changes or the component unmounts.
15641
- return () => {
15642
- if (previous === null) {
15643
- root.removeAttribute('data-theme');
16026
+ const names = classPrefix
16027
+ ? ['data-theme', `data-${classPrefix}theme`]
16028
+ : ['data-theme'];
16029
+ const previous = names.map(name => root.getAttribute(name));
16030
+ for (const name of names) {
16031
+ if (colorMode === 'system') {
16032
+ root.removeAttribute(name);
16033
+ continue;
15644
16034
  }
15645
- else {
15646
- root.setAttribute('data-theme', previous);
16035
+ try {
16036
+ root.setAttribute(name, colorMode);
15647
16037
  }
16038
+ catch {
16039
+ // A class prefix can hold characters an attribute name cannot.
16040
+ // `data-theme` is still written, as it was before the prefixed
16041
+ // attribute existed, but a sheet built with that prefix keeps its
16042
+ // scheme, so say so.
16043
+ warnOnce('Theme:color-mode-prefix', `[bestax-bulma] <Theme colorMode="${colorMode}">: the class ` +
16044
+ `prefix "${classPrefix}" makes ${name} an invalid attribute ` +
16045
+ 'name, so only data-theme is written, and a stylesheet built ' +
16046
+ 'with that prefix keeps its scheme.');
16047
+ }
16048
+ }
16049
+ // Restore the previous values when colorMode changes or the component unmounts.
16050
+ return () => {
16051
+ names.forEach((name, i) => {
16052
+ const value = previous[i];
16053
+ if (value === null) {
16054
+ root.removeAttribute(name);
16055
+ }
16056
+ else {
16057
+ root.setAttribute(name, value);
16058
+ }
16059
+ });
15648
16060
  };
15649
- }, [colorMode]);
16061
+ }, [colorMode, classPrefix]);
15650
16062
  // For local injection (when isRoot is false), prepare style object for CSS vars
15651
16063
  const style = React.useMemo(() => {
15652
16064
  if (isRoot) {