@allxsmith/bestax-bulma 5.26.6 → 5.27.0

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 (50) 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 +752 -79
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +751 -80
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/components/Tabs.d.ts +25 -3
  10. package/dist/types/form/DateInputBase.d.ts +5 -2
  11. package/dist/types/form/DateRangeInput.d.ts +91 -0
  12. package/dist/types/form/DateRangeInputBase.d.ts +172 -0
  13. package/dist/types/form/DateTimeInputBase.d.ts +7 -2
  14. package/dist/types/form/File.d.ts +15 -2
  15. package/dist/types/form/FormContext.d.ts +11 -0
  16. package/dist/types/form/TimeInputBase.d.ts +1 -1
  17. package/dist/types/form/_pickerInternals/PickerPopover.d.ts +1 -0
  18. package/dist/types/form/_pickerInternals/dateUtils.d.ts +7 -0
  19. package/dist/types/form/_pickerInternals/formatters.d.ts +14 -0
  20. package/dist/types/form/_pickerInternals/pickerTypes.d.ts +13 -0
  21. package/dist/types/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
  22. package/dist/types/index.d.ts +4 -0
  23. package/dist/types-cjs/components/Tabs.d.ts +25 -3
  24. package/dist/types-cjs/form/DateInputBase.d.ts +5 -2
  25. package/dist/types-cjs/form/DateRangeInput.d.ts +91 -0
  26. package/dist/types-cjs/form/DateRangeInputBase.d.ts +172 -0
  27. package/dist/types-cjs/form/DateTimeInputBase.d.ts +7 -2
  28. package/dist/types-cjs/form/File.d.ts +15 -2
  29. package/dist/types-cjs/form/FormContext.d.ts +11 -0
  30. package/dist/types-cjs/form/TimeInputBase.d.ts +1 -1
  31. package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +1 -0
  32. package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +7 -0
  33. package/dist/types-cjs/form/_pickerInternals/formatters.d.ts +14 -0
  34. package/dist/types-cjs/form/_pickerInternals/pickerTypes.d.ts +13 -0
  35. package/dist/types-cjs/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
  36. package/dist/types-cjs/index.d.ts +4 -0
  37. package/dist/versions/bestax-no-dark-mode.css +1 -1
  38. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  39. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  40. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  41. package/dist/versions/bestax-no-helpers.css +1 -1
  42. package/dist/versions/bestax-no-helpers.css.map +1 -1
  43. package/dist/versions/bestax-prefixed.css +1 -1
  44. package/dist/versions/bestax-prefixed.css.map +1 -1
  45. package/package.json +1 -1
  46. package/src/scss/components/_tabs.scss +44 -0
  47. package/src/scss/form/_dateinput.scss +50 -2
  48. package/src/scss/form/_daterangeinput.scss +63 -0
  49. package/src/scss/form/_file.scss +37 -0
  50. package/src/scss/form/_index.scss +2 -0
package/dist/index.cjs CHANGED
@@ -4141,9 +4141,65 @@ const TabsContext = React.createContext(null);
4141
4141
  function useTabsContext() {
4142
4142
  return React.useContext(TabsContext);
4143
4143
  }
4144
+ /**
4145
+ * The tabs and panels currently mounted under one `Tabs`. Each registers from
4146
+ * an effect and removes itself on cleanup, so the links between them only
4147
+ * ever name elements that exist, wherever in the tree the panels sit.
4148
+ */
4149
+ function useRegistry() {
4150
+ const [records, setRecords] = React.useState([]);
4151
+ const register = React.useCallback((record) => {
4152
+ setRecords(prev => [...prev, record]);
4153
+ return () => setRecords(prev => prev.filter(r => r !== record));
4154
+ }, []);
4155
+ return [records, register];
4156
+ }
4157
+ /**
4158
+ * The list a tab belongs to: its `role="tablist"` ancestor, so an element
4159
+ * wrapped around a tab doesn't cut it off from the others, or its parent when
4160
+ * it sits outside a tab list.
4161
+ */
4162
+ function tabListOf(tab) {
4163
+ return (tab.closest('[role="tablist"]') ?? tab.parentElement);
4164
+ }
4165
+ /**
4166
+ * The tab a key press moves focus to, found among the tabs of the same list
4167
+ * in DOM order, skipping disabled ones and wrapping at either end.
4168
+ * `undefined` for a key the tab list does not handle, or when no enabled tab
4169
+ * is left to go to.
4170
+ */
4171
+ function findTargetTab(current, key, vertical) {
4172
+ const tabs = Array.from(tabListOf(current).querySelectorAll('[role="tab"]'));
4173
+ const isEnabled = (el) => el.getAttribute('aria-disabled') !== 'true';
4174
+ if (key === 'Home')
4175
+ return tabs.find(isEnabled);
4176
+ if (key === 'End')
4177
+ return [...tabs].reverse().find(isEnabled);
4178
+ let step;
4179
+ if (key === 'ArrowRight' || (vertical && key === 'ArrowDown'))
4180
+ step = 1;
4181
+ else if (key === 'ArrowLeft' || (vertical && key === 'ArrowUp'))
4182
+ step = -1;
4183
+ else
4184
+ return undefined;
4185
+ const start = tabs.indexOf(current);
4186
+ for (let i = 1; i <= tabs.length; i++) {
4187
+ const candidate = tabs[(((start + step * i) % tabs.length) + tabs.length) % tabs.length];
4188
+ if (isEnabled(candidate))
4189
+ return candidate;
4190
+ }
4191
+ return undefined;
4192
+ }
4144
4193
  /**
4145
4194
  * The `Tabs` component provides flexible and fully-featured Bulma tab navigation for your Bulma React UI.
4146
4195
  *
4196
+ * Built from `Tabs.Tab`, the tabs follow the WAI-ARIA tabs pattern with
4197
+ * manual activation: the tab list is a single tab stop, the arrow keys, Home
4198
+ * and End move focus between tabs (skipping disabled ones), and Enter or
4199
+ * Space activates the focused tab. Each tab names its `Tabs.Content.Item` in
4200
+ * `aria-controls`, and each panel names its tab in `aria-labelledby`. Built
4201
+ * from `Tabs.Item` with links inside, the tabs stay plain Bulma navigation.
4202
+ *
4147
4203
  * @function
4148
4204
  * @param {TabsProps} props - Props for the Tabs component.
4149
4205
  * @returns {JSX.Element} The rendered tabs component.
@@ -4175,7 +4231,45 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
4175
4231
  }
4176
4232
  onChange?.(index);
4177
4233
  }, [isControlled, onChange]);
4178
- const contextValue = { activeTab, setActiveTab };
4234
+ const baseId = React.useId();
4235
+ const [tabs, registerTab] = useRegistry();
4236
+ const [panels, registerPanel] = useRegistry();
4237
+ const [focusedTab, setFocusedTab] = React.useState(null);
4238
+ // One tab stop for the whole list (roving tabindex). While focus is inside
4239
+ // the list the stop follows it, so Tab and Shift+Tab leave the list from
4240
+ // whichever tab has focus; once focus leaves, the stop is the selected tab
4241
+ // again. A disabled tab never holds it, and when the selected tab is
4242
+ // disabled or missing the enabled tab with the lowest index does, so the
4243
+ // list can always be reached. Before the tabs register (the server render
4244
+ // and the first client render), the selected tab holds it. The registry is
4245
+ // in the order the tabs last registered, which a tab changing its
4246
+ // `disabled` or `id` reshuffles, hence the sort.
4247
+ const enabledTabs = tabs
4248
+ .filter(t => !t.disabled)
4249
+ .sort((a, b) => a.index - b.index);
4250
+ const canHoldStop = (index) => enabledTabs.some(t => t.index === index);
4251
+ let tabStop;
4252
+ if (canHoldStop(focusedTab))
4253
+ tabStop = focusedTab;
4254
+ else if (tabs.length === 0 || canHoldStop(activeTab))
4255
+ tabStop = activeTab;
4256
+ else
4257
+ tabStop = enabledTabs.length > 0 ? enabledTabs[0].index : null;
4258
+ // Check if children include TabsContent
4259
+ const childArray = React.Children.toArray(children);
4260
+ const hasContent = childArray.some(child => React.isValidElement(child) && child.type === TabsContent);
4261
+ const contextValue = {
4262
+ activeTab,
4263
+ setActiveTab,
4264
+ baseId,
4265
+ tabStop,
4266
+ setFocusedTab,
4267
+ tabId: index => tabs.find(t => t.index === index)?.id,
4268
+ panelId: index => panels.find(p => p.index === index)?.id,
4269
+ registerTab,
4270
+ registerPanel,
4271
+ orientation: hasContent && vertical ? 'vertical' : 'horizontal',
4272
+ };
4179
4273
  // Build classes for the .tabs div
4180
4274
  const tabsClasses = usePrefixedClassNames('tabs', {
4181
4275
  [`is-${align}`]: align,
@@ -4186,9 +4280,6 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
4186
4280
  'is-toggle': toggle,
4187
4281
  'is-toggle-rounded': rounded,
4188
4282
  });
4189
- // Check if children include TabsContent
4190
- const childArray = React.Children.toArray(children);
4191
- const hasContent = childArray.some(child => React.isValidElement(child) && child.type === TabsContent);
4192
4283
  // Hoisted unconditionally to respect rules-of-hooks. Modifiers gate themselves
4193
4284
  // via their truthy values — `tabs-root` is always prefixed, modifiers only
4194
4285
  // apply in the vertical-with-content branch.
@@ -4237,26 +4328,82 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
4237
4328
  * @param {TabListProps} props - Props for the TabList component.
4238
4329
  * @returns {JSX.Element} The rendered tab list.
4239
4330
  */
4240
- const TabList = ({ className, children, ...props }) => (jsxRuntime.jsx("ul", { role: "tablist", className: classNames(className) || undefined, ...props, children: children }));
4331
+ const TabList = ({ className, children, ...props }) => {
4332
+ const ctx = useTabsContext();
4333
+ return (jsxRuntime.jsx("ul", { role: "tablist", "aria-orientation": ctx?.orientation === 'vertical' ? 'vertical' : undefined, className: classNames(className) || undefined, ...props, children: children }));
4334
+ };
4241
4335
  /**
4242
4336
  * Individual tab button. Consumes Tabs context for active state management.
4243
4337
  * Renders `<a>` internally — consumers provide only the label text/children.
4338
+ * Inside a `Tabs` it takes part in the tab list's keyboard support and links
4339
+ * to the `Tabs.Content.Item` with the same `index`.
4244
4340
  *
4245
4341
  * @function
4246
4342
  * @param {TabProps} props - Props for the Tab component.
4247
4343
  * @returns {JSX.Element} The rendered tab.
4248
4344
  */
4249
- const Tab = ({ index, disabled, icon, iconLibrary, iconVariant, iconSize = 'small', iconFeatures, className, children, ...props }) => {
4345
+ const Tab = ({ index, disabled, icon, iconLibrary, iconVariant, iconSize = 'small', iconFeatures, className, children, id, onKeyDown, onFocus, onBlur, ...props }) => {
4250
4346
  const ctx = useTabsContext();
4251
4347
  const isActive = ctx ? ctx.activeTab === index : false;
4348
+ const anchorRef = React.useRef(null);
4252
4349
  const activeClass = usePrefixedClassNames({ 'is-active': isActive });
4350
+ const tabId = id ?? (ctx ? `${ctx.baseId}-tab-${index}` : undefined);
4351
+ const registerTab = ctx?.registerTab;
4352
+ React.useEffect(() => {
4353
+ if (!registerTab || tabId === undefined)
4354
+ return undefined;
4355
+ return registerTab({ index, id: tabId, disabled: !!disabled });
4356
+ }, [registerTab, index, tabId, disabled]);
4357
+ // Outside a Tabs there is no list to rove, so every enabled tab stays in
4358
+ // the page tab order, as it always did.
4359
+ let tabIndex;
4360
+ if (disabled)
4361
+ tabIndex = -1;
4362
+ else if (ctx)
4363
+ tabIndex = ctx.tabStop === index ? 0 : -1;
4364
+ else
4365
+ tabIndex = 0;
4253
4366
  const handleClick = React.useCallback((e) => {
4254
4367
  e.preventDefault();
4255
4368
  if (!disabled && ctx) {
4256
4369
  ctx.setActiveTab(index);
4257
4370
  }
4258
4371
  }, [disabled, ctx, index]);
4259
- return (jsxRuntime.jsx("li", { className: classNames(activeClass, className) || undefined, role: "tab", "aria-selected": isActive, tabIndex: disabled ? -1 : 0, ...props, children: jsxRuntime.jsxs("a", { onClick: handleClick, "aria-disabled": disabled || undefined, children: [icon && (jsxRuntime.jsx(Icon, { name: icon, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })), children && jsxRuntime.jsx("span", { children: children })] }) }));
4372
+ const handleKeyDown = (e) => {
4373
+ onKeyDown?.(e);
4374
+ // A key on something inside the tab is that element's to answer.
4375
+ if (e.defaultPrevented ||
4376
+ e.target !== e.currentTarget ||
4377
+ e.altKey ||
4378
+ e.ctrlKey ||
4379
+ e.metaKey) {
4380
+ return;
4381
+ }
4382
+ if (e.key === 'Enter' || e.key === ' ') {
4383
+ e.preventDefault();
4384
+ // Activate through a click on the inner <a>, the element a pointer
4385
+ // clicks, so click listeners see keyboard activation the way they see
4386
+ // a native button's.
4387
+ anchorRef.current.click();
4388
+ return;
4389
+ }
4390
+ const target = findTargetTab(e.currentTarget, e.key, ctx?.orientation === 'vertical');
4391
+ if (target) {
4392
+ e.preventDefault();
4393
+ target.focus();
4394
+ }
4395
+ };
4396
+ const handleFocus = (e) => {
4397
+ onFocus?.(e);
4398
+ ctx?.setFocusedTab(index);
4399
+ };
4400
+ const handleBlur = (e) => {
4401
+ onBlur?.(e);
4402
+ if (!tabListOf(e.currentTarget).contains(e.relatedTarget)) {
4403
+ ctx?.setFocusedTab(null);
4404
+ }
4405
+ };
4406
+ return (jsxRuntime.jsx("li", { className: classNames(activeClass, className) || undefined, role: "tab", id: tabId, "aria-selected": isActive, "aria-controls": ctx?.panelId(index), "aria-disabled": disabled || undefined, tabIndex: tabIndex, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, ...props, children: jsxRuntime.jsxs("a", { ref: anchorRef, onClick: handleClick, "aria-disabled": disabled || undefined, children: [icon && (jsxRuntime.jsx(Icon, { name: icon, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })), children && jsxRuntime.jsx("span", { children: children })] }) }));
4260
4407
  };
4261
4408
  /**
4262
4409
  * Each tab; accepts `active`, `onClick`, etc.
@@ -4286,13 +4433,20 @@ const TabsContentComponent = ({ className, children, ...props }) => {
4286
4433
  * @param {TabContentItemProps} props - Props for the TabContentItem component.
4287
4434
  * @returns {JSX.Element} The rendered tab content panel.
4288
4435
  */
4289
- const TabContentItem = ({ index, className, children, ...props }) => {
4436
+ const TabContentItem = ({ index, className, children, id, ...props }) => {
4290
4437
  const ctx = useTabsContext();
4291
4438
  const isActive = ctx ? ctx.activeTab === index : false;
4292
4439
  const itemClass = usePrefixedClassNames('tabs-content-item', {
4293
4440
  'is-active': isActive,
4294
4441
  });
4295
- return (jsxRuntime.jsx("div", { className: classNames(itemClass, className), role: "tabpanel", "aria-hidden": !isActive, ...props, children: children }));
4442
+ const panelId = id ?? (ctx ? `${ctx.baseId}-panel-${index}` : undefined);
4443
+ const registerPanel = ctx?.registerPanel;
4444
+ React.useEffect(() => {
4445
+ if (!registerPanel || panelId === undefined)
4446
+ return undefined;
4447
+ return registerPanel({ index, id: panelId });
4448
+ }, [registerPanel, index, panelId]);
4449
+ return (jsxRuntime.jsx("div", { className: classNames(itemClass, className), role: "tabpanel", id: panelId, "aria-labelledby": ctx?.tabId(index), "aria-hidden": !isActive, ...props, children: children }));
4296
4450
  };
4297
4451
  // ---------------------------------------------------------------------------
4298
4452
  // Static property attachment
@@ -8850,6 +9004,10 @@ const ON_CONTROL = {
8850
9004
  * only the props it hands its own `Control`, as the caller gave them, so a
8851
9005
  * default the wrapper fills in itself never warns. A prop counts when it is
8852
9006
  * truthy, since a falsy one would change nothing on a `Control` either.
9007
+ * `iconLeftSize` and `hasIconsLeft` show nothing on a `Control` without a
9008
+ * glyph, so when they move and the caller chose no left icon, the advice
9009
+ * names the wrapper's default glyph too, and following it draws what the
9010
+ * wrapper's own `Control` did.
8853
9011
  * Internal; not part of the public API.
8854
9012
  */
8855
9013
  const rendersOwnControl = (component, options) => {
@@ -8860,18 +9018,32 @@ const rendersOwnControl = (component, options) => {
8860
9018
  return false;
8861
9019
  const names = held.join(' ');
8862
9020
  const doNothing = held.length > 1 ? 'those props do' : 'that prop does';
9021
+ let message;
8863
9022
  if (options.inline) {
8864
- warnOnce(`${component}:Control-props-inline:${held.join('+')}`, `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
8865
- `in inline mode, inside a <Control> or not, so ${doNothing} ` +
8866
- `nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
8867
- `inline picker.`);
9023
+ message =
9024
+ `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
9025
+ `in inline mode, inside a <Control> or not, so ${doNothing} ` +
9026
+ `nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
9027
+ `inline picker.`;
8868
9028
  }
8869
9029
  else {
8870
- warnOnce(`${component}:Control-props-in-Control:${held.join('+')}`, `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
8871
- `no <Control> of its own, so ${doNothing} nothing there. Set ` +
8872
- `${listOf(held.map(prop => ON_CONTROL[prop]))} on that <Control> ` +
8873
- `instead.`);
9030
+ const toSet = held.map(prop => ON_CONTROL[prop]);
9031
+ const needsGlyph = (options.iconLeftSize || options.hasIconsLeft) &&
9032
+ !options.iconLeft &&
9033
+ options.iconLeftName === undefined &&
9034
+ options.defaultIconLeftName;
9035
+ if (needsGlyph) {
9036
+ toSet.push(`iconLeftName="${options.defaultIconLeftName}" (its default icon)`);
9037
+ }
9038
+ message =
9039
+ `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
9040
+ `no <Control> of its own, so ${doNothing} nothing there. Set ` +
9041
+ `${listOf(toSet)} on that <Control> instead.`;
8874
9042
  }
9043
+ // Keyed by the message itself: the props named do not settle the advice
9044
+ // (an empty `iconLeftName` and an unset one name the same props), so two
9045
+ // calls that would advise differently must not share a key.
9046
+ warnOnce(message, message);
8875
9047
  return false;
8876
9048
  };
8877
9049
  const FieldLabelIdContext = React.createContext(undefined);
@@ -9358,7 +9530,40 @@ const Checkboxes = withSubComponents(CheckboxesComponent, { Checkbox }, 'Checkbo
9358
9530
  */
9359
9531
  const File = React.forwardRef(({
9360
9532
  // Field props
9361
- label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
9533
+ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, pickedFilesLabel, onChange, ...props }, ref) => {
9534
+ // What the user last picked, and the form a reset of which clears it.
9535
+ const [picked, setPicked] = React.useState();
9536
+ const pickedForm = picked?.form;
9537
+ React.useEffect(() => {
9538
+ if (!pickedForm)
9539
+ return;
9540
+ let timer;
9541
+ // The reset event fires before the form resets, and a listener after this one can still
9542
+ // cancel it, so the name is cleared a task later, and only if the reset went ahead.
9543
+ const onReset = (event) => {
9544
+ timer = setTimeout(() => {
9545
+ if (!event.defaultPrevented)
9546
+ setPicked(undefined);
9547
+ });
9548
+ };
9549
+ pickedForm.addEventListener('reset', onReset);
9550
+ return () => {
9551
+ pickedForm.removeEventListener('reset', onReset);
9552
+ clearTimeout(timer);
9553
+ };
9554
+ }, [pickedForm]);
9555
+ const handleChange = (event) => {
9556
+ const { files, form } = event.target;
9557
+ setPicked(files?.length
9558
+ ? { count: files.length, firstName: files[0].name, form }
9559
+ : undefined);
9560
+ onChange?.(event);
9561
+ };
9562
+ const pickedText = picked &&
9563
+ (picked.count === 1
9564
+ ? picked.firstName
9565
+ : (pickedFilesLabel?.(picked.count) ?? `${picked.count} files`));
9566
+ const shownName = fileName ?? pickedText;
9362
9567
  const insideField = useInsideField();
9363
9568
  const insideControl = useInsideControl();
9364
9569
  const ownField = rendersOwnField('File', {
@@ -9404,7 +9609,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
9404
9609
  [`is-${messageColor}`]: !!messageColor,
9405
9610
  });
9406
9611
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
9407
- const fileElement = (jsxRuntime.jsx("div", { className: fileClass, children: jsxRuntime.jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsxRuntime.jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxRuntime.jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsxRuntime.jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
9612
+ const fileElement = (jsxRuntime.jsx("div", { className: fileClass, children: jsxRuntime.jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsxRuntime.jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, onChange: handleChange, ...rest }), jsxRuntime.jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsxRuntime.jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && shownName && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: shownName }))] }) }));
9408
9613
  if (ownField) {
9409
9614
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
9410
9615
  }
@@ -11481,6 +11686,12 @@ fieldClassName, controlClassName,
11481
11686
  });
11482
11687
  TextArea.displayName = 'TextArea';
11483
11688
 
11689
+ /**
11690
+ * Whether a key is Alt+ArrowDown or Alt+ArrowUp, which open and close a
11691
+ * picker's popover. The calendar and the time wheels leave both to it rather
11692
+ * than reading them as plain arrows.
11693
+ */
11694
+ const isPopoverToggleKey = (e) => e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp');
11484
11695
  const DEFAULT_PICKER_LABELS = {
11485
11696
  prevMonth: 'Previous month',
11486
11697
  nextMonth: 'Next month',
@@ -11492,6 +11703,8 @@ const DEFAULT_PICKER_LABELS = {
11492
11703
  rangeStart: 'Start date',
11493
11704
  rangeEnd: 'End date',
11494
11705
  rangePreviewEnd: 'Choose as end date',
11706
+ rangeSeparator: '–',
11707
+ chooseDateRange: 'Choose date range',
11495
11708
  hours: 'hours',
11496
11709
  minutes: 'minutes',
11497
11710
  seconds: 'seconds',
@@ -11699,6 +11912,12 @@ function addMonths(d, n) {
11699
11912
  function addYears(d, n) {
11700
11913
  return addMonths(d, n * 12);
11701
11914
  }
11915
+ /**
11916
+ * A copy of `d` with the given parts of its time of day set and the rest
11917
+ * left as they are. A time started from the clock rather than from a value
11918
+ * should pass the seconds its picker does not show, and the milliseconds,
11919
+ * as `0`, or it keeps the ones the clock had.
11920
+ */
11702
11921
  function setTimeOfDay(d, parts) {
11703
11922
  const r = new Date(d);
11704
11923
  if (parts.hours !== undefined)
@@ -11707,6 +11926,8 @@ function setTimeOfDay(d, parts) {
11707
11926
  r.setMinutes(parts.minutes);
11708
11927
  if (parts.seconds !== undefined)
11709
11928
  r.setSeconds(parts.seconds);
11929
+ if (parts.milliseconds !== undefined)
11930
+ r.setMilliseconds(parts.milliseconds);
11710
11931
  return r;
11711
11932
  }
11712
11933
  /**
@@ -11855,6 +12076,36 @@ function formatDateTime(d, fmt, locale) {
11855
12076
  return formatTokenString(d, f);
11856
12077
  return formatWithIntl(d, f, locale);
11857
12078
  }
12079
+ /**
12080
+ * The value as a native input and a hidden form input carry it:
12081
+ * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
12082
+ * `<input type="month">` use. The year is padded to four digits, as those
12083
+ * inputs require and as the `YYYY` token displays it. HTML has no such shape
12084
+ * for a year before 1, so a date then is empty, as those inputs would make it.
12085
+ */
12086
+ function toIsoValue(d, granularity) {
12087
+ if (d.getFullYear() < FIRST_YEAR)
12088
+ return '';
12089
+ const year = pad(d.getFullYear(), 4);
12090
+ if (granularity === 'year')
12091
+ return year;
12092
+ const month = `${year}-${pad(d.getMonth() + 1)}`;
12093
+ return granularity === 'month' ? month : `${month}-${pad(d.getDate())}`;
12094
+ }
12095
+ /**
12096
+ * Read a native `type="date"` or `type="month"` value back into a Date. HTML
12097
+ * allows a year of four or more digits, as `toIsoValue` writes one past 9999.
12098
+ */
12099
+ function fromIsoValue(s, granularity) {
12100
+ if (granularity === 'month') {
12101
+ const m = /^(\d{4,})-(\d{2})$/.exec(s);
12102
+ return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
12103
+ }
12104
+ const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
12105
+ if (!m)
12106
+ return null;
12107
+ return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
12108
+ }
11858
12109
  /**
11859
12110
  * Derive the hour cycle (`'12'` or `'24'`) a display format will render, from
11860
12111
  * its first hour token: `h`/`hh` → 12-hour with meridiem, `H`/`HH` → 24-hour.
@@ -12356,6 +12607,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12356
12607
  }
12357
12608
  }, [min, max, isDateUnselectable, onFocusedDateChange]);
12358
12609
  const handleKeyDown = React.useCallback((e) => {
12610
+ if (isPopoverToggleKey(e))
12611
+ return;
12359
12612
  // A key takes the range preview back from the pointer.
12360
12613
  setHoverDate(null);
12361
12614
  switch (e.key) {
@@ -12448,6 +12701,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12448
12701
  }
12449
12702
  }, [min, max, isMonthUnselectable, onFocusedDateChange]);
12450
12703
  const handleMonthKeyDown = React.useCallback((e) => {
12704
+ if (isPopoverToggleKey(e))
12705
+ return;
12451
12706
  if (e.key === 'Enter' || e.key === ' ') {
12452
12707
  e.preventDefault();
12453
12708
  if (!isMonthUnselectable(focusedDate)) {
@@ -12582,6 +12837,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12582
12837
  }
12583
12838
  }, [yearList, disabledYears, onFocusedDateChange, min, max]);
12584
12839
  const handleYearListKeyDown = React.useCallback((e) => {
12840
+ if (isPopoverToggleKey(e))
12841
+ return;
12585
12842
  if (isYearGranularity) {
12586
12843
  const step = yearStep(e.key, focusedYear, yearList);
12587
12844
  if (!step)
@@ -12843,6 +13100,8 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12843
13100
  document.addEventListener('pointerdown', handler);
12844
13101
  return () => document.removeEventListener('pointerdown', handler);
12845
13102
  }, [isOpen, closeOnClickOutside, anchorRef, onClose]);
13103
+ // Escape closes it, and so does Alt+ArrowUp, the combobox's key for it,
13104
+ // which the calendar and the time wheels inside leave to it.
12846
13105
  React.useEffect(() => {
12847
13106
  if (!isOpen || !closeOnEscape || !isBrowser)
12848
13107
  return undefined;
@@ -12851,6 +13110,11 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12851
13110
  e.stopPropagation();
12852
13111
  onClose();
12853
13112
  }
13113
+ else if (e.altKey && e.key === 'ArrowUp') {
13114
+ e.preventDefault();
13115
+ e.stopPropagation();
13116
+ onClose();
13117
+ }
12854
13118
  };
12855
13119
  document.addEventListener('keydown', handler);
12856
13120
  return () => document.removeEventListener('keydown', handler);
@@ -13254,7 +13518,7 @@ const SEPARATOR_RE = /[/:\-.\s]/;
13254
13518
  * null and the input transparently falls back to free-form text entry.
13255
13519
  */
13256
13520
  function useSegmentedEntry(params) {
13257
- const { format, skipKinds, value, commitValue, formatFn, tryParse, text, setText, makeBaseDate, locale, min, max, isBlocked, disabled, readOnly, editable = true, popover = true, openOnFocus = true, closeOnSelect = false, isOpen, setOpen, inputRef, containerRef, onFocus, onClick, onKeyDown, onBlur, } = params;
13521
+ const { format, skipKinds, value, commitValue, formatFn, tryParse, text, setText, makeBaseDate, locale, min, max, isBlocked, isUnfinishedBlocked, disabled, readOnly, editable = true, popover = true, openOnFocus = true, closeOnSelect = false, isOpen, setOpen, inputRef, containerRef, onFocus, onClick, onKeyDown, onBlur, } = params;
13258
13522
  const segmentMap = React.useMemo(() => {
13259
13523
  const map = typeof format === 'string' ? buildSegmentMap(format) : null;
13260
13524
  if (!map || !skipKinds)
@@ -13450,7 +13714,7 @@ function useSegmentedEntry(params) {
13450
13714
  // Clicking the field opens the popover only when `openOnFocus` is on —
13451
13715
  // a click is just a focus. With `openOnFocus={false}` (manual-entry
13452
13716
  // mode) the click positions the caret for typing and the popover is
13453
- // opened explicitly via the right launcher button (or ArrowDown).
13717
+ // opened explicitly via the right launcher button (or Alt+ArrowDown).
13454
13718
  if (openOnFocus && popover && !disabled && !readOnly)
13455
13719
  setOpen(true);
13456
13720
  if (segmentEditable && segmentMap && inputRef.current) {
@@ -13486,14 +13750,23 @@ function useSegmentedEntry(params) {
13486
13750
  const isPm = base.getHours() >= 12;
13487
13751
  switch (e.key) {
13488
13752
  case 'ArrowUp':
13489
- e.preventDefault();
13490
- typedDigitsRef.current = '';
13491
- applyDateFromSegment(incrementSegmentValue(seg, base, 1, isPm));
13492
- return;
13493
13753
  case 'ArrowDown':
13494
13754
  e.preventDefault();
13755
+ // The plain arrows step the segment, so the popover opens on
13756
+ // Alt+ArrowDown and closes on Alt+ArrowUp, as a combobox's does,
13757
+ // and neither steps it.
13758
+ if (e.altKey) {
13759
+ if (e.key === 'ArrowUp') {
13760
+ if (isOpen)
13761
+ setOpen(false);
13762
+ }
13763
+ else if (!isOpen && popover) {
13764
+ setOpen(true);
13765
+ }
13766
+ return;
13767
+ }
13495
13768
  typedDigitsRef.current = '';
13496
- applyDateFromSegment(incrementSegmentValue(seg, base, -1, isPm));
13769
+ applyDateFromSegment(incrementSegmentValue(seg, base, e.key === 'ArrowUp' ? 1 : -1, isPm));
13497
13770
  return;
13498
13771
  case 'ArrowRight':
13499
13772
  e.preventDefault();
@@ -13559,6 +13832,8 @@ function useSegmentedEntry(params) {
13559
13832
  const nextBuffer = typedDigitsRef.current + e.key;
13560
13833
  typedDigitsRef.current = nextBuffer;
13561
13834
  const { date, advance } = setSegmentValue(seg, base, nextBuffer, isPm);
13835
+ if (!advance && isUnfinishedBlocked?.(date))
13836
+ return;
13562
13837
  applyDateFromSegment(date);
13563
13838
  if (advance) {
13564
13839
  typedDigitsRef.current = '';
@@ -13569,11 +13844,23 @@ function useSegmentedEntry(params) {
13569
13844
  }
13570
13845
  }
13571
13846
  // ----- free-form / popover key handling -----
13572
- if (e.key === 'ArrowDown' && !isOpen && popover) {
13847
+ // With no segment to step, ArrowDown opens the popover with or without
13848
+ // Alt, unless the field is read-only or disabled, as focus and the
13849
+ // launcher don't open it then either. Alt+ArrowUp closes it.
13850
+ if (e.key === 'ArrowDown' &&
13851
+ !isOpen &&
13852
+ popover &&
13853
+ !disabled &&
13854
+ !readOnly) {
13573
13855
  e.preventDefault();
13574
13856
  setOpen(true);
13575
13857
  return;
13576
13858
  }
13859
+ if (e.key === 'ArrowUp' && e.altKey && isOpen) {
13860
+ e.preventDefault();
13861
+ setOpen(false);
13862
+ return;
13863
+ }
13577
13864
  if (e.key === 'Escape' && isOpen) {
13578
13865
  e.preventDefault();
13579
13866
  setOpen(false);
@@ -13603,11 +13890,14 @@ function useSegmentedEntry(params) {
13603
13890
  activeSegmentIdx,
13604
13891
  segmentBaseDate,
13605
13892
  applyDateFromSegment,
13893
+ isUnfinishedBlocked,
13606
13894
  moveSegment,
13607
13895
  isOpen,
13608
13896
  setOpen,
13609
13897
  closeOnSelect,
13610
13898
  popover,
13899
+ disabled,
13900
+ readOnly,
13611
13901
  isUntouched,
13612
13902
  tryParse,
13613
13903
  text,
@@ -13652,37 +13942,6 @@ function supportsInputType(type) {
13652
13942
  return supported;
13653
13943
  }
13654
13944
 
13655
- const pad2$1 = (n) => String(n).padStart(2, '0');
13656
- /**
13657
- * The value as the native input and the hidden form input carry it:
13658
- * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
13659
- * `<input type="month">` use. The year is padded to four digits, as those
13660
- * inputs require and as the `YYYY` token displays it. HTML has no such shape
13661
- * for a year before 1, so a date then is empty, as those inputs would make it.
13662
- */
13663
- const toIsoValue = (d, granularity) => {
13664
- if (d.getFullYear() < FIRST_YEAR)
13665
- return '';
13666
- const year = String(d.getFullYear()).padStart(4, '0');
13667
- if (granularity === 'year')
13668
- return year;
13669
- const month = `${year}-${pad2$1(d.getMonth() + 1)}`;
13670
- return granularity === 'month' ? month : `${month}-${pad2$1(d.getDate())}`;
13671
- };
13672
- /**
13673
- * Read a native `type="date"` or `type="month"` value back into a Date. HTML
13674
- * allows a year of four or more digits, as `toIsoValue` writes one past 9999.
13675
- */
13676
- const fromIsoValue = (s, granularity) => {
13677
- if (granularity === 'month') {
13678
- const m = /^(\d{4,})-(\d{2})$/.exec(s);
13679
- return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
13680
- }
13681
- const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
13682
- if (!m)
13683
- return null;
13684
- return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
13685
- };
13686
13945
  const DEFAULT_FORMATS = {
13687
13946
  day: DEFAULT_DATE_FORMAT,
13688
13947
  month: DEFAULT_MONTH_FORMAT,
@@ -13784,7 +14043,11 @@ const DateInputBase = React.forwardRef((props, ref) => {
13784
14043
  // them, so the month or year containing `min` stays reachable.
13785
14044
  const periodMin = React.useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
13786
14045
  const periodMax = React.useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
13787
- const handleSelect = React.useCallback((d) => {
14046
+ // A pick is the day at midnight, as a click on its cell gives. Enter hands
14047
+ // over the focused date, which can carry a time of day from the clock, a
14048
+ // value or a bound, so its time is dropped here.
14049
+ const handleSelect = React.useCallback((picked) => {
14050
+ const d = startOfDay(picked);
13788
14051
  if (!isWithin(d, periodMin, periodMax))
13789
14052
  return;
13790
14053
  commitValue(d);
@@ -13956,13 +14219,14 @@ const DateInput = React.forwardRef((props, ref) => {
13956
14219
  // icon-left container has nothing to anchor to, and it renders no
13957
14220
  // Control inside one or out. The left icon is checked as the caller
13958
14221
  // passed it: the default glyph is this component's own choice, not a
13959
- // prop the caller set.
14222
+ // prop the caller set, so it goes in only for the advice.
13960
14223
  const ownControl = rendersOwnControl('DateInput', {
13961
14224
  insideControl,
13962
14225
  inline: baseProps.inline,
13963
14226
  isLoading,
13964
14227
  iconLeft,
13965
14228
  iconLeftName: props.iconLeftName,
14229
+ defaultIconLeftName: iconLeftName,
13966
14230
  iconLeftSize,
13967
14231
  iconRight,
13968
14232
  iconRightName,
@@ -14352,23 +14616,28 @@ const WheelInner = ({ values, index, onChange, formatLabel, formatAriaText, aria
14352
14616
  cancelRaf();
14353
14617
  commitPosition(Math.round(positionRef.current) + delta);
14354
14618
  }, [disabled, cancelRaf, commitPosition]);
14619
+ // The keys are a spinbutton's: up raises the value and down lowers it. The
14620
+ // values run down the wheel, so up brings the one below the band into it,
14621
+ // the way a drag or a scroll that moves the items up does.
14355
14622
  const onKeyDown = React.useCallback((e) => {
14623
+ if (isPopoverToggleKey(e))
14624
+ return;
14356
14625
  switch (e.key) {
14357
14626
  case 'ArrowUp':
14358
14627
  e.preventDefault();
14359
- moveBy(-1);
14628
+ moveBy(1);
14360
14629
  break;
14361
14630
  case 'ArrowDown':
14362
14631
  e.preventDefault();
14363
- moveBy(1);
14632
+ moveBy(-1);
14364
14633
  break;
14365
14634
  case 'PageUp':
14366
14635
  e.preventDefault();
14367
- moveBy(-5);
14636
+ moveBy(5);
14368
14637
  break;
14369
14638
  case 'PageDown':
14370
14639
  e.preventDefault();
14371
- moveBy(5);
14640
+ moveBy(-5);
14372
14641
  break;
14373
14642
  case 'Home':
14374
14643
  e.preventDefault();
@@ -14685,6 +14954,7 @@ const TimeWheels = ({ value, onChange, hourFormat = '24', enableSeconds = false,
14685
14954
  hours: h,
14686
14955
  minutes: m,
14687
14956
  seconds: s,
14957
+ milliseconds: 0,
14688
14958
  });
14689
14959
  return !unselectableTimes(probe);
14690
14960
  }, [unselectableTimes]);
@@ -14951,9 +15221,16 @@ const TimeInputBase = React.forwardRef((props, ref) => {
14951
15221
  setInternalValue(next);
14952
15222
  onChange?.(next);
14953
15223
  }, [isControlled, onChange]);
15224
+ // An empty field starts from today, at the whole minute or second the
15225
+ // wheels show.
14954
15226
  const handleSpinnerChange = React.useCallback((parts) => {
14955
- const base = value ?? new Date();
14956
- const next = setTimeOfDay(base, parts);
15227
+ const next = value
15228
+ ? setTimeOfDay(value, parts)
15229
+ : setTimeOfDay(new Date(), {
15230
+ ...parts,
15231
+ seconds: parts.seconds ?? 0,
15232
+ milliseconds: 0,
15233
+ });
14957
15234
  if (!isWithin(next, lowerBound, max))
14958
15235
  return;
14959
15236
  commitValue(next);
@@ -15084,13 +15361,14 @@ const TimeInput = React.forwardRef((props, ref) => {
15084
15361
  // icon-left container has nothing to anchor to, and it renders no
15085
15362
  // Control inside one or out. The left icon is checked as the caller
15086
15363
  // passed it: the default glyph is this component's own choice, not a
15087
- // prop the caller set.
15364
+ // prop the caller set, so it goes in only for the advice.
15088
15365
  const ownControl = rendersOwnControl('TimeInput', {
15089
15366
  insideControl,
15090
15367
  inline: baseProps.inline,
15091
15368
  isLoading,
15092
15369
  iconLeft,
15093
15370
  iconLeftName: props.iconLeftName,
15371
+ defaultIconLeftName: iconLeftName,
15094
15372
  iconLeftSize,
15095
15373
  iconRight,
15096
15374
  iconRightName,
@@ -15268,6 +15546,34 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
15268
15546
  if (!open)
15269
15547
  setTimeOpen(false);
15270
15548
  }, [open]);
15549
+ // The footer's Time button opens the wheels, and the hours wheel takes
15550
+ // focus so the keys turn it straight away. The calendar they cover is
15551
+ // inert meanwhile, so no tab stop, pointer or screen reader reaches a day
15552
+ // behind them; the attribute is set on the element, as React 18 has no
15553
+ // `inert` prop. Closing the wheels hands focus back to the button: focus
15554
+ // in them would fall to the page as they unmount, and a pointer press
15555
+ // outside them has already dropped it on the panel or the page. Focus the
15556
+ // keys moved on to the footer, such as Reset, stays there.
15557
+ const panelRef = React.useRef(null);
15558
+ const calendarRef = React.useRef(null);
15559
+ const timeButtonRef = React.useRef(null);
15560
+ const timeOverlayRef = React.useRef(null);
15561
+ React.useEffect(() => {
15562
+ calendarRef.current?.toggleAttribute('inert', timeOpen);
15563
+ if (!timeOpen)
15564
+ return;
15565
+ timeOverlayRef.current
15566
+ ?.querySelector('[role="spinbutton"]')
15567
+ ?.focus();
15568
+ }, [timeOpen]);
15569
+ const closeTime = React.useCallback(() => {
15570
+ const panel = panelRef.current;
15571
+ const active = getActiveElementInTree(panel);
15572
+ const movedOn = !!panel?.contains(active) && !timeOverlayRef.current?.contains(active);
15573
+ if (!movedOn)
15574
+ timeButtonRef.current?.focus();
15575
+ setTimeOpen(false);
15576
+ }, []);
15271
15577
  const commitValue = React.useCallback((next) => {
15272
15578
  if (!isControlled)
15273
15579
  setInternalValue(next);
@@ -15276,20 +15582,32 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
15276
15582
  setFocusedDate(next);
15277
15583
  onChange?.(next);
15278
15584
  }, [isControlled, onChange]);
15585
+ // A picked day takes the value's whole time of day, seconds and all, or
15586
+ // midnight in an empty field. The day handed over by the keyboard is the
15587
+ // calendar's focused date, which can carry the time of the clock it
15588
+ // started from, so none of its time is kept.
15279
15589
  const handleDateSelect = React.useCallback((d) => {
15280
15590
  const merged = setTimeOfDay(d, {
15281
15591
  hours: value?.getHours() ?? 0,
15282
15592
  minutes: value?.getMinutes() ?? 0,
15283
- seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
15593
+ seconds: value?.getSeconds() ?? 0,
15594
+ milliseconds: value?.getMilliseconds() ?? 0,
15284
15595
  });
15285
15596
  if (!isWithin(merged, lowerBound, max))
15286
15597
  return;
15287
15598
  commitValue(merged);
15288
15599
  setFocusedDate(d);
15289
- }, [value, enableSeconds, lowerBound, max, commitValue]);
15600
+ }, [value, lowerBound, max, commitValue]);
15601
+ // The wheels set the value's time. An empty field starts from the focused
15602
+ // day, at the whole minute or second the wheels show.
15290
15603
  const handleTimeChange = React.useCallback((parts) => {
15291
- const base = value ?? focusedDate;
15292
- const next = setTimeOfDay(base, parts);
15604
+ const next = value
15605
+ ? setTimeOfDay(value, parts)
15606
+ : setTimeOfDay(focusedDate, {
15607
+ ...parts,
15608
+ seconds: parts.seconds ?? 0,
15609
+ milliseconds: 0,
15610
+ });
15293
15611
  if (!isWithin(next, lowerBound, max))
15294
15612
  return;
15295
15613
  commitValue(next);
@@ -15303,8 +15621,12 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
15303
15621
  const fmt = typeof defaultFormat === 'string' ? defaultFormat : undefined;
15304
15622
  return parseDate(trimmed, fmt ?? DEFAULT_DATETIME_FORMAT);
15305
15623
  }, [parse, defaultFormat, locale]);
15306
- // The Date the user edits when starting without a current value (now).
15307
- const makeBaseDate = React.useCallback(() => new Date(), []);
15624
+ // The Date the user edits when starting without a current value: now, at
15625
+ // the whole minute, or the whole second with `enableSeconds`.
15626
+ const makeBaseDate = React.useCallback(() => setTimeOfDay(new Date(), {
15627
+ seconds: enableSeconds ? undefined : 0,
15628
+ milliseconds: 0,
15629
+ }), [enableSeconds]);
15308
15630
  const inputReadOnlyAttr = !!readOnly || !editable;
15309
15631
  const canOpen = !!popover && !disabled && !readOnly;
15310
15632
  // Blocking predicate for manual entry, combining the calendar's
@@ -15368,20 +15690,20 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
15368
15690
  minutes: value?.getMinutes() ?? 0,
15369
15691
  seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
15370
15692
  };
15371
- const panel = (jsxRuntime.jsxs("div", { className: panelClass, onKeyDown: e => {
15693
+ const panel = (jsxRuntime.jsxs("div", { ref: panelRef, className: panelClass, onKeyDown: e => {
15372
15694
  // First Escape collapses the floating time wheels; the popover's own
15373
15695
  // Escape handler (which closes the whole panel) only sees the second.
15374
15696
  if (timeOpen && e.key === 'Escape') {
15375
15697
  e.stopPropagation();
15376
- setTimeOpen(false);
15698
+ closeTime();
15377
15699
  }
15378
- }, children: [jsxRuntime.jsxs("div", { className: calendarWrapClass, children: [jsxRuntime.jsx(Calendar, { value: value, focusedDate: focusedDate, onSelect: handleDateSelect, 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 }), timeOpen && (jsxRuntime.jsx("div", { className: timeOverlayClass, onClick: e => {
15700
+ }, children: [jsxRuntime.jsxs("div", { className: calendarWrapClass, children: [jsxRuntime.jsx("div", { ref: calendarRef, children: jsxRuntime.jsx(Calendar, { value: value, focusedDate: focusedDate, onSelect: handleDateSelect, 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 }) }), timeOpen && (jsxRuntime.jsx("div", { ref: timeOverlayRef, className: timeOverlayClass, onClick: e => {
15379
15701
  // Tap outside the wheel card (on the covered calendar area)
15380
15702
  // collapses the wheels without selecting a date, matching the
15381
15703
  // native behavior.
15382
15704
  if (e.target === e.currentTarget)
15383
- setTimeOpen(false);
15384
- }, children: jsxRuntime.jsx("div", { className: timeCardClass, children: jsxRuntime.jsx(TimeWheels, { value: spinnerValue, onChange: handleTimeChange, hourFormat: effectiveHourFormat, enableSeconds: enableSeconds, incrementHours: incrementHours, incrementMinutes: incrementMinutes, incrementSeconds: incrementSeconds, unselectableTimes: unselectableTimes, color: color, size: size, disabled: disabled, id: `${popoverId}-time`, labels: labels, itemHeight: wheelItemHeight, audioTick: effectiveAudioTick, onCommit: () => setOpen(false) }) }) }))] }), jsxRuntime.jsxs("div", { className: footerClass, children: [jsxRuntime.jsxs("button", { type: "button", className: footerTimeClass, onClick: () => setTimeOpen(o => !o), "aria-expanded": timeOpen, disabled: disabled, children: [jsxRuntime.jsx("span", { children: t.time }), jsxRuntime.jsx("span", { className: footerTimePillClass, children: value ? formatTime(value, timeDisplayFormat, locale) : '—' })] }), jsxRuntime.jsxs("div", { className: footerActionsClass, children: [jsxRuntime.jsx("button", { type: "button", className: footerResetClass, onClick: () => commitValue(valueAtOpenRef.current), children: t.reset }), jsxRuntime.jsx("button", { type: "button", className: footerDoneClass, "aria-label": t.done, onClick: () => setOpen(false), children: jsxRuntime.jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M5 11l4 4 8-8", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] })] }));
15705
+ closeTime();
15706
+ }, children: jsxRuntime.jsx("div", { className: timeCardClass, children: jsxRuntime.jsx(TimeWheels, { value: spinnerValue, onChange: handleTimeChange, hourFormat: effectiveHourFormat, enableSeconds: enableSeconds, incrementHours: incrementHours, incrementMinutes: incrementMinutes, incrementSeconds: incrementSeconds, unselectableTimes: unselectableTimes, color: color, size: size, disabled: disabled, id: `${popoverId}-time`, labels: labels, itemHeight: wheelItemHeight, audioTick: effectiveAudioTick, onCommit: () => setOpen(false) }) }) }))] }), jsxRuntime.jsxs("div", { className: footerClass, children: [jsxRuntime.jsxs("button", { ref: timeButtonRef, type: "button", className: footerTimeClass, onClick: () => (timeOpen ? closeTime() : setTimeOpen(true)), "aria-expanded": timeOpen, disabled: disabled, children: [jsxRuntime.jsx("span", { children: t.time }), jsxRuntime.jsx("span", { className: footerTimePillClass, children: value ? formatTime(value, timeDisplayFormat, locale) : '—' })] }), jsxRuntime.jsxs("div", { className: footerActionsClass, children: [jsxRuntime.jsx("button", { type: "button", className: footerResetClass, onClick: () => commitValue(valueAtOpenRef.current), children: t.reset }), jsxRuntime.jsx("button", { type: "button", className: footerDoneClass, "aria-label": t.done, onClick: () => setOpen(false), children: jsxRuntime.jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M5 11l4 4 8-8", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] })] }));
15385
15707
  if (inline) {
15386
15708
  return (jsxRuntime.jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [panel, name && (jsxRuntime.jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoDateTime(value, enableSeconds) : '', required: required }))] }));
15387
15709
  }
@@ -15425,13 +15747,14 @@ const DateTimeInput = React.forwardRef((props, ref) => {
15425
15747
  // icon-left container has nothing to anchor to, and it renders no
15426
15748
  // Control inside one or out. The left icon is checked as the caller
15427
15749
  // passed it: the default glyph is this component's own choice, not a
15428
- // prop the caller set.
15750
+ // prop the caller set, so it goes in only for the advice.
15429
15751
  const ownControl = rendersOwnControl('DateTimeInput', {
15430
15752
  insideControl,
15431
15753
  inline: baseProps.inline,
15432
15754
  isLoading,
15433
15755
  iconLeft,
15434
15756
  iconLeftName: props.iconLeftName,
15757
+ defaultIconLeftName: iconLeftName,
15435
15758
  iconLeftSize,
15436
15759
  iconRight,
15437
15760
  iconRightName,
@@ -15466,6 +15789,354 @@ const DateTimeInput = React.forwardRef((props, ref) => {
15466
15789
  });
15467
15790
  DateTimeInput.displayName = 'DateTimeInput';
15468
15791
 
15792
+ const EMPTY_RANGE = [null, null];
15793
+ /** A range's end as the form and the native inputs carry it. */
15794
+ const isoDay = (d) => (d ? toIsoValue(d, 'day') : '');
15795
+ /** Today at local midnight, where typing into an empty end starts. */
15796
+ const today = () => startOfDay(new Date());
15797
+ /**
15798
+ * Raw DateRangeInput: the two inputs and the popover calendar without
15799
+ * Field/Control wrapping. Use `DateRangeInput` for the convenience wrapper.
15800
+ *
15801
+ * @function
15802
+ * @param {DateRangeInputBaseProps} props
15803
+ * @returns {JSX.Element}
15804
+ */
15805
+ const DateRangeInputBase = React.forwardRef((props, ref) => {
15806
+ 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 = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, allowDisabledInRange = false, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', labels, name, startName, endName, form, required, id, className, onFocus, onFocusCapture, ...rest } = props;
15807
+ // The launcher gives way to the loading spinner of a Control this sits in.
15808
+ const controlLoading = useControlLoading();
15809
+ const triggerIcon = triggerIconProp ?? !controlLoading;
15810
+ const t = mergeLabels(labels);
15811
+ const resolvedFormat = format ?? DEFAULT_DATE_FORMAT;
15812
+ const isControlled = controlledValue !== undefined;
15813
+ const [internalValue, setInternalValue] = React.useState(defaultValue ?? EMPTY_RANGE);
15814
+ const range = isControlled ? controlledValue : internalValue;
15815
+ const [start, end] = range;
15816
+ // Nothing before year 1 is in range, as in HTML's date inputs.
15817
+ const lowerBound = React.useMemo(() => floorMin(min), [min]);
15818
+ const [focusedDate, setFocusedDate] = React.useState(() => clampDate(start ?? end ?? new Date(), lowerBound, max));
15819
+ // Re-clamp the focused day if the bounds change after mount, so the
15820
+ // calendar never shows a month outside them.
15821
+ React.useEffect(() => {
15822
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
15823
+ }, [lowerBound, max]);
15824
+ const [open, setOpenState] = React.useState(false);
15825
+ const [startText, setStartText] = React.useState(() => start ? formatDate(start, resolvedFormat, locale) : '');
15826
+ const [endText, setEndText] = React.useState(() => end ? formatDate(end, resolvedFormat, locale) : '');
15827
+ // Each input shows its end of the range as it changes from outside.
15828
+ React.useEffect(() => {
15829
+ setStartText(start ? formatDate(start, resolvedFormat, locale) : '');
15830
+ }, [start, resolvedFormat, locale]);
15831
+ React.useEffect(() => {
15832
+ setEndText(end ? formatDate(end, resolvedFormat, locale) : '');
15833
+ }, [end, resolvedFormat, locale]);
15834
+ const containerRef = React.useRef(null);
15835
+ const startRef = React.useRef(null);
15836
+ const endRef = React.useRef(null);
15837
+ const reactId = React.useId();
15838
+ const popoverId = id ? `${id}-popover` : `picker-${reactId}`;
15839
+ const { bulmaHelperClasses, rest: cleanRest } = useBulmaClasses(rest);
15840
+ const { shouldUseNative } = useNativeMobilePicker({
15841
+ force: mobileNative === 'auto' ? undefined : mobileNative,
15842
+ });
15843
+ const useNative = !inline && shouldUseNative;
15844
+ // The root borrows DateInput's container class, which places the launcher
15845
+ // and sizes a Control's icons around the field.
15846
+ const containerClass = usePrefixedClassNames('daterangeinput', 'dateinput-container');
15847
+ const fieldClass = usePrefixedClassNames('input', 'daterangeinput-field', {
15848
+ [`is-${color}`]: !!color,
15849
+ [`is-${size}`]: !!size,
15850
+ 'is-rounded': isRounded,
15851
+ 'is-disabled': disabled,
15852
+ });
15853
+ const inputClass = usePrefixedClassNames('daterangeinput-input');
15854
+ const separatorClass = usePrefixedClassNames('daterangeinput-separator');
15855
+ const triggerClass = usePrefixedClassNames('dateinput-trigger');
15856
+ // What the latest request asked for, so a second request in the same event
15857
+ // sees it before React renders. The callbacks fire here rather than in a
15858
+ // state updater, which StrictMode calls twice.
15859
+ const requestedOpenRef = React.useRef(false);
15860
+ const setOpen = React.useCallback((next) => {
15861
+ if (requestedOpenRef.current === next)
15862
+ return;
15863
+ requestedOpenRef.current = next;
15864
+ setOpenState(next);
15865
+ if (next)
15866
+ onOpen?.();
15867
+ else
15868
+ onClose?.();
15869
+ }, [onOpen, onClose]);
15870
+ // Focus that moves inside the field, from one input to the other or from
15871
+ // the launcher, is not focus arriving, so it opens nothing. Set as the
15872
+ // focus is captured on the way in, and cleared once the input has handled
15873
+ // it, so a click that follows still opens the popover.
15874
+ const focusFromInsideRef = React.useRef(false);
15875
+ const handleFocusCapture = (e) => {
15876
+ const from = e.relatedTarget;
15877
+ focusFromInsideRef.current = !!from && e.currentTarget.contains(from);
15878
+ onFocusCapture?.(e);
15879
+ };
15880
+ const handleFocus = (e) => {
15881
+ focusFromInsideRef.current = false;
15882
+ onFocus?.(e);
15883
+ };
15884
+ const requestOpen = React.useCallback((next) => {
15885
+ if (next && focusFromInsideRef.current)
15886
+ return;
15887
+ setOpen(next);
15888
+ }, [setOpen]);
15889
+ const commitRange = React.useCallback((next) => {
15890
+ if (!isControlled)
15891
+ setInternalValue(next);
15892
+ onChange?.(next);
15893
+ }, [isControlled, onChange]);
15894
+ const constraints = React.useMemo(() => ({ shouldDisableDate, unselectableDates }), [shouldDisableDate, unselectableDates]);
15895
+ // A day the calendar disables: shouldDisableDate or unselectableDates.
15896
+ // Typing and the native inputs check the bounds alongside it.
15897
+ const isBlockedDay = React.useCallback((d) => !!shouldDisableDate?.(d) ||
15898
+ !!unselectableDates?.some(u => isSameDay(u, d)), [shouldDisableDate, unselectableDates]);
15899
+ // An end the range can take: it is not disabled, and it closes a range
15900
+ // from the start, which it can't when it comes first or, unless allowed,
15901
+ // reaches over a disabled day.
15902
+ const isBlockedEnd = React.useCallback((d) => isBlockedDay(d) ||
15903
+ (!!start && !canCloseRange(start, d, constraints, allowDisabledInRange)), [isBlockedDay, start, constraints, allowDisabledInRange]);
15904
+ // A new start keeps the end only while the two still make a range, so a
15905
+ // start moved past the end, or over a disabled day before it, clears it.
15906
+ const commitStart = React.useCallback((next) => {
15907
+ const keepsEnd = !next ||
15908
+ !end ||
15909
+ canCloseRange(next, end, constraints, allowDisabledInRange);
15910
+ commitRange([next, keepsEnd ? end : null]);
15911
+ if (next)
15912
+ setFocusedDate(next);
15913
+ }, [end, constraints, allowDisabledInRange, commitRange]);
15914
+ // Every digit typed into the start commits, so a start passes through
15915
+ // values on its way to the one being typed. The first digits of a year make
15916
+ // a start centuries back, which can't keep the end once a disabled day may
15917
+ // lie between. Such a value waits for the segment's next digit rather than
15918
+ // clearing an end the finished start may keep.
15919
+ const startCostsEnd = React.useCallback((d) => !!end && !canCloseRange(d, end, constraints, allowDisabledInRange), [end, constraints, allowDisabledInRange]);
15920
+ const commitEnd = React.useCallback((next) => {
15921
+ commitRange([start, next]);
15922
+ if (next)
15923
+ setFocusedDate(next);
15924
+ }, [start, commitRange]);
15925
+ const handleRangeSelect = React.useCallback((from, to) => {
15926
+ commitRange([from, to]);
15927
+ setFocusedDate(to);
15928
+ if (closeOnSelect)
15929
+ setOpen(false);
15930
+ }, [commitRange, closeOnSelect, setOpen]);
15931
+ const tryParse = React.useCallback((s) => {
15932
+ const trimmed = s.trim();
15933
+ if (!trimmed)
15934
+ return null;
15935
+ const fmt = typeof format === 'string' ? format : undefined;
15936
+ return parse
15937
+ ? parse(trimmed)
15938
+ : parseDate(trimmed, fmt ?? DEFAULT_DATE_FORMAT);
15939
+ }, [parse, format, locale]);
15940
+ // Typing into an empty end starts from the start, so only what differs
15941
+ // needs typing; an empty start starts from today.
15942
+ const makeEndBase = React.useCallback(() => (start ? startOfDay(start) : today()), [start]);
15943
+ // Where focus goes back as the popover closes: the input focused last,
15944
+ // which is the one that opened it unless the launcher did, or the start
15945
+ // input before either has had focus.
15946
+ const openerRef = React.useRef(null);
15947
+ const restoreFocusRef = React.useMemo(() => ({
15948
+ get current() {
15949
+ return openerRef.current ?? startRef.current;
15950
+ },
15951
+ }), []);
15952
+ const shared = {
15953
+ format: resolvedFormat,
15954
+ formatFn: formatDate,
15955
+ tryParse,
15956
+ locale,
15957
+ min: lowerBound,
15958
+ max,
15959
+ disabled,
15960
+ readOnly,
15961
+ editable,
15962
+ popover,
15963
+ openOnFocus,
15964
+ closeOnSelect,
15965
+ isOpen: open,
15966
+ setOpen: requestOpen,
15967
+ };
15968
+ // Each input is its own picker field. Its blur counts focus leaving it,
15969
+ // even for the other input, so text typed into one is read as focus moves
15970
+ // to the other.
15971
+ const { inputHandlers: startHandlers } = useSegmentedEntry({
15972
+ ...shared,
15973
+ value: start,
15974
+ commitValue: commitStart,
15975
+ text: startText,
15976
+ setText: setStartText,
15977
+ makeBaseDate: today,
15978
+ isBlocked: isBlockedDay,
15979
+ isUnfinishedBlocked: startCostsEnd,
15980
+ inputRef: startRef,
15981
+ containerRef: startRef,
15982
+ onFocus: () => {
15983
+ openerRef.current = startRef.current;
15984
+ },
15985
+ });
15986
+ const { inputHandlers: endHandlers } = useSegmentedEntry({
15987
+ ...shared,
15988
+ value: end,
15989
+ commitValue: commitEnd,
15990
+ text: endText,
15991
+ setText: setEndText,
15992
+ makeBaseDate: makeEndBase,
15993
+ isBlocked: isBlockedEnd,
15994
+ inputRef: endRef,
15995
+ containerRef: endRef,
15996
+ onFocus: () => {
15997
+ openerRef.current = endRef.current;
15998
+ },
15999
+ });
16000
+ const combinedRef = React.useCallback((node) => {
16001
+ startRef.current =
16002
+ node;
16003
+ if (typeof ref === 'function')
16004
+ ref(node);
16005
+ else if (ref)
16006
+ ref.current = node;
16007
+ }, [ref]);
16008
+ const handlePopoverClose = React.useCallback(() => setOpen(false), [setOpen]);
16009
+ const startField = startName ?? (name ? `${name}[start]` : undefined);
16010
+ const endField = endName ?? (name ? `${name}[end]` : undefined);
16011
+ const hiddenInputs = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [startField && (jsxRuntime.jsx("input", { type: "hidden", name: startField, form: form, value: isoDay(start) })), endField && (jsxRuntime.jsx("input", { type: "hidden", name: endField, form: form, value: isoDay(end) }))] }));
16012
+ const separator = (jsxRuntime.jsx("span", { className: separatorClass, "aria-hidden": "true", children: t.rangeSeparator }));
16013
+ // Native mobile path: two <input type="date">s, held to the same rules as
16014
+ // typing. A change the rules refuse leaves the input showing the range.
16015
+ if (useNative) {
16016
+ const nativeChange = (commit, isBlocked) => (e) => {
16017
+ // An empty value is the end cleared. A date input gives nothing else
16018
+ // that doesn't read as a date.
16019
+ const parsed = fromIsoValue(e.target.value, 'day');
16020
+ if (!parsed)
16021
+ commit(null);
16022
+ else if (isWithin(parsed, lowerBound, max) && !isBlocked(parsed)) {
16023
+ commit(parsed);
16024
+ }
16025
+ };
16026
+ const minIso = min ? isoDay(lowerBound) : undefined;
16027
+ const maxIso = max ? isoDay(max) : undefined;
16028
+ return (jsxRuntime.jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: onFocus, onFocusCapture: onFocusCapture, children: [jsxRuntime.jsxs("div", { className: fieldClass, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "date", id: id, "aria-label": t.rangeStart, className: inputClass, value: isoDay(start), onChange: nativeChange(commitStart, isBlockedDay), min: minIso, max: maxIso, disabled: disabled, readOnly: readOnly, required: required }), separator, jsxRuntime.jsx("input", { ref: endRef, type: "date", id: id ? `${id}-end` : undefined, "aria-label": t.rangeEnd, className: inputClass, value: isoDay(end), onChange: nativeChange(commitEnd, isBlockedEnd), min: start ? isoDay(start) : minIso, max: maxIso, disabled: disabled, readOnly: readOnly, required: required })] }), hiddenInputs] }));
16029
+ }
16030
+ const calendar = (jsxRuntime.jsx(Calendar, { range: range, onRangeSelect: handleRangeSelect, allowDisabledInRange: allowDisabledInRange, focusedDate: focusedDate, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size,
16031
+ // The popover panel takes `popoverId`, so the calendar inside it takes
16032
+ // one of its own. Inline there is no panel.
16033
+ id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }));
16034
+ if (inline) {
16035
+ return (jsxRuntime.jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: onFocus, onFocusCapture: onFocusCapture, children: [calendar, hiddenInputs] }));
16036
+ }
16037
+ const canOpen = !!popover && !disabled && !readOnly;
16038
+ const inputReadOnly = !!readOnly || !editable;
16039
+ // The field reads as one text box, so a press on it that misses both
16040
+ // inputs, on its padding, the gap or the separator, goes to the nearer
16041
+ // input and does what a press on that input does.
16042
+ const handleFieldMouseDown = (e) => {
16043
+ const startInput = startRef.current;
16044
+ const endInput = endRef.current;
16045
+ if (disabled || e.button !== 0)
16046
+ return;
16047
+ if (e.target === startInput || e.target === endInput)
16048
+ return;
16049
+ e.preventDefault();
16050
+ const middle = (startInput.getBoundingClientRect().right +
16051
+ endInput.getBoundingClientRect().left) /
16052
+ 2;
16053
+ (e.clientX < middle ? startInput : endInput).focus();
16054
+ if (openOnFocus && canOpen)
16055
+ requestOpen(true);
16056
+ };
16057
+ const comboboxProps = {
16058
+ type: 'text',
16059
+ role: 'combobox',
16060
+ 'aria-haspopup': 'dialog',
16061
+ 'aria-expanded': open,
16062
+ 'aria-controls': popoverId,
16063
+ autoComplete: 'off',
16064
+ className: inputClass,
16065
+ placeholder,
16066
+ disabled,
16067
+ readOnly: inputReadOnly,
16068
+ required,
16069
+ };
16070
+ return (jsxRuntime.jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: handleFocus, onFocusCapture: handleFocusCapture, children: [jsxRuntime.jsxs("div", { className: fieldClass, onMouseDown: handleFieldMouseDown, children: [jsxRuntime.jsx("input", { ...comboboxProps, ref: combinedRef, id: id, "aria-label": t.rangeStart, value: startText, ...startHandlers }), separator, jsxRuntime.jsx("input", { ...comboboxProps, ref: endRef, id: id ? `${id}-end` : undefined, "aria-label": t.rangeEnd, value: endText, ...endHandlers })] }), popover && triggerIcon && (jsxRuntime.jsx("button", { type: "button", className: triggerClass,
16071
+ // Disabled whenever the popover can't open, so a click can.
16072
+ onClick: () => setOpen(!open), disabled: !canOpen, "aria-label": t.chooseDateRange, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsxRuntime.jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsxRuntime.jsx(PickerPopover, { isOpen: open, onClose: handlePopoverClose, anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: t.chooseDateRange, id: popoverId, restoreFocusRef: restoreFocusRef, initialFocusSelector: CALENDAR_FOCUSED_CELL, children: calendar })), hiddenInputs] }));
16073
+ });
16074
+ DateRangeInputBase.displayName = 'DateRangeInputBase';
16075
+
16076
+ /**
16077
+ * The `DateRangeInput` component is a form input for a start and end date, picked from one popover calendar or typed into two segmented inputs.
16078
+ *
16079
+ * @function
16080
+ * @param {DateRangeInputProps} props - Props for the DateRangeInput.
16081
+ * @returns {JSX.Element}
16082
+ *
16083
+ * @example
16084
+ * <DateRangeInput label="Stay" name="stay" min={new Date()} />
16085
+ */
16086
+ const DateRangeInput = React.forwardRef((props, ref) => {
16087
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
16088
+ const insideField = useInsideField();
16089
+ const insideControl = useInsideControl();
16090
+ const ownField = rendersOwnField('DateRangeInput', {
16091
+ insideField,
16092
+ insideControl,
16093
+ label,
16094
+ message,
16095
+ horizontal,
16096
+ fieldClassName,
16097
+ });
16098
+ // Inline mode renders a bare calendar with no input, so the Control's
16099
+ // icon containers have nothing to anchor to, and it renders no Control
16100
+ // inside one or out. The left icon is checked as the caller passed it:
16101
+ // the default glyph is this component's own choice, not a prop the
16102
+ // caller set, so it goes in only for the advice.
16103
+ const ownControl = rendersOwnControl('DateRangeInput', {
16104
+ insideControl,
16105
+ inline: baseProps.inline,
16106
+ isLoading,
16107
+ iconLeft,
16108
+ iconLeftName: props.iconLeftName,
16109
+ defaultIconLeftName: iconLeftName,
16110
+ iconLeftSize,
16111
+ iconRight,
16112
+ iconRightName,
16113
+ iconRightSize,
16114
+ hasIconsLeft,
16115
+ hasIconsRight,
16116
+ isExpanded,
16117
+ controlSize,
16118
+ controlClassName,
16119
+ });
16120
+ const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
16121
+ label,
16122
+ labelProps,
16123
+ rendersLabel: ownField,
16124
+ });
16125
+ const helpClass = usePrefixedClassNames('help', {
16126
+ [`is-${messageColor}`]: !!messageColor,
16127
+ });
16128
+ let content = (jsxRuntime.jsx(DateRangeInputBase, { ref: ref, "aria-labelledby": ariaLabelledBy, ...baseProps }));
16129
+ if (ownControl) {
16130
+ content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
16131
+ }
16132
+ const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
16133
+ if (ownField) {
16134
+ return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
16135
+ }
16136
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
16137
+ });
16138
+ DateRangeInput.displayName = 'DateRangeInput';
16139
+
15469
16140
  /**
15470
16141
  * The `Cell` component provides a single Bulma grid cell for use inside the [`Grid`](./grid.md) component.
15471
16142
  *
@@ -16872,6 +17543,8 @@ exports.Content = Content;
16872
17543
  exports.Control = Control;
16873
17544
  exports.DateInput = DateInput;
16874
17545
  exports.DateInputBase = DateInputBase;
17546
+ exports.DateRangeInput = DateRangeInput;
17547
+ exports.DateRangeInputBase = DateRangeInputBase;
16875
17548
  exports.DateTimeInput = DateTimeInput;
16876
17549
  exports.DateTimeInputBase = DateTimeInputBase;
16877
17550
  exports.Delete = Delete;