@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.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,108 @@ 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
+ };
8822
+ // Each Control-level prop, with what takes its place on the `Control` the
8823
+ // caller wrapped around the wrapper. `bare-control.test.tsx` holds this to
8824
+ // what each wrapper hands its own `Control`, so a new Control-level prop
8825
+ // fails there until it is added here and passed to `rendersOwnControl`.
8826
+ const ON_CONTROL = {
8827
+ isLoading: 'isLoading',
8828
+ iconLeft: 'iconLeft',
8829
+ iconLeftName: 'iconLeftName',
8830
+ iconLeftSize: 'iconLeftSize',
8831
+ iconRight: 'iconRight',
8832
+ iconRightName: 'iconRightName',
8833
+ iconRightSize: 'iconRightSize',
8834
+ hasIconsLeft: 'hasIconsLeft',
8835
+ hasIconsRight: 'hasIconsRight',
8836
+ isExpanded: 'isExpanded',
8837
+ controlSize: 'size (for controlSize)',
8838
+ controlClassName: 'className (for controlClassName)',
8839
+ };
8840
+ /**
8841
+ * Whether a convenience form wrapper renders a `Control` of its own (#921):
8842
+ * only outside one. Inside a `Control` it renders none, so the props it
8843
+ * would have handed its own do nothing there, and it warns in development,
8844
+ * naming them and saying to set them on that `Control`. An `inline` picker
8845
+ * renders none in either place, so it drops them inside a `Control` or not,
8846
+ * and its warning says so without pointing at a `Control`. A wrapper passes
8847
+ * only the props it hands its own `Control`, as the caller gave them, so a
8848
+ * default the wrapper fills in itself never warns. A prop counts when it is
8849
+ * truthy, since a falsy one would change nothing on a `Control` either.
8850
+ * Internal; not part of the public API.
8851
+ */
8852
+ const rendersOwnControl = (component, options) => {
8853
+ if (!options.inline && !options.insideControl)
8854
+ return true;
8855
+ const held = Object.keys(ON_CONTROL).filter(prop => options[prop]);
8856
+ if (held.length === 0)
8857
+ return false;
8858
+ const names = held.join(' ');
8859
+ const doNothing = held.length > 1 ? 'those props do' : 'that prop does';
8860
+ if (options.inline) {
8861
+ warnOnce(`${component}:Control-props-inline:${held.join('+')}`, `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
8862
+ `in inline mode, inside a <Control> or not, so ${doNothing} ` +
8863
+ `nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
8864
+ `inline picker.`);
8865
+ }
8866
+ else {
8867
+ warnOnce(`${component}:Control-props-in-Control:${held.join('+')}`, `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
8868
+ `no <Control> of its own, so ${doNothing} nothing there. Set ` +
8869
+ `${listOf(held.map(prop => ON_CONTROL[prop]))} on that <Control> ` +
8870
+ `instead.`);
8871
+ }
8872
+ return false;
8873
+ };
8738
8874
  const FieldLabelIdContext = createContext(undefined);
8739
8875
  /**
8740
8876
  * The id a labeled Field wants its single composed control to adopt (#495).
@@ -8865,6 +9001,19 @@ const Checkbox = forwardRef(({ color, size, className, children, textColor, disa
8865
9001
  });
8866
9002
  Checkbox.displayName = 'Checkbox';
8867
9003
 
9004
+ // Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
9005
+ // picker bases read it so their right-side launcher gives way to that
9006
+ // spinner, which sits at the same edge, whether the `Control` is the one a
9007
+ // convenience input renders or one the caller wrapped around the input.
9008
+ // Autocomplete reads it for its clear button, which sits there too.
9009
+ // Kept apart from `FormContext`, whose contexts only report whether a `Field`
9010
+ // or `Control` is present. Internal; not part of the public API.
9011
+ const ControlLoadingContext = createContext(false);
9012
+ /** Provider for the loading state, used internally by `Control`. */
9013
+ const ControlLoadingProvider = ControlLoadingContext.Provider;
9014
+ /** Whether the nearest enclosing `Control` is loading; `false` outside one. */
9015
+ const useControlLoading = () => useContext(ControlLoadingContext);
9016
+
8868
9017
  const allowedColors = [...validColors, 'inherit', 'current'];
8869
9018
  /**
8870
9019
  * 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 +9073,7 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
8924
9073
  });
8925
9074
  const controlClass = classNames(mainClass, bulmaHelperClasses, className);
8926
9075
  // --- 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 &&
9076
+ return (jsx(ControlProvider, { value: true, children: jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
8928
9077
  (isIconProps(leftIconValue) ? (jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
8929
9078
  (isIconProps(rightIconValue) ? (jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
8930
9079
  });
@@ -9149,10 +9298,18 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, }) {
9149
9298
  const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
9150
9299
  const insideField = useInsideField();
9151
9300
  const insideControl = useInsideControl();
9301
+ const ownField = rendersOwnField('Checkboxes', {
9302
+ insideField,
9303
+ insideControl,
9304
+ label,
9305
+ message,
9306
+ horizontal,
9307
+ fieldClassName,
9308
+ });
9152
9309
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
9153
9310
  label,
9154
9311
  labelProps,
9155
- rendersLabel: !insideField,
9312
+ rendersLabel: ownField,
9156
9313
  });
9157
9314
  const { bulmaHelperClasses, rest } = useBulmaClasses({
9158
9315
  ...props,
@@ -9178,10 +9335,10 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
9178
9335
  }), [name, groupActive, currentValue, handleChange]);
9179
9336
  const checkboxesElement = (jsx("div", { className: wrapperClass, role: "group", "aria-labelledby": ariaLabelledBy, ...rest, children: jsx(CheckboxesProvider, { value: ctx, children: children }) }));
9180
9337
  let content = checkboxesElement;
9181
- if (!insideControl) {
9338
+ if (rendersOwnControl('Checkboxes', { insideControl })) {
9182
9339
  content = jsx(Control, { children: content });
9183
9340
  }
9184
- if (!insideField) {
9341
+ if (ownField) {
9185
9342
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9186
9343
  }
9187
9344
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -9200,11 +9357,20 @@ const File = forwardRef(({
9200
9357
  // Field props
9201
9358
  label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
9202
9359
  const insideField = useInsideField();
9360
+ const insideControl = useInsideControl();
9361
+ const ownField = rendersOwnField('File', {
9362
+ insideField,
9363
+ insideControl,
9364
+ label,
9365
+ message,
9366
+ horizontal,
9367
+ fieldClassName,
9368
+ });
9203
9369
  const { controlId, fieldLabelProps } = useAutoLabelId({
9204
9370
  label,
9205
9371
  id: props.id,
9206
9372
  labelProps,
9207
- rendersLabel: !insideField,
9373
+ rendersLabel: ownField,
9208
9374
  });
9209
9375
  const { classPrefix } = useConfig();
9210
9376
  const { bulmaHelperClasses, rest } = useBulmaClasses({
@@ -9236,7 +9402,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
9236
9402
  });
9237
9403
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
9238
9404
  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) {
9405
+ if (ownField) {
9240
9406
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
9241
9407
  }
9242
9408
  return (jsxs(Fragment, { children: [fileElement, messageEl] }));
@@ -9369,10 +9535,18 @@ Radio.displayName = 'Radio';
9369
9535
  const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
9370
9536
  const insideField = useInsideField();
9371
9537
  const insideControl = useInsideControl();
9538
+ const ownField = rendersOwnField('Radios', {
9539
+ insideField,
9540
+ insideControl,
9541
+ label,
9542
+ message,
9543
+ horizontal,
9544
+ fieldClassName,
9545
+ });
9372
9546
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
9373
9547
  label,
9374
9548
  labelProps,
9375
- rendersLabel: !insideField,
9549
+ rendersLabel: ownField,
9376
9550
  });
9377
9551
  const { bulmaHelperClasses, rest } = useBulmaClasses({
9378
9552
  ...props,
@@ -9401,10 +9575,10 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
9401
9575
  }), [name, groupActive, currentValue, handleChange]);
9402
9576
  const radiosElement = (jsx("div", { className: wrapperClass, role: "radiogroup", "aria-labelledby": ariaLabelledBy, ...rest, children: jsx(RadiosProvider, { value: ctx, children: children }) }));
9403
9577
  let content = radiosElement;
9404
- if (!insideControl) {
9578
+ if (rendersOwnControl('Radios', { insideControl })) {
9405
9579
  content = jsx(Control, { children: content });
9406
9580
  }
9407
- if (!insideField) {
9581
+ if (ownField) {
9408
9582
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9409
9583
  }
9410
9584
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -9584,6 +9758,14 @@ const Slider = forwardRef((props, ref) => {
9584
9758
  range: _range, value: _value, defaultValue: _defaultValue, onChange: _onChange, ariaLabel: _ariaLabel, minDistance: _minDistance, nameLow: _nameLow, nameHigh: _nameHigh, ...restProps } = props;
9585
9759
  const insideField = useInsideField();
9586
9760
  const insideControl = useInsideControl();
9761
+ const ownField = rendersOwnField('Slider', {
9762
+ insideField,
9763
+ insideControl,
9764
+ label,
9765
+ message,
9766
+ horizontal,
9767
+ fieldClassName,
9768
+ });
9587
9769
  const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
9588
9770
  // In range mode the low thumb carries `rest` (and any user id), so the
9589
9771
  // label targets it; each thumb keeps its own aria-label for AT.
@@ -9591,7 +9773,7 @@ const Slider = forwardRef((props, ref) => {
9591
9773
  label,
9592
9774
  id: restProps.id,
9593
9775
  labelProps,
9594
- rendersLabel: !insideField,
9776
+ rendersLabel: ownField,
9595
9777
  });
9596
9778
  // Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
9597
9779
  const tooltipMode = tooltip ?? (showOutput ? 'auto' : 'hidden');
@@ -9927,10 +10109,10 @@ const Slider = forwardRef((props, ref) => {
9927
10109
  '--slider-output-arrow-offset': `calc(50% - ${nudgeXHigh}px)`,
9928
10110
  }, children: formatValue(currentRange[1]) })), renderTicks()] }));
9929
10111
  let content = sliderElement;
9930
- if (!insideControl) {
10112
+ if (rendersOwnControl('Slider', { insideControl })) {
9931
10113
  content = jsx(Control, { children: content });
9932
10114
  }
9933
- if (!insideField) {
10115
+ if (ownField) {
9934
10116
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
9935
10117
  }
9936
10118
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -10160,10 +10342,18 @@ function getFillPercent(iconIndex, value) {
10160
10342
  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
10343
  const insideField = useInsideField();
10162
10344
  const insideControl = useInsideControl();
10345
+ const ownField = rendersOwnField('Rate', {
10346
+ insideField,
10347
+ insideControl,
10348
+ label,
10349
+ message,
10350
+ horizontal,
10351
+ fieldClassName,
10352
+ });
10163
10353
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
10164
10354
  label,
10165
10355
  labelProps,
10166
- rendersLabel: !insideField,
10356
+ rendersLabel: ownField,
10167
10357
  });
10168
10358
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10169
10359
  const [internalValue, setInternalValue] = useState(defaultValue);
@@ -10363,10 +10553,10 @@ const Rate = forwardRef(({ label, labelSize, labelProps, horizontal, message, me
10363
10553
  const rateCustomTextClass = usePrefixedClassNames('rate-custom-text');
10364
10554
  const rateElement = (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: [jsx("div", { className: rateItemsClass, children: renderIcons() }), showScore && (jsx("span", { className: rateScoreClass, children: getScoreDisplay() })), text && jsx("span", { className: rateTextClass, children: text }), customText && (jsx("span", { className: rateCustomTextClass, children: customText })), name && (jsx("input", { type: "hidden", name: name, value: currentValue, form: form }))] }));
10365
10555
  let content = rateElement;
10366
- if (!insideControl) {
10556
+ if (rendersOwnControl('Rate', { insideControl })) {
10367
10557
  content = jsx(Control, { children: content });
10368
10558
  }
10369
- if (!insideField) {
10559
+ if (ownField) {
10370
10560
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
10371
10561
  }
10372
10562
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -10602,7 +10792,11 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10602
10792
  [`is-${messageColor}`]: !!messageColor,
10603
10793
  });
10604
10794
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
10605
- const autocompleteElement = (jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxs("div", { className: controlClasses, children: [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 && (jsx("span", { className: iconRightClickableClass, onClick: handleClear, role: "button", "aria-label": "Clear", children: 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: [jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsx("span", { className: iconRightClass, children: jsx("span", { className: loaderClass }) }))] }), isActive && (filteredData.length > 0 || empty) && (jsx("div", { className: dropdownMenuClasses, children: jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, role: "listbox", onScroll: handleDropdownScroll, children: [header && jsx("div", { className: dropdownHeaderClass, children: header }), filteredData.length > 0 ? (filteredData.map((item, index) => {
10795
+ // The clear button gives way to a spinner at its spot: the `loading` one,
10796
+ // or that of a loading Control this sits in.
10797
+ const controlLoading = useControlLoading();
10798
+ const showClear = clearable && !!inputValue && !disabled && !loading && !controlLoading;
10799
+ const autocompleteElement = (jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxs("div", { className: controlClasses, children: [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 && (jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: 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: [jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsx("span", { className: iconRightClass, children: jsx("span", { className: loaderClass }) }))] }), isActive && (filteredData.length > 0 || empty) && (jsx("div", { className: dropdownMenuClasses, children: jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, role: "listbox", onScroll: handleDropdownScroll, children: [header && jsx("div", { className: dropdownHeaderClass, children: header }), filteredData.length > 0 ? (filteredData.map((item, index) => {
10606
10800
  const isDisabled = typeof item !== 'string' && item.disabled;
10607
10801
  const isHighlighted = index === highlightedIndex;
10608
10802
  const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
@@ -10647,6 +10841,15 @@ const Taginput = forwardRef(({
10647
10841
  // Field props
10648
10842
  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
10843
  const insideField = useInsideField();
10844
+ const insideControl = useInsideControl();
10845
+ const ownField = rendersOwnField('Taginput', {
10846
+ insideField,
10847
+ insideControl,
10848
+ label,
10849
+ message,
10850
+ horizontal,
10851
+ fieldClassName,
10852
+ });
10650
10853
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10651
10854
  const { classPrefix } = useConfig();
10652
10855
  const defaultIconLibrary = useIconLibrary();
@@ -10668,7 +10871,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10668
10871
  label,
10669
10872
  id,
10670
10873
  labelProps,
10671
- rendersLabel: !insideField && !isMaxReached,
10874
+ rendersLabel: ownField && !isMaxReached,
10672
10875
  });
10673
10876
  // Get display value from tag
10674
10877
  const getDisplayValue = (tag) => {
@@ -10940,7 +11143,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10940
11143
  'is-active': index === highlightedIndex,
10941
11144
  }), onClick: () => addTag(item), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: item }, index))) }) })), showCounter && jsx("small", { className: counterClasses, children: counterText }), name &&
10942
11145
  tags.map((tag, i) => (jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
10943
- if (!insideField) {
11146
+ if (ownField) {
10944
11147
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
10945
11148
  }
10946
11149
  return (jsxs(Fragment, { children: [taginputElement, messageEl] }));
@@ -11093,21 +11296,44 @@ fieldClassName, controlClassName,
11093
11296
  ...inputProps }, ref) => {
11094
11297
  const insideField = useInsideField();
11095
11298
  const insideControl = useInsideControl();
11299
+ const ownField = rendersOwnField('Input', {
11300
+ insideField,
11301
+ insideControl,
11302
+ label,
11303
+ message,
11304
+ horizontal,
11305
+ fieldClassName,
11306
+ });
11307
+ const ownControl = rendersOwnControl('Input', {
11308
+ insideControl,
11309
+ isLoading,
11310
+ iconLeft,
11311
+ iconLeftName,
11312
+ iconLeftSize,
11313
+ iconRight,
11314
+ iconRightName,
11315
+ iconRightSize,
11316
+ hasIconsLeft,
11317
+ hasIconsRight,
11318
+ isExpanded,
11319
+ controlSize,
11320
+ controlClassName,
11321
+ });
11096
11322
  const { controlId, fieldLabelProps } = useAutoLabelId({
11097
11323
  label,
11098
11324
  id: inputProps.id,
11099
11325
  labelProps,
11100
- rendersLabel: !insideField,
11326
+ rendersLabel: ownField,
11101
11327
  });
11102
11328
  const helpClass = usePrefixedClassNames('help', {
11103
11329
  [`is-${messageColor}`]: !!messageColor,
11104
11330
  });
11105
11331
  let content = jsx(InputBase, { ref: ref, id: controlId, ...inputProps });
11106
- if (!insideControl) {
11332
+ if (ownControl) {
11107
11333
  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
11334
  }
11109
11335
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
11110
- if (!insideField) {
11336
+ if (ownField) {
11111
11337
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11112
11338
  }
11113
11339
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -11143,21 +11369,40 @@ fieldClassName, controlClassName,
11143
11369
  ...selectProps }, ref) => {
11144
11370
  const insideField = useInsideField();
11145
11371
  const insideControl = useInsideControl();
11372
+ const ownField = rendersOwnField('Select', {
11373
+ insideField,
11374
+ insideControl,
11375
+ label,
11376
+ message,
11377
+ horizontal,
11378
+ fieldClassName,
11379
+ });
11380
+ // Not isLoading, which the select draws itself and so keeps in a Control.
11381
+ const ownControl = rendersOwnControl('Select', {
11382
+ insideControl,
11383
+ iconLeft,
11384
+ iconLeftName,
11385
+ iconLeftSize,
11386
+ hasIconsLeft,
11387
+ isExpanded,
11388
+ controlSize,
11389
+ controlClassName,
11390
+ });
11146
11391
  const { controlId, fieldLabelProps } = useAutoLabelId({
11147
11392
  label,
11148
11393
  id: selectProps.id,
11149
11394
  labelProps,
11150
- rendersLabel: !insideField,
11395
+ rendersLabel: ownField,
11151
11396
  });
11152
11397
  const helpClass = usePrefixedClassNames('help', {
11153
11398
  [`is-${messageColor}`]: !!messageColor,
11154
11399
  });
11155
11400
  let content = jsx(SelectBase, { ref: ref, id: controlId, ...selectProps });
11156
- if (!insideControl) {
11401
+ if (ownControl) {
11157
11402
  content = (jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11158
11403
  }
11159
11404
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
11160
- if (!insideField) {
11405
+ if (ownField) {
11161
11406
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11162
11407
  }
11163
11408
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -11194,21 +11439,35 @@ fieldClassName, controlClassName,
11194
11439
  ...textAreaProps }, ref) => {
11195
11440
  const insideField = useInsideField();
11196
11441
  const insideControl = useInsideControl();
11442
+ const ownField = rendersOwnField('TextArea', {
11443
+ insideField,
11444
+ insideControl,
11445
+ label,
11446
+ message,
11447
+ horizontal,
11448
+ fieldClassName,
11449
+ });
11450
+ const ownControl = rendersOwnControl('TextArea', {
11451
+ insideControl,
11452
+ isLoading: controlIsLoading,
11453
+ controlSize,
11454
+ controlClassName,
11455
+ });
11197
11456
  const { controlId, fieldLabelProps } = useAutoLabelId({
11198
11457
  label,
11199
11458
  id: textAreaProps.id,
11200
11459
  labelProps,
11201
- rendersLabel: !insideField,
11460
+ rendersLabel: ownField,
11202
11461
  });
11203
11462
  const helpClass = usePrefixedClassNames('help', {
11204
11463
  [`is-${messageColor}`]: !!messageColor,
11205
11464
  });
11206
11465
  let content = jsx(TextAreaBase, { ref: ref, id: controlId, ...textAreaProps });
11207
- if (!insideControl) {
11466
+ if (ownControl) {
11208
11467
  content = (jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
11209
11468
  }
11210
11469
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
11211
- if (!insideField) {
11470
+ if (ownField) {
11212
11471
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
11213
11472
  }
11214
11473
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -11295,6 +11554,21 @@ function makeDate(year, month = 0, day = 1) {
11295
11554
  d.setFullYear(year, month, day);
11296
11555
  return d;
11297
11556
  }
11557
+ /**
11558
+ * The earliest year the pickers accept. HTML's date, month and datetime-local
11559
+ * inputs hold no year below 1, so the calendar, typing and parsing stop there
11560
+ * too.
11561
+ */
11562
+ const FIRST_YEAR = 1;
11563
+ /**
11564
+ * The lower bound a picker applies: `min`, raised to local midnight on
11565
+ * 1 January of {@link FIRST_YEAR} when it is earlier or absent. A `min` in
11566
+ * range comes back as the same object.
11567
+ */
11568
+ function floorMin(min) {
11569
+ const floor = makeDate(FIRST_YEAR);
11570
+ return min && min.getTime() >= floor.getTime() ? min : floor;
11571
+ }
11298
11572
  /**
11299
11573
  * First instant of the day, month or year containing `d`. Works by setters on
11300
11574
  * a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
@@ -11388,12 +11662,14 @@ function setTimeOfDay(d, parts) {
11388
11662
  r.setSeconds(parts.seconds);
11389
11663
  return r;
11390
11664
  }
11665
+ /**
11666
+ * A copy of `d` moved into `[min, max]`. When `min` is after `max` nothing is
11667
+ * in range and `min` wins, so clamping twice gives the same date and a
11668
+ * picker's focus stays off the years before 1 under a `max` before then.
11669
+ */
11391
11670
  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);
11671
+ const capped = max && isAfter(d, max) ? max : d;
11672
+ return new Date(min && isBefore(capped, min) ? min : capped);
11397
11673
  }
11398
11674
  /**
11399
11675
  * Snap a Date's time-of-day to the nearest grid defined by the given
@@ -11466,7 +11742,9 @@ function formatToken(d, token) {
11466
11742
  case 'YYYY':
11467
11743
  // Zero-pad to a fixed 4 chars so the rendered width matches the
11468
11744
  // segment-map width during digit-by-digit keyboard entry (and to match
11469
- // ISO 8601). Years >9999 keep their natural width.
11745
+ // ISO 8601), so year 19 shows as `0019`. Years >9999 keep their natural
11746
+ // width. The pickers commit no year below 1, so a minus sign reaches
11747
+ // this only from a `value` passed in.
11470
11748
  return pad(d.getFullYear(), 4);
11471
11749
  case 'YY':
11472
11750
  return pad(d.getFullYear() % 100);
@@ -11740,9 +12018,10 @@ function getMonthNames(locale, length = 'long') {
11740
12018
 
11741
12019
  /**
11742
12020
  * 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.
12021
+ * year list is on show, for a popover to put focus on as it opens. It runs
12022
+ * against the whole panel, so it matches only inside the grid or the list.
11744
12023
  */
11745
- const CALENDAR_FOCUSED_CELL = '[data-focused="true"], [data-focused-year="true"]';
12024
+ const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
11746
12025
  const BASE_VIEW = {
11747
12026
  day: 'days',
11748
12027
  month: 'months',
@@ -11816,10 +12095,11 @@ function nearestEnabled(i, disabled) {
11816
12095
  return i;
11817
12096
  }
11818
12097
  /**
11819
- * The year list's keyboard when it is the selection surface: arrows move a
11820
- * year or a row, Home/End go to the list's ends. Returns the year to start
11821
- * from and the direction to search past disabled years, or `null` for a key
11822
- * the list doesn't handle.
12098
+ * The year list's keyboard, as the selection surface and as navigation:
12099
+ * arrows move a year or a row, Home/End go to the list's ends. Returns the
12100
+ * year to start from and the direction to search past disabled years, or
12101
+ * `null` for a key the list doesn't handle. As navigation no year is
12102
+ * disabled, so only the year is used.
11823
12103
  */
11824
12104
  function yearStep(key, focusedYear, years) {
11825
12105
  switch (key) {
@@ -11839,7 +12119,7 @@ function yearStep(key, focusedYear, years) {
11839
12119
  return null;
11840
12120
  }
11841
12121
  }
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', }) => {
12122
+ 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
12123
  const gridRef = useRef(null);
11844
12124
  const monthGridRef = useRef(null);
11845
12125
  const yearGridRef = useRef(null);
@@ -11849,6 +12129,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11849
12129
  const isDayGranularity = granularity === 'day';
11850
12130
  const isYearGranularity = granularity === 'year';
11851
12131
  const [view, setView] = useState(baseView);
12132
+ // Nothing before year 1 can be picked or reached, as in HTML's date inputs.
12133
+ const min = useMemo(() => floorMin(minProp), [minProp]);
11852
12134
  const computedDayNames = useMemo(() => {
11853
12135
  if (dayNames && dayNames.length === 7)
11854
12136
  return dayNames;
@@ -11919,7 +12201,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11919
12201
  const moveFocus = useCallback((next, direction) => {
11920
12202
  let candidate = next;
11921
12203
  for (let i = 0; i < 366; i++) {
11922
- if (min && candidate.getTime() < startOfDay(min).getTime())
12204
+ if (candidate.getTime() < startOfDay(min).getTime())
11923
12205
  return;
11924
12206
  if (max && candidate.getTime() > startOfDay(max).getTime())
11925
12207
  return;
@@ -11985,7 +12267,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
11985
12267
  let candidate = next;
11986
12268
  for (let i = 0; i < 120; i++) {
11987
12269
  const month = startOfMonth(candidate).getTime();
11988
- if (min && month < startOfMonth(min).getTime())
12270
+ if (month < startOfMonth(min).getTime())
11989
12271
  return;
11990
12272
  if (max && month > startOfMonth(max).getTime())
11991
12273
  return;
@@ -12033,25 +12315,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12033
12315
  const today = new Date();
12034
12316
  const todayYear = today.getFullYear();
12035
12317
  // 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);
12318
+ // time over the month grid, kept inside min/max. A step is off once the
12319
+ // month or year on show holds the bound or lies past it, as picking a year
12320
+ // from the list can leave it.
12039
12321
  const prevAnchor = isDayGranularity
12040
- ? prevMonthAnchor
12322
+ ? addMonths(focusedDate, -1)
12041
12323
  : clampDate(addYears(focusedDate, -1), min, max);
12042
12324
  const nextAnchor = isDayGranularity
12043
- ? nextMonthAnchor
12325
+ ? addMonths(focusedDate, 1)
12044
12326
  : clampDate(addYears(focusedDate, 1), min, max);
12327
+ const focusedMonth = startOfMonth(focusedDate).getTime();
12045
12328
  const prevDisabled = isDayGranularity
12046
- ? !!(min &&
12047
- prevMonthAnchor.getTime() < startOfDay(min).getTime() &&
12048
- isSameMonth(focusedDate, min))
12049
- : !!(min && focusedYear <= min.getFullYear());
12329
+ ? focusedMonth <= startOfMonth(min).getTime()
12330
+ : focusedYear <= min.getFullYear();
12050
12331
  const nextDisabled = isDayGranularity
12051
- ? !!(max &&
12052
- nextMonthAnchor.getTime() > startOfDay(max).getTime() &&
12053
- isSameMonth(focusedDate, max))
12054
- : !!(max && focusedYear >= max.getFullYear());
12332
+ ? !!max && focusedMonth >= startOfMonth(max).getTime()
12333
+ : !!max && focusedYear >= max.getFullYear();
12055
12334
  const labelId = id ? `${id}-label` : undefined;
12056
12335
  const monthLabel = isDayGranularity
12057
12336
  ? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
@@ -12062,7 +12341,9 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12062
12341
  const [mountYear] = useState(focusedYear);
12063
12342
  const windowYear = isYearGranularity ? mountYear : focusedYear;
12064
12343
  const yearList = useMemo(() => {
12065
- const minYear = min ? min.getFullYear() : windowYear - 100;
12344
+ const minYear = minProp
12345
+ ? min.getFullYear()
12346
+ : Math.max(FIRST_YEAR, windowYear - 100);
12066
12347
  const maxYear = max ? max.getFullYear() : windowYear + 100;
12067
12348
  const [lo, hi] = yearsRange ?? [minYear, maxYear];
12068
12349
  const start = Math.max(lo, minYear);
@@ -12071,20 +12352,31 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12071
12352
  for (let y = start; y <= end; y++)
12072
12353
  out.push(y);
12073
12354
  return out;
12074
- }, [windowYear, min, max, yearsRange]);
12355
+ }, [windowYear, minProp, min, max, yearsRange]);
12075
12356
  // Only the selection surface disables years; as navigation every listed
12076
12357
  // year can be visited.
12077
12358
  const disabledYears = useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
12078
12359
  // The year list's one tab stop, after the month grid's: when the focused
12079
12360
  // 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.
12361
+ // and focusing it makes it the focused year. As the selection surface a
12362
+ // year outside a non-empty list starts from the end it fell past, so the
12363
+ // list keeps a stop. As navigation it marks the current year, so a year
12364
+ // outside the list marks no option, and the stop itself follows focus.
12082
12365
  const tabStopYear = useMemo(() => {
12083
- const i = yearList.indexOf(focusedYear);
12084
- if (i < 0)
12085
- return focusedYear;
12366
+ let i = yearList.indexOf(focusedYear);
12367
+ if (i < 0) {
12368
+ if (!isYearGranularity || yearList.length === 0)
12369
+ return focusedYear;
12370
+ i = focusedYear < yearList[0] ? 0 : yearList.length - 1;
12371
+ }
12086
12372
  return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
12087
- }, [yearList, focusedYear, disabledYears]);
12373
+ }, [yearList, focusedYear, disabledYears, isYearGranularity]);
12374
+ // As navigation the keys move focus and leave the focused year alone, so
12375
+ // the tab stop goes to the year that last had focus while it is listed,
12376
+ // and Tab leaves the list rather than landing on the current year. It
12377
+ // starts over each time the list opens.
12378
+ const [navYear, setNavYear] = useState(null);
12379
+ const listTabStop = navYear !== null && yearList.includes(navYear) ? navYear : tabStopYear;
12088
12380
  // Which of the focused year's months have no selectable day.
12089
12381
  const disabledMonths = useMemo(() => granularity === 'month'
12090
12382
  ? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
@@ -12120,19 +12412,39 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12120
12412
  }
12121
12413
  }, [yearList, disabledYears, onFocusedDateChange, min, max]);
12122
12414
  const handleYearListKeyDown = useCallback((e) => {
12123
- if (!isYearGranularity) {
12124
- // As navigation, Escape returns to the grid the list was opened from.
12125
- if (e.key === 'Escape') {
12126
- e.preventDefault();
12127
- setView(baseView);
12128
- }
12415
+ if (isYearGranularity) {
12416
+ const step = yearStep(e.key, focusedYear, yearList);
12417
+ if (!step)
12418
+ return;
12419
+ e.preventDefault();
12420
+ moveYearFocus(step[0], step[1]);
12421
+ return;
12422
+ }
12423
+ // As navigation, Escape returns to the grid the list was opened from.
12424
+ // It stops there, so a popover around the calendar stays open and only
12425
+ // a second Escape closes it.
12426
+ if (e.key === 'Escape') {
12427
+ e.preventDefault();
12428
+ e.stopPropagation();
12429
+ setView(baseView);
12129
12430
  return;
12130
12431
  }
12131
- const step = yearStep(e.key, focusedYear, yearList);
12432
+ // The other keys move DOM focus from the year that has it and leave
12433
+ // the focused date alone. The list is centred on that date, so moving
12434
+ // it would reflow the list under the keyboard, and it would turn the
12435
+ // grid behind the list before anything is picked. Enter or Space
12436
+ // clicks the year with focus, which picks it.
12437
+ const options = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
12438
+ const from = options.indexOf(e.target);
12439
+ if (from < 0)
12440
+ return;
12441
+ const step = yearStep(e.key, yearList[from], yearList);
12132
12442
  if (!step)
12133
12443
  return;
12134
12444
  e.preventDefault();
12135
- moveYearFocus(step[0], step[1]);
12445
+ // The listed years run one apart, so a year's index is its distance
12446
+ // from the first. A step past either end finds no option.
12447
+ options[step[0] - yearList[0]]?.focus();
12136
12448
  }, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
12137
12449
  // The day or month grid takes focus as the popover opens on it, or comes
12138
12450
  // back to it from the year list.
@@ -12203,7 +12515,10 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12203
12515
  list.querySelector('[data-focused-year="true"]')?.focus();
12204
12516
  }, [isYearGranularity, focusedYear]);
12205
12517
  const navArrow = (d) => (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
12206
- return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
12518
+ return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => {
12519
+ setNavYear(null);
12520
+ setView(v => (v === 'years' ? baseView : 'years'));
12521
+ }, children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
12207
12522
  const disabled = isDateUnselectable(cell.date);
12208
12523
  const isSelected = !!value && isSameDay(value, cell.date);
12209
12524
  const isFocused = isSameDay(cell.date, tabStopDay);
@@ -12263,10 +12578,16 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
12263
12578
  'is-today': isToday,
12264
12579
  'is-disabled': disabled,
12265
12580
  });
12266
- return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: isFocused ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
12267
- // Reached by Tab while the focused year is disabled, or by
12268
- // pointer: keys move on from here.
12269
- if (isYearGranularity && year !== focusedYear) {
12581
+ return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
12582
+ // As navigation, focusing a year by key or pointer only
12583
+ // moves the tab stop: nothing is committed until a year is
12584
+ // picked. As the selection surface it is reached by Tab
12585
+ // while the focused year is disabled, or by pointer, and
12586
+ // keys move on from here.
12587
+ if (!isYearGranularity) {
12588
+ setNavYear(year);
12589
+ }
12590
+ else if (year !== focusedYear) {
12270
12591
  onFocusedDateChange(clampDate(makeDate(year), min, max));
12271
12592
  }
12272
12593
  }, onClick: () => isYearGranularity
@@ -12291,11 +12612,10 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12291
12612
  active: isOpen,
12292
12613
  position,
12293
12614
  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,
12615
+ // The gap comes from `--bulma-picker-popover-offset` as a margin, in place
12616
+ // or portaled, so the coordinates leave it out and `auto` reads it off
12617
+ // the panel.
12618
+ offset: 0,
12299
12619
  styledGap: marginGap,
12300
12620
  });
12301
12621
  useEffect(() => {
@@ -12741,33 +13061,35 @@ function useSegmentedEntry(params) {
12741
13061
  // within the same event tick.
12742
13062
  const typedDigitsRef = useRef('');
12743
13063
  // 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.
13064
+ // leaves.
12745
13065
  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);
13066
+ // Whether the text is still what the picker put there, so nothing in it was
13067
+ // typed: the value's display, or an empty picker's seed until an edit drops
13068
+ // it. Against a value it compares text alone, so typing that ends back on
13069
+ // the display counts as untouched.
13070
+ const isUntouched = useCallback((s) => value
13071
+ ? s === formatFn(value, format, locale)
13072
+ : seedRef.current !== null && s === seedRef.current, [value, formatFn, format, locale]);
12749
13073
  // A seed the popover never turned into a value closes with it, wherever
12750
13074
  // focus goes, so a dismissed empty field stays empty.
12751
13075
  //
12752
13076
  // As the popover closes, its focus trap hands focus back to the input. That
12753
13077
  // 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.
13078
+ // `openOnFocus`, or seed an empty picker. `closingRef` is set in the layout
13079
+ // phase of the commit that closes it and cleared by that commit's effects,
13080
+ // and React runs a commit's effect cleanups, the trap's among them, before
13081
+ // any of its effects, so a focus in between is the one handed back.
12759
13082
  const closingRef = useRef(false);
12760
13083
  const wasOpenRef = useRef(isOpen);
12761
13084
  useLayoutEffect(() => {
12762
13085
  if (wasOpenRef.current && !isOpen) {
12763
13086
  closingRef.current = true;
12764
- if (!value && seedRef.current !== null && text === seedRef.current) {
13087
+ if (!value && isUntouched(text))
12765
13088
  setText('');
12766
- }
12767
13089
  seedRef.current = null;
12768
13090
  }
12769
13091
  wasOpenRef.current = isOpen;
12770
- }, [isOpen, value, text, setText]);
13092
+ }, [isOpen, value, isUntouched, text, setText]);
12771
13093
  useEffect(() => {
12772
13094
  closingRef.current = false;
12773
13095
  }, [isOpen]);
@@ -12845,13 +13167,18 @@ function useSegmentedEntry(params) {
12845
13167
  }
12846
13168
  setActiveSegmentIdx(null);
12847
13169
  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)) {
13170
+ // Leaving commits only what was typed. Re-parsing the value's display
13171
+ // could drop what the format leaves out, such as seconds, and a seed
13172
+ // nobody typed over is not the user's date.
13173
+ const untouched = isUntouched(text);
13174
+ // An open popover keeps an unused seed until it closes, so one portaled
13175
+ // outside the picker, whose focus reads as leaving, keeps it as one
13176
+ // inside does.
13177
+ if (!isOpen)
13178
+ seedRef.current = null;
13179
+ if (untouched) {
13180
+ if (!value && !isOpen)
13181
+ setText('');
12855
13182
  onBlur?.(e);
12856
13183
  return;
12857
13184
  }
@@ -12865,6 +13192,8 @@ function useSegmentedEntry(params) {
12865
13192
  onBlur?.(e);
12866
13193
  }, [
12867
13194
  containerRef,
13195
+ isUntouched,
13196
+ isOpen,
12868
13197
  tryParse,
12869
13198
  text,
12870
13199
  isAllowed,
@@ -12878,7 +13207,6 @@ function useSegmentedEntry(params) {
12878
13207
  ]);
12879
13208
  const handleFocus = useCallback((e) => {
12880
13209
  const handedBack = closingRef.current;
12881
- handedBackRef.current = handedBack;
12882
13210
  if (!handedBack && openOnFocus && popover && !disabled && !readOnly) {
12883
13211
  setOpen(true);
12884
13212
  }
@@ -13046,12 +13374,20 @@ function useSegmentedEntry(params) {
13046
13374
  }
13047
13375
  if (e.key === 'Enter') {
13048
13376
  e.preventDefault();
13049
- const parsed = tryParse(text);
13050
- if (parsed && isAllowed(parsed)) {
13051
- commitValue(parsed);
13377
+ // Untouched text has nothing to commit, as on leaving, but Enter
13378
+ // still confirms what the field shows.
13379
+ if (isUntouched(text)) {
13052
13380
  if (closeOnSelect)
13053
13381
  setOpen(false);
13054
13382
  }
13383
+ else {
13384
+ const parsed = tryParse(text);
13385
+ if (parsed && isAllowed(parsed)) {
13386
+ commitValue(parsed);
13387
+ if (closeOnSelect)
13388
+ setOpen(false);
13389
+ }
13390
+ }
13055
13391
  }
13056
13392
  onKeyDown?.(e);
13057
13393
  }, [
@@ -13065,6 +13401,7 @@ function useSegmentedEntry(params) {
13065
13401
  setOpen,
13066
13402
  closeOnSelect,
13067
13403
  popover,
13404
+ isUntouched,
13068
13405
  tryParse,
13069
13406
  text,
13070
13407
  isAllowed,
@@ -13113,9 +13450,12 @@ const pad2$1 = (n) => String(n).padStart(2, '0');
13113
13450
  * The value as the native input and the hidden form input carry it:
13114
13451
  * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
13115
13452
  * `<input type="month">` use. The year is padded to four digits, as those
13116
- * inputs require and as the `YYYY` token displays it.
13453
+ * inputs require and as the `YYYY` token displays it. HTML has no such shape
13454
+ * for a year before 1, so a date then is empty, as those inputs would make it.
13117
13455
  */
13118
13456
  const toIsoValue = (d, granularity) => {
13457
+ if (d.getFullYear() < FIRST_YEAR)
13458
+ return '';
13119
13459
  const year = String(d.getFullYear()).padStart(4, '0');
13120
13460
  if (granularity === 'year')
13121
13461
  return year;
@@ -13156,14 +13496,19 @@ const SKIPPED_SEGMENTS = {
13156
13496
  * @returns {JSX.Element}
13157
13497
  */
13158
13498
  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;
13499
+ 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;
13500
+ // The launcher gives way to the loading spinner of a Control this sits in.
13501
+ const controlLoading = useControlLoading();
13502
+ const triggerIcon = triggerIconProp ?? !controlLoading;
13160
13503
  const t = mergeLabels(labels);
13161
13504
  const isDayGranularity = granularity === 'day';
13162
13505
  const resolvedFormat = format ?? DEFAULT_FORMATS[granularity];
13163
13506
  const isControlled = controlledValue !== undefined;
13164
13507
  const [internalValue, setInternalValue] = useState(defaultValue ?? null);
13165
13508
  const value = isControlled ? (controlledValue ?? null) : internalValue;
13166
- const initialFocused = useMemo(() => clampDate(value ?? new Date(), min, max),
13509
+ // Nothing before year 1 is in range, as in HTML's date inputs.
13510
+ const lowerBound = useMemo(() => floorMin(min), [min]);
13511
+ const initialFocused = useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
13167
13512
  // intentionally only on mount: keep focusedDate stable until value/open change
13168
13513
  // eslint-disable-next-line react-hooks/exhaustive-deps
13169
13514
  []);
@@ -13171,8 +13516,8 @@ const DateInputBase = forwardRef((props, ref) => {
13171
13516
  // Re-clamp focusedDate if min/max change after mount so the focused cell
13172
13517
  // never disappears outside the displayable range.
13173
13518
  useEffect(() => {
13174
- setFocusedDate(prev => clampDate(prev, min, max));
13175
- }, [min, max]);
13519
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
13520
+ }, [lowerBound, max]);
13176
13521
  const [open, setOpenState] = useState(false);
13177
13522
  const [text, setText] = useState(value ? formatDate(value, resolvedFormat, locale) : '');
13178
13523
  const inputRef = useRef(null);
@@ -13230,7 +13575,7 @@ const DateInputBase = forwardRef((props, ref) => {
13230
13575
  }, [isControlled, onChange, isDayGranularity, granularity]);
13231
13576
  // At month or year granularity the bounds widen to the periods holding
13232
13577
  // them, so the month or year containing `min` stays reachable.
13233
- const periodMin = useMemo(() => (min && !isDayGranularity ? startOfPeriod(min, granularity) : min), [min, isDayGranularity, granularity]);
13578
+ const periodMin = useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
13234
13579
  const periodMax = useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
13235
13580
  const handleSelect = useCallback((d) => {
13236
13581
  if (!isWithin(d, periodMin, periodMax))
@@ -13281,7 +13626,7 @@ const DateInputBase = forwardRef((props, ref) => {
13281
13626
  ]);
13282
13627
  // Typing, and the re-parse on blur or Enter, that lands in the period the
13283
13628
  // value already holds commits nothing, so a `value` elsewhere in its month
13284
- // or year isn't rewritten by focus passing through the field.
13629
+ // or year changes only when the user picks or types a different period.
13285
13630
  const commitTyped = useCallback((next) => {
13286
13631
  if (next &&
13287
13632
  value &&
@@ -13347,7 +13692,7 @@ const DateInputBase = forwardRef((props, ref) => {
13347
13692
  ? fromIsoValue(e.target.value, granularity)
13348
13693
  : null;
13349
13694
  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 }));
13695
+ }, 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
13696
  }
13352
13697
  const chooseLabel = granularity === 'month'
13353
13698
  ? t.chooseMonth
@@ -13356,7 +13701,11 @@ const DateInputBase = forwardRef((props, ref) => {
13356
13701
  : t.chooseDate;
13357
13702
  const calendar = (jsx(Calendar
13358
13703
  // 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));
13704
+ , { 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,
13705
+ // The popover panel takes `popoverId`, so the calendar inside it
13706
+ // takes one of its own. Inline there is no panel, and the calendar
13707
+ // keeps the id it released with.
13708
+ id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
13360
13709
  if (inline) {
13361
13710
  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
13711
  }
@@ -13384,29 +13733,57 @@ DateInputBase.displayName = 'DateInputBase';
13384
13733
  * shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
13385
13734
  * />
13386
13735
  */
13387
- 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) => {
13736
+ const DateInput = forwardRef((props, ref) => {
13737
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
13388
13738
  const insideField = useInsideField();
13389
13739
  const insideControl = useInsideControl();
13740
+ const ownField = rendersOwnField('DateInput', {
13741
+ insideField,
13742
+ insideControl,
13743
+ label,
13744
+ message,
13745
+ horizontal,
13746
+ fieldClassName,
13747
+ });
13748
+ // Inline mode renders a bare calendar with no input, so the Control's
13749
+ // icon-left container has nothing to anchor to, and it renders no
13750
+ // Control inside one or out. The left icon is checked as the caller
13751
+ // passed it: the default glyph is this component's own choice, not a
13752
+ // prop the caller set.
13753
+ const ownControl = rendersOwnControl('DateInput', {
13754
+ insideControl,
13755
+ inline: baseProps.inline,
13756
+ isLoading,
13757
+ iconLeft,
13758
+ iconLeftName: props.iconLeftName,
13759
+ iconLeftSize,
13760
+ iconRight,
13761
+ iconRightName,
13762
+ iconRightSize,
13763
+ hasIconsLeft,
13764
+ hasIconsRight,
13765
+ isExpanded,
13766
+ controlSize,
13767
+ controlClassName,
13768
+ });
13390
13769
  const { controlId, fieldLabelProps } = useAutoLabelId({
13391
13770
  label,
13392
13771
  id: baseProps.id,
13393
13772
  labelProps,
13394
13773
  // Inline mode renders a bare calendar with no input to label.
13395
- rendersLabel: !insideField && !baseProps.inline,
13774
+ rendersLabel: ownField && !baseProps.inline,
13396
13775
  });
13397
13776
  const helpClass = usePrefixedClassNames('help', {
13398
13777
  [`is-${messageColor}`]: !!messageColor,
13399
13778
  });
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 }));
13403
- // Inline mode renders a bare calendar with no input, so the Control's
13404
- // icon-left container has nothing to anchor to. Skip the Control wrap.
13405
- if (!insideControl && !baseProps.inline) {
13779
+ // The base hides its launcher while the Control it sits in is loading,
13780
+ // whether that is the one rendered below or an enclosing one.
13781
+ let content = (jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps }));
13782
+ if (ownControl) {
13406
13783
  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
13784
  }
13408
13785
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
13409
- if (!insideField) {
13786
+ if (ownField) {
13410
13787
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
13411
13788
  }
13412
13789
  return (jsxs(Fragment, { children: [content, messageEl] }));
@@ -14280,7 +14657,10 @@ const fromIsoTime = (s) => {
14280
14657
  * @returns {JSX.Element}
14281
14658
  */
14282
14659
  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;
14660
+ 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;
14661
+ // The launcher gives way to the loading spinner of a Control this sits in.
14662
+ const controlLoading = useControlLoading();
14663
+ const triggerIcon = triggerIconProp ?? !controlLoading;
14284
14664
  // Platform-appropriate feedback routing: when `haptics` is opted in and
14285
14665
  // the runtime has no Vibration API (notably iOS Safari), enable the
14286
14666
  // audio thunk so the user still gets *some* tactile cue. When vibrate
@@ -14357,6 +14737,8 @@ const TimeInputBase = forwardRef((props, ref) => {
14357
14737
  useEffect(() => {
14358
14738
  setText(value ? formatTime(value, defaultFormat, locale) : '');
14359
14739
  }, [value, defaultFormat, locale]);
14740
+ // A value's date before year 1 is out of range, as in the date pickers.
14741
+ const lowerBound = useMemo(() => floorMin(min), [min]);
14360
14742
  const commitValue = useCallback((next) => {
14361
14743
  if (!isControlled)
14362
14744
  setInternalValue(next);
@@ -14365,10 +14747,10 @@ const TimeInputBase = forwardRef((props, ref) => {
14365
14747
  const handleSpinnerChange = useCallback((parts) => {
14366
14748
  const base = value ?? new Date();
14367
14749
  const next = setTimeOfDay(base, parts);
14368
- if (!isWithin(next, min, max))
14750
+ if (!isWithin(next, lowerBound, max))
14369
14751
  return;
14370
14752
  commitValue(next);
14371
- }, [value, min, max, commitValue]);
14753
+ }, [value, lowerBound, max, commitValue]);
14372
14754
  const tryParse = useCallback((s) => {
14373
14755
  const trimmed = s.trim();
14374
14756
  if (!trimmed)
@@ -14400,7 +14782,7 @@ const TimeInputBase = forwardRef((props, ref) => {
14400
14782
  setText,
14401
14783
  makeBaseDate,
14402
14784
  locale,
14403
- min,
14785
+ min: lowerBound,
14404
14786
  max,
14405
14787
  isBlocked: unselectableTimes,
14406
14788
  disabled,
@@ -14479,37 +14861,69 @@ TimeInputBase.displayName = 'TimeInputBase';
14479
14861
  * @example
14480
14862
  * <TimeInput label="Slot" hourFormat="12" incrementMinutes={15} />
14481
14863
  */
14482
- 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) => {
14864
+ const TimeInput = forwardRef((props, ref) => {
14865
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
14483
14866
  const insideField = useInsideField();
14484
14867
  const insideControl = useInsideControl();
14868
+ const ownField = rendersOwnField('TimeInput', {
14869
+ insideField,
14870
+ insideControl,
14871
+ label,
14872
+ message,
14873
+ horizontal,
14874
+ fieldClassName,
14875
+ });
14876
+ // Inline mode renders a bare panel with no input, so the Control's
14877
+ // icon-left container has nothing to anchor to, and it renders no
14878
+ // Control inside one or out. The left icon is checked as the caller
14879
+ // passed it: the default glyph is this component's own choice, not a
14880
+ // prop the caller set.
14881
+ const ownControl = rendersOwnControl('TimeInput', {
14882
+ insideControl,
14883
+ inline: baseProps.inline,
14884
+ isLoading,
14885
+ iconLeft,
14886
+ iconLeftName: props.iconLeftName,
14887
+ iconLeftSize,
14888
+ iconRight,
14889
+ iconRightName,
14890
+ iconRightSize,
14891
+ hasIconsLeft,
14892
+ hasIconsRight,
14893
+ isExpanded,
14894
+ controlSize,
14895
+ controlClassName,
14896
+ });
14485
14897
  const { controlId, fieldLabelProps } = useAutoLabelId({
14486
14898
  label,
14487
14899
  id: baseProps.id,
14488
14900
  labelProps,
14489
14901
  // Inline mode renders a bare panel with no input to label.
14490
- rendersLabel: !insideField && !baseProps.inline,
14902
+ rendersLabel: ownField && !baseProps.inline,
14491
14903
  });
14492
14904
  const helpClass = usePrefixedClassNames('help', {
14493
14905
  [`is-${messageColor}`]: !!messageColor,
14494
14906
  });
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 }));
14498
- // Inline mode renders a bare panel with no input, so the Control's
14499
- // icon-left container has nothing to anchor to. Skip the Control wrap.
14500
- if (!insideControl && !baseProps.inline) {
14907
+ // The base hides its launcher while the Control it sits in is loading,
14908
+ // whether that is the one rendered below or an enclosing one.
14909
+ let content = (jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps }));
14910
+ if (ownControl) {
14501
14911
  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
14912
  }
14503
14913
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
14504
- if (!insideField) {
14914
+ if (ownField) {
14505
14915
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14506
14916
  }
14507
14917
  return (jsxs(Fragment, { children: [content, messageEl] }));
14508
14918
  });
14509
14919
  TimeInput.displayName = 'TimeInput';
14510
14920
 
14511
- // The year is padded to four digits, as `datetime-local` requires.
14921
+ // The year is padded to four digits, as `datetime-local` requires. HTML has
14922
+ // no such shape for a year before 1, so a date then is empty, as that input
14923
+ // would make it.
14512
14924
  const toIsoDateTime = (d, withSeconds) => {
14925
+ if (d.getFullYear() < FIRST_YEAR)
14926
+ return '';
14513
14927
  const yyyy = String(d.getFullYear()).padStart(4, '0');
14514
14928
  const mo = String(d.getMonth() + 1).padStart(2, '0');
14515
14929
  const dd = String(d.getDate()).padStart(2, '0');
@@ -14542,7 +14956,10 @@ const fromIsoDateTime = (s) => {
14542
14956
  const DateTimeInputBase = forwardRef((props, ref) => {
14543
14957
  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
14958
  // 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;
14959
+ 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;
14960
+ // The launcher gives way to the loading spinner of a Control this sits in.
14961
+ const controlLoading = useControlLoading();
14962
+ const triggerIcon = triggerIconProp ?? !controlLoading;
14546
14963
  const t = mergeLabels(labels);
14547
14964
  // Platform-appropriate feedback routing for the time wheel: on iOS (no
14548
14965
  // navigator.vibrate) the audio thunk fills in; on Android the real haptic
@@ -14552,14 +14969,16 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14552
14969
  const isControlled = controlledValue !== undefined;
14553
14970
  const [internalValue, setInternalValue] = useState(defaultValue ?? null);
14554
14971
  const value = isControlled ? (controlledValue ?? null) : internalValue;
14555
- const initialFocused = useMemo(() => clampDate(value ?? new Date(), min, max),
14972
+ // Nothing before year 1 is in range, as in HTML's date inputs.
14973
+ const lowerBound = useMemo(() => floorMin(min), [min]);
14974
+ const initialFocused = useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
14556
14975
  // eslint-disable-next-line react-hooks/exhaustive-deps
14557
14976
  []);
14558
14977
  const [focusedDate, setFocusedDate] = useState(initialFocused);
14559
14978
  // Re-clamp focusedDate when min/max change so the focused cell stays valid.
14560
14979
  useEffect(() => {
14561
- setFocusedDate(prev => clampDate(prev, min, max));
14562
- }, [min, max]);
14980
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
14981
+ }, [lowerBound, max]);
14563
14982
  const defaultFormat = format ??
14564
14983
  (hourFormat === '12'
14565
14984
  ? enableSeconds
@@ -14656,18 +15075,18 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14656
15075
  minutes: value?.getMinutes() ?? 0,
14657
15076
  seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
14658
15077
  });
14659
- if (!isWithin(merged, min, max))
15078
+ if (!isWithin(merged, lowerBound, max))
14660
15079
  return;
14661
15080
  commitValue(merged);
14662
15081
  setFocusedDate(d);
14663
- }, [value, enableSeconds, min, max, commitValue]);
15082
+ }, [value, enableSeconds, lowerBound, max, commitValue]);
14664
15083
  const handleTimeChange = useCallback((parts) => {
14665
15084
  const base = value ?? focusedDate;
14666
15085
  const next = setTimeOfDay(base, parts);
14667
- if (!isWithin(next, min, max))
15086
+ if (!isWithin(next, lowerBound, max))
14668
15087
  return;
14669
15088
  commitValue(next);
14670
- }, [value, focusedDate, min, max, commitValue]);
15089
+ }, [value, focusedDate, lowerBound, max, commitValue]);
14671
15090
  const tryParse = useCallback((s) => {
14672
15091
  const trimmed = s.trim();
14673
15092
  if (!trimmed)
@@ -14702,7 +15121,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14702
15121
  setText,
14703
15122
  makeBaseDate,
14704
15123
  locale,
14705
- min,
15124
+ min: lowerBound,
14706
15125
  max,
14707
15126
  isBlocked,
14708
15127
  disabled,
@@ -14735,7 +15154,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
14735
15154
  ? fromIsoDateTime(e.target.value)
14736
15155
  : null;
14737
15156
  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 }));
15157
+ }, 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
15158
  }
14740
15159
  const spinnerValue = {
14741
15160
  hours: value?.getHours() ?? 0,
@@ -14783,29 +15202,57 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
14783
15202
  * shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
14784
15203
  * />
14785
15204
  */
14786
- 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) => {
15205
+ const DateTimeInput = forwardRef((props, ref) => {
15206
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
14787
15207
  const insideField = useInsideField();
14788
15208
  const insideControl = useInsideControl();
15209
+ const ownField = rendersOwnField('DateTimeInput', {
15210
+ insideField,
15211
+ insideControl,
15212
+ label,
15213
+ message,
15214
+ horizontal,
15215
+ fieldClassName,
15216
+ });
15217
+ // Inline mode renders a bare picker with no input, so the Control's
15218
+ // icon-left container has nothing to anchor to, and it renders no
15219
+ // Control inside one or out. The left icon is checked as the caller
15220
+ // passed it: the default glyph is this component's own choice, not a
15221
+ // prop the caller set.
15222
+ const ownControl = rendersOwnControl('DateTimeInput', {
15223
+ insideControl,
15224
+ inline: baseProps.inline,
15225
+ isLoading,
15226
+ iconLeft,
15227
+ iconLeftName: props.iconLeftName,
15228
+ iconLeftSize,
15229
+ iconRight,
15230
+ iconRightName,
15231
+ iconRightSize,
15232
+ hasIconsLeft,
15233
+ hasIconsRight,
15234
+ isExpanded,
15235
+ controlSize,
15236
+ controlClassName,
15237
+ });
14789
15238
  const { controlId, fieldLabelProps } = useAutoLabelId({
14790
15239
  label,
14791
15240
  id: baseProps.id,
14792
15241
  labelProps,
14793
15242
  // Inline mode renders a bare picker with no input to label.
14794
- rendersLabel: !insideField && !baseProps.inline,
15243
+ rendersLabel: ownField && !baseProps.inline,
14795
15244
  });
14796
15245
  const helpClass = usePrefixedClassNames('help', {
14797
15246
  [`is-${messageColor}`]: !!messageColor,
14798
15247
  });
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 }));
14802
- // Inline mode renders a bare picker with no input, so the Control's
14803
- // icon-left container has nothing to anchor to. Skip the Control wrap.
14804
- if (!insideControl && !baseProps.inline) {
15248
+ // The base hides its launcher while the Control it sits in is loading,
15249
+ // whether that is the one rendered below or an enclosing one.
15250
+ let content = (jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps }));
15251
+ if (ownControl) {
14805
15252
  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
15253
  }
14807
15254
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
14808
- if (!insideField) {
15255
+ if (ownField) {
14809
15256
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14810
15257
  }
14811
15258
  return (jsxs(Fragment, { children: [content, messageEl] }));