@allxsmith/bestax-bulma 5.26.5 → 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 (56) 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 +974 -96
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +973 -97
  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/Numberinput.d.ts +8 -1
  17. package/dist/types/form/TimeInputBase.d.ts +1 -1
  18. package/dist/types/form/_pickerInternals/Calendar.d.ts +45 -4
  19. package/dist/types/form/_pickerInternals/PickerPopover.d.ts +1 -0
  20. package/dist/types/form/_pickerInternals/dateUtils.d.ts +24 -0
  21. package/dist/types/form/_pickerInternals/formatters.d.ts +14 -0
  22. package/dist/types/form/_pickerInternals/pickerTypes.d.ts +27 -0
  23. package/dist/types/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
  24. package/dist/types/helpers/statusRegion.d.ts +1 -0
  25. package/dist/types/index.d.ts +4 -0
  26. package/dist/types-cjs/components/Tabs.d.ts +25 -3
  27. package/dist/types-cjs/form/DateInputBase.d.ts +5 -2
  28. package/dist/types-cjs/form/DateRangeInput.d.ts +91 -0
  29. package/dist/types-cjs/form/DateRangeInputBase.d.ts +172 -0
  30. package/dist/types-cjs/form/DateTimeInputBase.d.ts +7 -2
  31. package/dist/types-cjs/form/File.d.ts +15 -2
  32. package/dist/types-cjs/form/FormContext.d.ts +11 -0
  33. package/dist/types-cjs/form/Numberinput.d.ts +8 -1
  34. package/dist/types-cjs/form/TimeInputBase.d.ts +1 -1
  35. package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +45 -4
  36. package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +1 -0
  37. package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +24 -0
  38. package/dist/types-cjs/form/_pickerInternals/formatters.d.ts +14 -0
  39. package/dist/types-cjs/form/_pickerInternals/pickerTypes.d.ts +27 -0
  40. package/dist/types-cjs/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
  41. package/dist/types-cjs/helpers/statusRegion.d.ts +1 -0
  42. package/dist/types-cjs/index.d.ts +4 -0
  43. package/dist/versions/bestax-no-dark-mode.css +1 -1
  44. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  45. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  46. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  47. package/dist/versions/bestax-no-helpers.css +1 -1
  48. package/dist/versions/bestax-no-helpers.css.map +1 -1
  49. package/dist/versions/bestax-prefixed.css +1 -1
  50. package/dist/versions/bestax-prefixed.css.map +1 -1
  51. package/package.json +1 -1
  52. package/src/scss/components/_tabs.scss +44 -0
  53. package/src/scss/form/_dateinput.scss +58 -3
  54. package/src/scss/form/_daterangeinput.scss +63 -0
  55. package/src/scss/form/_file.scss +37 -0
  56. 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
@@ -7691,7 +7845,8 @@ const noAnnouncementsYet = {
7691
7845
  // the accessibility tree. The declarations match the `extras-sr-only` mixin,
7692
7846
  // plus `clip-path`, which replaces the deprecated `clip`. The negative margin
7693
7847
  // pulls the 1px box back inside the page, so it doesn't add scrollable
7694
- // overflow.
7848
+ // overflow. Exported for other live regions that must work without a
7849
+ // stylesheet, such as a range calendar's.
7695
7850
  const visuallyHidden = {
7696
7851
  position: 'absolute',
7697
7852
  width: '1px',
@@ -8849,6 +9004,10 @@ const ON_CONTROL = {
8849
9004
  * only the props it hands its own `Control`, as the caller gave them, so a
8850
9005
  * default the wrapper fills in itself never warns. A prop counts when it is
8851
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.
8852
9011
  * Internal; not part of the public API.
8853
9012
  */
8854
9013
  const rendersOwnControl = (component, options) => {
@@ -8859,18 +9018,32 @@ const rendersOwnControl = (component, options) => {
8859
9018
  return false;
8860
9019
  const names = held.join(' ');
8861
9020
  const doNothing = held.length > 1 ? 'those props do' : 'that prop does';
9021
+ let message;
8862
9022
  if (options.inline) {
8863
- warnOnce(`${component}:Control-props-inline:${held.join('+')}`, `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
8864
- `in inline mode, inside a <Control> or not, so ${doNothing} ` +
8865
- `nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
8866
- `inline picker.`);
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.`;
8867
9028
  }
8868
9029
  else {
8869
- warnOnce(`${component}:Control-props-in-Control:${held.join('+')}`, `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
8870
- `no <Control> of its own, so ${doNothing} nothing there. Set ` +
8871
- `${listOf(held.map(prop => ON_CONTROL[prop]))} on that <Control> ` +
8872
- `instead.`);
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.`;
8873
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);
8874
9047
  return false;
8875
9048
  };
8876
9049
  const FieldLabelIdContext = React.createContext(undefined);
@@ -9357,7 +9530,40 @@ const Checkboxes = withSubComponents(CheckboxesComponent, { Checkbox }, 'Checkbo
9357
9530
  */
9358
9531
  const File = React.forwardRef(({
9359
9532
  // Field props
9360
- 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;
9361
9567
  const insideField = useInsideField();
9362
9568
  const insideControl = useInsideControl();
9363
9569
  const ownField = rendersOwnField('File', {
@@ -9403,7 +9609,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
9403
9609
  [`is-${messageColor}`]: !!messageColor,
9404
9610
  });
9405
9611
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
9406
- const fileElement = (jsxRuntime.jsx("div", { className: fileClass, children: jsxRuntime.jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsxRuntime.jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxRuntime.jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsxRuntime.jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
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 }))] }) }));
9407
9613
  if (ownField) {
9408
9614
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
9409
9615
  }
@@ -10142,7 +10348,11 @@ const Numberinput = React.forwardRef(({
10142
10348
  // Field props
10143
10349
  label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue, min, max, step = 1, size, color, inputColor, controlsPosition = 'both', controlsRounded = false, compact = false, bare, variant = 'plusminus', disabled = false, editable = true, isLoading = false, exponential = false, onChange, className, ...props }, ref) => {
10144
10350
  const insideField = useInsideField();
10145
- const effectiveBare = bare ?? insideField;
10351
+ // Bare controls join their parent's row, which a `.control` does not lay
10352
+ // out. `useInsideControl()` is true anywhere below a `Control`, even past
10353
+ // a `Field` nested in it, so there they keep their own row as well.
10354
+ const insideControl = useInsideControl();
10355
+ const effectiveBare = bare ?? (insideField && !insideControl);
10146
10356
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10147
10357
  const [internalValue, setInternalValue] = React.useState(defaultValue);
10148
10358
  const inputRef = React.useRef(null);
@@ -11476,6 +11686,12 @@ fieldClassName, controlClassName,
11476
11686
  });
11477
11687
  TextArea.displayName = 'TextArea';
11478
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');
11479
11695
  const DEFAULT_PICKER_LABELS = {
11480
11696
  prevMonth: 'Previous month',
11481
11697
  nextMonth: 'Next month',
@@ -11484,6 +11700,11 @@ const DEFAULT_PICKER_LABELS = {
11484
11700
  chooseDate: 'Choose date',
11485
11701
  chooseMonth: 'Choose month',
11486
11702
  chooseYear: 'Choose year',
11703
+ rangeStart: 'Start date',
11704
+ rangeEnd: 'End date',
11705
+ rangePreviewEnd: 'Choose as end date',
11706
+ rangeSeparator: '–',
11707
+ chooseDateRange: 'Choose date range',
11487
11708
  hours: 'hours',
11488
11709
  minutes: 'minutes',
11489
11710
  seconds: 'seconds',
@@ -11636,6 +11857,43 @@ function isPeriodUnselectable(d, granularity, c) {
11636
11857
  }
11637
11858
  return true;
11638
11859
  }
11860
+ /**
11861
+ * The most days {@link canCloseRange} walks between a range's ends, about
11862
+ * eleven years. A span it would have to walk further is refused, so the check
11863
+ * stays cheap enough to run on every pointer move over the calendar.
11864
+ */
11865
+ const RANGE_WALK_LIMIT = 4000;
11866
+ /**
11867
+ * Whether `end` can close a range that opens on `start`: it falls on `start`'s
11868
+ * day or later, and, unless `allowDisabled`, no day between the two is one
11869
+ * that `shouldDisableDate` or `unselectableDates` disables. Checking that
11870
+ * walks the days between, so with either rule set and `allowDisabled` off,
11871
+ * an `end` more than {@link RANGE_WALK_LIMIT} days after `start` is refused.
11872
+ * The ends' own constraints, `min` and `max` among them, are the caller's to
11873
+ * check. Bounds can't fall between two days inside them, so they are not
11874
+ * walked.
11875
+ */
11876
+ function canCloseRange(start, end, c, allowDisabled = false) {
11877
+ const first = startOfDay(start);
11878
+ const last = startOfDay(end).getTime();
11879
+ if (last < first.getTime())
11880
+ return false;
11881
+ if (allowDisabled)
11882
+ return true;
11883
+ const blocks = {
11884
+ shouldDisableDate: c.shouldDisableDate,
11885
+ unselectableDates: c.unselectableDates,
11886
+ };
11887
+ if (!blocks.shouldDisableDate && !blocks.unselectableDates?.length) {
11888
+ return true;
11889
+ }
11890
+ for (let d = addDays(first, 1), walked = 1; d.getTime() < last; d = addDays(d, 1), walked++) {
11891
+ if (walked >= RANGE_WALK_LIMIT || isDayUnselectable(d, blocks)) {
11892
+ return false;
11893
+ }
11894
+ }
11895
+ return true;
11896
+ }
11639
11897
  function addDays(d, n) {
11640
11898
  const r = new Date(d);
11641
11899
  r.setDate(r.getDate() + n);
@@ -11654,6 +11912,12 @@ function addMonths(d, n) {
11654
11912
  function addYears(d, n) {
11655
11913
  return addMonths(d, n * 12);
11656
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
+ */
11657
11921
  function setTimeOfDay(d, parts) {
11658
11922
  const r = new Date(d);
11659
11923
  if (parts.hours !== undefined)
@@ -11662,6 +11926,8 @@ function setTimeOfDay(d, parts) {
11662
11926
  r.setMinutes(parts.minutes);
11663
11927
  if (parts.seconds !== undefined)
11664
11928
  r.setSeconds(parts.seconds);
11929
+ if (parts.milliseconds !== undefined)
11930
+ r.setMilliseconds(parts.milliseconds);
11665
11931
  return r;
11666
11932
  }
11667
11933
  /**
@@ -11810,6 +12076,36 @@ function formatDateTime(d, fmt, locale) {
11810
12076
  return formatTokenString(d, f);
11811
12077
  return formatWithIntl(d, f, locale);
11812
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
+ }
11813
12109
  /**
11814
12110
  * Derive the hour cycle (`'12'` or `'24'`) a display format will render, from
11815
12111
  * its first hour token: `h`/`hh` → 12-hour with meridiem, `H`/`HH` → 24-hour.
@@ -12096,6 +12392,28 @@ function nearestEnabled(i, disabled) {
12096
12392
  }
12097
12393
  return i;
12098
12394
  }
12395
+ /** The long, spoken form of a date in a range's announcement. */
12396
+ const LONG_DATE = {
12397
+ year: 'numeric',
12398
+ month: 'long',
12399
+ day: 'numeric',
12400
+ };
12401
+ /** A range's pending start: its start, when it has no end. */
12402
+ const pendingStartOf = (range) => range?.[0] && !range[1] ? startOfDay(range[0]) : null;
12403
+ /**
12404
+ * The day grid's cells in rows of a week. A grid owns rows and rows own
12405
+ * cells, so assistive technology can walk the grid, and the selection it
12406
+ * reports, row by row.
12407
+ */
12408
+ const inWeeks = (days, className) => {
12409
+ const rows = [];
12410
+ for (let i = 0; i < days.length; i += 7) {
12411
+ rows.push(jsxRuntime.jsx("div", { role: "row", className: className, children: days.slice(i, i + 7) }, i));
12412
+ }
12413
+ return rows;
12414
+ };
12415
+ /** A date's time, or `null` for none, for comparing ranges by value. */
12416
+ const timeOf = (d) => d ? d.getTime() : null;
12099
12417
  /**
12100
12418
  * The year list's keyboard, as the selection surface and as navigation:
12101
12419
  * arrows move a year or a row, Home/End go to the list's ends. Returns the
@@ -12121,7 +12439,7 @@ function yearStep(key, focusedYear, years) {
12121
12439
  return null;
12122
12440
  }
12123
12441
  }
12124
- const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minProp, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
12442
+ 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', range, onRangeSelect, allowDisabledInRange = false, }) => {
12125
12443
  const gridRef = React.useRef(null);
12126
12444
  const monthGridRef = React.useRef(null);
12127
12445
  const yearGridRef = React.useRef(null);
@@ -12178,6 +12496,80 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12178
12496
  const i = nearestEnabled(focusedDate.getDate() - 1, days.map(isDateUnselectable));
12179
12497
  return days[i];
12180
12498
  }, [cells, focusedDate, isDateUnselectable]);
12499
+ // ----- Range mode -----
12500
+ // Range props come only with the day grid.
12501
+ const rangeMode = !!range;
12502
+ const valuePending = pendingStartOf(range);
12503
+ // The start of a range being picked, which nothing outside the calendar
12504
+ // sees until its end is picked too. It starts as the range's own pending
12505
+ // start, and starts there again whenever the range changes from outside.
12506
+ const [anchor, setAnchor] = React.useState(valuePending);
12507
+ const rangeKey = `${timeOf(range?.[0])}|${timeOf(range?.[1])}`;
12508
+ const [anchorRangeKey, setAnchorRangeKey] = React.useState(rangeKey);
12509
+ if (anchorRangeKey !== rangeKey) {
12510
+ setAnchorRangeKey(rangeKey);
12511
+ setAnchor(valuePending);
12512
+ }
12513
+ // The day under the pointer while a start is pending, which the preview
12514
+ // runs to ahead of the focused day.
12515
+ const [hoverDate, setHoverDate] = React.useState(null);
12516
+ // Whether `day` can end a range that starts on `start`. The start is
12517
+ // checked as well as the end: one the range brought, or one picked before
12518
+ // the bounds or the disabled days changed, can rule itself out.
12519
+ const canEndAt = React.useCallback((start, day) => !isDateUnselectable(start) &&
12520
+ !isDateUnselectable(day) &&
12521
+ canCloseRange(start, day, { shouldDisableDate, unselectableDates }, allowDisabledInRange), [
12522
+ isDateUnselectable,
12523
+ shouldDisableDate,
12524
+ unselectableDates,
12525
+ allowDisabledInRange,
12526
+ ]);
12527
+ // A pick in range mode: the end of the pending range when it can be one,
12528
+ // and otherwise the start of a new one.
12529
+ const pickRangeDay = React.useCallback((picked) => {
12530
+ const day = startOfDay(picked);
12531
+ // The pointer is on the picked day, which the focused day now is too.
12532
+ setHoverDate(null);
12533
+ if (anchor && canEndAt(anchor, day)) {
12534
+ setAnchor(null);
12535
+ onRangeSelect?.(anchor, day);
12536
+ }
12537
+ else {
12538
+ setAnchor(day);
12539
+ }
12540
+ }, [anchor, canEndAt, onRangeSelect]);
12541
+ // What the grid shows: while a start is pending, the range from it to the
12542
+ // hovered or focused day, when that day can end it; otherwise the
12543
+ // committed range.
12544
+ let shownStart;
12545
+ let shownEnd;
12546
+ if (anchor) {
12547
+ shownStart = anchor;
12548
+ const previewEnd = startOfDay(hoverDate ?? focusedDate);
12549
+ shownEnd = canEndAt(anchor, previewEnd) ? previewEnd : null;
12550
+ }
12551
+ else {
12552
+ shownStart = range?.[0] ? startOfDay(range[0]) : null;
12553
+ shownEnd = range?.[1] ? startOfDay(range[1]) : null;
12554
+ }
12555
+ const previewing = !!anchor;
12556
+ // Ids for the words that name a range's ends and state the whole range.
12557
+ const reactId = React.useId();
12558
+ const rangeIdBase = id ?? `calendar-${reactId}`;
12559
+ const rangeStartId = `${rangeIdBase}-range-start`;
12560
+ const rangeEndId = `${rangeIdBase}-range-end`;
12561
+ const rangePreviewEndId = `${rangeIdBase}-range-preview-end`;
12562
+ const rangeSummaryId = `${rangeIdBase}-range-summary`;
12563
+ // The selection in words: the pending start alone, or the committed ends.
12564
+ const selectedEnd = previewing ? null : shownEnd;
12565
+ const rangeSummary = [
12566
+ shownStart &&
12567
+ `${t.rangeStart}: ${formatDate(shownStart, LONG_DATE, locale)}`,
12568
+ selectedEnd &&
12569
+ `${t.rangeEnd}: ${formatDate(selectedEnd, LONG_DATE, locale)}`,
12570
+ ]
12571
+ .filter(Boolean)
12572
+ .join(', ');
12181
12573
  // Whether the grid on show holds focus, or held it last before focus went
12182
12574
  // nowhere: the cell that had it left the page or became disabled. A blur
12183
12575
  // that takes focus out of the grid, to nowhere included, is the user
@@ -12215,6 +12607,10 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12215
12607
  }
12216
12608
  }, [min, max, isDateUnselectable, onFocusedDateChange]);
12217
12609
  const handleKeyDown = React.useCallback((e) => {
12610
+ if (isPopoverToggleKey(e))
12611
+ return;
12612
+ // A key takes the range preview back from the pointer.
12613
+ setHoverDate(null);
12218
12614
  switch (e.key) {
12219
12615
  case 'ArrowLeft':
12220
12616
  e.preventDefault();
@@ -12257,11 +12653,35 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12257
12653
  case 'Enter':
12258
12654
  case ' ':
12259
12655
  e.preventDefault();
12260
- if (!isDateUnselectable(focusedDate))
12261
- onSelect(focusedDate);
12656
+ if (isDateUnselectable(focusedDate))
12657
+ break;
12658
+ if (rangeMode)
12659
+ pickRangeDay(focusedDate);
12660
+ else
12661
+ onSelect?.(focusedDate);
12662
+ break;
12663
+ case 'Escape':
12664
+ // A pending pick is taken back, and the key stops there so a
12665
+ // popover around the calendar stays open. The range's own pending
12666
+ // start is no pick, so Escape goes on to close the popover.
12667
+ if (anchor && !(valuePending && isSameDay(anchor, valuePending))) {
12668
+ e.preventDefault();
12669
+ e.stopPropagation();
12670
+ setAnchor(valuePending);
12671
+ }
12262
12672
  break;
12263
12673
  }
12264
- }, [focusedDate, firstDayOfWeek, isDateUnselectable, moveFocus, onSelect]);
12674
+ }, [
12675
+ focusedDate,
12676
+ firstDayOfWeek,
12677
+ isDateUnselectable,
12678
+ moveFocus,
12679
+ onSelect,
12680
+ rangeMode,
12681
+ pickRangeDay,
12682
+ anchor,
12683
+ valuePending,
12684
+ ]);
12265
12685
  // The month grid's counterpart to `moveFocus`: step by `direction` months
12266
12686
  // until a month with a selectable day turns up, stopping at the min/max
12267
12687
  // months. The cap guards against a range with nothing selectable.
@@ -12281,10 +12701,12 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12281
12701
  }
12282
12702
  }, [min, max, isMonthUnselectable, onFocusedDateChange]);
12283
12703
  const handleMonthKeyDown = React.useCallback((e) => {
12704
+ if (isPopoverToggleKey(e))
12705
+ return;
12284
12706
  if (e.key === 'Enter' || e.key === ' ') {
12285
12707
  e.preventDefault();
12286
12708
  if (!isMonthUnselectable(focusedDate)) {
12287
- onSelect(startOfMonth(focusedDate));
12709
+ onSelect?.(startOfMonth(focusedDate));
12288
12710
  }
12289
12711
  return;
12290
12712
  }
@@ -12310,6 +12732,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12310
12732
  const dayNamesRowClass = usePrefixedClassNames('dateinput-day-names');
12311
12733
  const dayNameClass = usePrefixedClassNames('dateinput-day-name');
12312
12734
  const gridClass = usePrefixedClassNames('dateinput-grid');
12735
+ const weekClass = usePrefixedClassNames('dateinput-week');
12313
12736
  const monthsGridClass = usePrefixedClassNames('dateinput-months-grid');
12314
12737
  const monthsRowClass = usePrefixedClassNames('dateinput-months-row');
12315
12738
  const yearsGridClass = usePrefixedClassNames('dateinput-years-grid');
@@ -12399,7 +12822,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12399
12822
  // pick and `onSelect` gets the year's first day.
12400
12823
  const handleYearPick = React.useCallback((year) => {
12401
12824
  onFocusedDateChange(clampDate(makeDate(year), min, max));
12402
- onSelect(makeDate(year));
12825
+ onSelect?.(makeDate(year));
12403
12826
  }, [min, max, onFocusedDateChange, onSelect]);
12404
12827
  // Step through the listed years from `year` by `direction`, skipping
12405
12828
  // disabled ones, and focus the first selectable year found.
@@ -12414,6 +12837,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12414
12837
  }
12415
12838
  }, [yearList, disabledYears, onFocusedDateChange, min, max]);
12416
12839
  const handleYearListKeyDown = React.useCallback((e) => {
12840
+ if (isPopoverToggleKey(e))
12841
+ return;
12417
12842
  if (isYearGranularity) {
12418
12843
  const step = yearStep(e.key, focusedYear, yearList);
12419
12844
  if (!step)
@@ -12520,32 +12945,69 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12520
12945
  return (jsxRuntime.jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxRuntime.jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxRuntime.jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => {
12521
12946
  setNavYear(null);
12522
12947
  setView(v => (v === 'years' ? baseView : 'years'));
12523
- }, children: [jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsxRuntime.jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxRuntime.jsxs("div", { className: navGroupClass, children: [jsxRuntime.jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsxRuntime.jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsxRuntime.jsx("div", { className: dayNameClass, children: name }, i))) }), jsxRuntime.jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
12948
+ }, children: [jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsxRuntime.jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxRuntime.jsxs("div", { className: navGroupClass, children: [jsxRuntime.jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsxRuntime.jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsxRuntime.jsx("div", { className: dayNameClass, children: name }, i))) }), jsxRuntime.jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, "aria-multiselectable": rangeMode || undefined, "aria-describedby": rangeMode ? rangeSummaryId : undefined, className: gridClass, onKeyDown: handleKeyDown, onMouseLeave: anchor ? () => setHoverDate(null) : undefined, ...gridFocusHandlers, children: inWeeks(cells.map(cell => {
12524
12949
  const disabled = isDateUnselectable(cell.date);
12525
- const isSelected = !!value && isSameDay(value, cell.date);
12950
+ const time = cell.date.getTime();
12951
+ // Range mode. The range on show has its ends filled. Every day
12952
+ // of a committed range is selected, and of a preview only the
12953
+ // start.
12954
+ const isStart = time === timeOf(shownStart);
12955
+ const isEnd = time === timeOf(shownEnd);
12956
+ const inRange = !!shownStart &&
12957
+ !!shownEnd &&
12958
+ time > shownStart.getTime() &&
12959
+ time < shownEnd.getTime();
12960
+ // The day a pending range would end on, which isn't picked
12961
+ // yet, so its description says what picking it does.
12962
+ const isPreviewEnd = previewing && isEnd && !isStart;
12963
+ const isPreview = (previewing && inRange) || isPreviewEnd;
12964
+ const isFilled = rangeMode
12965
+ ? isStart || (isEnd && !previewing)
12966
+ : !!value && isSameDay(value, cell.date);
12967
+ const isSelected = isFilled || (rangeMode && !previewing && inRange);
12968
+ const describedBy = rangeMode
12969
+ ? [
12970
+ isStart && rangeStartId,
12971
+ isEnd && !previewing && rangeEndId,
12972
+ isPreviewEnd && rangePreviewEndId,
12973
+ ]
12974
+ .filter(Boolean)
12975
+ .join(' ') || undefined
12976
+ : undefined;
12526
12977
  const isFocused = isSameDay(cell.date, tabStopDay);
12527
12978
  const otherMonth = !cell.inCurrentMonth;
12528
12979
  const cellClass = prefixedClassNames(classPrefix, 'dateinput-cell', {
12529
- 'is-selected': isSelected,
12980
+ 'is-selected': isFilled,
12981
+ 'is-range-start': isStart,
12982
+ 'is-range-end': isEnd,
12983
+ 'is-in-range': inRange,
12984
+ 'is-preview': isPreview,
12530
12985
  'is-today': cell.isToday,
12531
12986
  'is-disabled': disabled,
12532
12987
  'is-other-month': otherMonth,
12533
12988
  });
12534
12989
  const display = !otherMonth || nearbyMonthDays;
12535
- return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
12990
+ return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "aria-describedby": describedBy, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
12536
12991
  // Reached by Tab while the focused day is disabled, or
12537
12992
  // by pointer: keys move on from here. A nearby month's
12538
12993
  // day waits for its click, which turns the grid.
12539
12994
  if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
12540
12995
  onFocusedDateChange(cell.date);
12541
12996
  }
12542
- }, onClick: () => {
12997
+ },
12998
+ // Mouseover rather than mouseenter: browsers send it to a
12999
+ // disabled day as well, where React holds mouseenter back,
13000
+ // so the preview leaves the last enabled day behind.
13001
+ onMouseOver: anchor ? () => setHoverDate(cell.date) : undefined, onClick: () => {
12543
13002
  if (disabled)
12544
13003
  return;
12545
13004
  onFocusedDateChange(cell.date);
12546
- onSelect(cell.date);
13005
+ if (rangeMode)
13006
+ pickRangeDay(cell.date);
13007
+ else
13008
+ onSelect?.(cell.date);
12547
13009
  }, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
12548
- }) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
13010
+ }), weekClass) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
12549
13011
  const date = makeDate(focusedYear, month);
12550
13012
  const disabled = disabledMonths[month];
12551
13013
  const isSelected = !!value && isSameMonth(value, date);
@@ -12564,7 +13026,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12564
13026
  }
12565
13027
  }, onClick: () => {
12566
13028
  onFocusedDateChange(clampDate(date, min, max));
12567
- onSelect(date);
13029
+ onSelect?.(date);
12568
13030
  }, children: monthCellNames[month] }, month));
12569
13031
  }) }, row[0]))) })), view === 'years' && (jsxRuntime.jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
12570
13032
  const isFocused = year === tabStopYear;
@@ -12595,7 +13057,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12595
13057
  }, onClick: () => isYearGranularity
12596
13058
  ? handleYearPick(year)
12597
13059
  : handleYearSelect(year), children: year }, year));
12598
- }) }))] }));
13060
+ }) })), rangeMode && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { id: rangeStartId, hidden: true, children: t.rangeStart }), jsxRuntime.jsx("span", { id: rangeEndId, hidden: true, children: t.rangeEnd }), jsxRuntime.jsx("span", { id: rangePreviewEndId, hidden: true, children: t.rangePreviewEnd }), jsxRuntime.jsx("div", { id: rangeSummaryId, "aria-live": "polite", style: visuallyHidden, children: rangeSummary })] }))] }));
12599
13061
  };
12600
13062
  Calendar.displayName = 'Calendar';
12601
13063
 
@@ -12638,6 +13100,8 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12638
13100
  document.addEventListener('pointerdown', handler);
12639
13101
  return () => document.removeEventListener('pointerdown', handler);
12640
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.
12641
13105
  React.useEffect(() => {
12642
13106
  if (!isOpen || !closeOnEscape || !isBrowser)
12643
13107
  return undefined;
@@ -12646,6 +13110,11 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12646
13110
  e.stopPropagation();
12647
13111
  onClose();
12648
13112
  }
13113
+ else if (e.altKey && e.key === 'ArrowUp') {
13114
+ e.preventDefault();
13115
+ e.stopPropagation();
13116
+ onClose();
13117
+ }
12649
13118
  };
12650
13119
  document.addEventListener('keydown', handler);
12651
13120
  return () => document.removeEventListener('keydown', handler);
@@ -13049,7 +13518,7 @@ const SEPARATOR_RE = /[/:\-.\s]/;
13049
13518
  * null and the input transparently falls back to free-form text entry.
13050
13519
  */
13051
13520
  function useSegmentedEntry(params) {
13052
- 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;
13053
13522
  const segmentMap = React.useMemo(() => {
13054
13523
  const map = typeof format === 'string' ? buildSegmentMap(format) : null;
13055
13524
  if (!map || !skipKinds)
@@ -13245,7 +13714,7 @@ function useSegmentedEntry(params) {
13245
13714
  // Clicking the field opens the popover only when `openOnFocus` is on —
13246
13715
  // a click is just a focus. With `openOnFocus={false}` (manual-entry
13247
13716
  // mode) the click positions the caret for typing and the popover is
13248
- // opened explicitly via the right launcher button (or ArrowDown).
13717
+ // opened explicitly via the right launcher button (or Alt+ArrowDown).
13249
13718
  if (openOnFocus && popover && !disabled && !readOnly)
13250
13719
  setOpen(true);
13251
13720
  if (segmentEditable && segmentMap && inputRef.current) {
@@ -13281,14 +13750,23 @@ function useSegmentedEntry(params) {
13281
13750
  const isPm = base.getHours() >= 12;
13282
13751
  switch (e.key) {
13283
13752
  case 'ArrowUp':
13284
- e.preventDefault();
13285
- typedDigitsRef.current = '';
13286
- applyDateFromSegment(incrementSegmentValue(seg, base, 1, isPm));
13287
- return;
13288
13753
  case 'ArrowDown':
13289
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
+ }
13290
13768
  typedDigitsRef.current = '';
13291
- applyDateFromSegment(incrementSegmentValue(seg, base, -1, isPm));
13769
+ applyDateFromSegment(incrementSegmentValue(seg, base, e.key === 'ArrowUp' ? 1 : -1, isPm));
13292
13770
  return;
13293
13771
  case 'ArrowRight':
13294
13772
  e.preventDefault();
@@ -13354,6 +13832,8 @@ function useSegmentedEntry(params) {
13354
13832
  const nextBuffer = typedDigitsRef.current + e.key;
13355
13833
  typedDigitsRef.current = nextBuffer;
13356
13834
  const { date, advance } = setSegmentValue(seg, base, nextBuffer, isPm);
13835
+ if (!advance && isUnfinishedBlocked?.(date))
13836
+ return;
13357
13837
  applyDateFromSegment(date);
13358
13838
  if (advance) {
13359
13839
  typedDigitsRef.current = '';
@@ -13364,11 +13844,23 @@ function useSegmentedEntry(params) {
13364
13844
  }
13365
13845
  }
13366
13846
  // ----- free-form / popover key handling -----
13367
- 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) {
13368
13855
  e.preventDefault();
13369
13856
  setOpen(true);
13370
13857
  return;
13371
13858
  }
13859
+ if (e.key === 'ArrowUp' && e.altKey && isOpen) {
13860
+ e.preventDefault();
13861
+ setOpen(false);
13862
+ return;
13863
+ }
13372
13864
  if (e.key === 'Escape' && isOpen) {
13373
13865
  e.preventDefault();
13374
13866
  setOpen(false);
@@ -13398,11 +13890,14 @@ function useSegmentedEntry(params) {
13398
13890
  activeSegmentIdx,
13399
13891
  segmentBaseDate,
13400
13892
  applyDateFromSegment,
13893
+ isUnfinishedBlocked,
13401
13894
  moveSegment,
13402
13895
  isOpen,
13403
13896
  setOpen,
13404
13897
  closeOnSelect,
13405
13898
  popover,
13899
+ disabled,
13900
+ readOnly,
13406
13901
  isUntouched,
13407
13902
  tryParse,
13408
13903
  text,
@@ -13447,37 +13942,6 @@ function supportsInputType(type) {
13447
13942
  return supported;
13448
13943
  }
13449
13944
 
13450
- const pad2$1 = (n) => String(n).padStart(2, '0');
13451
- /**
13452
- * The value as the native input and the hidden form input carry it:
13453
- * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
13454
- * `<input type="month">` use. The year is padded to four digits, as those
13455
- * inputs require and as the `YYYY` token displays it. HTML has no such shape
13456
- * for a year before 1, so a date then is empty, as those inputs would make it.
13457
- */
13458
- const toIsoValue = (d, granularity) => {
13459
- if (d.getFullYear() < FIRST_YEAR)
13460
- return '';
13461
- const year = String(d.getFullYear()).padStart(4, '0');
13462
- if (granularity === 'year')
13463
- return year;
13464
- const month = `${year}-${pad2$1(d.getMonth() + 1)}`;
13465
- return granularity === 'month' ? month : `${month}-${pad2$1(d.getDate())}`;
13466
- };
13467
- /**
13468
- * Read a native `type="date"` or `type="month"` value back into a Date. HTML
13469
- * allows a year of four or more digits, as `toIsoValue` writes one past 9999.
13470
- */
13471
- const fromIsoValue = (s, granularity) => {
13472
- if (granularity === 'month') {
13473
- const m = /^(\d{4,})-(\d{2})$/.exec(s);
13474
- return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
13475
- }
13476
- const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
13477
- if (!m)
13478
- return null;
13479
- return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
13480
- };
13481
13945
  const DEFAULT_FORMATS = {
13482
13946
  day: DEFAULT_DATE_FORMAT,
13483
13947
  month: DEFAULT_MONTH_FORMAT,
@@ -13579,7 +14043,11 @@ const DateInputBase = React.forwardRef((props, ref) => {
13579
14043
  // them, so the month or year containing `min` stays reachable.
13580
14044
  const periodMin = React.useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
13581
14045
  const periodMax = React.useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
13582
- 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);
13583
14051
  if (!isWithin(d, periodMin, periodMax))
13584
14052
  return;
13585
14053
  commitValue(d);
@@ -13751,13 +14219,14 @@ const DateInput = React.forwardRef((props, ref) => {
13751
14219
  // icon-left container has nothing to anchor to, and it renders no
13752
14220
  // Control inside one or out. The left icon is checked as the caller
13753
14221
  // passed it: the default glyph is this component's own choice, not a
13754
- // prop the caller set.
14222
+ // prop the caller set, so it goes in only for the advice.
13755
14223
  const ownControl = rendersOwnControl('DateInput', {
13756
14224
  insideControl,
13757
14225
  inline: baseProps.inline,
13758
14226
  isLoading,
13759
14227
  iconLeft,
13760
14228
  iconLeftName: props.iconLeftName,
14229
+ defaultIconLeftName: iconLeftName,
13761
14230
  iconLeftSize,
13762
14231
  iconRight,
13763
14232
  iconRightName,
@@ -14147,23 +14616,28 @@ const WheelInner = ({ values, index, onChange, formatLabel, formatAriaText, aria
14147
14616
  cancelRaf();
14148
14617
  commitPosition(Math.round(positionRef.current) + delta);
14149
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.
14150
14622
  const onKeyDown = React.useCallback((e) => {
14623
+ if (isPopoverToggleKey(e))
14624
+ return;
14151
14625
  switch (e.key) {
14152
14626
  case 'ArrowUp':
14153
14627
  e.preventDefault();
14154
- moveBy(-1);
14628
+ moveBy(1);
14155
14629
  break;
14156
14630
  case 'ArrowDown':
14157
14631
  e.preventDefault();
14158
- moveBy(1);
14632
+ moveBy(-1);
14159
14633
  break;
14160
14634
  case 'PageUp':
14161
14635
  e.preventDefault();
14162
- moveBy(-5);
14636
+ moveBy(5);
14163
14637
  break;
14164
14638
  case 'PageDown':
14165
14639
  e.preventDefault();
14166
- moveBy(5);
14640
+ moveBy(-5);
14167
14641
  break;
14168
14642
  case 'Home':
14169
14643
  e.preventDefault();
@@ -14480,6 +14954,7 @@ const TimeWheels = ({ value, onChange, hourFormat = '24', enableSeconds = false,
14480
14954
  hours: h,
14481
14955
  minutes: m,
14482
14956
  seconds: s,
14957
+ milliseconds: 0,
14483
14958
  });
14484
14959
  return !unselectableTimes(probe);
14485
14960
  }, [unselectableTimes]);
@@ -14746,9 +15221,16 @@ const TimeInputBase = React.forwardRef((props, ref) => {
14746
15221
  setInternalValue(next);
14747
15222
  onChange?.(next);
14748
15223
  }, [isControlled, onChange]);
15224
+ // An empty field starts from today, at the whole minute or second the
15225
+ // wheels show.
14749
15226
  const handleSpinnerChange = React.useCallback((parts) => {
14750
- const base = value ?? new Date();
14751
- 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
+ });
14752
15234
  if (!isWithin(next, lowerBound, max))
14753
15235
  return;
14754
15236
  commitValue(next);
@@ -14879,13 +15361,14 @@ const TimeInput = React.forwardRef((props, ref) => {
14879
15361
  // icon-left container has nothing to anchor to, and it renders no
14880
15362
  // Control inside one or out. The left icon is checked as the caller
14881
15363
  // passed it: the default glyph is this component's own choice, not a
14882
- // prop the caller set.
15364
+ // prop the caller set, so it goes in only for the advice.
14883
15365
  const ownControl = rendersOwnControl('TimeInput', {
14884
15366
  insideControl,
14885
15367
  inline: baseProps.inline,
14886
15368
  isLoading,
14887
15369
  iconLeft,
14888
15370
  iconLeftName: props.iconLeftName,
15371
+ defaultIconLeftName: iconLeftName,
14889
15372
  iconLeftSize,
14890
15373
  iconRight,
14891
15374
  iconRightName,
@@ -15063,6 +15546,34 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
15063
15546
  if (!open)
15064
15547
  setTimeOpen(false);
15065
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
+ }, []);
15066
15577
  const commitValue = React.useCallback((next) => {
15067
15578
  if (!isControlled)
15068
15579
  setInternalValue(next);
@@ -15071,20 +15582,32 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
15071
15582
  setFocusedDate(next);
15072
15583
  onChange?.(next);
15073
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.
15074
15589
  const handleDateSelect = React.useCallback((d) => {
15075
15590
  const merged = setTimeOfDay(d, {
15076
15591
  hours: value?.getHours() ?? 0,
15077
15592
  minutes: value?.getMinutes() ?? 0,
15078
- seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
15593
+ seconds: value?.getSeconds() ?? 0,
15594
+ milliseconds: value?.getMilliseconds() ?? 0,
15079
15595
  });
15080
15596
  if (!isWithin(merged, lowerBound, max))
15081
15597
  return;
15082
15598
  commitValue(merged);
15083
15599
  setFocusedDate(d);
15084
- }, [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.
15085
15603
  const handleTimeChange = React.useCallback((parts) => {
15086
- const base = value ?? focusedDate;
15087
- 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
+ });
15088
15611
  if (!isWithin(next, lowerBound, max))
15089
15612
  return;
15090
15613
  commitValue(next);
@@ -15098,8 +15621,12 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
15098
15621
  const fmt = typeof defaultFormat === 'string' ? defaultFormat : undefined;
15099
15622
  return parseDate(trimmed, fmt ?? DEFAULT_DATETIME_FORMAT);
15100
15623
  }, [parse, defaultFormat, locale]);
15101
- // The Date the user edits when starting without a current value (now).
15102
- 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]);
15103
15630
  const inputReadOnlyAttr = !!readOnly || !editable;
15104
15631
  const canOpen = !!popover && !disabled && !readOnly;
15105
15632
  // Blocking predicate for manual entry, combining the calendar's
@@ -15163,20 +15690,20 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
15163
15690
  minutes: value?.getMinutes() ?? 0,
15164
15691
  seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
15165
15692
  };
15166
- const panel = (jsxRuntime.jsxs("div", { className: panelClass, onKeyDown: e => {
15693
+ const panel = (jsxRuntime.jsxs("div", { ref: panelRef, className: panelClass, onKeyDown: e => {
15167
15694
  // First Escape collapses the floating time wheels; the popover's own
15168
15695
  // Escape handler (which closes the whole panel) only sees the second.
15169
15696
  if (timeOpen && e.key === 'Escape') {
15170
15697
  e.stopPropagation();
15171
- setTimeOpen(false);
15698
+ closeTime();
15172
15699
  }
15173
- }, 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 => {
15174
15701
  // Tap outside the wheel card (on the covered calendar area)
15175
15702
  // collapses the wheels without selecting a date, matching the
15176
15703
  // native behavior.
15177
15704
  if (e.target === e.currentTarget)
15178
- setTimeOpen(false);
15179
- }, 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" }) }) })] })] })] }));
15180
15707
  if (inline) {
15181
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 }))] }));
15182
15709
  }
@@ -15220,13 +15747,14 @@ const DateTimeInput = React.forwardRef((props, ref) => {
15220
15747
  // icon-left container has nothing to anchor to, and it renders no
15221
15748
  // Control inside one or out. The left icon is checked as the caller
15222
15749
  // passed it: the default glyph is this component's own choice, not a
15223
- // prop the caller set.
15750
+ // prop the caller set, so it goes in only for the advice.
15224
15751
  const ownControl = rendersOwnControl('DateTimeInput', {
15225
15752
  insideControl,
15226
15753
  inline: baseProps.inline,
15227
15754
  isLoading,
15228
15755
  iconLeft,
15229
15756
  iconLeftName: props.iconLeftName,
15757
+ defaultIconLeftName: iconLeftName,
15230
15758
  iconLeftSize,
15231
15759
  iconRight,
15232
15760
  iconRightName,
@@ -15261,6 +15789,354 @@ const DateTimeInput = React.forwardRef((props, ref) => {
15261
15789
  });
15262
15790
  DateTimeInput.displayName = 'DateTimeInput';
15263
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
+
15264
16140
  /**
15265
16141
  * The `Cell` component provides a single Bulma grid cell for use inside the [`Grid`](./grid.md) component.
15266
16142
  *
@@ -16667,6 +17543,8 @@ exports.Content = Content;
16667
17543
  exports.Control = Control;
16668
17544
  exports.DateInput = DateInput;
16669
17545
  exports.DateInputBase = DateInputBase;
17546
+ exports.DateRangeInput = DateRangeInput;
17547
+ exports.DateRangeInputBase = DateRangeInputBase;
16670
17548
  exports.DateTimeInput = DateTimeInput;
16671
17549
  exports.DateTimeInputBase = DateTimeInputBase;
16672
17550
  exports.Delete = Delete;