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