@allxsmith/bestax-bulma 5.26.3 → 5.26.5

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 (68) 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 +607 -160
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +607 -160
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/form/Autocomplete.d.ts +6 -1
  10. package/dist/types/form/Checkboxes.d.ts +7 -0
  11. package/dist/types/form/DateInput.d.ts +22 -3
  12. package/dist/types/form/DateInputBase.d.ts +17 -3
  13. package/dist/types/form/DateTimeInput.d.ts +22 -3
  14. package/dist/types/form/DateTimeInputBase.d.ts +17 -3
  15. package/dist/types/form/File.d.ts +6 -0
  16. package/dist/types/form/FormContext.d.ts +86 -7
  17. package/dist/types/form/Input.d.ts +8 -2
  18. package/dist/types/form/Numberinput.d.ts +3 -0
  19. package/dist/types/form/Radios.d.ts +7 -0
  20. package/dist/types/form/Rate.d.ts +7 -0
  21. package/dist/types/form/Select.d.ts +8 -2
  22. package/dist/types/form/Slider.d.ts +7 -0
  23. package/dist/types/form/Taginput.d.ts +6 -0
  24. package/dist/types/form/TextArea.d.ts +8 -2
  25. package/dist/types/form/TimeInput.d.ts +22 -3
  26. package/dist/types/form/TimeInputBase.d.ts +16 -3
  27. package/dist/types/form/_pickerInternals/Calendar.d.ts +9 -3
  28. package/dist/types/form/_pickerInternals/dateUtils.d.ts +17 -0
  29. package/dist/types/form/controlLoading.d.ts +4 -0
  30. package/dist/types/helpers/useAnchoredPosition.d.ts +4 -1
  31. package/dist/types/helpers/useFocusTrap.d.ts +11 -4
  32. package/dist/types-cjs/form/Autocomplete.d.ts +6 -1
  33. package/dist/types-cjs/form/Checkboxes.d.ts +7 -0
  34. package/dist/types-cjs/form/DateInput.d.ts +22 -3
  35. package/dist/types-cjs/form/DateInputBase.d.ts +17 -3
  36. package/dist/types-cjs/form/DateTimeInput.d.ts +22 -3
  37. package/dist/types-cjs/form/DateTimeInputBase.d.ts +17 -3
  38. package/dist/types-cjs/form/File.d.ts +6 -0
  39. package/dist/types-cjs/form/FormContext.d.ts +86 -7
  40. package/dist/types-cjs/form/Input.d.ts +8 -2
  41. package/dist/types-cjs/form/Numberinput.d.ts +3 -0
  42. package/dist/types-cjs/form/Radios.d.ts +7 -0
  43. package/dist/types-cjs/form/Rate.d.ts +7 -0
  44. package/dist/types-cjs/form/Select.d.ts +8 -2
  45. package/dist/types-cjs/form/Slider.d.ts +7 -0
  46. package/dist/types-cjs/form/Taginput.d.ts +6 -0
  47. package/dist/types-cjs/form/TextArea.d.ts +8 -2
  48. package/dist/types-cjs/form/TimeInput.d.ts +22 -3
  49. package/dist/types-cjs/form/TimeInputBase.d.ts +16 -3
  50. package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +9 -3
  51. package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +17 -0
  52. package/dist/types-cjs/form/controlLoading.d.ts +4 -0
  53. package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +4 -1
  54. package/dist/types-cjs/helpers/useFocusTrap.d.ts +11 -4
  55. package/dist/versions/bestax-no-dark-mode.css +1 -1
  56. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  57. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  58. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  59. package/dist/versions/bestax-no-helpers.css +1 -1
  60. package/dist/versions/bestax-no-helpers.css.map +1 -1
  61. package/dist/versions/bestax-prefixed.css +1 -1
  62. package/dist/versions/bestax-prefixed.css.map +1 -1
  63. package/package.json +1 -1
  64. package/src/scss/form/_datetimeinput.scss +9 -0
  65. package/src/scss/form/_picker-popover.scss +12 -9
  66. package/src/scss/form/_rate.scss +3 -1
  67. package/src/scss/form/_taginput.scss +8 -0
  68. package/src/scss/form/_timeinput.scss +5 -0
package/dist/index.cjs CHANGED
@@ -5201,10 +5201,17 @@ const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
5201
5201
  * container, or give them a trap of their own. To trap content a `Portal`
5202
5202
  * renders, pass a ref to the element inside the `Portal` as `containerRef`.
5203
5203
  *
5204
- * The container has to be in the DOM when the trap turns on. The trap waits
5205
- * for hydration, so a container that only appears once the page has
5206
- * hydrated, the way portaled content does, still has it attach in the commit
5207
- * where the container appears.
5204
+ * The trap looks at `containerRef` again each time the component calling it
5205
+ * renders. A container that mounts after the trap turns on, such as a panel
5206
+ * waiting for a `Portal` target held in state, has it attach once that
5207
+ * component renders with the container in the DOM. When the container leaves
5208
+ * or is replaced, the trap lets go of it as it would turning off, handing
5209
+ * focus back, and attaches to the next one.
5210
+ * A child that mounts the container in a render of its own (from its own
5211
+ * state, or as a `Suspense` boundary resolves) goes unseen until the
5212
+ * component calling the hook renders again. The trap waits for hydration, so
5213
+ * that component renders again when the page hydrates, and portaled content,
5214
+ * which first appears then, is found.
5208
5215
  *
5209
5216
  * @function useFocusTrap
5210
5217
  * @param containerRef - Ref to the element focus stays inside.
@@ -5224,12 +5231,18 @@ function useFocusTrap(containerRef, options = {}) {
5224
5231
  // Portaled content has no server-rendered counterpart, so a portaled
5225
5232
  // container only exists from the commit after hydration.
5226
5233
  const hydrated = useIsHydrated();
5234
+ // The container the trap is attached to, or `null` while it has none.
5235
+ const heldRef = React.useRef(null);
5236
+ // Bumped to run the trap again when the container in the ref changes.
5237
+ const [refChanges, setRefChanges] = React.useState(0);
5227
5238
  React.useEffect(() => {
5239
+ heldRef.current = null;
5228
5240
  if (!active || !hydrated)
5229
5241
  return undefined;
5230
5242
  const container = containerRef.current;
5231
5243
  if (!container)
5232
5244
  return undefined;
5245
+ heldRef.current = container;
5233
5246
  const doc = container.ownerDocument;
5234
5247
  const opener = focusedElement(doc);
5235
5248
  // `initialFocusRef` counts only when focusing it puts focus inside. Focus
@@ -5304,7 +5317,28 @@ function useFocusTrap(containerRef, options = {}) {
5304
5317
  target.focus();
5305
5318
  }
5306
5319
  };
5307
- }, [active, hydrated, containerRef, initialFocusRef, restoreFocus]);
5320
+ }, [
5321
+ active,
5322
+ hydrated,
5323
+ containerRef,
5324
+ initialFocusRef,
5325
+ restoreFocus,
5326
+ refChanges,
5327
+ ]);
5328
+ // Nothing says when a ref is set, so after every render, a ref that no
5329
+ // longer matches what the trap holds (a container that mounted, left or was
5330
+ // replaced) runs it again. Effects run in order, so whenever the effect
5331
+ // above ran in this commit, it read the ref just before this does, and a
5332
+ // mismatch means the ref changed in a render it didn't run for. The render
5333
+ // a bump asks for runs it, so the two match and the trap settles. An empty
5334
+ // ref matches a trap holding nothing, so a container that never mounts
5335
+ // costs no render.
5336
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
5337
+ React.useEffect(() => {
5338
+ if (active && hydrated && containerRef.current !== heldRef.current) {
5339
+ setRefChanges(n => n + 1);
5340
+ }
5341
+ });
5308
5342
  }
5309
5343
 
5310
5344
  // INTERNAL: deliberately not exported from src/index.ts.
@@ -8737,6 +8771,108 @@ const useInsideControl = () => React.useContext(ControlContext);
8737
8771
  const FieldProvider = FieldContext.Provider;
8738
8772
  /** Provider for Control context — used internally by Control component. */
8739
8773
  const ControlProvider = ControlContext.Provider;
8774
+ // Each Field-shaping prop, with what takes its place on a `Field` the caller
8775
+ // wraps around the `Control`. `message` needs nothing there, because the
8776
+ // wrapper still renders it inside `Field > Control`. The other Field-level
8777
+ // props (`labelSize`, `labelProps`, `messageColor`) change nothing unless a
8778
+ // `label` or `message` renders, so on their own they keep no `Field`.
8779
+ // `bare-control.test.tsx` holds this list to what each wrapper hands its
8780
+ // `Field` and to which props change its markup, so a new Field-level prop
8781
+ // fails there until it is sorted into one group or the other.
8782
+ const FIELD_SHAPING = [
8783
+ ['label', 'label'],
8784
+ ['message', undefined],
8785
+ ['horizontal', 'horizontal'],
8786
+ ['fieldClassName', 'className (for fieldClassName)'],
8787
+ ];
8788
+ /** `a`, `a and b`, `a, b and c`. */
8789
+ const listOf = (items) => items.length > 1
8790
+ ? `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`
8791
+ : items[0];
8792
+ /**
8793
+ * Whether a convenience form wrapper renders a `Field` of its own (#905).
8794
+ * Never inside an outer `Field`. Inside a `Control` with no `Field` around
8795
+ * it, a `Field` of its own would put a `.field` inside the `.control`, where
8796
+ * Bulma's control styles break (its icon rules are sibling selectors on the
8797
+ * input). So it renders one there only when a prop that shapes the `Field`'s
8798
+ * markup is set (`label`, `message`, `horizontal`, `fieldClassName`), keeping
8799
+ * the markup it always had, and warns in development that the fix is a
8800
+ * `Field` around that `Control`. A prop counts when it is truthy, the same
8801
+ * test that decides whether it changes the `Field`'s output.
8802
+ * Internal; not part of the public API.
8803
+ */
8804
+ const rendersOwnField = (component, options) => {
8805
+ if (options.insideField)
8806
+ return false;
8807
+ if (!options.insideControl)
8808
+ return true;
8809
+ const held = FIELD_SHAPING.filter(([prop]) => options[prop]);
8810
+ if (held.length === 0)
8811
+ return false;
8812
+ const names = held.map(([prop]) => prop);
8813
+ const moved = held
8814
+ .map(([, onField]) => onField)
8815
+ .filter((onField) => onField !== undefined);
8816
+ warnOnce(`${component}:Field-in-bare-Control:${names.join('+')}`, `[bestax-bulma] <${component} ${names.join(' ')}> inside a <Control> ` +
8817
+ `with no <Field> around it renders its own <Field> for ` +
8818
+ `${names.length > 1 ? 'those props' : 'that prop'}, which puts a ` +
8819
+ `.field inside the .control, a nesting Bulma's styles are not ` +
8820
+ `written for. Wrap the <Control> in a <Field> instead` +
8821
+ (moved.length > 0 ? `, and set ${listOf(moved)} on that <Field>.` : '.'));
8822
+ return true;
8823
+ };
8824
+ // Each Control-level prop, with what takes its place on the `Control` the
8825
+ // caller wrapped around the wrapper. `bare-control.test.tsx` holds this to
8826
+ // what each wrapper hands its own `Control`, so a new Control-level prop
8827
+ // fails there until it is added here and passed to `rendersOwnControl`.
8828
+ const ON_CONTROL = {
8829
+ isLoading: 'isLoading',
8830
+ iconLeft: 'iconLeft',
8831
+ iconLeftName: 'iconLeftName',
8832
+ iconLeftSize: 'iconLeftSize',
8833
+ iconRight: 'iconRight',
8834
+ iconRightName: 'iconRightName',
8835
+ iconRightSize: 'iconRightSize',
8836
+ hasIconsLeft: 'hasIconsLeft',
8837
+ hasIconsRight: 'hasIconsRight',
8838
+ isExpanded: 'isExpanded',
8839
+ controlSize: 'size (for controlSize)',
8840
+ controlClassName: 'className (for controlClassName)',
8841
+ };
8842
+ /**
8843
+ * Whether a convenience form wrapper renders a `Control` of its own (#921):
8844
+ * only outside one. Inside a `Control` it renders none, so the props it
8845
+ * would have handed its own do nothing there, and it warns in development,
8846
+ * naming them and saying to set them on that `Control`. An `inline` picker
8847
+ * renders none in either place, so it drops them inside a `Control` or not,
8848
+ * and its warning says so without pointing at a `Control`. A wrapper passes
8849
+ * only the props it hands its own `Control`, as the caller gave them, so a
8850
+ * default the wrapper fills in itself never warns. A prop counts when it is
8851
+ * truthy, since a falsy one would change nothing on a `Control` either.
8852
+ * Internal; not part of the public API.
8853
+ */
8854
+ const rendersOwnControl = (component, options) => {
8855
+ if (!options.inline && !options.insideControl)
8856
+ return true;
8857
+ const held = Object.keys(ON_CONTROL).filter(prop => options[prop]);
8858
+ if (held.length === 0)
8859
+ return false;
8860
+ const names = held.join(' ');
8861
+ const doNothing = held.length > 1 ? 'those props do' : 'that prop does';
8862
+ if (options.inline) {
8863
+ warnOnce(`${component}:Control-props-inline:${held.join('+')}`, `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
8864
+ `in inline mode, inside a <Control> or not, so ${doNothing} ` +
8865
+ `nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
8866
+ `inline picker.`);
8867
+ }
8868
+ else {
8869
+ warnOnce(`${component}:Control-props-in-Control:${held.join('+')}`, `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
8870
+ `no <Control> of its own, so ${doNothing} nothing there. Set ` +
8871
+ `${listOf(held.map(prop => ON_CONTROL[prop]))} on that <Control> ` +
8872
+ `instead.`);
8873
+ }
8874
+ return false;
8875
+ };
8740
8876
  const FieldLabelIdContext = React.createContext(undefined);
8741
8877
  /**
8742
8878
  * The id a labeled Field wants its single composed control to adopt (#495).
@@ -8867,6 +9003,19 @@ const Checkbox = React.forwardRef(({ color, size, className, children, textColor
8867
9003
  });
8868
9004
  Checkbox.displayName = 'Checkbox';
8869
9005
 
9006
+ // Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
9007
+ // picker bases read it so their right-side launcher gives way to that
9008
+ // spinner, which sits at the same edge, whether the `Control` is the one a
9009
+ // convenience input renders or one the caller wrapped around the input.
9010
+ // Autocomplete reads it for its clear button, which sits there too.
9011
+ // Kept apart from `FormContext`, whose contexts only report whether a `Field`
9012
+ // or `Control` is present. Internal; not part of the public API.
9013
+ const ControlLoadingContext = React.createContext(false);
9014
+ /** Provider for the loading state, used internally by `Control`. */
9015
+ const ControlLoadingProvider = ControlLoadingContext.Provider;
9016
+ /** Whether the nearest enclosing `Control` is loading; `false` outside one. */
9017
+ const useControlLoading = () => React.useContext(ControlLoadingContext);
9018
+
8870
9019
  const allowedColors = [...validColors, 'inherit', 'current'];
8871
9020
  /**
8872
9021
  * 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.
@@ -8926,7 +9075,7 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
8926
9075
  });
8927
9076
  const controlClass = classNames(mainClass, bulmaHelperClasses, className);
8928
9077
  // --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
8929
- return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [children, leftIconValue &&
9078
+ return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsxRuntime.jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
8930
9079
  (isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
8931
9080
  (isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
8932
9081
  });
@@ -9151,10 +9300,18 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, }) {
9151
9300
  const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
9152
9301
  const insideField = useInsideField();
9153
9302
  const insideControl = useInsideControl();
9303
+ const ownField = rendersOwnField('Checkboxes', {
9304
+ insideField,
9305
+ insideControl,
9306
+ label,
9307
+ message,
9308
+ horizontal,
9309
+ fieldClassName,
9310
+ });
9154
9311
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
9155
9312
  label,
9156
9313
  labelProps,
9157
- rendersLabel: !insideField,
9314
+ rendersLabel: ownField,
9158
9315
  });
9159
9316
  const { bulmaHelperClasses, rest } = useBulmaClasses({
9160
9317
  ...props,
@@ -9180,10 +9337,10 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
9180
9337
  }), [name, groupActive, currentValue, handleChange]);
9181
9338
  const checkboxesElement = (jsxRuntime.jsx("div", { className: wrapperClass, role: "group", "aria-labelledby": ariaLabelledBy, ...rest, children: jsxRuntime.jsx(CheckboxesProvider, { value: ctx, children: children }) }));
9182
9339
  let content = checkboxesElement;
9183
- if (!insideControl) {
9340
+ if (rendersOwnControl('Checkboxes', { insideControl })) {
9184
9341
  content = jsxRuntime.jsx(Control, { children: content });
9185
9342
  }
9186
- if (!insideField) {
9343
+ if (ownField) {
9187
9344
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9188
9345
  }
9189
9346
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -9202,11 +9359,20 @@ const File = React.forwardRef(({
9202
9359
  // Field props
9203
9360
  label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
9204
9361
  const insideField = useInsideField();
9362
+ const insideControl = useInsideControl();
9363
+ const ownField = rendersOwnField('File', {
9364
+ insideField,
9365
+ insideControl,
9366
+ label,
9367
+ message,
9368
+ horizontal,
9369
+ fieldClassName,
9370
+ });
9205
9371
  const { controlId, fieldLabelProps } = useAutoLabelId({
9206
9372
  label,
9207
9373
  id: props.id,
9208
9374
  labelProps,
9209
- rendersLabel: !insideField,
9375
+ rendersLabel: ownField,
9210
9376
  });
9211
9377
  const { classPrefix } = useConfig();
9212
9378
  const { bulmaHelperClasses, rest } = useBulmaClasses({
@@ -9238,7 +9404,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
9238
9404
  });
9239
9405
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
9240
9406
  const fileElement = (jsxRuntime.jsx("div", { className: fileClass, children: jsxRuntime.jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsxRuntime.jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxRuntime.jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsxRuntime.jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
9241
- if (!insideField) {
9407
+ if (ownField) {
9242
9408
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
9243
9409
  }
9244
9410
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [fileElement, messageEl] }));
@@ -9371,10 +9537,18 @@ Radio.displayName = 'Radio';
9371
9537
  const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
9372
9538
  const insideField = useInsideField();
9373
9539
  const insideControl = useInsideControl();
9540
+ const ownField = rendersOwnField('Radios', {
9541
+ insideField,
9542
+ insideControl,
9543
+ label,
9544
+ message,
9545
+ horizontal,
9546
+ fieldClassName,
9547
+ });
9374
9548
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
9375
9549
  label,
9376
9550
  labelProps,
9377
- rendersLabel: !insideField,
9551
+ rendersLabel: ownField,
9378
9552
  });
9379
9553
  const { bulmaHelperClasses, rest } = useBulmaClasses({
9380
9554
  ...props,
@@ -9403,10 +9577,10 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
9403
9577
  }), [name, groupActive, currentValue, handleChange]);
9404
9578
  const radiosElement = (jsxRuntime.jsx("div", { className: wrapperClass, role: "radiogroup", "aria-labelledby": ariaLabelledBy, ...rest, children: jsxRuntime.jsx(RadiosProvider, { value: ctx, children: children }) }));
9405
9579
  let content = radiosElement;
9406
- if (!insideControl) {
9580
+ if (rendersOwnControl('Radios', { insideControl })) {
9407
9581
  content = jsxRuntime.jsx(Control, { children: content });
9408
9582
  }
9409
- if (!insideField) {
9583
+ if (ownField) {
9410
9584
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9411
9585
  }
9412
9586
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -9586,6 +9760,14 @@ const Slider = React.forwardRef((props, ref) => {
9586
9760
  range: _range, value: _value, defaultValue: _defaultValue, onChange: _onChange, ariaLabel: _ariaLabel, minDistance: _minDistance, nameLow: _nameLow, nameHigh: _nameHigh, ...restProps } = props;
9587
9761
  const insideField = useInsideField();
9588
9762
  const insideControl = useInsideControl();
9763
+ const ownField = rendersOwnField('Slider', {
9764
+ insideField,
9765
+ insideControl,
9766
+ label,
9767
+ message,
9768
+ horizontal,
9769
+ fieldClassName,
9770
+ });
9589
9771
  const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
9590
9772
  // In range mode the low thumb carries `rest` (and any user id), so the
9591
9773
  // label targets it; each thumb keeps its own aria-label for AT.
@@ -9593,7 +9775,7 @@ const Slider = React.forwardRef((props, ref) => {
9593
9775
  label,
9594
9776
  id: restProps.id,
9595
9777
  labelProps,
9596
- rendersLabel: !insideField,
9778
+ rendersLabel: ownField,
9597
9779
  });
9598
9780
  // Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
9599
9781
  const tooltipMode = tooltip ?? (showOutput ? 'auto' : 'hidden');
@@ -9929,10 +10111,10 @@ const Slider = React.forwardRef((props, ref) => {
9929
10111
  '--slider-output-arrow-offset': `calc(50% - ${nudgeXHigh}px)`,
9930
10112
  }, children: formatValue(currentRange[1]) })), renderTicks()] }));
9931
10113
  let content = sliderElement;
9932
- if (!insideControl) {
10114
+ if (rendersOwnControl('Slider', { insideControl })) {
9933
10115
  content = jsxRuntime.jsx(Control, { children: content });
9934
10116
  }
9935
- if (!insideField) {
10117
+ if (ownField) {
9936
10118
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9937
10119
  }
9938
10120
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -10162,10 +10344,18 @@ function getFillPercent(iconIndex, value) {
10162
10344
  const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
10163
10345
  const insideField = useInsideField();
10164
10346
  const insideControl = useInsideControl();
10347
+ const ownField = rendersOwnField('Rate', {
10348
+ insideField,
10349
+ insideControl,
10350
+ label,
10351
+ message,
10352
+ horizontal,
10353
+ fieldClassName,
10354
+ });
10165
10355
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
10166
10356
  label,
10167
10357
  labelProps,
10168
- rendersLabel: !insideField,
10358
+ rendersLabel: ownField,
10169
10359
  });
10170
10360
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10171
10361
  const [internalValue, setInternalValue] = React.useState(defaultValue);
@@ -10365,10 +10555,10 @@ const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, messa
10365
10555
  const rateCustomTextClass = usePrefixedClassNames('rate-custom-text');
10366
10556
  const rateElement = (jsxRuntime.jsxs("div", { ref: ref, className: combinedClasses, role: "radiogroup", "aria-label": ariaLabelledBy ? undefined : 'Rating', "aria-labelledby": ariaLabelledBy, "aria-valuenow": currentValue, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuetext": ariaValueText, tabIndex: disabled ? -1 : 0, onKeyDown: handleKeyDown, ...rest, children: [jsxRuntime.jsx("div", { className: rateItemsClass, children: renderIcons() }), showScore && (jsxRuntime.jsx("span", { className: rateScoreClass, children: getScoreDisplay() })), text && jsxRuntime.jsx("span", { className: rateTextClass, children: text }), customText && (jsxRuntime.jsx("span", { className: rateCustomTextClass, children: customText })), name && (jsxRuntime.jsx("input", { type: "hidden", name: name, value: currentValue, form: form }))] }));
10367
10557
  let content = rateElement;
10368
- if (!insideControl) {
10558
+ if (rendersOwnControl('Rate', { insideControl })) {
10369
10559
  content = jsxRuntime.jsx(Control, { children: content });
10370
10560
  }
10371
- if (!insideField) {
10561
+ if (ownField) {
10372
10562
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
10373
10563
  }
10374
10564
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -10604,7 +10794,11 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10604
10794
  [`is-${messageColor}`]: !!messageColor,
10605
10795
  });
10606
10796
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
10607
- const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": isActive, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), clearable && inputValue && !disabled && (jsxRuntime.jsx("span", { className: iconRightClickableClass, onClick: handleClear, role: "button", "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), isActive && (filteredData.length > 0 || empty) && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, role: "listbox", onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), filteredData.length > 0 ? (filteredData.map((item, index) => {
10797
+ // The clear button gives way to a spinner at its spot: the `loading` one,
10798
+ // or that of a loading Control this sits in.
10799
+ const controlLoading = useControlLoading();
10800
+ const showClear = clearable && !!inputValue && !disabled && !loading && !controlLoading;
10801
+ const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": isActive, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), showClear && (jsxRuntime.jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", "aria-hidden": "true", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), isActive && (filteredData.length > 0 || empty) && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, role: "listbox", onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), filteredData.length > 0 ? (filteredData.map((item, index) => {
10608
10802
  const isDisabled = typeof item !== 'string' && item.disabled;
10609
10803
  const isHighlighted = index === highlightedIndex;
10610
10804
  const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
@@ -10649,6 +10843,15 @@ const Taginput = React.forwardRef(({
10649
10843
  // Field props
10650
10844
  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) => {
10651
10845
  const insideField = useInsideField();
10846
+ const insideControl = useInsideControl();
10847
+ const ownField = rendersOwnField('Taginput', {
10848
+ insideField,
10849
+ insideControl,
10850
+ label,
10851
+ message,
10852
+ horizontal,
10853
+ fieldClassName,
10854
+ });
10652
10855
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10653
10856
  const { classPrefix } = useConfig();
10654
10857
  const defaultIconLibrary = useIconLibrary();
@@ -10670,7 +10873,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10670
10873
  label,
10671
10874
  id,
10672
10875
  labelProps,
10673
- rendersLabel: !insideField && !isMaxReached,
10876
+ rendersLabel: ownField && !isMaxReached,
10674
10877
  });
10675
10878
  // Get display value from tag
10676
10879
  const getDisplayValue = (tag) => {
@@ -10942,7 +11145,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10942
11145
  'is-active': index === highlightedIndex,
10943
11146
  }), onClick: () => addTag(item), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: item }, index))) }) })), showCounter && jsxRuntime.jsx("small", { className: counterClasses, children: counterText }), name &&
10944
11147
  tags.map((tag, i) => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
10945
- if (!insideField) {
11148
+ if (ownField) {
10946
11149
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
10947
11150
  }
10948
11151
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [taginputElement, messageEl] }));
@@ -11095,21 +11298,44 @@ fieldClassName, controlClassName,
11095
11298
  ...inputProps }, ref) => {
11096
11299
  const insideField = useInsideField();
11097
11300
  const insideControl = useInsideControl();
11301
+ const ownField = rendersOwnField('Input', {
11302
+ insideField,
11303
+ insideControl,
11304
+ label,
11305
+ message,
11306
+ horizontal,
11307
+ fieldClassName,
11308
+ });
11309
+ const ownControl = rendersOwnControl('Input', {
11310
+ insideControl,
11311
+ isLoading,
11312
+ iconLeft,
11313
+ iconLeftName,
11314
+ iconLeftSize,
11315
+ iconRight,
11316
+ iconRightName,
11317
+ iconRightSize,
11318
+ hasIconsLeft,
11319
+ hasIconsRight,
11320
+ isExpanded,
11321
+ controlSize,
11322
+ controlClassName,
11323
+ });
11098
11324
  const { controlId, fieldLabelProps } = useAutoLabelId({
11099
11325
  label,
11100
11326
  id: inputProps.id,
11101
11327
  labelProps,
11102
- rendersLabel: !insideField,
11328
+ rendersLabel: ownField,
11103
11329
  });
11104
11330
  const helpClass = usePrefixedClassNames('help', {
11105
11331
  [`is-${messageColor}`]: !!messageColor,
11106
11332
  });
11107
11333
  let content = jsxRuntime.jsx(InputBase, { ref: ref, id: controlId, ...inputProps });
11108
- if (!insideControl) {
11334
+ if (ownControl) {
11109
11335
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11110
11336
  }
11111
11337
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
11112
- if (!insideField) {
11338
+ if (ownField) {
11113
11339
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11114
11340
  }
11115
11341
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -11145,21 +11371,40 @@ fieldClassName, controlClassName,
11145
11371
  ...selectProps }, ref) => {
11146
11372
  const insideField = useInsideField();
11147
11373
  const insideControl = useInsideControl();
11374
+ const ownField = rendersOwnField('Select', {
11375
+ insideField,
11376
+ insideControl,
11377
+ label,
11378
+ message,
11379
+ horizontal,
11380
+ fieldClassName,
11381
+ });
11382
+ // Not isLoading, which the select draws itself and so keeps in a Control.
11383
+ const ownControl = rendersOwnControl('Select', {
11384
+ insideControl,
11385
+ iconLeft,
11386
+ iconLeftName,
11387
+ iconLeftSize,
11388
+ hasIconsLeft,
11389
+ isExpanded,
11390
+ controlSize,
11391
+ controlClassName,
11392
+ });
11148
11393
  const { controlId, fieldLabelProps } = useAutoLabelId({
11149
11394
  label,
11150
11395
  id: selectProps.id,
11151
11396
  labelProps,
11152
- rendersLabel: !insideField,
11397
+ rendersLabel: ownField,
11153
11398
  });
11154
11399
  const helpClass = usePrefixedClassNames('help', {
11155
11400
  [`is-${messageColor}`]: !!messageColor,
11156
11401
  });
11157
11402
  let content = jsxRuntime.jsx(SelectBase, { ref: ref, id: controlId, ...selectProps });
11158
- if (!insideControl) {
11403
+ if (ownControl) {
11159
11404
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11160
11405
  }
11161
11406
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
11162
- if (!insideField) {
11407
+ if (ownField) {
11163
11408
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11164
11409
  }
11165
11410
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -11196,21 +11441,35 @@ fieldClassName, controlClassName,
11196
11441
  ...textAreaProps }, ref) => {
11197
11442
  const insideField = useInsideField();
11198
11443
  const insideControl = useInsideControl();
11444
+ const ownField = rendersOwnField('TextArea', {
11445
+ insideField,
11446
+ insideControl,
11447
+ label,
11448
+ message,
11449
+ horizontal,
11450
+ fieldClassName,
11451
+ });
11452
+ const ownControl = rendersOwnControl('TextArea', {
11453
+ insideControl,
11454
+ isLoading: controlIsLoading,
11455
+ controlSize,
11456
+ controlClassName,
11457
+ });
11199
11458
  const { controlId, fieldLabelProps } = useAutoLabelId({
11200
11459
  label,
11201
11460
  id: textAreaProps.id,
11202
11461
  labelProps,
11203
- rendersLabel: !insideField,
11462
+ rendersLabel: ownField,
11204
11463
  });
11205
11464
  const helpClass = usePrefixedClassNames('help', {
11206
11465
  [`is-${messageColor}`]: !!messageColor,
11207
11466
  });
11208
11467
  let content = jsxRuntime.jsx(TextAreaBase, { ref: ref, id: controlId, ...textAreaProps });
11209
- if (!insideControl) {
11468
+ if (ownControl) {
11210
11469
  content = (jsxRuntime.jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
11211
11470
  }
11212
11471
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
11213
- if (!insideField) {
11472
+ if (ownField) {
11214
11473
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11215
11474
  }
11216
11475
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -11297,6 +11556,21 @@ function makeDate(year, month = 0, day = 1) {
11297
11556
  d.setFullYear(year, month, day);
11298
11557
  return d;
11299
11558
  }
11559
+ /**
11560
+ * The earliest year the pickers accept. HTML's date, month and datetime-local
11561
+ * inputs hold no year below 1, so the calendar, typing and parsing stop there
11562
+ * too.
11563
+ */
11564
+ const FIRST_YEAR = 1;
11565
+ /**
11566
+ * The lower bound a picker applies: `min`, raised to local midnight on
11567
+ * 1 January of {@link FIRST_YEAR} when it is earlier or absent. A `min` in
11568
+ * range comes back as the same object.
11569
+ */
11570
+ function floorMin(min) {
11571
+ const floor = makeDate(FIRST_YEAR);
11572
+ return min && min.getTime() >= floor.getTime() ? min : floor;
11573
+ }
11300
11574
  /**
11301
11575
  * First instant of the day, month or year containing `d`. Works by setters on
11302
11576
  * a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
@@ -11390,12 +11664,14 @@ function setTimeOfDay(d, parts) {
11390
11664
  r.setSeconds(parts.seconds);
11391
11665
  return r;
11392
11666
  }
11667
+ /**
11668
+ * A copy of `d` moved into `[min, max]`. When `min` is after `max` nothing is
11669
+ * in range and `min` wins, so clamping twice gives the same date and a
11670
+ * picker's focus stays off the years before 1 under a `max` before then.
11671
+ */
11393
11672
  function clampDate(d, min, max) {
11394
- if (min && isBefore(d, min))
11395
- return new Date(min);
11396
- if (max && isAfter(d, max))
11397
- return new Date(max);
11398
- return new Date(d);
11673
+ const capped = max && isAfter(d, max) ? max : d;
11674
+ return new Date(min && isBefore(capped, min) ? min : capped);
11399
11675
  }
11400
11676
  /**
11401
11677
  * Snap a Date's time-of-day to the nearest grid defined by the given
@@ -11468,7 +11744,9 @@ function formatToken(d, token) {
11468
11744
  case 'YYYY':
11469
11745
  // Zero-pad to a fixed 4 chars so the rendered width matches the
11470
11746
  // segment-map width during digit-by-digit keyboard entry (and to match
11471
- // ISO 8601). Years >9999 keep their natural width.
11747
+ // ISO 8601), so year 19 shows as `0019`. Years >9999 keep their natural
11748
+ // width. The pickers commit no year below 1, so a minus sign reaches
11749
+ // this only from a `value` passed in.
11472
11750
  return pad(d.getFullYear(), 4);
11473
11751
  case 'YY':
11474
11752
  return pad(d.getFullYear() % 100);
@@ -11742,9 +12020,10 @@ function getMonthNames(locale, length = 'long') {
11742
12020
 
11743
12021
  /**
11744
12022
  * Selects the calendar's focused cell, the one tab stop of whichever grid or
11745
- * year list is on show, for a popover to put focus on as it opens.
12023
+ * year list is on show, for a popover to put focus on as it opens. It runs
12024
+ * against the whole panel, so it matches only inside the grid or the list.
11746
12025
  */
11747
- const CALENDAR_FOCUSED_CELL = '[data-focused="true"], [data-focused-year="true"]';
12026
+ const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
11748
12027
  const BASE_VIEW = {
11749
12028
  day: 'days',
11750
12029
  month: 'months',
@@ -11818,10 +12097,11 @@ function nearestEnabled(i, disabled) {
11818
12097
  return i;
11819
12098
  }
11820
12099
  /**
11821
- * The year list's keyboard when it is the selection surface: arrows move a
11822
- * year or a row, Home/End go to the list's ends. Returns the year to start
11823
- * from and the direction to search past disabled years, or `null` for a key
11824
- * the list doesn't handle.
12100
+ * The year list's keyboard, as the selection surface and as navigation:
12101
+ * arrows move a year or a row, Home/End go to the list's ends. Returns the
12102
+ * year to start from and the direction to search past disabled years, or
12103
+ * `null` for a key the list doesn't handle. As navigation no year is
12104
+ * disabled, so only the year is used.
11825
12105
  */
11826
12106
  function yearStep(key, focusedYear, years) {
11827
12107
  switch (key) {
@@ -11841,7 +12121,7 @@ function yearStep(key, focusedYear, years) {
11841
12121
  return null;
11842
12122
  }
11843
12123
  }
11844
- 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', }) => {
12124
+ 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', }) => {
11845
12125
  const gridRef = React.useRef(null);
11846
12126
  const monthGridRef = React.useRef(null);
11847
12127
  const yearGridRef = React.useRef(null);
@@ -11851,6 +12131,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11851
12131
  const isDayGranularity = granularity === 'day';
11852
12132
  const isYearGranularity = granularity === 'year';
11853
12133
  const [view, setView] = React.useState(baseView);
12134
+ // Nothing before year 1 can be picked or reached, as in HTML's date inputs.
12135
+ const min = React.useMemo(() => floorMin(minProp), [minProp]);
11854
12136
  const computedDayNames = React.useMemo(() => {
11855
12137
  if (dayNames && dayNames.length === 7)
11856
12138
  return dayNames;
@@ -11921,7 +12203,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11921
12203
  const moveFocus = React.useCallback((next, direction) => {
11922
12204
  let candidate = next;
11923
12205
  for (let i = 0; i < 366; i++) {
11924
- if (min && candidate.getTime() < startOfDay(min).getTime())
12206
+ if (candidate.getTime() < startOfDay(min).getTime())
11925
12207
  return;
11926
12208
  if (max && candidate.getTime() > startOfDay(max).getTime())
11927
12209
  return;
@@ -11987,7 +12269,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11987
12269
  let candidate = next;
11988
12270
  for (let i = 0; i < 120; i++) {
11989
12271
  const month = startOfMonth(candidate).getTime();
11990
- if (min && month < startOfMonth(min).getTime())
12272
+ if (month < startOfMonth(min).getTime())
11991
12273
  return;
11992
12274
  if (max && month > startOfMonth(max).getTime())
11993
12275
  return;
@@ -12035,25 +12317,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12035
12317
  const today = new Date();
12036
12318
  const todayYear = today.getFullYear();
12037
12319
  // The header steps a month at a time over the day grid and a year at a
12038
- // time over the month grid, kept inside min/max.
12039
- const prevMonthAnchor = addMonths(focusedDate, -1);
12040
- const nextMonthAnchor = addMonths(focusedDate, 1);
12320
+ // time over the month grid, kept inside min/max. A step is off once the
12321
+ // month or year on show holds the bound or lies past it, as picking a year
12322
+ // from the list can leave it.
12041
12323
  const prevAnchor = isDayGranularity
12042
- ? prevMonthAnchor
12324
+ ? addMonths(focusedDate, -1)
12043
12325
  : clampDate(addYears(focusedDate, -1), min, max);
12044
12326
  const nextAnchor = isDayGranularity
12045
- ? nextMonthAnchor
12327
+ ? addMonths(focusedDate, 1)
12046
12328
  : clampDate(addYears(focusedDate, 1), min, max);
12329
+ const focusedMonth = startOfMonth(focusedDate).getTime();
12047
12330
  const prevDisabled = isDayGranularity
12048
- ? !!(min &&
12049
- prevMonthAnchor.getTime() < startOfDay(min).getTime() &&
12050
- isSameMonth(focusedDate, min))
12051
- : !!(min && focusedYear <= min.getFullYear());
12331
+ ? focusedMonth <= startOfMonth(min).getTime()
12332
+ : focusedYear <= min.getFullYear();
12052
12333
  const nextDisabled = isDayGranularity
12053
- ? !!(max &&
12054
- nextMonthAnchor.getTime() > startOfDay(max).getTime() &&
12055
- isSameMonth(focusedDate, max))
12056
- : !!(max && focusedYear >= max.getFullYear());
12334
+ ? !!max && focusedMonth >= startOfMonth(max).getTime()
12335
+ : !!max && focusedYear >= max.getFullYear();
12057
12336
  const labelId = id ? `${id}-label` : undefined;
12058
12337
  const monthLabel = isDayGranularity
12059
12338
  ? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
@@ -12064,7 +12343,9 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12064
12343
  const [mountYear] = React.useState(focusedYear);
12065
12344
  const windowYear = isYearGranularity ? mountYear : focusedYear;
12066
12345
  const yearList = React.useMemo(() => {
12067
- const minYear = min ? min.getFullYear() : windowYear - 100;
12346
+ const minYear = minProp
12347
+ ? min.getFullYear()
12348
+ : Math.max(FIRST_YEAR, windowYear - 100);
12068
12349
  const maxYear = max ? max.getFullYear() : windowYear + 100;
12069
12350
  const [lo, hi] = yearsRange ?? [minYear, maxYear];
12070
12351
  const start = Math.max(lo, minYear);
@@ -12073,20 +12354,31 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12073
12354
  for (let y = start; y <= end; y++)
12074
12355
  out.push(y);
12075
12356
  return out;
12076
- }, [windowYear, min, max, yearsRange]);
12357
+ }, [windowYear, minProp, min, max, yearsRange]);
12077
12358
  // Only the selection surface disables years; as navigation every listed
12078
12359
  // year can be visited.
12079
12360
  const disabledYears = React.useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
12080
12361
  // The year list's one tab stop, after the month grid's: when the focused
12081
12362
  // year is disabled the stop moves to the nearest enabled year in the list,
12082
- // and focusing it makes it the focused year. A year outside the list keeps
12083
- // the stop, which then marks no option.
12363
+ // and focusing it makes it the focused year. As the selection surface a
12364
+ // year outside a non-empty list starts from the end it fell past, so the
12365
+ // list keeps a stop. As navigation it marks the current year, so a year
12366
+ // outside the list marks no option, and the stop itself follows focus.
12084
12367
  const tabStopYear = React.useMemo(() => {
12085
- const i = yearList.indexOf(focusedYear);
12086
- if (i < 0)
12087
- return focusedYear;
12368
+ let i = yearList.indexOf(focusedYear);
12369
+ if (i < 0) {
12370
+ if (!isYearGranularity || yearList.length === 0)
12371
+ return focusedYear;
12372
+ i = focusedYear < yearList[0] ? 0 : yearList.length - 1;
12373
+ }
12088
12374
  return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
12089
- }, [yearList, focusedYear, disabledYears]);
12375
+ }, [yearList, focusedYear, disabledYears, isYearGranularity]);
12376
+ // As navigation the keys move focus and leave the focused year alone, so
12377
+ // the tab stop goes to the year that last had focus while it is listed,
12378
+ // and Tab leaves the list rather than landing on the current year. It
12379
+ // starts over each time the list opens.
12380
+ const [navYear, setNavYear] = React.useState(null);
12381
+ const listTabStop = navYear !== null && yearList.includes(navYear) ? navYear : tabStopYear;
12090
12382
  // Which of the focused year's months have no selectable day.
12091
12383
  const disabledMonths = React.useMemo(() => granularity === 'month'
12092
12384
  ? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
@@ -12122,19 +12414,39 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12122
12414
  }
12123
12415
  }, [yearList, disabledYears, onFocusedDateChange, min, max]);
12124
12416
  const handleYearListKeyDown = React.useCallback((e) => {
12125
- if (!isYearGranularity) {
12126
- // As navigation, Escape returns to the grid the list was opened from.
12127
- if (e.key === 'Escape') {
12128
- e.preventDefault();
12129
- setView(baseView);
12130
- }
12417
+ if (isYearGranularity) {
12418
+ const step = yearStep(e.key, focusedYear, yearList);
12419
+ if (!step)
12420
+ return;
12421
+ e.preventDefault();
12422
+ moveYearFocus(step[0], step[1]);
12423
+ return;
12424
+ }
12425
+ // As navigation, Escape returns to the grid the list was opened from.
12426
+ // It stops there, so a popover around the calendar stays open and only
12427
+ // a second Escape closes it.
12428
+ if (e.key === 'Escape') {
12429
+ e.preventDefault();
12430
+ e.stopPropagation();
12431
+ setView(baseView);
12131
12432
  return;
12132
12433
  }
12133
- const step = yearStep(e.key, focusedYear, yearList);
12434
+ // The other keys move DOM focus from the year that has it and leave
12435
+ // the focused date alone. The list is centred on that date, so moving
12436
+ // it would reflow the list under the keyboard, and it would turn the
12437
+ // grid behind the list before anything is picked. Enter or Space
12438
+ // clicks the year with focus, which picks it.
12439
+ const options = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
12440
+ const from = options.indexOf(e.target);
12441
+ if (from < 0)
12442
+ return;
12443
+ const step = yearStep(e.key, yearList[from], yearList);
12134
12444
  if (!step)
12135
12445
  return;
12136
12446
  e.preventDefault();
12137
- moveYearFocus(step[0], step[1]);
12447
+ // The listed years run one apart, so a year's index is its distance
12448
+ // from the first. A step past either end finds no option.
12449
+ options[step[0] - yearList[0]]?.focus();
12138
12450
  }, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
12139
12451
  // The day or month grid takes focus as the popover opens on it, or comes
12140
12452
  // back to it from the year list.
@@ -12205,7 +12517,10 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12205
12517
  list.querySelector('[data-focused-year="true"]')?.focus();
12206
12518
  }, [isYearGranularity, focusedYear]);
12207
12519
  const navArrow = (d) => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
12208
- return (jsxRuntime.jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxRuntime.jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxRuntime.jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsxRuntime.jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxRuntime.jsxs("div", { className: navGroupClass, children: [jsxRuntime.jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsxRuntime.jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsxRuntime.jsx("div", { className: dayNameClass, children: name }, i))) }), jsxRuntime.jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
12520
+ return (jsxRuntime.jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxRuntime.jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxRuntime.jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => {
12521
+ setNavYear(null);
12522
+ setView(v => (v === 'years' ? baseView : 'years'));
12523
+ }, children: [jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsxRuntime.jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxRuntime.jsxs("div", { className: navGroupClass, children: [jsxRuntime.jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsxRuntime.jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsxRuntime.jsx("div", { className: dayNameClass, children: name }, i))) }), jsxRuntime.jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
12209
12524
  const disabled = isDateUnselectable(cell.date);
12210
12525
  const isSelected = !!value && isSameDay(value, cell.date);
12211
12526
  const isFocused = isSameDay(cell.date, tabStopDay);
@@ -12265,10 +12580,16 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12265
12580
  'is-today': isToday,
12266
12581
  'is-disabled': disabled,
12267
12582
  });
12268
- return (jsxRuntime.jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: isFocused ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
12269
- // Reached by Tab while the focused year is disabled, or by
12270
- // pointer: keys move on from here.
12271
- if (isYearGranularity && year !== focusedYear) {
12583
+ return (jsxRuntime.jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
12584
+ // As navigation, focusing a year by key or pointer only
12585
+ // moves the tab stop: nothing is committed until a year is
12586
+ // picked. As the selection surface it is reached by Tab
12587
+ // while the focused year is disabled, or by pointer, and
12588
+ // keys move on from here.
12589
+ if (!isYearGranularity) {
12590
+ setNavYear(year);
12591
+ }
12592
+ else if (year !== focusedYear) {
12272
12593
  onFocusedDateChange(clampDate(makeDate(year), min, max));
12273
12594
  }
12274
12595
  }, onClick: () => isYearGranularity
@@ -12293,11 +12614,10 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12293
12614
  active: isOpen,
12294
12615
  position,
12295
12616
  fixed: appendToBody,
12296
- // A portaled panel takes its gap from `--bulma-picker-popover-offset` as
12297
- // a margin, so its coordinates leave the gap out and `auto` reads it off
12298
- // the panel. An in-place panel's gap is in its `top` or `bottom`, which
12299
- // `auto` cannot read, so it keeps the hook's default estimate.
12300
- offset: appendToBody ? 0 : undefined,
12617
+ // The gap comes from `--bulma-picker-popover-offset` as a margin, in place
12618
+ // or portaled, so the coordinates leave it out and `auto` reads it off
12619
+ // the panel.
12620
+ offset: 0,
12301
12621
  styledGap: marginGap,
12302
12622
  });
12303
12623
  React.useEffect(() => {
@@ -12743,33 +13063,35 @@ function useSegmentedEntry(params) {
12743
13063
  // within the same event tick.
12744
13064
  const typedDigitsRef = React.useRef('');
12745
13065
  // The text focus seeded an empty picker with, until the user edits it or
12746
- // leaves. While the text still matches it, nothing has been typed.
13066
+ // leaves.
12747
13067
  const seedRef = React.useRef(null);
12748
- // Whether the input's current focus is the one a closing popover handed
12749
- // back, until focus leaves the picker.
12750
- const handedBackRef = React.useRef(false);
13068
+ // Whether the text is still what the picker put there, so nothing in it was
13069
+ // typed: the value's display, or an empty picker's seed until an edit drops
13070
+ // it. Against a value it compares text alone, so typing that ends back on
13071
+ // the display counts as untouched.
13072
+ const isUntouched = React.useCallback((s) => value
13073
+ ? s === formatFn(value, format, locale)
13074
+ : seedRef.current !== null && s === seedRef.current, [value, formatFn, format, locale]);
12751
13075
  // A seed the popover never turned into a value closes with it, wherever
12752
13076
  // focus goes, so a dismissed empty field stays empty.
12753
13077
  //
12754
13078
  // As the popover closes, its focus trap hands focus back to the input. That
12755
13079
  // focus is not the user arriving: it must not open the popover again under
12756
- // `openOnFocus`, or seed an empty picker that leaving would then commit.
12757
- // `closingRef` is set in the layout phase of the commit that closes it and
12758
- // cleared by that commit's effects, and React runs a commit's effect
12759
- // cleanups, the trap's among them, before any of its effects, so a focus
12760
- // in between is the one handed back.
13080
+ // `openOnFocus`, or seed an empty picker. `closingRef` is set in the layout
13081
+ // phase of the commit that closes it and cleared by that commit's effects,
13082
+ // and React runs a commit's effect cleanups, the trap's among them, before
13083
+ // any of its effects, so a focus in between is the one handed back.
12761
13084
  const closingRef = React.useRef(false);
12762
13085
  const wasOpenRef = React.useRef(isOpen);
12763
13086
  React.useLayoutEffect(() => {
12764
13087
  if (wasOpenRef.current && !isOpen) {
12765
13088
  closingRef.current = true;
12766
- if (!value && seedRef.current !== null && text === seedRef.current) {
13089
+ if (!value && isUntouched(text))
12767
13090
  setText('');
12768
- }
12769
13091
  seedRef.current = null;
12770
13092
  }
12771
13093
  wasOpenRef.current = isOpen;
12772
- }, [isOpen, value, text, setText]);
13094
+ }, [isOpen, value, isUntouched, text, setText]);
12773
13095
  React.useEffect(() => {
12774
13096
  closingRef.current = false;
12775
13097
  }, [isOpen]);
@@ -12847,13 +13169,18 @@ function useSegmentedEntry(params) {
12847
13169
  }
12848
13170
  setActiveSegmentIdx(null);
12849
13171
  typedDigitsRef.current = '';
12850
- seedRef.current = null;
12851
- const handedBack = handedBackRef.current;
12852
- handedBackRef.current = false;
12853
- // Leaving after a dismiss commits only what was typed since. Text that
12854
- // still shows the value wasn't, and re-parsing it could drop what the
12855
- // format leaves out, such as seconds.
12856
- if (handedBack && value && text === formatFn(value, format, locale)) {
13172
+ // Leaving commits only what was typed. Re-parsing the value's display
13173
+ // could drop what the format leaves out, such as seconds, and a seed
13174
+ // nobody typed over is not the user's date.
13175
+ const untouched = isUntouched(text);
13176
+ // An open popover keeps an unused seed until it closes, so one portaled
13177
+ // outside the picker, whose focus reads as leaving, keeps it as one
13178
+ // inside does.
13179
+ if (!isOpen)
13180
+ seedRef.current = null;
13181
+ if (untouched) {
13182
+ if (!value && !isOpen)
13183
+ setText('');
12857
13184
  onBlur?.(e);
12858
13185
  return;
12859
13186
  }
@@ -12867,6 +13194,8 @@ function useSegmentedEntry(params) {
12867
13194
  onBlur?.(e);
12868
13195
  }, [
12869
13196
  containerRef,
13197
+ isUntouched,
13198
+ isOpen,
12870
13199
  tryParse,
12871
13200
  text,
12872
13201
  isAllowed,
@@ -12880,7 +13209,6 @@ function useSegmentedEntry(params) {
12880
13209
  ]);
12881
13210
  const handleFocus = React.useCallback((e) => {
12882
13211
  const handedBack = closingRef.current;
12883
- handedBackRef.current = handedBack;
12884
13212
  if (!handedBack && openOnFocus && popover && !disabled && !readOnly) {
12885
13213
  setOpen(true);
12886
13214
  }
@@ -13048,12 +13376,20 @@ function useSegmentedEntry(params) {
13048
13376
  }
13049
13377
  if (e.key === 'Enter') {
13050
13378
  e.preventDefault();
13051
- const parsed = tryParse(text);
13052
- if (parsed && isAllowed(parsed)) {
13053
- commitValue(parsed);
13379
+ // Untouched text has nothing to commit, as on leaving, but Enter
13380
+ // still confirms what the field shows.
13381
+ if (isUntouched(text)) {
13054
13382
  if (closeOnSelect)
13055
13383
  setOpen(false);
13056
13384
  }
13385
+ else {
13386
+ const parsed = tryParse(text);
13387
+ if (parsed && isAllowed(parsed)) {
13388
+ commitValue(parsed);
13389
+ if (closeOnSelect)
13390
+ setOpen(false);
13391
+ }
13392
+ }
13057
13393
  }
13058
13394
  onKeyDown?.(e);
13059
13395
  }, [
@@ -13067,6 +13403,7 @@ function useSegmentedEntry(params) {
13067
13403
  setOpen,
13068
13404
  closeOnSelect,
13069
13405
  popover,
13406
+ isUntouched,
13070
13407
  tryParse,
13071
13408
  text,
13072
13409
  isAllowed,
@@ -13115,9 +13452,12 @@ const pad2$1 = (n) => String(n).padStart(2, '0');
13115
13452
  * The value as the native input and the hidden form input carry it:
13116
13453
  * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
13117
13454
  * `<input type="month">` use. The year is padded to four digits, as those
13118
- * inputs require and as the `YYYY` token displays it.
13455
+ * inputs require and as the `YYYY` token displays it. HTML has no such shape
13456
+ * for a year before 1, so a date then is empty, as those inputs would make it.
13119
13457
  */
13120
13458
  const toIsoValue = (d, granularity) => {
13459
+ if (d.getFullYear() < FIRST_YEAR)
13460
+ return '';
13121
13461
  const year = String(d.getFullYear()).padStart(4, '0');
13122
13462
  if (granularity === 'year')
13123
13463
  return year;
@@ -13158,14 +13498,19 @@ const SKIPPED_SEGMENTS = {
13158
13498
  * @returns {JSX.Element}
13159
13499
  */
13160
13500
  const DateInputBase = React.forwardRef((props, ref) => {
13161
- 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;
13501
+ 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;
13502
+ // The launcher gives way to the loading spinner of a Control this sits in.
13503
+ const controlLoading = useControlLoading();
13504
+ const triggerIcon = triggerIconProp ?? !controlLoading;
13162
13505
  const t = mergeLabels(labels);
13163
13506
  const isDayGranularity = granularity === 'day';
13164
13507
  const resolvedFormat = format ?? DEFAULT_FORMATS[granularity];
13165
13508
  const isControlled = controlledValue !== undefined;
13166
13509
  const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
13167
13510
  const value = isControlled ? (controlledValue ?? null) : internalValue;
13168
- const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), min, max),
13511
+ // Nothing before year 1 is in range, as in HTML's date inputs.
13512
+ const lowerBound = React.useMemo(() => floorMin(min), [min]);
13513
+ const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
13169
13514
  // intentionally only on mount: keep focusedDate stable until value/open change
13170
13515
  // eslint-disable-next-line react-hooks/exhaustive-deps
13171
13516
  []);
@@ -13173,8 +13518,8 @@ const DateInputBase = React.forwardRef((props, ref) => {
13173
13518
  // Re-clamp focusedDate if min/max change after mount so the focused cell
13174
13519
  // never disappears outside the displayable range.
13175
13520
  React.useEffect(() => {
13176
- setFocusedDate(prev => clampDate(prev, min, max));
13177
- }, [min, max]);
13521
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
13522
+ }, [lowerBound, max]);
13178
13523
  const [open, setOpenState] = React.useState(false);
13179
13524
  const [text, setText] = React.useState(value ? formatDate(value, resolvedFormat, locale) : '');
13180
13525
  const inputRef = React.useRef(null);
@@ -13232,7 +13577,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
13232
13577
  }, [isControlled, onChange, isDayGranularity, granularity]);
13233
13578
  // At month or year granularity the bounds widen to the periods holding
13234
13579
  // them, so the month or year containing `min` stays reachable.
13235
- const periodMin = React.useMemo(() => (min && !isDayGranularity ? startOfPeriod(min, granularity) : min), [min, isDayGranularity, granularity]);
13580
+ const periodMin = React.useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
13236
13581
  const periodMax = React.useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
13237
13582
  const handleSelect = React.useCallback((d) => {
13238
13583
  if (!isWithin(d, periodMin, periodMax))
@@ -13283,7 +13628,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
13283
13628
  ]);
13284
13629
  // Typing, and the re-parse on blur or Enter, that lands in the period the
13285
13630
  // value already holds commits nothing, so a `value` elsewhere in its month
13286
- // or year isn't rewritten by focus passing through the field.
13631
+ // or year changes only when the user picks or types a different period.
13287
13632
  const commitTyped = React.useCallback((next) => {
13288
13633
  if (next &&
13289
13634
  value &&
@@ -13349,7 +13694,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
13349
13694
  ? fromIsoValue(e.target.value, granularity)
13350
13695
  : null;
13351
13696
  commitValue(parsed);
13352
- }, 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 }));
13697
+ }, 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 }));
13353
13698
  }
13354
13699
  const chooseLabel = granularity === 'month'
13355
13700
  ? t.chooseMonth
@@ -13358,7 +13703,11 @@ const DateInputBase = React.forwardRef((props, ref) => {
13358
13703
  : t.chooseDate;
13359
13704
  const calendar = (jsxRuntime.jsx(Calendar
13360
13705
  // A new granularity starts the calendar over on its own view.
13361
- , { granularity: granularity, value: value, focusedDate: focusedDate, onSelect: handleSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size, id: `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
13706
+ , { 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,
13707
+ // The popover panel takes `popoverId`, so the calendar inside it
13708
+ // takes one of its own. Inline there is no panel, and the calendar
13709
+ // keeps the id it released with.
13710
+ id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
13362
13711
  if (inline) {
13363
13712
  return (jsxRuntime.jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [calendar, name && (jsxRuntime.jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoValue(value, granularity) : '', required: required }))] }));
13364
13713
  }
@@ -13386,29 +13735,57 @@ DateInputBase.displayName = 'DateInputBase';
13386
13735
  * shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
13387
13736
  * />
13388
13737
  */
13389
- const DateInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
13738
+ const DateInput = React.forwardRef((props, ref) => {
13739
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
13390
13740
  const insideField = useInsideField();
13391
13741
  const insideControl = useInsideControl();
13742
+ const ownField = rendersOwnField('DateInput', {
13743
+ insideField,
13744
+ insideControl,
13745
+ label,
13746
+ message,
13747
+ horizontal,
13748
+ fieldClassName,
13749
+ });
13750
+ // Inline mode renders a bare calendar with no input, so the Control's
13751
+ // icon-left container has nothing to anchor to, and it renders no
13752
+ // Control inside one or out. The left icon is checked as the caller
13753
+ // passed it: the default glyph is this component's own choice, not a
13754
+ // prop the caller set.
13755
+ const ownControl = rendersOwnControl('DateInput', {
13756
+ insideControl,
13757
+ inline: baseProps.inline,
13758
+ isLoading,
13759
+ iconLeft,
13760
+ iconLeftName: props.iconLeftName,
13761
+ iconLeftSize,
13762
+ iconRight,
13763
+ iconRightName,
13764
+ iconRightSize,
13765
+ hasIconsLeft,
13766
+ hasIconsRight,
13767
+ isExpanded,
13768
+ controlSize,
13769
+ controlClassName,
13770
+ });
13392
13771
  const { controlId, fieldLabelProps } = useAutoLabelId({
13393
13772
  label,
13394
13773
  id: baseProps.id,
13395
13774
  labelProps,
13396
13775
  // Inline mode renders a bare calendar with no input to label.
13397
- rendersLabel: !insideField && !baseProps.inline,
13776
+ rendersLabel: ownField && !baseProps.inline,
13398
13777
  });
13399
13778
  const helpClass = usePrefixedClassNames('help', {
13400
13779
  [`is-${messageColor}`]: !!messageColor,
13401
13780
  });
13402
- // The right-side launcher is on by default; suppress it while the Control
13403
- // shows its loading spinner (also on the right) unless explicitly set.
13404
- let content = (jsxRuntime.jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
13405
- // Inline mode renders a bare calendar with no input, so the Control's
13406
- // icon-left container has nothing to anchor to. Skip the Control wrap.
13407
- if (!insideControl && !baseProps.inline) {
13781
+ // The base hides its launcher while the Control it sits in is loading,
13782
+ // whether that is the one rendered below or an enclosing one.
13783
+ let content = (jsxRuntime.jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps }));
13784
+ if (ownControl) {
13408
13785
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
13409
13786
  }
13410
13787
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
13411
- if (!insideField) {
13788
+ if (ownField) {
13412
13789
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
13413
13790
  }
13414
13791
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
@@ -14282,7 +14659,10 @@ const fromIsoTime = (s) => {
14282
14659
  * @returns {JSX.Element}
14283
14660
  */
14284
14661
  const TimeInputBase = React.forwardRef((props, ref) => {
14285
- 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;
14662
+ 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;
14663
+ // The launcher gives way to the loading spinner of a Control this sits in.
14664
+ const controlLoading = useControlLoading();
14665
+ const triggerIcon = triggerIconProp ?? !controlLoading;
14286
14666
  // Platform-appropriate feedback routing: when `haptics` is opted in and
14287
14667
  // the runtime has no Vibration API (notably iOS Safari), enable the
14288
14668
  // audio thunk so the user still gets *some* tactile cue. When vibrate
@@ -14359,6 +14739,8 @@ const TimeInputBase = React.forwardRef((props, ref) => {
14359
14739
  React.useEffect(() => {
14360
14740
  setText(value ? formatTime(value, defaultFormat, locale) : '');
14361
14741
  }, [value, defaultFormat, locale]);
14742
+ // A value's date before year 1 is out of range, as in the date pickers.
14743
+ const lowerBound = React.useMemo(() => floorMin(min), [min]);
14362
14744
  const commitValue = React.useCallback((next) => {
14363
14745
  if (!isControlled)
14364
14746
  setInternalValue(next);
@@ -14367,10 +14749,10 @@ const TimeInputBase = React.forwardRef((props, ref) => {
14367
14749
  const handleSpinnerChange = React.useCallback((parts) => {
14368
14750
  const base = value ?? new Date();
14369
14751
  const next = setTimeOfDay(base, parts);
14370
- if (!isWithin(next, min, max))
14752
+ if (!isWithin(next, lowerBound, max))
14371
14753
  return;
14372
14754
  commitValue(next);
14373
- }, [value, min, max, commitValue]);
14755
+ }, [value, lowerBound, max, commitValue]);
14374
14756
  const tryParse = React.useCallback((s) => {
14375
14757
  const trimmed = s.trim();
14376
14758
  if (!trimmed)
@@ -14402,7 +14784,7 @@ const TimeInputBase = React.forwardRef((props, ref) => {
14402
14784
  setText,
14403
14785
  makeBaseDate,
14404
14786
  locale,
14405
- min,
14787
+ min: lowerBound,
14406
14788
  max,
14407
14789
  isBlocked: unselectableTimes,
14408
14790
  disabled,
@@ -14481,37 +14863,69 @@ TimeInputBase.displayName = 'TimeInputBase';
14481
14863
  * @example
14482
14864
  * <TimeInput label="Slot" hourFormat="12" incrementMinutes={15} />
14483
14865
  */
14484
- const TimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
14866
+ const TimeInput = React.forwardRef((props, ref) => {
14867
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
14485
14868
  const insideField = useInsideField();
14486
14869
  const insideControl = useInsideControl();
14870
+ const ownField = rendersOwnField('TimeInput', {
14871
+ insideField,
14872
+ insideControl,
14873
+ label,
14874
+ message,
14875
+ horizontal,
14876
+ fieldClassName,
14877
+ });
14878
+ // Inline mode renders a bare panel with no input, so the Control's
14879
+ // icon-left container has nothing to anchor to, and it renders no
14880
+ // Control inside one or out. The left icon is checked as the caller
14881
+ // passed it: the default glyph is this component's own choice, not a
14882
+ // prop the caller set.
14883
+ const ownControl = rendersOwnControl('TimeInput', {
14884
+ insideControl,
14885
+ inline: baseProps.inline,
14886
+ isLoading,
14887
+ iconLeft,
14888
+ iconLeftName: props.iconLeftName,
14889
+ iconLeftSize,
14890
+ iconRight,
14891
+ iconRightName,
14892
+ iconRightSize,
14893
+ hasIconsLeft,
14894
+ hasIconsRight,
14895
+ isExpanded,
14896
+ controlSize,
14897
+ controlClassName,
14898
+ });
14487
14899
  const { controlId, fieldLabelProps } = useAutoLabelId({
14488
14900
  label,
14489
14901
  id: baseProps.id,
14490
14902
  labelProps,
14491
14903
  // Inline mode renders a bare panel with no input to label.
14492
- rendersLabel: !insideField && !baseProps.inline,
14904
+ rendersLabel: ownField && !baseProps.inline,
14493
14905
  });
14494
14906
  const helpClass = usePrefixedClassNames('help', {
14495
14907
  [`is-${messageColor}`]: !!messageColor,
14496
14908
  });
14497
- // The right-side launcher is on by default; suppress it while the Control
14498
- // shows its loading spinner (also on the right) unless explicitly set.
14499
- let content = (jsxRuntime.jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
14500
- // Inline mode renders a bare panel with no input, so the Control's
14501
- // icon-left container has nothing to anchor to. Skip the Control wrap.
14502
- if (!insideControl && !baseProps.inline) {
14909
+ // The base hides its launcher while the Control it sits in is loading,
14910
+ // whether that is the one rendered below or an enclosing one.
14911
+ let content = (jsxRuntime.jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps }));
14912
+ if (ownControl) {
14503
14913
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
14504
14914
  }
14505
14915
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14506
- if (!insideField) {
14916
+ if (ownField) {
14507
14917
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14508
14918
  }
14509
14919
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
14510
14920
  });
14511
14921
  TimeInput.displayName = 'TimeInput';
14512
14922
 
14513
- // The year is padded to four digits, as `datetime-local` requires.
14923
+ // The year is padded to four digits, as `datetime-local` requires. HTML has
14924
+ // no such shape for a year before 1, so a date then is empty, as that input
14925
+ // would make it.
14514
14926
  const toIsoDateTime = (d, withSeconds) => {
14927
+ if (d.getFullYear() < FIRST_YEAR)
14928
+ return '';
14515
14929
  const yyyy = String(d.getFullYear()).padStart(4, '0');
14516
14930
  const mo = String(d.getMonth() + 1).padStart(2, '0');
14517
14931
  const dd = String(d.getDate()).padStart(2, '0');
@@ -14544,7 +14958,10 @@ const fromIsoDateTime = (s) => {
14544
14958
  const DateTimeInputBase = React.forwardRef((props, ref) => {
14545
14959
  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,
14546
14960
  // Step between visible wheel values; 1 gives the iOS-style every-minute wheel.
14547
- 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;
14961
+ 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;
14962
+ // The launcher gives way to the loading spinner of a Control this sits in.
14963
+ const controlLoading = useControlLoading();
14964
+ const triggerIcon = triggerIconProp ?? !controlLoading;
14548
14965
  const t = mergeLabels(labels);
14549
14966
  // Platform-appropriate feedback routing for the time wheel: on iOS (no
14550
14967
  // navigator.vibrate) the audio thunk fills in; on Android the real haptic
@@ -14554,14 +14971,16 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14554
14971
  const isControlled = controlledValue !== undefined;
14555
14972
  const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
14556
14973
  const value = isControlled ? (controlledValue ?? null) : internalValue;
14557
- const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), min, max),
14974
+ // Nothing before year 1 is in range, as in HTML's date inputs.
14975
+ const lowerBound = React.useMemo(() => floorMin(min), [min]);
14976
+ const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
14558
14977
  // eslint-disable-next-line react-hooks/exhaustive-deps
14559
14978
  []);
14560
14979
  const [focusedDate, setFocusedDate] = React.useState(initialFocused);
14561
14980
  // Re-clamp focusedDate when min/max change so the focused cell stays valid.
14562
14981
  React.useEffect(() => {
14563
- setFocusedDate(prev => clampDate(prev, min, max));
14564
- }, [min, max]);
14982
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
14983
+ }, [lowerBound, max]);
14565
14984
  const defaultFormat = format ??
14566
14985
  (hourFormat === '12'
14567
14986
  ? enableSeconds
@@ -14658,18 +15077,18 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14658
15077
  minutes: value?.getMinutes() ?? 0,
14659
15078
  seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
14660
15079
  });
14661
- if (!isWithin(merged, min, max))
15080
+ if (!isWithin(merged, lowerBound, max))
14662
15081
  return;
14663
15082
  commitValue(merged);
14664
15083
  setFocusedDate(d);
14665
- }, [value, enableSeconds, min, max, commitValue]);
15084
+ }, [value, enableSeconds, lowerBound, max, commitValue]);
14666
15085
  const handleTimeChange = React.useCallback((parts) => {
14667
15086
  const base = value ?? focusedDate;
14668
15087
  const next = setTimeOfDay(base, parts);
14669
- if (!isWithin(next, min, max))
15088
+ if (!isWithin(next, lowerBound, max))
14670
15089
  return;
14671
15090
  commitValue(next);
14672
- }, [value, focusedDate, min, max, commitValue]);
15091
+ }, [value, focusedDate, lowerBound, max, commitValue]);
14673
15092
  const tryParse = React.useCallback((s) => {
14674
15093
  const trimmed = s.trim();
14675
15094
  if (!trimmed)
@@ -14704,7 +15123,7 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14704
15123
  setText,
14705
15124
  makeBaseDate,
14706
15125
  locale,
14707
- min,
15126
+ min: lowerBound,
14708
15127
  max,
14709
15128
  isBlocked,
14710
15129
  disabled,
@@ -14737,7 +15156,7 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
14737
15156
  ? fromIsoDateTime(e.target.value)
14738
15157
  : null;
14739
15158
  commitValue(parsed);
14740
- }, 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 }));
15159
+ }, 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 }));
14741
15160
  }
14742
15161
  const spinnerValue = {
14743
15162
  hours: value?.getHours() ?? 0,
@@ -14785,29 +15204,57 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
14785
15204
  * shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
14786
15205
  * />
14787
15206
  */
14788
- const DateTimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
15207
+ const DateTimeInput = React.forwardRef((props, ref) => {
15208
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
14789
15209
  const insideField = useInsideField();
14790
15210
  const insideControl = useInsideControl();
15211
+ const ownField = rendersOwnField('DateTimeInput', {
15212
+ insideField,
15213
+ insideControl,
15214
+ label,
15215
+ message,
15216
+ horizontal,
15217
+ fieldClassName,
15218
+ });
15219
+ // Inline mode renders a bare picker with no input, so the Control's
15220
+ // icon-left container has nothing to anchor to, and it renders no
15221
+ // Control inside one or out. The left icon is checked as the caller
15222
+ // passed it: the default glyph is this component's own choice, not a
15223
+ // prop the caller set.
15224
+ const ownControl = rendersOwnControl('DateTimeInput', {
15225
+ insideControl,
15226
+ inline: baseProps.inline,
15227
+ isLoading,
15228
+ iconLeft,
15229
+ iconLeftName: props.iconLeftName,
15230
+ iconLeftSize,
15231
+ iconRight,
15232
+ iconRightName,
15233
+ iconRightSize,
15234
+ hasIconsLeft,
15235
+ hasIconsRight,
15236
+ isExpanded,
15237
+ controlSize,
15238
+ controlClassName,
15239
+ });
14791
15240
  const { controlId, fieldLabelProps } = useAutoLabelId({
14792
15241
  label,
14793
15242
  id: baseProps.id,
14794
15243
  labelProps,
14795
15244
  // Inline mode renders a bare picker with no input to label.
14796
- rendersLabel: !insideField && !baseProps.inline,
15245
+ rendersLabel: ownField && !baseProps.inline,
14797
15246
  });
14798
15247
  const helpClass = usePrefixedClassNames('help', {
14799
15248
  [`is-${messageColor}`]: !!messageColor,
14800
15249
  });
14801
- // The right-side launcher is on by default; suppress it while the Control
14802
- // shows its loading spinner (also on the right) unless explicitly set.
14803
- let content = (jsxRuntime.jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
14804
- // Inline mode renders a bare picker with no input, so the Control's
14805
- // icon-left container has nothing to anchor to. Skip the Control wrap.
14806
- if (!insideControl && !baseProps.inline) {
15250
+ // The base hides its launcher while the Control it sits in is loading,
15251
+ // whether that is the one rendered below or an enclosing one.
15252
+ let content = (jsxRuntime.jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps }));
15253
+ if (ownControl) {
14807
15254
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
14808
15255
  }
14809
15256
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14810
- if (!insideField) {
15257
+ if (ownField) {
14811
15258
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14812
15259
  }
14813
15260
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));