@allxsmith/bestax-bulma 5.26.3 → 5.26.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) 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 +385 -123
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +385 -123
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/form/Checkboxes.d.ts +7 -0
  10. package/dist/types/form/DateInput.d.ts +18 -3
  11. package/dist/types/form/DateInputBase.d.ts +17 -3
  12. package/dist/types/form/DateTimeInput.d.ts +18 -3
  13. package/dist/types/form/DateTimeInputBase.d.ts +17 -3
  14. package/dist/types/form/File.d.ts +6 -0
  15. package/dist/types/form/FormContext.d.ts +39 -7
  16. package/dist/types/form/Input.d.ts +7 -2
  17. package/dist/types/form/Radios.d.ts +7 -0
  18. package/dist/types/form/Rate.d.ts +7 -0
  19. package/dist/types/form/Select.d.ts +7 -2
  20. package/dist/types/form/Slider.d.ts +7 -0
  21. package/dist/types/form/Taginput.d.ts +6 -0
  22. package/dist/types/form/TextArea.d.ts +7 -2
  23. package/dist/types/form/TimeInput.d.ts +18 -3
  24. package/dist/types/form/TimeInputBase.d.ts +16 -3
  25. package/dist/types/form/_pickerInternals/Calendar.d.ts +9 -3
  26. package/dist/types/form/_pickerInternals/dateUtils.d.ts +17 -0
  27. package/dist/types/form/controlLoading.d.ts +4 -0
  28. package/dist/types/helpers/useAnchoredPosition.d.ts +4 -1
  29. package/dist/types/helpers/useFocusTrap.d.ts +11 -4
  30. package/dist/types-cjs/form/Checkboxes.d.ts +7 -0
  31. package/dist/types-cjs/form/DateInput.d.ts +18 -3
  32. package/dist/types-cjs/form/DateInputBase.d.ts +17 -3
  33. package/dist/types-cjs/form/DateTimeInput.d.ts +18 -3
  34. package/dist/types-cjs/form/DateTimeInputBase.d.ts +17 -3
  35. package/dist/types-cjs/form/File.d.ts +6 -0
  36. package/dist/types-cjs/form/FormContext.d.ts +39 -7
  37. package/dist/types-cjs/form/Input.d.ts +7 -2
  38. package/dist/types-cjs/form/Radios.d.ts +7 -0
  39. package/dist/types-cjs/form/Rate.d.ts +7 -0
  40. package/dist/types-cjs/form/Select.d.ts +7 -2
  41. package/dist/types-cjs/form/Slider.d.ts +7 -0
  42. package/dist/types-cjs/form/Taginput.d.ts +6 -0
  43. package/dist/types-cjs/form/TextArea.d.ts +7 -2
  44. package/dist/types-cjs/form/TimeInput.d.ts +18 -3
  45. package/dist/types-cjs/form/TimeInputBase.d.ts +16 -3
  46. package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +9 -3
  47. package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +17 -0
  48. package/dist/types-cjs/form/controlLoading.d.ts +4 -0
  49. package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +4 -1
  50. package/dist/types-cjs/helpers/useFocusTrap.d.ts +11 -4
  51. package/dist/versions/bestax-no-dark-mode.css +1 -1
  52. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  53. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  54. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  55. package/dist/versions/bestax-no-helpers.css +1 -1
  56. package/dist/versions/bestax-no-helpers.css.map +1 -1
  57. package/dist/versions/bestax-prefixed.css +1 -1
  58. package/dist/versions/bestax-prefixed.css.map +1 -1
  59. package/package.json +1 -1
  60. package/src/scss/form/_datetimeinput.scss +9 -0
  61. package/src/scss/form/_picker-popover.scss +12 -9
  62. package/src/scss/form/_rate.scss +3 -1
  63. package/src/scss/form/_timeinput.scss +5 -0
package/dist/index.esm.js CHANGED
@@ -5199,10 +5199,17 @@ const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
5199
5199
  * container, or give them a trap of their own. To trap content a `Portal`
5200
5200
  * renders, pass a ref to the element inside the `Portal` as `containerRef`.
5201
5201
  *
5202
- * The container has to be in the DOM when the trap turns on. The trap waits
5203
- * for hydration, so a container that only appears once the page has
5204
- * hydrated, the way portaled content does, still has it attach in the commit
5205
- * where the container appears.
5202
+ * The trap looks at `containerRef` again each time the component calling it
5203
+ * renders. A container that mounts after the trap turns on, such as a panel
5204
+ * waiting for a `Portal` target held in state, has it attach once that
5205
+ * component renders with the container in the DOM. When the container leaves
5206
+ * or is replaced, the trap lets go of it as it would turning off, handing
5207
+ * focus back, and attaches to the next one.
5208
+ * A child that mounts the container in a render of its own (from its own
5209
+ * state, or as a `Suspense` boundary resolves) goes unseen until the
5210
+ * component calling the hook renders again. The trap waits for hydration, so
5211
+ * that component renders again when the page hydrates, and portaled content,
5212
+ * which first appears then, is found.
5206
5213
  *
5207
5214
  * @function useFocusTrap
5208
5215
  * @param containerRef - Ref to the element focus stays inside.
@@ -5222,12 +5229,18 @@ function useFocusTrap(containerRef, options = {}) {
5222
5229
  // Portaled content has no server-rendered counterpart, so a portaled
5223
5230
  // container only exists from the commit after hydration.
5224
5231
  const hydrated = useIsHydrated();
5232
+ // The container the trap is attached to, or `null` while it has none.
5233
+ const heldRef = useRef(null);
5234
+ // Bumped to run the trap again when the container in the ref changes.
5235
+ const [refChanges, setRefChanges] = useState(0);
5225
5236
  useEffect(() => {
5237
+ heldRef.current = null;
5226
5238
  if (!active || !hydrated)
5227
5239
  return undefined;
5228
5240
  const container = containerRef.current;
5229
5241
  if (!container)
5230
5242
  return undefined;
5243
+ heldRef.current = container;
5231
5244
  const doc = container.ownerDocument;
5232
5245
  const opener = focusedElement(doc);
5233
5246
  // `initialFocusRef` counts only when focusing it puts focus inside. Focus
@@ -5302,7 +5315,28 @@ function useFocusTrap(containerRef, options = {}) {
5302
5315
  target.focus();
5303
5316
  }
5304
5317
  };
5305
- }, [active, hydrated, containerRef, initialFocusRef, restoreFocus]);
5318
+ }, [
5319
+ active,
5320
+ hydrated,
5321
+ containerRef,
5322
+ initialFocusRef,
5323
+ restoreFocus,
5324
+ refChanges,
5325
+ ]);
5326
+ // Nothing says when a ref is set, so after every render, a ref that no
5327
+ // longer matches what the trap holds (a container that mounted, left or was
5328
+ // replaced) runs it again. Effects run in order, so whenever the effect
5329
+ // above ran in this commit, it read the ref just before this does, and a
5330
+ // mismatch means the ref changed in a render it didn't run for. The render
5331
+ // a bump asks for runs it, so the two match and the trap settles. An empty
5332
+ // ref matches a trap holding nothing, so a container that never mounts
5333
+ // costs no render.
5334
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
5335
+ useEffect(() => {
5336
+ if (active && hydrated && containerRef.current !== heldRef.current) {
5337
+ setRefChanges(n => n + 1);
5338
+ }
5339
+ });
5306
5340
  }
5307
5341
 
5308
5342
  // INTERNAL: deliberately not exported from src/index.ts.
@@ -8735,6 +8769,56 @@ const useInsideControl = () => useContext(ControlContext);
8735
8769
  const FieldProvider = FieldContext.Provider;
8736
8770
  /** Provider for Control context — used internally by Control component. */
8737
8771
  const ControlProvider = ControlContext.Provider;
8772
+ // Each Field-shaping prop, with what takes its place on a `Field` the caller
8773
+ // wraps around the `Control`. `message` needs nothing there, because the
8774
+ // wrapper still renders it inside `Field > Control`. The other Field-level
8775
+ // props (`labelSize`, `labelProps`, `messageColor`) change nothing unless a
8776
+ // `label` or `message` renders, so on their own they keep no `Field`.
8777
+ // `bare-control.test.tsx` holds this list to what each wrapper hands its
8778
+ // `Field` and to which props change its markup, so a new Field-level prop
8779
+ // fails there until it is sorted into one group or the other.
8780
+ const FIELD_SHAPING = [
8781
+ ['label', 'label'],
8782
+ ['message', undefined],
8783
+ ['horizontal', 'horizontal'],
8784
+ ['fieldClassName', 'className (for fieldClassName)'],
8785
+ ];
8786
+ /** `a`, `a and b`, `a, b and c`. */
8787
+ const listOf = (items) => items.length > 1
8788
+ ? `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`
8789
+ : items[0];
8790
+ /**
8791
+ * Whether a convenience form wrapper renders a `Field` of its own (#905).
8792
+ * Never inside an outer `Field`. Inside a `Control` with no `Field` around
8793
+ * it, a `Field` of its own would put a `.field` inside the `.control`, where
8794
+ * Bulma's control styles break (its icon rules are sibling selectors on the
8795
+ * input). So it renders one there only when a prop that shapes the `Field`'s
8796
+ * markup is set (`label`, `message`, `horizontal`, `fieldClassName`), keeping
8797
+ * the markup it always had, and warns in development that the fix is a
8798
+ * `Field` around that `Control`. A prop counts when it is truthy, the same
8799
+ * test that decides whether it changes the `Field`'s output.
8800
+ * Internal; not part of the public API.
8801
+ */
8802
+ const rendersOwnField = (component, options) => {
8803
+ if (options.insideField)
8804
+ return false;
8805
+ if (!options.insideControl)
8806
+ return true;
8807
+ const held = FIELD_SHAPING.filter(([prop]) => options[prop]);
8808
+ if (held.length === 0)
8809
+ return false;
8810
+ const names = held.map(([prop]) => prop);
8811
+ const moved = held
8812
+ .map(([, onField]) => onField)
8813
+ .filter((onField) => onField !== undefined);
8814
+ warnOnce(`${component}:Field-in-bare-Control:${names.join('+')}`, `[bestax-bulma] <${component} ${names.join(' ')}> inside a <Control> ` +
8815
+ `with no <Field> around it renders its own <Field> for ` +
8816
+ `${names.length > 1 ? 'those props' : 'that prop'}, which puts a ` +
8817
+ `.field inside the .control, a nesting Bulma's styles are not ` +
8818
+ `written for. Wrap the <Control> in a <Field> instead` +
8819
+ (moved.length > 0 ? `, and set ${listOf(moved)} on that <Field>.` : '.'));
8820
+ return true;
8821
+ };
8738
8822
  const FieldLabelIdContext = createContext(undefined);
8739
8823
  /**
8740
8824
  * The id a labeled Field wants its single composed control to adopt (#495).
@@ -8865,6 +8949,18 @@ const Checkbox = forwardRef(({ color, size, className, children, textColor, disa
8865
8949
  });
8866
8950
  Checkbox.displayName = 'Checkbox';
8867
8951
 
8952
+ // Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
8953
+ // picker bases read it so their right-side launcher gives way to that
8954
+ // spinner, which sits at the same edge, whether the `Control` is the one a
8955
+ // convenience input renders or one the caller wrapped around the input.
8956
+ // Kept apart from `FormContext`, whose contexts only report whether a `Field`
8957
+ // or `Control` is present. Internal; not part of the public API.
8958
+ const ControlLoadingContext = createContext(false);
8959
+ /** Provider for the loading state, used internally by `Control`. */
8960
+ const ControlLoadingProvider = ControlLoadingContext.Provider;
8961
+ /** Whether the nearest enclosing `Control` is loading; `false` outside one. */
8962
+ const useControlLoading = () => useContext(ControlLoadingContext);
8963
+
8868
8964
  const allowedColors = [...validColors, 'inherit', 'current'];
8869
8965
  /**
8870
8966
  * The `Control` component is a Bulma-styled wrapper for form controls (`Input`, `Select`, `TextArea`, etc.), supporting icons (left/right), loading state, expansion, size, and Bulma helper props for layout and color.
@@ -8924,7 +9020,7 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
8924
9020
  });
8925
9021
  const controlClass = classNames(mainClass, bulmaHelperClasses, className);
8926
9022
  // --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
8927
- return (jsx(ControlProvider, { value: true, children: jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [children, leftIconValue &&
9023
+ return (jsx(ControlProvider, { value: true, children: jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
8928
9024
  (isIconProps(leftIconValue) ? (jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
8929
9025
  (isIconProps(rightIconValue) ? (jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
8930
9026
  });
@@ -9149,10 +9245,18 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, }) {
9149
9245
  const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
9150
9246
  const insideField = useInsideField();
9151
9247
  const insideControl = useInsideControl();
9248
+ const ownField = rendersOwnField('Checkboxes', {
9249
+ insideField,
9250
+ insideControl,
9251
+ label,
9252
+ message,
9253
+ horizontal,
9254
+ fieldClassName,
9255
+ });
9152
9256
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
9153
9257
  label,
9154
9258
  labelProps,
9155
- rendersLabel: !insideField,
9259
+ rendersLabel: ownField,
9156
9260
  });
9157
9261
  const { bulmaHelperClasses, rest } = useBulmaClasses({
9158
9262
  ...props,
@@ -9181,7 +9285,7 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
9181
9285
  if (!insideControl) {
9182
9286
  content = jsx(Control, { children: content });
9183
9287
  }
9184
- if (!insideField) {
9288
+ if (ownField) {
9185
9289
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9186
9290
  }
9187
9291
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -9200,11 +9304,20 @@ const File = forwardRef(({
9200
9304
  // Field props
9201
9305
  label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
9202
9306
  const insideField = useInsideField();
9307
+ const insideControl = useInsideControl();
9308
+ const ownField = rendersOwnField('File', {
9309
+ insideField,
9310
+ insideControl,
9311
+ label,
9312
+ message,
9313
+ horizontal,
9314
+ fieldClassName,
9315
+ });
9203
9316
  const { controlId, fieldLabelProps } = useAutoLabelId({
9204
9317
  label,
9205
9318
  id: props.id,
9206
9319
  labelProps,
9207
- rendersLabel: !insideField,
9320
+ rendersLabel: ownField,
9208
9321
  });
9209
9322
  const { classPrefix } = useConfig();
9210
9323
  const { bulmaHelperClasses, rest } = useBulmaClasses({
@@ -9236,7 +9349,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
9236
9349
  });
9237
9350
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
9238
9351
  const fileElement = (jsx("div", { className: fileClass, children: jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
9239
- if (!insideField) {
9352
+ if (ownField) {
9240
9353
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
9241
9354
  }
9242
9355
  return (jsxs(Fragment, { children: [fileElement, messageEl] }));
@@ -9369,10 +9482,18 @@ Radio.displayName = 'Radio';
9369
9482
  const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
9370
9483
  const insideField = useInsideField();
9371
9484
  const insideControl = useInsideControl();
9485
+ const ownField = rendersOwnField('Radios', {
9486
+ insideField,
9487
+ insideControl,
9488
+ label,
9489
+ message,
9490
+ horizontal,
9491
+ fieldClassName,
9492
+ });
9372
9493
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
9373
9494
  label,
9374
9495
  labelProps,
9375
- rendersLabel: !insideField,
9496
+ rendersLabel: ownField,
9376
9497
  });
9377
9498
  const { bulmaHelperClasses, rest } = useBulmaClasses({
9378
9499
  ...props,
@@ -9404,7 +9525,7 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
9404
9525
  if (!insideControl) {
9405
9526
  content = jsx(Control, { children: content });
9406
9527
  }
9407
- if (!insideField) {
9528
+ if (ownField) {
9408
9529
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9409
9530
  }
9410
9531
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -9584,6 +9705,14 @@ const Slider = forwardRef((props, ref) => {
9584
9705
  range: _range, value: _value, defaultValue: _defaultValue, onChange: _onChange, ariaLabel: _ariaLabel, minDistance: _minDistance, nameLow: _nameLow, nameHigh: _nameHigh, ...restProps } = props;
9585
9706
  const insideField = useInsideField();
9586
9707
  const insideControl = useInsideControl();
9708
+ const ownField = rendersOwnField('Slider', {
9709
+ insideField,
9710
+ insideControl,
9711
+ label,
9712
+ message,
9713
+ horizontal,
9714
+ fieldClassName,
9715
+ });
9587
9716
  const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
9588
9717
  // In range mode the low thumb carries `rest` (and any user id), so the
9589
9718
  // label targets it; each thumb keeps its own aria-label for AT.
@@ -9591,7 +9720,7 @@ const Slider = forwardRef((props, ref) => {
9591
9720
  label,
9592
9721
  id: restProps.id,
9593
9722
  labelProps,
9594
- rendersLabel: !insideField,
9723
+ rendersLabel: ownField,
9595
9724
  });
9596
9725
  // Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
9597
9726
  const tooltipMode = tooltip ?? (showOutput ? 'auto' : 'hidden');
@@ -9930,7 +10059,7 @@ const Slider = forwardRef((props, ref) => {
9930
10059
  if (!insideControl) {
9931
10060
  content = jsx(Control, { children: content });
9932
10061
  }
9933
- if (!insideField) {
10062
+ if (ownField) {
9934
10063
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9935
10064
  }
9936
10065
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -10160,10 +10289,18 @@ function getFillPercent(iconIndex, value) {
10160
10289
  const Rate = forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
10161
10290
  const insideField = useInsideField();
10162
10291
  const insideControl = useInsideControl();
10292
+ const ownField = rendersOwnField('Rate', {
10293
+ insideField,
10294
+ insideControl,
10295
+ label,
10296
+ message,
10297
+ horizontal,
10298
+ fieldClassName,
10299
+ });
10163
10300
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
10164
10301
  label,
10165
10302
  labelProps,
10166
- rendersLabel: !insideField,
10303
+ rendersLabel: ownField,
10167
10304
  });
10168
10305
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10169
10306
  const [internalValue, setInternalValue] = useState(defaultValue);
@@ -10366,7 +10503,7 @@ const Rate = forwardRef(({ label, labelSize, labelProps, horizontal, message, me
10366
10503
  if (!insideControl) {
10367
10504
  content = jsx(Control, { children: content });
10368
10505
  }
10369
- if (!insideField) {
10506
+ if (ownField) {
10370
10507
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
10371
10508
  }
10372
10509
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -10647,6 +10784,15 @@ const Taginput = forwardRef(({
10647
10784
  // Field props
10648
10785
  label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = [], data = [], placeholder, field = 'label', allowNew = true, allowDuplicates = false, openOnFocus = false, removeOnKeys = true, confirmKeys = ['Enter', ','], closable = true, attached = false, maxTags, maxlength, disabled = false, readonly = false, rounded = false, ellipsis = false, hasCounter = true, onPasteSeparators = [','], beforeAdding, createTag, keepFirst = false, keepOpen = true, loading = false, ariaCloseLabel, icon, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, tagColor, size, onChange, onAdd, onRemove, onTyping, tagTemplate, name, form, id, className, ...props }, ref) => {
10649
10786
  const insideField = useInsideField();
10787
+ const insideControl = useInsideControl();
10788
+ const ownField = rendersOwnField('Taginput', {
10789
+ insideField,
10790
+ insideControl,
10791
+ label,
10792
+ message,
10793
+ horizontal,
10794
+ fieldClassName,
10795
+ });
10650
10796
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10651
10797
  const { classPrefix } = useConfig();
10652
10798
  const defaultIconLibrary = useIconLibrary();
@@ -10668,7 +10814,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10668
10814
  label,
10669
10815
  id,
10670
10816
  labelProps,
10671
- rendersLabel: !insideField && !isMaxReached,
10817
+ rendersLabel: ownField && !isMaxReached,
10672
10818
  });
10673
10819
  // Get display value from tag
10674
10820
  const getDisplayValue = (tag) => {
@@ -10940,7 +11086,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10940
11086
  'is-active': index === highlightedIndex,
10941
11087
  }), onClick: () => addTag(item), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: item }, index))) }) })), showCounter && jsx("small", { className: counterClasses, children: counterText }), name &&
10942
11088
  tags.map((tag, i) => (jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
10943
- if (!insideField) {
11089
+ if (ownField) {
10944
11090
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
10945
11091
  }
10946
11092
  return (jsxs(Fragment, { children: [taginputElement, messageEl] }));
@@ -11093,11 +11239,19 @@ fieldClassName, controlClassName,
11093
11239
  ...inputProps }, ref) => {
11094
11240
  const insideField = useInsideField();
11095
11241
  const insideControl = useInsideControl();
11242
+ const ownField = rendersOwnField('Input', {
11243
+ insideField,
11244
+ insideControl,
11245
+ label,
11246
+ message,
11247
+ horizontal,
11248
+ fieldClassName,
11249
+ });
11096
11250
  const { controlId, fieldLabelProps } = useAutoLabelId({
11097
11251
  label,
11098
11252
  id: inputProps.id,
11099
11253
  labelProps,
11100
- rendersLabel: !insideField,
11254
+ rendersLabel: ownField,
11101
11255
  });
11102
11256
  const helpClass = usePrefixedClassNames('help', {
11103
11257
  [`is-${messageColor}`]: !!messageColor,
@@ -11107,7 +11261,7 @@ fieldClassName, controlClassName,
11107
11261
  content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11108
11262
  }
11109
11263
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
11110
- if (!insideField) {
11264
+ if (ownField) {
11111
11265
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11112
11266
  }
11113
11267
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -11143,11 +11297,19 @@ fieldClassName, controlClassName,
11143
11297
  ...selectProps }, ref) => {
11144
11298
  const insideField = useInsideField();
11145
11299
  const insideControl = useInsideControl();
11300
+ const ownField = rendersOwnField('Select', {
11301
+ insideField,
11302
+ insideControl,
11303
+ label,
11304
+ message,
11305
+ horizontal,
11306
+ fieldClassName,
11307
+ });
11146
11308
  const { controlId, fieldLabelProps } = useAutoLabelId({
11147
11309
  label,
11148
11310
  id: selectProps.id,
11149
11311
  labelProps,
11150
- rendersLabel: !insideField,
11312
+ rendersLabel: ownField,
11151
11313
  });
11152
11314
  const helpClass = usePrefixedClassNames('help', {
11153
11315
  [`is-${messageColor}`]: !!messageColor,
@@ -11157,7 +11319,7 @@ fieldClassName, controlClassName,
11157
11319
  content = (jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11158
11320
  }
11159
11321
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
11160
- if (!insideField) {
11322
+ if (ownField) {
11161
11323
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11162
11324
  }
11163
11325
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -11194,11 +11356,19 @@ fieldClassName, controlClassName,
11194
11356
  ...textAreaProps }, ref) => {
11195
11357
  const insideField = useInsideField();
11196
11358
  const insideControl = useInsideControl();
11359
+ const ownField = rendersOwnField('TextArea', {
11360
+ insideField,
11361
+ insideControl,
11362
+ label,
11363
+ message,
11364
+ horizontal,
11365
+ fieldClassName,
11366
+ });
11197
11367
  const { controlId, fieldLabelProps } = useAutoLabelId({
11198
11368
  label,
11199
11369
  id: textAreaProps.id,
11200
11370
  labelProps,
11201
- rendersLabel: !insideField,
11371
+ rendersLabel: ownField,
11202
11372
  });
11203
11373
  const helpClass = usePrefixedClassNames('help', {
11204
11374
  [`is-${messageColor}`]: !!messageColor,
@@ -11208,7 +11378,7 @@ fieldClassName, controlClassName,
11208
11378
  content = (jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
11209
11379
  }
11210
11380
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
11211
- if (!insideField) {
11381
+ if (ownField) {
11212
11382
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11213
11383
  }
11214
11384
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -11295,6 +11465,21 @@ function makeDate(year, month = 0, day = 1) {
11295
11465
  d.setFullYear(year, month, day);
11296
11466
  return d;
11297
11467
  }
11468
+ /**
11469
+ * The earliest year the pickers accept. HTML's date, month and datetime-local
11470
+ * inputs hold no year below 1, so the calendar, typing and parsing stop there
11471
+ * too.
11472
+ */
11473
+ const FIRST_YEAR = 1;
11474
+ /**
11475
+ * The lower bound a picker applies: `min`, raised to local midnight on
11476
+ * 1 January of {@link FIRST_YEAR} when it is earlier or absent. A `min` in
11477
+ * range comes back as the same object.
11478
+ */
11479
+ function floorMin(min) {
11480
+ const floor = makeDate(FIRST_YEAR);
11481
+ return min && min.getTime() >= floor.getTime() ? min : floor;
11482
+ }
11298
11483
  /**
11299
11484
  * First instant of the day, month or year containing `d`. Works by setters on
11300
11485
  * a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
@@ -11388,12 +11573,14 @@ function setTimeOfDay(d, parts) {
11388
11573
  r.setSeconds(parts.seconds);
11389
11574
  return r;
11390
11575
  }
11576
+ /**
11577
+ * A copy of `d` moved into `[min, max]`. When `min` is after `max` nothing is
11578
+ * in range and `min` wins, so clamping twice gives the same date and a
11579
+ * picker's focus stays off the years before 1 under a `max` before then.
11580
+ */
11391
11581
  function clampDate(d, min, max) {
11392
- if (min && isBefore(d, min))
11393
- return new Date(min);
11394
- if (max && isAfter(d, max))
11395
- return new Date(max);
11396
- return new Date(d);
11582
+ const capped = max && isAfter(d, max) ? max : d;
11583
+ return new Date(min && isBefore(capped, min) ? min : capped);
11397
11584
  }
11398
11585
  /**
11399
11586
  * Snap a Date's time-of-day to the nearest grid defined by the given
@@ -11466,7 +11653,9 @@ function formatToken(d, token) {
11466
11653
  case 'YYYY':
11467
11654
  // Zero-pad to a fixed 4 chars so the rendered width matches the
11468
11655
  // segment-map width during digit-by-digit keyboard entry (and to match
11469
- // ISO 8601). Years >9999 keep their natural width.
11656
+ // ISO 8601), so year 19 shows as `0019`. Years >9999 keep their natural
11657
+ // width. The pickers commit no year below 1, so a minus sign reaches
11658
+ // this only from a `value` passed in.
11470
11659
  return pad(d.getFullYear(), 4);
11471
11660
  case 'YY':
11472
11661
  return pad(d.getFullYear() % 100);
@@ -11740,9 +11929,10 @@ function getMonthNames(locale, length = 'long') {
11740
11929
 
11741
11930
  /**
11742
11931
  * Selects the calendar's focused cell, the one tab stop of whichever grid or
11743
- * year list is on show, for a popover to put focus on as it opens.
11932
+ * year list is on show, for a popover to put focus on as it opens. It runs
11933
+ * against the whole panel, so it matches only inside the grid or the list.
11744
11934
  */
11745
- const CALENDAR_FOCUSED_CELL = '[data-focused="true"], [data-focused-year="true"]';
11935
+ const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
11746
11936
  const BASE_VIEW = {
11747
11937
  day: 'days',
11748
11938
  month: 'months',
@@ -11839,7 +12029,7 @@ function yearStep(key, focusedYear, years) {
11839
12029
  return null;
11840
12030
  }
11841
12031
  }
11842
- const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
12032
+ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minProp, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
11843
12033
  const gridRef = useRef(null);
11844
12034
  const monthGridRef = useRef(null);
11845
12035
  const yearGridRef = useRef(null);
@@ -11849,6 +12039,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11849
12039
  const isDayGranularity = granularity === 'day';
11850
12040
  const isYearGranularity = granularity === 'year';
11851
12041
  const [view, setView] = useState(baseView);
12042
+ // Nothing before year 1 can be picked or reached, as in HTML's date inputs.
12043
+ const min = useMemo(() => floorMin(minProp), [minProp]);
11852
12044
  const computedDayNames = useMemo(() => {
11853
12045
  if (dayNames && dayNames.length === 7)
11854
12046
  return dayNames;
@@ -11919,7 +12111,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11919
12111
  const moveFocus = useCallback((next, direction) => {
11920
12112
  let candidate = next;
11921
12113
  for (let i = 0; i < 366; i++) {
11922
- if (min && candidate.getTime() < startOfDay(min).getTime())
12114
+ if (candidate.getTime() < startOfDay(min).getTime())
11923
12115
  return;
11924
12116
  if (max && candidate.getTime() > startOfDay(max).getTime())
11925
12117
  return;
@@ -11985,7 +12177,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11985
12177
  let candidate = next;
11986
12178
  for (let i = 0; i < 120; i++) {
11987
12179
  const month = startOfMonth(candidate).getTime();
11988
- if (min && month < startOfMonth(min).getTime())
12180
+ if (month < startOfMonth(min).getTime())
11989
12181
  return;
11990
12182
  if (max && month > startOfMonth(max).getTime())
11991
12183
  return;
@@ -12033,25 +12225,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12033
12225
  const today = new Date();
12034
12226
  const todayYear = today.getFullYear();
12035
12227
  // The header steps a month at a time over the day grid and a year at a
12036
- // time over the month grid, kept inside min/max.
12037
- const prevMonthAnchor = addMonths(focusedDate, -1);
12038
- const nextMonthAnchor = addMonths(focusedDate, 1);
12228
+ // time over the month grid, kept inside min/max. A step is off once the
12229
+ // month or year on show holds the bound or lies past it, as picking a year
12230
+ // from the list can leave it.
12039
12231
  const prevAnchor = isDayGranularity
12040
- ? prevMonthAnchor
12232
+ ? addMonths(focusedDate, -1)
12041
12233
  : clampDate(addYears(focusedDate, -1), min, max);
12042
12234
  const nextAnchor = isDayGranularity
12043
- ? nextMonthAnchor
12235
+ ? addMonths(focusedDate, 1)
12044
12236
  : clampDate(addYears(focusedDate, 1), min, max);
12237
+ const focusedMonth = startOfMonth(focusedDate).getTime();
12045
12238
  const prevDisabled = isDayGranularity
12046
- ? !!(min &&
12047
- prevMonthAnchor.getTime() < startOfDay(min).getTime() &&
12048
- isSameMonth(focusedDate, min))
12049
- : !!(min && focusedYear <= min.getFullYear());
12239
+ ? focusedMonth <= startOfMonth(min).getTime()
12240
+ : focusedYear <= min.getFullYear();
12050
12241
  const nextDisabled = isDayGranularity
12051
- ? !!(max &&
12052
- nextMonthAnchor.getTime() > startOfDay(max).getTime() &&
12053
- isSameMonth(focusedDate, max))
12054
- : !!(max && focusedYear >= max.getFullYear());
12242
+ ? !!max && focusedMonth >= startOfMonth(max).getTime()
12243
+ : !!max && focusedYear >= max.getFullYear();
12055
12244
  const labelId = id ? `${id}-label` : undefined;
12056
12245
  const monthLabel = isDayGranularity
12057
12246
  ? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
@@ -12062,7 +12251,9 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12062
12251
  const [mountYear] = useState(focusedYear);
12063
12252
  const windowYear = isYearGranularity ? mountYear : focusedYear;
12064
12253
  const yearList = useMemo(() => {
12065
- const minYear = min ? min.getFullYear() : windowYear - 100;
12254
+ const minYear = minProp
12255
+ ? min.getFullYear()
12256
+ : Math.max(FIRST_YEAR, windowYear - 100);
12066
12257
  const maxYear = max ? max.getFullYear() : windowYear + 100;
12067
12258
  const [lo, hi] = yearsRange ?? [minYear, maxYear];
12068
12259
  const start = Math.max(lo, minYear);
@@ -12071,20 +12262,25 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12071
12262
  for (let y = start; y <= end; y++)
12072
12263
  out.push(y);
12073
12264
  return out;
12074
- }, [windowYear, min, max, yearsRange]);
12265
+ }, [windowYear, minProp, min, max, yearsRange]);
12075
12266
  // Only the selection surface disables years; as navigation every listed
12076
12267
  // year can be visited.
12077
12268
  const disabledYears = useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
12078
12269
  // The year list's one tab stop, after the month grid's: when the focused
12079
12270
  // year is disabled the stop moves to the nearest enabled year in the list,
12080
- // and focusing it makes it the focused year. A year outside the list keeps
12081
- // the stop, which then marks no option.
12271
+ // and focusing it makes it the focused year. As the selection surface a
12272
+ // year outside a non-empty list starts from the end it fell past, so the
12273
+ // list keeps a stop. As navigation the stop marks the current year, so a
12274
+ // year outside the list marks no option.
12082
12275
  const tabStopYear = useMemo(() => {
12083
- const i = yearList.indexOf(focusedYear);
12084
- if (i < 0)
12085
- return focusedYear;
12276
+ let i = yearList.indexOf(focusedYear);
12277
+ if (i < 0) {
12278
+ if (!isYearGranularity || yearList.length === 0)
12279
+ return focusedYear;
12280
+ i = focusedYear < yearList[0] ? 0 : yearList.length - 1;
12281
+ }
12086
12282
  return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
12087
- }, [yearList, focusedYear, disabledYears]);
12283
+ }, [yearList, focusedYear, disabledYears, isYearGranularity]);
12088
12284
  // Which of the focused year's months have no selectable day.
12089
12285
  const disabledMonths = useMemo(() => granularity === 'month'
12090
12286
  ? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
@@ -12291,11 +12487,10 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12291
12487
  active: isOpen,
12292
12488
  position,
12293
12489
  fixed: appendToBody,
12294
- // A portaled panel takes its gap from `--bulma-picker-popover-offset` as
12295
- // a margin, so its coordinates leave the gap out and `auto` reads it off
12296
- // the panel. An in-place panel's gap is in its `top` or `bottom`, which
12297
- // `auto` cannot read, so it keeps the hook's default estimate.
12298
- offset: appendToBody ? 0 : undefined,
12490
+ // The gap comes from `--bulma-picker-popover-offset` as a margin, in place
12491
+ // or portaled, so the coordinates leave it out and `auto` reads it off
12492
+ // the panel.
12493
+ offset: 0,
12299
12494
  styledGap: marginGap,
12300
12495
  });
12301
12496
  useEffect(() => {
@@ -12741,33 +12936,35 @@ function useSegmentedEntry(params) {
12741
12936
  // within the same event tick.
12742
12937
  const typedDigitsRef = useRef('');
12743
12938
  // The text focus seeded an empty picker with, until the user edits it or
12744
- // leaves. While the text still matches it, nothing has been typed.
12939
+ // leaves.
12745
12940
  const seedRef = useRef(null);
12746
- // Whether the input's current focus is the one a closing popover handed
12747
- // back, until focus leaves the picker.
12748
- const handedBackRef = useRef(false);
12941
+ // Whether the text is still what the picker put there, so nothing in it was
12942
+ // typed: the value's display, or an empty picker's seed until an edit drops
12943
+ // it. Against a value it compares text alone, so typing that ends back on
12944
+ // the display counts as untouched.
12945
+ const isUntouched = useCallback((s) => value
12946
+ ? s === formatFn(value, format, locale)
12947
+ : seedRef.current !== null && s === seedRef.current, [value, formatFn, format, locale]);
12749
12948
  // A seed the popover never turned into a value closes with it, wherever
12750
12949
  // focus goes, so a dismissed empty field stays empty.
12751
12950
  //
12752
12951
  // As the popover closes, its focus trap hands focus back to the input. That
12753
12952
  // focus is not the user arriving: it must not open the popover again under
12754
- // `openOnFocus`, or seed an empty picker that leaving would then commit.
12755
- // `closingRef` is set in the layout phase of the commit that closes it and
12756
- // cleared by that commit's effects, and React runs a commit's effect
12757
- // cleanups, the trap's among them, before any of its effects, so a focus
12758
- // in between is the one handed back.
12953
+ // `openOnFocus`, or seed an empty picker. `closingRef` is set in the layout
12954
+ // phase of the commit that closes it and cleared by that commit's effects,
12955
+ // and React runs a commit's effect cleanups, the trap's among them, before
12956
+ // any of its effects, so a focus in between is the one handed back.
12759
12957
  const closingRef = useRef(false);
12760
12958
  const wasOpenRef = useRef(isOpen);
12761
12959
  useLayoutEffect(() => {
12762
12960
  if (wasOpenRef.current && !isOpen) {
12763
12961
  closingRef.current = true;
12764
- if (!value && seedRef.current !== null && text === seedRef.current) {
12962
+ if (!value && isUntouched(text))
12765
12963
  setText('');
12766
- }
12767
12964
  seedRef.current = null;
12768
12965
  }
12769
12966
  wasOpenRef.current = isOpen;
12770
- }, [isOpen, value, text, setText]);
12967
+ }, [isOpen, value, isUntouched, text, setText]);
12771
12968
  useEffect(() => {
12772
12969
  closingRef.current = false;
12773
12970
  }, [isOpen]);
@@ -12845,13 +13042,18 @@ function useSegmentedEntry(params) {
12845
13042
  }
12846
13043
  setActiveSegmentIdx(null);
12847
13044
  typedDigitsRef.current = '';
12848
- seedRef.current = null;
12849
- const handedBack = handedBackRef.current;
12850
- handedBackRef.current = false;
12851
- // Leaving after a dismiss commits only what was typed since. Text that
12852
- // still shows the value wasn't, and re-parsing it could drop what the
12853
- // format leaves out, such as seconds.
12854
- if (handedBack && value && text === formatFn(value, format, locale)) {
13045
+ // Leaving commits only what was typed. Re-parsing the value's display
13046
+ // could drop what the format leaves out, such as seconds, and a seed
13047
+ // nobody typed over is not the user's date.
13048
+ const untouched = isUntouched(text);
13049
+ // An open popover keeps an unused seed until it closes, so one portaled
13050
+ // outside the picker, whose focus reads as leaving, keeps it as one
13051
+ // inside does.
13052
+ if (!isOpen)
13053
+ seedRef.current = null;
13054
+ if (untouched) {
13055
+ if (!value && !isOpen)
13056
+ setText('');
12855
13057
  onBlur?.(e);
12856
13058
  return;
12857
13059
  }
@@ -12865,6 +13067,8 @@ function useSegmentedEntry(params) {
12865
13067
  onBlur?.(e);
12866
13068
  }, [
12867
13069
  containerRef,
13070
+ isUntouched,
13071
+ isOpen,
12868
13072
  tryParse,
12869
13073
  text,
12870
13074
  isAllowed,
@@ -12878,7 +13082,6 @@ function useSegmentedEntry(params) {
12878
13082
  ]);
12879
13083
  const handleFocus = useCallback((e) => {
12880
13084
  const handedBack = closingRef.current;
12881
- handedBackRef.current = handedBack;
12882
13085
  if (!handedBack && openOnFocus && popover && !disabled && !readOnly) {
12883
13086
  setOpen(true);
12884
13087
  }
@@ -13046,12 +13249,20 @@ function useSegmentedEntry(params) {
13046
13249
  }
13047
13250
  if (e.key === 'Enter') {
13048
13251
  e.preventDefault();
13049
- const parsed = tryParse(text);
13050
- if (parsed && isAllowed(parsed)) {
13051
- commitValue(parsed);
13252
+ // Untouched text has nothing to commit, as on leaving, but Enter
13253
+ // still confirms what the field shows.
13254
+ if (isUntouched(text)) {
13052
13255
  if (closeOnSelect)
13053
13256
  setOpen(false);
13054
13257
  }
13258
+ else {
13259
+ const parsed = tryParse(text);
13260
+ if (parsed && isAllowed(parsed)) {
13261
+ commitValue(parsed);
13262
+ if (closeOnSelect)
13263
+ setOpen(false);
13264
+ }
13265
+ }
13055
13266
  }
13056
13267
  onKeyDown?.(e);
13057
13268
  }, [
@@ -13065,6 +13276,7 @@ function useSegmentedEntry(params) {
13065
13276
  setOpen,
13066
13277
  closeOnSelect,
13067
13278
  popover,
13279
+ isUntouched,
13068
13280
  tryParse,
13069
13281
  text,
13070
13282
  isAllowed,
@@ -13113,9 +13325,12 @@ const pad2$1 = (n) => String(n).padStart(2, '0');
13113
13325
  * The value as the native input and the hidden form input carry it:
13114
13326
  * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
13115
13327
  * `<input type="month">` use. The year is padded to four digits, as those
13116
- * inputs require and as the `YYYY` token displays it.
13328
+ * inputs require and as the `YYYY` token displays it. HTML has no such shape
13329
+ * for a year before 1, so a date then is empty, as those inputs would make it.
13117
13330
  */
13118
13331
  const toIsoValue = (d, granularity) => {
13332
+ if (d.getFullYear() < FIRST_YEAR)
13333
+ return '';
13119
13334
  const year = String(d.getFullYear()).padStart(4, '0');
13120
13335
  if (granularity === 'year')
13121
13336
  return year;
@@ -13156,14 +13371,19 @@ const SKIPPED_SEGMENTS = {
13156
13371
  * @returns {JSX.Element}
13157
13372
  */
13158
13373
  const DateInputBase = forwardRef((props, ref) => {
13159
- const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, granularity = 'day', format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon = true, triggerIconName = 'chevron-down', labels, ...rest } = props;
13374
+ const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, granularity = 'day', format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', labels, ...rest } = props;
13375
+ // The launcher gives way to the loading spinner of a Control this sits in.
13376
+ const controlLoading = useControlLoading();
13377
+ const triggerIcon = triggerIconProp ?? !controlLoading;
13160
13378
  const t = mergeLabels(labels);
13161
13379
  const isDayGranularity = granularity === 'day';
13162
13380
  const resolvedFormat = format ?? DEFAULT_FORMATS[granularity];
13163
13381
  const isControlled = controlledValue !== undefined;
13164
13382
  const [internalValue, setInternalValue] = useState(defaultValue ?? null);
13165
13383
  const value = isControlled ? (controlledValue ?? null) : internalValue;
13166
- const initialFocused = useMemo(() => clampDate(value ?? new Date(), min, max),
13384
+ // Nothing before year 1 is in range, as in HTML's date inputs.
13385
+ const lowerBound = useMemo(() => floorMin(min), [min]);
13386
+ const initialFocused = useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
13167
13387
  // intentionally only on mount: keep focusedDate stable until value/open change
13168
13388
  // eslint-disable-next-line react-hooks/exhaustive-deps
13169
13389
  []);
@@ -13171,8 +13391,8 @@ const DateInputBase = forwardRef((props, ref) => {
13171
13391
  // Re-clamp focusedDate if min/max change after mount so the focused cell
13172
13392
  // never disappears outside the displayable range.
13173
13393
  useEffect(() => {
13174
- setFocusedDate(prev => clampDate(prev, min, max));
13175
- }, [min, max]);
13394
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
13395
+ }, [lowerBound, max]);
13176
13396
  const [open, setOpenState] = useState(false);
13177
13397
  const [text, setText] = useState(value ? formatDate(value, resolvedFormat, locale) : '');
13178
13398
  const inputRef = useRef(null);
@@ -13230,7 +13450,7 @@ const DateInputBase = forwardRef((props, ref) => {
13230
13450
  }, [isControlled, onChange, isDayGranularity, granularity]);
13231
13451
  // At month or year granularity the bounds widen to the periods holding
13232
13452
  // them, so the month or year containing `min` stays reachable.
13233
- const periodMin = useMemo(() => (min && !isDayGranularity ? startOfPeriod(min, granularity) : min), [min, isDayGranularity, granularity]);
13453
+ const periodMin = useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
13234
13454
  const periodMax = useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
13235
13455
  const handleSelect = useCallback((d) => {
13236
13456
  if (!isWithin(d, periodMin, periodMax))
@@ -13281,7 +13501,7 @@ const DateInputBase = forwardRef((props, ref) => {
13281
13501
  ]);
13282
13502
  // Typing, and the re-parse on blur or Enter, that lands in the period the
13283
13503
  // value already holds commits nothing, so a `value` elsewhere in its month
13284
- // or year isn't rewritten by focus passing through the field.
13504
+ // or year changes only when the user picks or types a different period.
13285
13505
  const commitTyped = useCallback((next) => {
13286
13506
  if (next &&
13287
13507
  value &&
@@ -13347,7 +13567,7 @@ const DateInputBase = forwardRef((props, ref) => {
13347
13567
  ? fromIsoValue(e.target.value, granularity)
13348
13568
  : null;
13349
13569
  commitValue(parsed);
13350
- }, min: min ? toIsoValue(min, granularity) : undefined, max: max ? toIsoValue(max, granularity) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
13570
+ }, min: min ? toIsoValue(lowerBound, granularity) : undefined, max: max ? toIsoValue(max, granularity) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
13351
13571
  }
13352
13572
  const chooseLabel = granularity === 'month'
13353
13573
  ? t.chooseMonth
@@ -13356,7 +13576,11 @@ const DateInputBase = forwardRef((props, ref) => {
13356
13576
  : t.chooseDate;
13357
13577
  const calendar = (jsx(Calendar
13358
13578
  // A new granularity starts the calendar over on its own view.
13359
- , { granularity: granularity, value: value, focusedDate: focusedDate, onSelect: handleSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size, id: `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
13579
+ , { granularity: granularity, value: value, focusedDate: focusedDate, onSelect: handleSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size,
13580
+ // The popover panel takes `popoverId`, so the calendar inside it
13581
+ // takes one of its own. Inline there is no panel, and the calendar
13582
+ // keeps the id it released with.
13583
+ id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
13360
13584
  if (inline) {
13361
13585
  return (jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [calendar, name && (jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoValue(value, granularity) : '', required: required }))] }));
13362
13586
  }
@@ -13387,26 +13611,34 @@ DateInputBase.displayName = 'DateInputBase';
13387
13611
  const DateInput = forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
13388
13612
  const insideField = useInsideField();
13389
13613
  const insideControl = useInsideControl();
13614
+ const ownField = rendersOwnField('DateInput', {
13615
+ insideField,
13616
+ insideControl,
13617
+ label,
13618
+ message,
13619
+ horizontal,
13620
+ fieldClassName,
13621
+ });
13390
13622
  const { controlId, fieldLabelProps } = useAutoLabelId({
13391
13623
  label,
13392
13624
  id: baseProps.id,
13393
13625
  labelProps,
13394
13626
  // Inline mode renders a bare calendar with no input to label.
13395
- rendersLabel: !insideField && !baseProps.inline,
13627
+ rendersLabel: ownField && !baseProps.inline,
13396
13628
  });
13397
13629
  const helpClass = usePrefixedClassNames('help', {
13398
13630
  [`is-${messageColor}`]: !!messageColor,
13399
13631
  });
13400
- // The right-side launcher is on by default; suppress it while the Control
13401
- // shows its loading spinner (also on the right) unless explicitly set.
13402
- let content = (jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
13632
+ // The base hides its launcher while the Control it sits in is loading,
13633
+ // whether that is the one rendered below or an enclosing one.
13634
+ let content = (jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps }));
13403
13635
  // Inline mode renders a bare calendar with no input, so the Control's
13404
13636
  // icon-left container has nothing to anchor to. Skip the Control wrap.
13405
13637
  if (!insideControl && !baseProps.inline) {
13406
13638
  content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
13407
13639
  }
13408
13640
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
13409
- if (!insideField) {
13641
+ if (ownField) {
13410
13642
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
13411
13643
  }
13412
13644
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -14280,7 +14512,10 @@ const fromIsoTime = (s) => {
14280
14512
  * @returns {JSX.Element}
14281
14513
  */
14282
14514
  const TimeInputBase = forwardRef((props, ref) => {
14283
- const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, hourFormat = '24', enableSeconds = false, incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon = true, triggerIconName = 'chevron-down', labels, audioTick = false, haptics = false, ...rest } = props;
14515
+ const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, hourFormat = '24', enableSeconds = false, incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', labels, audioTick = false, haptics = false, ...rest } = props;
14516
+ // The launcher gives way to the loading spinner of a Control this sits in.
14517
+ const controlLoading = useControlLoading();
14518
+ const triggerIcon = triggerIconProp ?? !controlLoading;
14284
14519
  // Platform-appropriate feedback routing: when `haptics` is opted in and
14285
14520
  // the runtime has no Vibration API (notably iOS Safari), enable the
14286
14521
  // audio thunk so the user still gets *some* tactile cue. When vibrate
@@ -14357,6 +14592,8 @@ const TimeInputBase = forwardRef((props, ref) => {
14357
14592
  useEffect(() => {
14358
14593
  setText(value ? formatTime(value, defaultFormat, locale) : '');
14359
14594
  }, [value, defaultFormat, locale]);
14595
+ // A value's date before year 1 is out of range, as in the date pickers.
14596
+ const lowerBound = useMemo(() => floorMin(min), [min]);
14360
14597
  const commitValue = useCallback((next) => {
14361
14598
  if (!isControlled)
14362
14599
  setInternalValue(next);
@@ -14365,10 +14602,10 @@ const TimeInputBase = forwardRef((props, ref) => {
14365
14602
  const handleSpinnerChange = useCallback((parts) => {
14366
14603
  const base = value ?? new Date();
14367
14604
  const next = setTimeOfDay(base, parts);
14368
- if (!isWithin(next, min, max))
14605
+ if (!isWithin(next, lowerBound, max))
14369
14606
  return;
14370
14607
  commitValue(next);
14371
- }, [value, min, max, commitValue]);
14608
+ }, [value, lowerBound, max, commitValue]);
14372
14609
  const tryParse = useCallback((s) => {
14373
14610
  const trimmed = s.trim();
14374
14611
  if (!trimmed)
@@ -14400,7 +14637,7 @@ const TimeInputBase = forwardRef((props, ref) => {
14400
14637
  setText,
14401
14638
  makeBaseDate,
14402
14639
  locale,
14403
- min,
14640
+ min: lowerBound,
14404
14641
  max,
14405
14642
  isBlocked: unselectableTimes,
14406
14643
  disabled,
@@ -14482,34 +14719,46 @@ TimeInputBase.displayName = 'TimeInputBase';
14482
14719
  const TimeInput = forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
14483
14720
  const insideField = useInsideField();
14484
14721
  const insideControl = useInsideControl();
14722
+ const ownField = rendersOwnField('TimeInput', {
14723
+ insideField,
14724
+ insideControl,
14725
+ label,
14726
+ message,
14727
+ horizontal,
14728
+ fieldClassName,
14729
+ });
14485
14730
  const { controlId, fieldLabelProps } = useAutoLabelId({
14486
14731
  label,
14487
14732
  id: baseProps.id,
14488
14733
  labelProps,
14489
14734
  // Inline mode renders a bare panel with no input to label.
14490
- rendersLabel: !insideField && !baseProps.inline,
14735
+ rendersLabel: ownField && !baseProps.inline,
14491
14736
  });
14492
14737
  const helpClass = usePrefixedClassNames('help', {
14493
14738
  [`is-${messageColor}`]: !!messageColor,
14494
14739
  });
14495
- // The right-side launcher is on by default; suppress it while the Control
14496
- // shows its loading spinner (also on the right) unless explicitly set.
14497
- let content = (jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
14740
+ // The base hides its launcher while the Control it sits in is loading,
14741
+ // whether that is the one rendered below or an enclosing one.
14742
+ let content = (jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps }));
14498
14743
  // Inline mode renders a bare panel with no input, so the Control's
14499
14744
  // icon-left container has nothing to anchor to. Skip the Control wrap.
14500
14745
  if (!insideControl && !baseProps.inline) {
14501
14746
  content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
14502
14747
  }
14503
14748
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
14504
- if (!insideField) {
14749
+ if (ownField) {
14505
14750
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14506
14751
  }
14507
14752
  return (jsxs(Fragment, { children: [content, messageEl] }));
14508
14753
  });
14509
14754
  TimeInput.displayName = 'TimeInput';
14510
14755
 
14511
- // The year is padded to four digits, as `datetime-local` requires.
14756
+ // The year is padded to four digits, as `datetime-local` requires. HTML has
14757
+ // no such shape for a year before 1, so a date then is empty, as that input
14758
+ // would make it.
14512
14759
  const toIsoDateTime = (d, withSeconds) => {
14760
+ if (d.getFullYear() < FIRST_YEAR)
14761
+ return '';
14513
14762
  const yyyy = String(d.getFullYear()).padStart(4, '0');
14514
14763
  const mo = String(d.getMonth() + 1).padStart(2, '0');
14515
14764
  const dd = String(d.getDate()).padStart(2, '0');
@@ -14542,7 +14791,10 @@ const fromIsoDateTime = (s) => {
14542
14791
  const DateTimeInputBase = forwardRef((props, ref) => {
14543
14792
  const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, hourFormat = '24', enableSeconds = false, incrementHours = 1,
14544
14793
  // Step between visible wheel values; 1 gives the iOS-style every-minute wheel.
14545
- incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon = true, triggerIconName = 'chevron-down', audioTick = false, haptics = false, labels, ...rest } = props;
14794
+ incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', audioTick = false, haptics = false, labels, ...rest } = props;
14795
+ // The launcher gives way to the loading spinner of a Control this sits in.
14796
+ const controlLoading = useControlLoading();
14797
+ const triggerIcon = triggerIconProp ?? !controlLoading;
14546
14798
  const t = mergeLabels(labels);
14547
14799
  // Platform-appropriate feedback routing for the time wheel: on iOS (no
14548
14800
  // navigator.vibrate) the audio thunk fills in; on Android the real haptic
@@ -14552,14 +14804,16 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14552
14804
  const isControlled = controlledValue !== undefined;
14553
14805
  const [internalValue, setInternalValue] = useState(defaultValue ?? null);
14554
14806
  const value = isControlled ? (controlledValue ?? null) : internalValue;
14555
- const initialFocused = useMemo(() => clampDate(value ?? new Date(), min, max),
14807
+ // Nothing before year 1 is in range, as in HTML's date inputs.
14808
+ const lowerBound = useMemo(() => floorMin(min), [min]);
14809
+ const initialFocused = useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
14556
14810
  // eslint-disable-next-line react-hooks/exhaustive-deps
14557
14811
  []);
14558
14812
  const [focusedDate, setFocusedDate] = useState(initialFocused);
14559
14813
  // Re-clamp focusedDate when min/max change so the focused cell stays valid.
14560
14814
  useEffect(() => {
14561
- setFocusedDate(prev => clampDate(prev, min, max));
14562
- }, [min, max]);
14815
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
14816
+ }, [lowerBound, max]);
14563
14817
  const defaultFormat = format ??
14564
14818
  (hourFormat === '12'
14565
14819
  ? enableSeconds
@@ -14656,18 +14910,18 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14656
14910
  minutes: value?.getMinutes() ?? 0,
14657
14911
  seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
14658
14912
  });
14659
- if (!isWithin(merged, min, max))
14913
+ if (!isWithin(merged, lowerBound, max))
14660
14914
  return;
14661
14915
  commitValue(merged);
14662
14916
  setFocusedDate(d);
14663
- }, [value, enableSeconds, min, max, commitValue]);
14917
+ }, [value, enableSeconds, lowerBound, max, commitValue]);
14664
14918
  const handleTimeChange = useCallback((parts) => {
14665
14919
  const base = value ?? focusedDate;
14666
14920
  const next = setTimeOfDay(base, parts);
14667
- if (!isWithin(next, min, max))
14921
+ if (!isWithin(next, lowerBound, max))
14668
14922
  return;
14669
14923
  commitValue(next);
14670
- }, [value, focusedDate, min, max, commitValue]);
14924
+ }, [value, focusedDate, lowerBound, max, commitValue]);
14671
14925
  const tryParse = useCallback((s) => {
14672
14926
  const trimmed = s.trim();
14673
14927
  if (!trimmed)
@@ -14702,7 +14956,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14702
14956
  setText,
14703
14957
  makeBaseDate,
14704
14958
  locale,
14705
- min,
14959
+ min: lowerBound,
14706
14960
  max,
14707
14961
  isBlocked,
14708
14962
  disabled,
@@ -14735,7 +14989,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14735
14989
  ? fromIsoDateTime(e.target.value)
14736
14990
  : null;
14737
14991
  commitValue(parsed);
14738
- }, min: min ? toIsoDateTime(min, enableSeconds) : undefined, max: max ? toIsoDateTime(max, enableSeconds) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
14992
+ }, min: min ? toIsoDateTime(lowerBound, enableSeconds) : undefined, max: max ? toIsoDateTime(max, enableSeconds) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
14739
14993
  }
14740
14994
  const spinnerValue = {
14741
14995
  hours: value?.getHours() ?? 0,
@@ -14786,26 +15040,34 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
14786
15040
  const DateTimeInput = forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
14787
15041
  const insideField = useInsideField();
14788
15042
  const insideControl = useInsideControl();
15043
+ const ownField = rendersOwnField('DateTimeInput', {
15044
+ insideField,
15045
+ insideControl,
15046
+ label,
15047
+ message,
15048
+ horizontal,
15049
+ fieldClassName,
15050
+ });
14789
15051
  const { controlId, fieldLabelProps } = useAutoLabelId({
14790
15052
  label,
14791
15053
  id: baseProps.id,
14792
15054
  labelProps,
14793
15055
  // Inline mode renders a bare picker with no input to label.
14794
- rendersLabel: !insideField && !baseProps.inline,
15056
+ rendersLabel: ownField && !baseProps.inline,
14795
15057
  });
14796
15058
  const helpClass = usePrefixedClassNames('help', {
14797
15059
  [`is-${messageColor}`]: !!messageColor,
14798
15060
  });
14799
- // The right-side launcher is on by default; suppress it while the Control
14800
- // shows its loading spinner (also on the right) unless explicitly set.
14801
- let content = (jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps, triggerIcon: baseProps.triggerIcon ?? !isLoading }));
15061
+ // The base hides its launcher while the Control it sits in is loading,
15062
+ // whether that is the one rendered below or an enclosing one.
15063
+ let content = (jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps }));
14802
15064
  // Inline mode renders a bare picker with no input, so the Control's
14803
15065
  // icon-left container has nothing to anchor to. Skip the Control wrap.
14804
15066
  if (!insideControl && !baseProps.inline) {
14805
15067
  content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
14806
15068
  }
14807
15069
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
14808
- if (!insideField) {
15070
+ if (ownField) {
14809
15071
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14810
15072
  }
14811
15073
  return (jsxs(Fragment, { children: [content, messageEl] }));