@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.esm.js CHANGED
@@ -4139,9 +4139,65 @@ const TabsContext = createContext(null);
4139
4139
  function useTabsContext() {
4140
4140
  return useContext(TabsContext);
4141
4141
  }
4142
+ /**
4143
+ * The tabs and panels currently mounted under one `Tabs`. Each registers from
4144
+ * an effect and removes itself on cleanup, so the links between them only
4145
+ * ever name elements that exist, wherever in the tree the panels sit.
4146
+ */
4147
+ function useRegistry() {
4148
+ const [records, setRecords] = useState([]);
4149
+ const register = useCallback((record) => {
4150
+ setRecords(prev => [...prev, record]);
4151
+ return () => setRecords(prev => prev.filter(r => r !== record));
4152
+ }, []);
4153
+ return [records, register];
4154
+ }
4155
+ /**
4156
+ * The list a tab belongs to: its `role="tablist"` ancestor, so an element
4157
+ * wrapped around a tab doesn't cut it off from the others, or its parent when
4158
+ * it sits outside a tab list.
4159
+ */
4160
+ function tabListOf(tab) {
4161
+ return (tab.closest('[role="tablist"]') ?? tab.parentElement);
4162
+ }
4163
+ /**
4164
+ * The tab a key press moves focus to, found among the tabs of the same list
4165
+ * in DOM order, skipping disabled ones and wrapping at either end.
4166
+ * `undefined` for a key the tab list does not handle, or when no enabled tab
4167
+ * is left to go to.
4168
+ */
4169
+ function findTargetTab(current, key, vertical) {
4170
+ const tabs = Array.from(tabListOf(current).querySelectorAll('[role="tab"]'));
4171
+ const isEnabled = (el) => el.getAttribute('aria-disabled') !== 'true';
4172
+ if (key === 'Home')
4173
+ return tabs.find(isEnabled);
4174
+ if (key === 'End')
4175
+ return [...tabs].reverse().find(isEnabled);
4176
+ let step;
4177
+ if (key === 'ArrowRight' || (vertical && key === 'ArrowDown'))
4178
+ step = 1;
4179
+ else if (key === 'ArrowLeft' || (vertical && key === 'ArrowUp'))
4180
+ step = -1;
4181
+ else
4182
+ return undefined;
4183
+ const start = tabs.indexOf(current);
4184
+ for (let i = 1; i <= tabs.length; i++) {
4185
+ const candidate = tabs[(((start + step * i) % tabs.length) + tabs.length) % tabs.length];
4186
+ if (isEnabled(candidate))
4187
+ return candidate;
4188
+ }
4189
+ return undefined;
4190
+ }
4142
4191
  /**
4143
4192
  * The `Tabs` component provides flexible and fully-featured Bulma tab navigation for your Bulma React UI.
4144
4193
  *
4194
+ * Built from `Tabs.Tab`, the tabs follow the WAI-ARIA tabs pattern with
4195
+ * manual activation: the tab list is a single tab stop, the arrow keys, Home
4196
+ * and End move focus between tabs (skipping disabled ones), and Enter or
4197
+ * Space activates the focused tab. Each tab names its `Tabs.Content.Item` in
4198
+ * `aria-controls`, and each panel names its tab in `aria-labelledby`. Built
4199
+ * from `Tabs.Item` with links inside, the tabs stay plain Bulma navigation.
4200
+ *
4145
4201
  * @function
4146
4202
  * @param {TabsProps} props - Props for the Tabs component.
4147
4203
  * @returns {JSX.Element} The rendered tabs component.
@@ -4173,7 +4229,45 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
4173
4229
  }
4174
4230
  onChange?.(index);
4175
4231
  }, [isControlled, onChange]);
4176
- const contextValue = { activeTab, setActiveTab };
4232
+ const baseId = useId();
4233
+ const [tabs, registerTab] = useRegistry();
4234
+ const [panels, registerPanel] = useRegistry();
4235
+ const [focusedTab, setFocusedTab] = useState(null);
4236
+ // One tab stop for the whole list (roving tabindex). While focus is inside
4237
+ // the list the stop follows it, so Tab and Shift+Tab leave the list from
4238
+ // whichever tab has focus; once focus leaves, the stop is the selected tab
4239
+ // again. A disabled tab never holds it, and when the selected tab is
4240
+ // disabled or missing the enabled tab with the lowest index does, so the
4241
+ // list can always be reached. Before the tabs register (the server render
4242
+ // and the first client render), the selected tab holds it. The registry is
4243
+ // in the order the tabs last registered, which a tab changing its
4244
+ // `disabled` or `id` reshuffles, hence the sort.
4245
+ const enabledTabs = tabs
4246
+ .filter(t => !t.disabled)
4247
+ .sort((a, b) => a.index - b.index);
4248
+ const canHoldStop = (index) => enabledTabs.some(t => t.index === index);
4249
+ let tabStop;
4250
+ if (canHoldStop(focusedTab))
4251
+ tabStop = focusedTab;
4252
+ else if (tabs.length === 0 || canHoldStop(activeTab))
4253
+ tabStop = activeTab;
4254
+ else
4255
+ tabStop = enabledTabs.length > 0 ? enabledTabs[0].index : null;
4256
+ // Check if children include TabsContent
4257
+ const childArray = React.Children.toArray(children);
4258
+ const hasContent = childArray.some(child => React.isValidElement(child) && child.type === TabsContent);
4259
+ const contextValue = {
4260
+ activeTab,
4261
+ setActiveTab,
4262
+ baseId,
4263
+ tabStop,
4264
+ setFocusedTab,
4265
+ tabId: index => tabs.find(t => t.index === index)?.id,
4266
+ panelId: index => panels.find(p => p.index === index)?.id,
4267
+ registerTab,
4268
+ registerPanel,
4269
+ orientation: hasContent && vertical ? 'vertical' : 'horizontal',
4270
+ };
4177
4271
  // Build classes for the .tabs div
4178
4272
  const tabsClasses = usePrefixedClassNames('tabs', {
4179
4273
  [`is-${align}`]: align,
@@ -4184,9 +4278,6 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
4184
4278
  'is-toggle': toggle,
4185
4279
  'is-toggle-rounded': rounded,
4186
4280
  });
4187
- // Check if children include TabsContent
4188
- const childArray = React.Children.toArray(children);
4189
- const hasContent = childArray.some(child => React.isValidElement(child) && child.type === TabsContent);
4190
4281
  // Hoisted unconditionally to respect rules-of-hooks. Modifiers gate themselves
4191
4282
  // via their truthy values — `tabs-root` is always prefixed, modifiers only
4192
4283
  // apply in the vertical-with-content branch.
@@ -4235,26 +4326,82 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
4235
4326
  * @param {TabListProps} props - Props for the TabList component.
4236
4327
  * @returns {JSX.Element} The rendered tab list.
4237
4328
  */
4238
- const TabList = ({ className, children, ...props }) => (jsx("ul", { role: "tablist", className: classNames(className) || undefined, ...props, children: children }));
4329
+ const TabList = ({ className, children, ...props }) => {
4330
+ const ctx = useTabsContext();
4331
+ return (jsx("ul", { role: "tablist", "aria-orientation": ctx?.orientation === 'vertical' ? 'vertical' : undefined, className: classNames(className) || undefined, ...props, children: children }));
4332
+ };
4239
4333
  /**
4240
4334
  * Individual tab button. Consumes Tabs context for active state management.
4241
4335
  * Renders `<a>` internally — consumers provide only the label text/children.
4336
+ * Inside a `Tabs` it takes part in the tab list's keyboard support and links
4337
+ * to the `Tabs.Content.Item` with the same `index`.
4242
4338
  *
4243
4339
  * @function
4244
4340
  * @param {TabProps} props - Props for the Tab component.
4245
4341
  * @returns {JSX.Element} The rendered tab.
4246
4342
  */
4247
- const Tab = ({ index, disabled, icon, iconLibrary, iconVariant, iconSize = 'small', iconFeatures, className, children, ...props }) => {
4343
+ const Tab = ({ index, disabled, icon, iconLibrary, iconVariant, iconSize = 'small', iconFeatures, className, children, id, onKeyDown, onFocus, onBlur, ...props }) => {
4248
4344
  const ctx = useTabsContext();
4249
4345
  const isActive = ctx ? ctx.activeTab === index : false;
4346
+ const anchorRef = useRef(null);
4250
4347
  const activeClass = usePrefixedClassNames({ 'is-active': isActive });
4348
+ const tabId = id ?? (ctx ? `${ctx.baseId}-tab-${index}` : undefined);
4349
+ const registerTab = ctx?.registerTab;
4350
+ useEffect(() => {
4351
+ if (!registerTab || tabId === undefined)
4352
+ return undefined;
4353
+ return registerTab({ index, id: tabId, disabled: !!disabled });
4354
+ }, [registerTab, index, tabId, disabled]);
4355
+ // Outside a Tabs there is no list to rove, so every enabled tab stays in
4356
+ // the page tab order, as it always did.
4357
+ let tabIndex;
4358
+ if (disabled)
4359
+ tabIndex = -1;
4360
+ else if (ctx)
4361
+ tabIndex = ctx.tabStop === index ? 0 : -1;
4362
+ else
4363
+ tabIndex = 0;
4251
4364
  const handleClick = useCallback((e) => {
4252
4365
  e.preventDefault();
4253
4366
  if (!disabled && ctx) {
4254
4367
  ctx.setActiveTab(index);
4255
4368
  }
4256
4369
  }, [disabled, ctx, index]);
4257
- return (jsx("li", { className: classNames(activeClass, className) || undefined, role: "tab", "aria-selected": isActive, tabIndex: disabled ? -1 : 0, ...props, children: jsxs("a", { onClick: handleClick, "aria-disabled": disabled || undefined, children: [icon && (jsx(Icon, { name: icon, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })), children && jsx("span", { children: children })] }) }));
4370
+ const handleKeyDown = (e) => {
4371
+ onKeyDown?.(e);
4372
+ // A key on something inside the tab is that element's to answer.
4373
+ if (e.defaultPrevented ||
4374
+ e.target !== e.currentTarget ||
4375
+ e.altKey ||
4376
+ e.ctrlKey ||
4377
+ e.metaKey) {
4378
+ return;
4379
+ }
4380
+ if (e.key === 'Enter' || e.key === ' ') {
4381
+ e.preventDefault();
4382
+ // Activate through a click on the inner <a>, the element a pointer
4383
+ // clicks, so click listeners see keyboard activation the way they see
4384
+ // a native button's.
4385
+ anchorRef.current.click();
4386
+ return;
4387
+ }
4388
+ const target = findTargetTab(e.currentTarget, e.key, ctx?.orientation === 'vertical');
4389
+ if (target) {
4390
+ e.preventDefault();
4391
+ target.focus();
4392
+ }
4393
+ };
4394
+ const handleFocus = (e) => {
4395
+ onFocus?.(e);
4396
+ ctx?.setFocusedTab(index);
4397
+ };
4398
+ const handleBlur = (e) => {
4399
+ onBlur?.(e);
4400
+ if (!tabListOf(e.currentTarget).contains(e.relatedTarget)) {
4401
+ ctx?.setFocusedTab(null);
4402
+ }
4403
+ };
4404
+ return (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: jsxs("a", { ref: anchorRef, onClick: handleClick, "aria-disabled": disabled || undefined, children: [icon && (jsx(Icon, { name: icon, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })), children && jsx("span", { children: children })] }) }));
4258
4405
  };
4259
4406
  /**
4260
4407
  * Each tab; accepts `active`, `onClick`, etc.
@@ -4284,13 +4431,20 @@ const TabsContentComponent = ({ className, children, ...props }) => {
4284
4431
  * @param {TabContentItemProps} props - Props for the TabContentItem component.
4285
4432
  * @returns {JSX.Element} The rendered tab content panel.
4286
4433
  */
4287
- const TabContentItem = ({ index, className, children, ...props }) => {
4434
+ const TabContentItem = ({ index, className, children, id, ...props }) => {
4288
4435
  const ctx = useTabsContext();
4289
4436
  const isActive = ctx ? ctx.activeTab === index : false;
4290
4437
  const itemClass = usePrefixedClassNames('tabs-content-item', {
4291
4438
  'is-active': isActive,
4292
4439
  });
4293
- return (jsx("div", { className: classNames(itemClass, className), role: "tabpanel", "aria-hidden": !isActive, ...props, children: children }));
4440
+ const panelId = id ?? (ctx ? `${ctx.baseId}-panel-${index}` : undefined);
4441
+ const registerPanel = ctx?.registerPanel;
4442
+ useEffect(() => {
4443
+ if (!registerPanel || panelId === undefined)
4444
+ return undefined;
4445
+ return registerPanel({ index, id: panelId });
4446
+ }, [registerPanel, index, panelId]);
4447
+ return (jsx("div", { className: classNames(itemClass, className), role: "tabpanel", id: panelId, "aria-labelledby": ctx?.tabId(index), "aria-hidden": !isActive, ...props, children: children }));
4294
4448
  };
4295
4449
  // ---------------------------------------------------------------------------
4296
4450
  // Static property attachment
@@ -7689,7 +7843,8 @@ const noAnnouncementsYet = {
7689
7843
  // the accessibility tree. The declarations match the `extras-sr-only` mixin,
7690
7844
  // plus `clip-path`, which replaces the deprecated `clip`. The negative margin
7691
7845
  // pulls the 1px box back inside the page, so it doesn't add scrollable
7692
- // overflow.
7846
+ // overflow. Exported for other live regions that must work without a
7847
+ // stylesheet, such as a range calendar's.
7693
7848
  const visuallyHidden = {
7694
7849
  position: 'absolute',
7695
7850
  width: '1px',
@@ -8847,6 +9002,10 @@ const ON_CONTROL = {
8847
9002
  * only the props it hands its own `Control`, as the caller gave them, so a
8848
9003
  * default the wrapper fills in itself never warns. A prop counts when it is
8849
9004
  * truthy, since a falsy one would change nothing on a `Control` either.
9005
+ * `iconLeftSize` and `hasIconsLeft` show nothing on a `Control` without a
9006
+ * glyph, so when they move and the caller chose no left icon, the advice
9007
+ * names the wrapper's default glyph too, and following it draws what the
9008
+ * wrapper's own `Control` did.
8850
9009
  * Internal; not part of the public API.
8851
9010
  */
8852
9011
  const rendersOwnControl = (component, options) => {
@@ -8857,18 +9016,32 @@ const rendersOwnControl = (component, options) => {
8857
9016
  return false;
8858
9017
  const names = held.join(' ');
8859
9018
  const doNothing = held.length > 1 ? 'those props do' : 'that prop does';
9019
+ let message;
8860
9020
  if (options.inline) {
8861
- warnOnce(`${component}:Control-props-inline:${held.join('+')}`, `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
8862
- `in inline mode, inside a <Control> or not, so ${doNothing} ` +
8863
- `nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
8864
- `inline picker.`);
9021
+ message =
9022
+ `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
9023
+ `in inline mode, inside a <Control> or not, so ${doNothing} ` +
9024
+ `nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
9025
+ `inline picker.`;
8865
9026
  }
8866
9027
  else {
8867
- warnOnce(`${component}:Control-props-in-Control:${held.join('+')}`, `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
8868
- `no <Control> of its own, so ${doNothing} nothing there. Set ` +
8869
- `${listOf(held.map(prop => ON_CONTROL[prop]))} on that <Control> ` +
8870
- `instead.`);
9028
+ const toSet = held.map(prop => ON_CONTROL[prop]);
9029
+ const needsGlyph = (options.iconLeftSize || options.hasIconsLeft) &&
9030
+ !options.iconLeft &&
9031
+ options.iconLeftName === undefined &&
9032
+ options.defaultIconLeftName;
9033
+ if (needsGlyph) {
9034
+ toSet.push(`iconLeftName="${options.defaultIconLeftName}" (its default icon)`);
9035
+ }
9036
+ message =
9037
+ `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
9038
+ `no <Control> of its own, so ${doNothing} nothing there. Set ` +
9039
+ `${listOf(toSet)} on that <Control> instead.`;
8871
9040
  }
9041
+ // Keyed by the message itself: the props named do not settle the advice
9042
+ // (an empty `iconLeftName` and an unset one name the same props), so two
9043
+ // calls that would advise differently must not share a key.
9044
+ warnOnce(message, message);
8872
9045
  return false;
8873
9046
  };
8874
9047
  const FieldLabelIdContext = createContext(undefined);
@@ -9355,7 +9528,40 @@ const Checkboxes = withSubComponents(CheckboxesComponent, { Checkbox }, 'Checkbo
9355
9528
  */
9356
9529
  const File = forwardRef(({
9357
9530
  // Field props
9358
- label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
9531
+ 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) => {
9532
+ // What the user last picked, and the form a reset of which clears it.
9533
+ const [picked, setPicked] = useState();
9534
+ const pickedForm = picked?.form;
9535
+ useEffect(() => {
9536
+ if (!pickedForm)
9537
+ return;
9538
+ let timer;
9539
+ // The reset event fires before the form resets, and a listener after this one can still
9540
+ // cancel it, so the name is cleared a task later, and only if the reset went ahead.
9541
+ const onReset = (event) => {
9542
+ timer = setTimeout(() => {
9543
+ if (!event.defaultPrevented)
9544
+ setPicked(undefined);
9545
+ });
9546
+ };
9547
+ pickedForm.addEventListener('reset', onReset);
9548
+ return () => {
9549
+ pickedForm.removeEventListener('reset', onReset);
9550
+ clearTimeout(timer);
9551
+ };
9552
+ }, [pickedForm]);
9553
+ const handleChange = (event) => {
9554
+ const { files, form } = event.target;
9555
+ setPicked(files?.length
9556
+ ? { count: files.length, firstName: files[0].name, form }
9557
+ : undefined);
9558
+ onChange?.(event);
9559
+ };
9560
+ const pickedText = picked &&
9561
+ (picked.count === 1
9562
+ ? picked.firstName
9563
+ : (pickedFilesLabel?.(picked.count) ?? `${picked.count} files`));
9564
+ const shownName = fileName ?? pickedText;
9359
9565
  const insideField = useInsideField();
9360
9566
  const insideControl = useInsideControl();
9361
9567
  const ownField = rendersOwnField('File', {
@@ -9401,7 +9607,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
9401
9607
  [`is-${messageColor}`]: !!messageColor,
9402
9608
  });
9403
9609
  const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
9404
- const fileElement = (jsx("div", { className: fileClass, children: jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
9610
+ const fileElement = (jsx("div", { className: fileClass, children: jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, onChange: handleChange, ...rest }), jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && shownName && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: shownName }))] }) }));
9405
9611
  if (ownField) {
9406
9612
  return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
9407
9613
  }
@@ -10140,7 +10346,11 @@ const Numberinput = forwardRef(({
10140
10346
  // Field props
10141
10347
  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) => {
10142
10348
  const insideField = useInsideField();
10143
- const effectiveBare = bare ?? insideField;
10349
+ // Bare controls join their parent's row, which a `.control` does not lay
10350
+ // out. `useInsideControl()` is true anywhere below a `Control`, even past
10351
+ // a `Field` nested in it, so there they keep their own row as well.
10352
+ const insideControl = useInsideControl();
10353
+ const effectiveBare = bare ?? (insideField && !insideControl);
10144
10354
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10145
10355
  const [internalValue, setInternalValue] = useState(defaultValue);
10146
10356
  const inputRef = useRef(null);
@@ -11474,6 +11684,12 @@ fieldClassName, controlClassName,
11474
11684
  });
11475
11685
  TextArea.displayName = 'TextArea';
11476
11686
 
11687
+ /**
11688
+ * Whether a key is Alt+ArrowDown or Alt+ArrowUp, which open and close a
11689
+ * picker's popover. The calendar and the time wheels leave both to it rather
11690
+ * than reading them as plain arrows.
11691
+ */
11692
+ const isPopoverToggleKey = (e) => e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp');
11477
11693
  const DEFAULT_PICKER_LABELS = {
11478
11694
  prevMonth: 'Previous month',
11479
11695
  nextMonth: 'Next month',
@@ -11482,6 +11698,11 @@ const DEFAULT_PICKER_LABELS = {
11482
11698
  chooseDate: 'Choose date',
11483
11699
  chooseMonth: 'Choose month',
11484
11700
  chooseYear: 'Choose year',
11701
+ rangeStart: 'Start date',
11702
+ rangeEnd: 'End date',
11703
+ rangePreviewEnd: 'Choose as end date',
11704
+ rangeSeparator: '–',
11705
+ chooseDateRange: 'Choose date range',
11485
11706
  hours: 'hours',
11486
11707
  minutes: 'minutes',
11487
11708
  seconds: 'seconds',
@@ -11634,6 +11855,43 @@ function isPeriodUnselectable(d, granularity, c) {
11634
11855
  }
11635
11856
  return true;
11636
11857
  }
11858
+ /**
11859
+ * The most days {@link canCloseRange} walks between a range's ends, about
11860
+ * eleven years. A span it would have to walk further is refused, so the check
11861
+ * stays cheap enough to run on every pointer move over the calendar.
11862
+ */
11863
+ const RANGE_WALK_LIMIT = 4000;
11864
+ /**
11865
+ * Whether `end` can close a range that opens on `start`: it falls on `start`'s
11866
+ * day or later, and, unless `allowDisabled`, no day between the two is one
11867
+ * that `shouldDisableDate` or `unselectableDates` disables. Checking that
11868
+ * walks the days between, so with either rule set and `allowDisabled` off,
11869
+ * an `end` more than {@link RANGE_WALK_LIMIT} days after `start` is refused.
11870
+ * The ends' own constraints, `min` and `max` among them, are the caller's to
11871
+ * check. Bounds can't fall between two days inside them, so they are not
11872
+ * walked.
11873
+ */
11874
+ function canCloseRange(start, end, c, allowDisabled = false) {
11875
+ const first = startOfDay(start);
11876
+ const last = startOfDay(end).getTime();
11877
+ if (last < first.getTime())
11878
+ return false;
11879
+ if (allowDisabled)
11880
+ return true;
11881
+ const blocks = {
11882
+ shouldDisableDate: c.shouldDisableDate,
11883
+ unselectableDates: c.unselectableDates,
11884
+ };
11885
+ if (!blocks.shouldDisableDate && !blocks.unselectableDates?.length) {
11886
+ return true;
11887
+ }
11888
+ for (let d = addDays(first, 1), walked = 1; d.getTime() < last; d = addDays(d, 1), walked++) {
11889
+ if (walked >= RANGE_WALK_LIMIT || isDayUnselectable(d, blocks)) {
11890
+ return false;
11891
+ }
11892
+ }
11893
+ return true;
11894
+ }
11637
11895
  function addDays(d, n) {
11638
11896
  const r = new Date(d);
11639
11897
  r.setDate(r.getDate() + n);
@@ -11652,6 +11910,12 @@ function addMonths(d, n) {
11652
11910
  function addYears(d, n) {
11653
11911
  return addMonths(d, n * 12);
11654
11912
  }
11913
+ /**
11914
+ * A copy of `d` with the given parts of its time of day set and the rest
11915
+ * left as they are. A time started from the clock rather than from a value
11916
+ * should pass the seconds its picker does not show, and the milliseconds,
11917
+ * as `0`, or it keeps the ones the clock had.
11918
+ */
11655
11919
  function setTimeOfDay(d, parts) {
11656
11920
  const r = new Date(d);
11657
11921
  if (parts.hours !== undefined)
@@ -11660,6 +11924,8 @@ function setTimeOfDay(d, parts) {
11660
11924
  r.setMinutes(parts.minutes);
11661
11925
  if (parts.seconds !== undefined)
11662
11926
  r.setSeconds(parts.seconds);
11927
+ if (parts.milliseconds !== undefined)
11928
+ r.setMilliseconds(parts.milliseconds);
11663
11929
  return r;
11664
11930
  }
11665
11931
  /**
@@ -11808,6 +12074,36 @@ function formatDateTime(d, fmt, locale) {
11808
12074
  return formatTokenString(d, f);
11809
12075
  return formatWithIntl(d, f, locale);
11810
12076
  }
12077
+ /**
12078
+ * The value as a native input and a hidden form input carry it:
12079
+ * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
12080
+ * `<input type="month">` use. The year is padded to four digits, as those
12081
+ * inputs require and as the `YYYY` token displays it. HTML has no such shape
12082
+ * for a year before 1, so a date then is empty, as those inputs would make it.
12083
+ */
12084
+ function toIsoValue(d, granularity) {
12085
+ if (d.getFullYear() < FIRST_YEAR)
12086
+ return '';
12087
+ const year = pad(d.getFullYear(), 4);
12088
+ if (granularity === 'year')
12089
+ return year;
12090
+ const month = `${year}-${pad(d.getMonth() + 1)}`;
12091
+ return granularity === 'month' ? month : `${month}-${pad(d.getDate())}`;
12092
+ }
12093
+ /**
12094
+ * Read a native `type="date"` or `type="month"` value back into a Date. HTML
12095
+ * allows a year of four or more digits, as `toIsoValue` writes one past 9999.
12096
+ */
12097
+ function fromIsoValue(s, granularity) {
12098
+ if (granularity === 'month') {
12099
+ const m = /^(\d{4,})-(\d{2})$/.exec(s);
12100
+ return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
12101
+ }
12102
+ const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
12103
+ if (!m)
12104
+ return null;
12105
+ return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
12106
+ }
11811
12107
  /**
11812
12108
  * Derive the hour cycle (`'12'` or `'24'`) a display format will render, from
11813
12109
  * its first hour token: `h`/`hh` → 12-hour with meridiem, `H`/`HH` → 24-hour.
@@ -12094,6 +12390,28 @@ function nearestEnabled(i, disabled) {
12094
12390
  }
12095
12391
  return i;
12096
12392
  }
12393
+ /** The long, spoken form of a date in a range's announcement. */
12394
+ const LONG_DATE = {
12395
+ year: 'numeric',
12396
+ month: 'long',
12397
+ day: 'numeric',
12398
+ };
12399
+ /** A range's pending start: its start, when it has no end. */
12400
+ const pendingStartOf = (range) => range?.[0] && !range[1] ? startOfDay(range[0]) : null;
12401
+ /**
12402
+ * The day grid's cells in rows of a week. A grid owns rows and rows own
12403
+ * cells, so assistive technology can walk the grid, and the selection it
12404
+ * reports, row by row.
12405
+ */
12406
+ const inWeeks = (days, className) => {
12407
+ const rows = [];
12408
+ for (let i = 0; i < days.length; i += 7) {
12409
+ rows.push(jsx("div", { role: "row", className: className, children: days.slice(i, i + 7) }, i));
12410
+ }
12411
+ return rows;
12412
+ };
12413
+ /** A date's time, or `null` for none, for comparing ranges by value. */
12414
+ const timeOf = (d) => d ? d.getTime() : null;
12097
12415
  /**
12098
12416
  * The year list's keyboard, as the selection surface and as navigation:
12099
12417
  * arrows move a year or a row, Home/End go to the list's ends. Returns the
@@ -12119,7 +12437,7 @@ function yearStep(key, focusedYear, years) {
12119
12437
  return null;
12120
12438
  }
12121
12439
  }
12122
- const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minProp, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
12440
+ 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, }) => {
12123
12441
  const gridRef = useRef(null);
12124
12442
  const monthGridRef = useRef(null);
12125
12443
  const yearGridRef = useRef(null);
@@ -12176,6 +12494,80 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12176
12494
  const i = nearestEnabled(focusedDate.getDate() - 1, days.map(isDateUnselectable));
12177
12495
  return days[i];
12178
12496
  }, [cells, focusedDate, isDateUnselectable]);
12497
+ // ----- Range mode -----
12498
+ // Range props come only with the day grid.
12499
+ const rangeMode = !!range;
12500
+ const valuePending = pendingStartOf(range);
12501
+ // The start of a range being picked, which nothing outside the calendar
12502
+ // sees until its end is picked too. It starts as the range's own pending
12503
+ // start, and starts there again whenever the range changes from outside.
12504
+ const [anchor, setAnchor] = useState(valuePending);
12505
+ const rangeKey = `${timeOf(range?.[0])}|${timeOf(range?.[1])}`;
12506
+ const [anchorRangeKey, setAnchorRangeKey] = useState(rangeKey);
12507
+ if (anchorRangeKey !== rangeKey) {
12508
+ setAnchorRangeKey(rangeKey);
12509
+ setAnchor(valuePending);
12510
+ }
12511
+ // The day under the pointer while a start is pending, which the preview
12512
+ // runs to ahead of the focused day.
12513
+ const [hoverDate, setHoverDate] = useState(null);
12514
+ // Whether `day` can end a range that starts on `start`. The start is
12515
+ // checked as well as the end: one the range brought, or one picked before
12516
+ // the bounds or the disabled days changed, can rule itself out.
12517
+ const canEndAt = useCallback((start, day) => !isDateUnselectable(start) &&
12518
+ !isDateUnselectable(day) &&
12519
+ canCloseRange(start, day, { shouldDisableDate, unselectableDates }, allowDisabledInRange), [
12520
+ isDateUnselectable,
12521
+ shouldDisableDate,
12522
+ unselectableDates,
12523
+ allowDisabledInRange,
12524
+ ]);
12525
+ // A pick in range mode: the end of the pending range when it can be one,
12526
+ // and otherwise the start of a new one.
12527
+ const pickRangeDay = useCallback((picked) => {
12528
+ const day = startOfDay(picked);
12529
+ // The pointer is on the picked day, which the focused day now is too.
12530
+ setHoverDate(null);
12531
+ if (anchor && canEndAt(anchor, day)) {
12532
+ setAnchor(null);
12533
+ onRangeSelect?.(anchor, day);
12534
+ }
12535
+ else {
12536
+ setAnchor(day);
12537
+ }
12538
+ }, [anchor, canEndAt, onRangeSelect]);
12539
+ // What the grid shows: while a start is pending, the range from it to the
12540
+ // hovered or focused day, when that day can end it; otherwise the
12541
+ // committed range.
12542
+ let shownStart;
12543
+ let shownEnd;
12544
+ if (anchor) {
12545
+ shownStart = anchor;
12546
+ const previewEnd = startOfDay(hoverDate ?? focusedDate);
12547
+ shownEnd = canEndAt(anchor, previewEnd) ? previewEnd : null;
12548
+ }
12549
+ else {
12550
+ shownStart = range?.[0] ? startOfDay(range[0]) : null;
12551
+ shownEnd = range?.[1] ? startOfDay(range[1]) : null;
12552
+ }
12553
+ const previewing = !!anchor;
12554
+ // Ids for the words that name a range's ends and state the whole range.
12555
+ const reactId = useId();
12556
+ const rangeIdBase = id ?? `calendar-${reactId}`;
12557
+ const rangeStartId = `${rangeIdBase}-range-start`;
12558
+ const rangeEndId = `${rangeIdBase}-range-end`;
12559
+ const rangePreviewEndId = `${rangeIdBase}-range-preview-end`;
12560
+ const rangeSummaryId = `${rangeIdBase}-range-summary`;
12561
+ // The selection in words: the pending start alone, or the committed ends.
12562
+ const selectedEnd = previewing ? null : shownEnd;
12563
+ const rangeSummary = [
12564
+ shownStart &&
12565
+ `${t.rangeStart}: ${formatDate(shownStart, LONG_DATE, locale)}`,
12566
+ selectedEnd &&
12567
+ `${t.rangeEnd}: ${formatDate(selectedEnd, LONG_DATE, locale)}`,
12568
+ ]
12569
+ .filter(Boolean)
12570
+ .join(', ');
12179
12571
  // Whether the grid on show holds focus, or held it last before focus went
12180
12572
  // nowhere: the cell that had it left the page or became disabled. A blur
12181
12573
  // that takes focus out of the grid, to nowhere included, is the user
@@ -12213,6 +12605,10 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12213
12605
  }
12214
12606
  }, [min, max, isDateUnselectable, onFocusedDateChange]);
12215
12607
  const handleKeyDown = useCallback((e) => {
12608
+ if (isPopoverToggleKey(e))
12609
+ return;
12610
+ // A key takes the range preview back from the pointer.
12611
+ setHoverDate(null);
12216
12612
  switch (e.key) {
12217
12613
  case 'ArrowLeft':
12218
12614
  e.preventDefault();
@@ -12255,11 +12651,35 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12255
12651
  case 'Enter':
12256
12652
  case ' ':
12257
12653
  e.preventDefault();
12258
- if (!isDateUnselectable(focusedDate))
12259
- onSelect(focusedDate);
12654
+ if (isDateUnselectable(focusedDate))
12655
+ break;
12656
+ if (rangeMode)
12657
+ pickRangeDay(focusedDate);
12658
+ else
12659
+ onSelect?.(focusedDate);
12660
+ break;
12661
+ case 'Escape':
12662
+ // A pending pick is taken back, and the key stops there so a
12663
+ // popover around the calendar stays open. The range's own pending
12664
+ // start is no pick, so Escape goes on to close the popover.
12665
+ if (anchor && !(valuePending && isSameDay(anchor, valuePending))) {
12666
+ e.preventDefault();
12667
+ e.stopPropagation();
12668
+ setAnchor(valuePending);
12669
+ }
12260
12670
  break;
12261
12671
  }
12262
- }, [focusedDate, firstDayOfWeek, isDateUnselectable, moveFocus, onSelect]);
12672
+ }, [
12673
+ focusedDate,
12674
+ firstDayOfWeek,
12675
+ isDateUnselectable,
12676
+ moveFocus,
12677
+ onSelect,
12678
+ rangeMode,
12679
+ pickRangeDay,
12680
+ anchor,
12681
+ valuePending,
12682
+ ]);
12263
12683
  // The month grid's counterpart to `moveFocus`: step by `direction` months
12264
12684
  // until a month with a selectable day turns up, stopping at the min/max
12265
12685
  // months. The cap guards against a range with nothing selectable.
@@ -12279,10 +12699,12 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12279
12699
  }
12280
12700
  }, [min, max, isMonthUnselectable, onFocusedDateChange]);
12281
12701
  const handleMonthKeyDown = useCallback((e) => {
12702
+ if (isPopoverToggleKey(e))
12703
+ return;
12282
12704
  if (e.key === 'Enter' || e.key === ' ') {
12283
12705
  e.preventDefault();
12284
12706
  if (!isMonthUnselectable(focusedDate)) {
12285
- onSelect(startOfMonth(focusedDate));
12707
+ onSelect?.(startOfMonth(focusedDate));
12286
12708
  }
12287
12709
  return;
12288
12710
  }
@@ -12308,6 +12730,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12308
12730
  const dayNamesRowClass = usePrefixedClassNames('dateinput-day-names');
12309
12731
  const dayNameClass = usePrefixedClassNames('dateinput-day-name');
12310
12732
  const gridClass = usePrefixedClassNames('dateinput-grid');
12733
+ const weekClass = usePrefixedClassNames('dateinput-week');
12311
12734
  const monthsGridClass = usePrefixedClassNames('dateinput-months-grid');
12312
12735
  const monthsRowClass = usePrefixedClassNames('dateinput-months-row');
12313
12736
  const yearsGridClass = usePrefixedClassNames('dateinput-years-grid');
@@ -12397,7 +12820,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12397
12820
  // pick and `onSelect` gets the year's first day.
12398
12821
  const handleYearPick = useCallback((year) => {
12399
12822
  onFocusedDateChange(clampDate(makeDate(year), min, max));
12400
- onSelect(makeDate(year));
12823
+ onSelect?.(makeDate(year));
12401
12824
  }, [min, max, onFocusedDateChange, onSelect]);
12402
12825
  // Step through the listed years from `year` by `direction`, skipping
12403
12826
  // disabled ones, and focus the first selectable year found.
@@ -12412,6 +12835,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12412
12835
  }
12413
12836
  }, [yearList, disabledYears, onFocusedDateChange, min, max]);
12414
12837
  const handleYearListKeyDown = useCallback((e) => {
12838
+ if (isPopoverToggleKey(e))
12839
+ return;
12415
12840
  if (isYearGranularity) {
12416
12841
  const step = yearStep(e.key, focusedYear, yearList);
12417
12842
  if (!step)
@@ -12518,32 +12943,69 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12518
12943
  return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => {
12519
12944
  setNavYear(null);
12520
12945
  setView(v => (v === 'years' ? baseView : 'years'));
12521
- }, children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
12946
+ }, children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, "aria-multiselectable": rangeMode || undefined, "aria-describedby": rangeMode ? rangeSummaryId : undefined, className: gridClass, onKeyDown: handleKeyDown, onMouseLeave: anchor ? () => setHoverDate(null) : undefined, ...gridFocusHandlers, children: inWeeks(cells.map(cell => {
12522
12947
  const disabled = isDateUnselectable(cell.date);
12523
- const isSelected = !!value && isSameDay(value, cell.date);
12948
+ const time = cell.date.getTime();
12949
+ // Range mode. The range on show has its ends filled. Every day
12950
+ // of a committed range is selected, and of a preview only the
12951
+ // start.
12952
+ const isStart = time === timeOf(shownStart);
12953
+ const isEnd = time === timeOf(shownEnd);
12954
+ const inRange = !!shownStart &&
12955
+ !!shownEnd &&
12956
+ time > shownStart.getTime() &&
12957
+ time < shownEnd.getTime();
12958
+ // The day a pending range would end on, which isn't picked
12959
+ // yet, so its description says what picking it does.
12960
+ const isPreviewEnd = previewing && isEnd && !isStart;
12961
+ const isPreview = (previewing && inRange) || isPreviewEnd;
12962
+ const isFilled = rangeMode
12963
+ ? isStart || (isEnd && !previewing)
12964
+ : !!value && isSameDay(value, cell.date);
12965
+ const isSelected = isFilled || (rangeMode && !previewing && inRange);
12966
+ const describedBy = rangeMode
12967
+ ? [
12968
+ isStart && rangeStartId,
12969
+ isEnd && !previewing && rangeEndId,
12970
+ isPreviewEnd && rangePreviewEndId,
12971
+ ]
12972
+ .filter(Boolean)
12973
+ .join(' ') || undefined
12974
+ : undefined;
12524
12975
  const isFocused = isSameDay(cell.date, tabStopDay);
12525
12976
  const otherMonth = !cell.inCurrentMonth;
12526
12977
  const cellClass = prefixedClassNames(classPrefix, 'dateinput-cell', {
12527
- 'is-selected': isSelected,
12978
+ 'is-selected': isFilled,
12979
+ 'is-range-start': isStart,
12980
+ 'is-range-end': isEnd,
12981
+ 'is-in-range': inRange,
12982
+ 'is-preview': isPreview,
12528
12983
  'is-today': cell.isToday,
12529
12984
  'is-disabled': disabled,
12530
12985
  'is-other-month': otherMonth,
12531
12986
  });
12532
12987
  const display = !otherMonth || nearbyMonthDays;
12533
- return (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: () => {
12988
+ return (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: () => {
12534
12989
  // Reached by Tab while the focused day is disabled, or
12535
12990
  // by pointer: keys move on from here. A nearby month's
12536
12991
  // day waits for its click, which turns the grid.
12537
12992
  if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
12538
12993
  onFocusedDateChange(cell.date);
12539
12994
  }
12540
- }, onClick: () => {
12995
+ },
12996
+ // Mouseover rather than mouseenter: browsers send it to a
12997
+ // disabled day as well, where React holds mouseenter back,
12998
+ // so the preview leaves the last enabled day behind.
12999
+ onMouseOver: anchor ? () => setHoverDate(cell.date) : undefined, onClick: () => {
12541
13000
  if (disabled)
12542
13001
  return;
12543
13002
  onFocusedDateChange(cell.date);
12544
- onSelect(cell.date);
13003
+ if (rangeMode)
13004
+ pickRangeDay(cell.date);
13005
+ else
13006
+ onSelect?.(cell.date);
12545
13007
  }, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
12546
- }) })] })), view === 'months' && (jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
13008
+ }), weekClass) })] })), view === 'months' && (jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
12547
13009
  const date = makeDate(focusedYear, month);
12548
13010
  const disabled = disabledMonths[month];
12549
13011
  const isSelected = !!value && isSameMonth(value, date);
@@ -12562,7 +13024,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12562
13024
  }
12563
13025
  }, onClick: () => {
12564
13026
  onFocusedDateChange(clampDate(date, min, max));
12565
- onSelect(date);
13027
+ onSelect?.(date);
12566
13028
  }, children: monthCellNames[month] }, month));
12567
13029
  }) }, row[0]))) })), view === 'years' && (jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
12568
13030
  const isFocused = year === tabStopYear;
@@ -12593,7 +13055,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12593
13055
  }, onClick: () => isYearGranularity
12594
13056
  ? handleYearPick(year)
12595
13057
  : handleYearSelect(year), children: year }, year));
12596
- }) }))] }));
13058
+ }) })), rangeMode && (jsxs(Fragment, { children: [jsx("span", { id: rangeStartId, hidden: true, children: t.rangeStart }), jsx("span", { id: rangeEndId, hidden: true, children: t.rangeEnd }), jsx("span", { id: rangePreviewEndId, hidden: true, children: t.rangePreviewEnd }), jsx("div", { id: rangeSummaryId, "aria-live": "polite", style: visuallyHidden, children: rangeSummary })] }))] }));
12597
13059
  };
12598
13060
  Calendar.displayName = 'Calendar';
12599
13061
 
@@ -12636,6 +13098,8 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12636
13098
  document.addEventListener('pointerdown', handler);
12637
13099
  return () => document.removeEventListener('pointerdown', handler);
12638
13100
  }, [isOpen, closeOnClickOutside, anchorRef, onClose]);
13101
+ // Escape closes it, and so does Alt+ArrowUp, the combobox's key for it,
13102
+ // which the calendar and the time wheels inside leave to it.
12639
13103
  useEffect(() => {
12640
13104
  if (!isOpen || !closeOnEscape || !isBrowser)
12641
13105
  return undefined;
@@ -12644,6 +13108,11 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
12644
13108
  e.stopPropagation();
12645
13109
  onClose();
12646
13110
  }
13111
+ else if (e.altKey && e.key === 'ArrowUp') {
13112
+ e.preventDefault();
13113
+ e.stopPropagation();
13114
+ onClose();
13115
+ }
12647
13116
  };
12648
13117
  document.addEventListener('keydown', handler);
12649
13118
  return () => document.removeEventListener('keydown', handler);
@@ -13047,7 +13516,7 @@ const SEPARATOR_RE = /[/:\-.\s]/;
13047
13516
  * null and the input transparently falls back to free-form text entry.
13048
13517
  */
13049
13518
  function useSegmentedEntry(params) {
13050
- 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;
13519
+ 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;
13051
13520
  const segmentMap = useMemo(() => {
13052
13521
  const map = typeof format === 'string' ? buildSegmentMap(format) : null;
13053
13522
  if (!map || !skipKinds)
@@ -13243,7 +13712,7 @@ function useSegmentedEntry(params) {
13243
13712
  // Clicking the field opens the popover only when `openOnFocus` is on —
13244
13713
  // a click is just a focus. With `openOnFocus={false}` (manual-entry
13245
13714
  // mode) the click positions the caret for typing and the popover is
13246
- // opened explicitly via the right launcher button (or ArrowDown).
13715
+ // opened explicitly via the right launcher button (or Alt+ArrowDown).
13247
13716
  if (openOnFocus && popover && !disabled && !readOnly)
13248
13717
  setOpen(true);
13249
13718
  if (segmentEditable && segmentMap && inputRef.current) {
@@ -13279,14 +13748,23 @@ function useSegmentedEntry(params) {
13279
13748
  const isPm = base.getHours() >= 12;
13280
13749
  switch (e.key) {
13281
13750
  case 'ArrowUp':
13282
- e.preventDefault();
13283
- typedDigitsRef.current = '';
13284
- applyDateFromSegment(incrementSegmentValue(seg, base, 1, isPm));
13285
- return;
13286
13751
  case 'ArrowDown':
13287
13752
  e.preventDefault();
13753
+ // The plain arrows step the segment, so the popover opens on
13754
+ // Alt+ArrowDown and closes on Alt+ArrowUp, as a combobox's does,
13755
+ // and neither steps it.
13756
+ if (e.altKey) {
13757
+ if (e.key === 'ArrowUp') {
13758
+ if (isOpen)
13759
+ setOpen(false);
13760
+ }
13761
+ else if (!isOpen && popover) {
13762
+ setOpen(true);
13763
+ }
13764
+ return;
13765
+ }
13288
13766
  typedDigitsRef.current = '';
13289
- applyDateFromSegment(incrementSegmentValue(seg, base, -1, isPm));
13767
+ applyDateFromSegment(incrementSegmentValue(seg, base, e.key === 'ArrowUp' ? 1 : -1, isPm));
13290
13768
  return;
13291
13769
  case 'ArrowRight':
13292
13770
  e.preventDefault();
@@ -13352,6 +13830,8 @@ function useSegmentedEntry(params) {
13352
13830
  const nextBuffer = typedDigitsRef.current + e.key;
13353
13831
  typedDigitsRef.current = nextBuffer;
13354
13832
  const { date, advance } = setSegmentValue(seg, base, nextBuffer, isPm);
13833
+ if (!advance && isUnfinishedBlocked?.(date))
13834
+ return;
13355
13835
  applyDateFromSegment(date);
13356
13836
  if (advance) {
13357
13837
  typedDigitsRef.current = '';
@@ -13362,11 +13842,23 @@ function useSegmentedEntry(params) {
13362
13842
  }
13363
13843
  }
13364
13844
  // ----- free-form / popover key handling -----
13365
- if (e.key === 'ArrowDown' && !isOpen && popover) {
13845
+ // With no segment to step, ArrowDown opens the popover with or without
13846
+ // Alt, unless the field is read-only or disabled, as focus and the
13847
+ // launcher don't open it then either. Alt+ArrowUp closes it.
13848
+ if (e.key === 'ArrowDown' &&
13849
+ !isOpen &&
13850
+ popover &&
13851
+ !disabled &&
13852
+ !readOnly) {
13366
13853
  e.preventDefault();
13367
13854
  setOpen(true);
13368
13855
  return;
13369
13856
  }
13857
+ if (e.key === 'ArrowUp' && e.altKey && isOpen) {
13858
+ e.preventDefault();
13859
+ setOpen(false);
13860
+ return;
13861
+ }
13370
13862
  if (e.key === 'Escape' && isOpen) {
13371
13863
  e.preventDefault();
13372
13864
  setOpen(false);
@@ -13396,11 +13888,14 @@ function useSegmentedEntry(params) {
13396
13888
  activeSegmentIdx,
13397
13889
  segmentBaseDate,
13398
13890
  applyDateFromSegment,
13891
+ isUnfinishedBlocked,
13399
13892
  moveSegment,
13400
13893
  isOpen,
13401
13894
  setOpen,
13402
13895
  closeOnSelect,
13403
13896
  popover,
13897
+ disabled,
13898
+ readOnly,
13404
13899
  isUntouched,
13405
13900
  tryParse,
13406
13901
  text,
@@ -13445,37 +13940,6 @@ function supportsInputType(type) {
13445
13940
  return supported;
13446
13941
  }
13447
13942
 
13448
- const pad2$1 = (n) => String(n).padStart(2, '0');
13449
- /**
13450
- * The value as the native input and the hidden form input carry it:
13451
- * `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
13452
- * `<input type="month">` use. The year is padded to four digits, as those
13453
- * inputs require and as the `YYYY` token displays it. HTML has no such shape
13454
- * for a year before 1, so a date then is empty, as those inputs would make it.
13455
- */
13456
- const toIsoValue = (d, granularity) => {
13457
- if (d.getFullYear() < FIRST_YEAR)
13458
- return '';
13459
- const year = String(d.getFullYear()).padStart(4, '0');
13460
- if (granularity === 'year')
13461
- return year;
13462
- const month = `${year}-${pad2$1(d.getMonth() + 1)}`;
13463
- return granularity === 'month' ? month : `${month}-${pad2$1(d.getDate())}`;
13464
- };
13465
- /**
13466
- * Read a native `type="date"` or `type="month"` value back into a Date. HTML
13467
- * allows a year of four or more digits, as `toIsoValue` writes one past 9999.
13468
- */
13469
- const fromIsoValue = (s, granularity) => {
13470
- if (granularity === 'month') {
13471
- const m = /^(\d{4,})-(\d{2})$/.exec(s);
13472
- return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
13473
- }
13474
- const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
13475
- if (!m)
13476
- return null;
13477
- return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
13478
- };
13479
13943
  const DEFAULT_FORMATS = {
13480
13944
  day: DEFAULT_DATE_FORMAT,
13481
13945
  month: DEFAULT_MONTH_FORMAT,
@@ -13577,7 +14041,11 @@ const DateInputBase = forwardRef((props, ref) => {
13577
14041
  // them, so the month or year containing `min` stays reachable.
13578
14042
  const periodMin = useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
13579
14043
  const periodMax = useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
13580
- const handleSelect = useCallback((d) => {
14044
+ // A pick is the day at midnight, as a click on its cell gives. Enter hands
14045
+ // over the focused date, which can carry a time of day from the clock, a
14046
+ // value or a bound, so its time is dropped here.
14047
+ const handleSelect = useCallback((picked) => {
14048
+ const d = startOfDay(picked);
13581
14049
  if (!isWithin(d, periodMin, periodMax))
13582
14050
  return;
13583
14051
  commitValue(d);
@@ -13749,13 +14217,14 @@ const DateInput = forwardRef((props, ref) => {
13749
14217
  // icon-left container has nothing to anchor to, and it renders no
13750
14218
  // Control inside one or out. The left icon is checked as the caller
13751
14219
  // passed it: the default glyph is this component's own choice, not a
13752
- // prop the caller set.
14220
+ // prop the caller set, so it goes in only for the advice.
13753
14221
  const ownControl = rendersOwnControl('DateInput', {
13754
14222
  insideControl,
13755
14223
  inline: baseProps.inline,
13756
14224
  isLoading,
13757
14225
  iconLeft,
13758
14226
  iconLeftName: props.iconLeftName,
14227
+ defaultIconLeftName: iconLeftName,
13759
14228
  iconLeftSize,
13760
14229
  iconRight,
13761
14230
  iconRightName,
@@ -14145,23 +14614,28 @@ const WheelInner = ({ values, index, onChange, formatLabel, formatAriaText, aria
14145
14614
  cancelRaf();
14146
14615
  commitPosition(Math.round(positionRef.current) + delta);
14147
14616
  }, [disabled, cancelRaf, commitPosition]);
14617
+ // The keys are a spinbutton's: up raises the value and down lowers it. The
14618
+ // values run down the wheel, so up brings the one below the band into it,
14619
+ // the way a drag or a scroll that moves the items up does.
14148
14620
  const onKeyDown = useCallback((e) => {
14621
+ if (isPopoverToggleKey(e))
14622
+ return;
14149
14623
  switch (e.key) {
14150
14624
  case 'ArrowUp':
14151
14625
  e.preventDefault();
14152
- moveBy(-1);
14626
+ moveBy(1);
14153
14627
  break;
14154
14628
  case 'ArrowDown':
14155
14629
  e.preventDefault();
14156
- moveBy(1);
14630
+ moveBy(-1);
14157
14631
  break;
14158
14632
  case 'PageUp':
14159
14633
  e.preventDefault();
14160
- moveBy(-5);
14634
+ moveBy(5);
14161
14635
  break;
14162
14636
  case 'PageDown':
14163
14637
  e.preventDefault();
14164
- moveBy(5);
14638
+ moveBy(-5);
14165
14639
  break;
14166
14640
  case 'Home':
14167
14641
  e.preventDefault();
@@ -14478,6 +14952,7 @@ const TimeWheels = ({ value, onChange, hourFormat = '24', enableSeconds = false,
14478
14952
  hours: h,
14479
14953
  minutes: m,
14480
14954
  seconds: s,
14955
+ milliseconds: 0,
14481
14956
  });
14482
14957
  return !unselectableTimes(probe);
14483
14958
  }, [unselectableTimes]);
@@ -14744,9 +15219,16 @@ const TimeInputBase = forwardRef((props, ref) => {
14744
15219
  setInternalValue(next);
14745
15220
  onChange?.(next);
14746
15221
  }, [isControlled, onChange]);
15222
+ // An empty field starts from today, at the whole minute or second the
15223
+ // wheels show.
14747
15224
  const handleSpinnerChange = useCallback((parts) => {
14748
- const base = value ?? new Date();
14749
- const next = setTimeOfDay(base, parts);
15225
+ const next = value
15226
+ ? setTimeOfDay(value, parts)
15227
+ : setTimeOfDay(new Date(), {
15228
+ ...parts,
15229
+ seconds: parts.seconds ?? 0,
15230
+ milliseconds: 0,
15231
+ });
14750
15232
  if (!isWithin(next, lowerBound, max))
14751
15233
  return;
14752
15234
  commitValue(next);
@@ -14877,13 +15359,14 @@ const TimeInput = forwardRef((props, ref) => {
14877
15359
  // icon-left container has nothing to anchor to, and it renders no
14878
15360
  // Control inside one or out. The left icon is checked as the caller
14879
15361
  // passed it: the default glyph is this component's own choice, not a
14880
- // prop the caller set.
15362
+ // prop the caller set, so it goes in only for the advice.
14881
15363
  const ownControl = rendersOwnControl('TimeInput', {
14882
15364
  insideControl,
14883
15365
  inline: baseProps.inline,
14884
15366
  isLoading,
14885
15367
  iconLeft,
14886
15368
  iconLeftName: props.iconLeftName,
15369
+ defaultIconLeftName: iconLeftName,
14887
15370
  iconLeftSize,
14888
15371
  iconRight,
14889
15372
  iconRightName,
@@ -15061,6 +15544,34 @@ const DateTimeInputBase = forwardRef((props, ref) => {
15061
15544
  if (!open)
15062
15545
  setTimeOpen(false);
15063
15546
  }, [open]);
15547
+ // The footer's Time button opens the wheels, and the hours wheel takes
15548
+ // focus so the keys turn it straight away. The calendar they cover is
15549
+ // inert meanwhile, so no tab stop, pointer or screen reader reaches a day
15550
+ // behind them; the attribute is set on the element, as React 18 has no
15551
+ // `inert` prop. Closing the wheels hands focus back to the button: focus
15552
+ // in them would fall to the page as they unmount, and a pointer press
15553
+ // outside them has already dropped it on the panel or the page. Focus the
15554
+ // keys moved on to the footer, such as Reset, stays there.
15555
+ const panelRef = useRef(null);
15556
+ const calendarRef = useRef(null);
15557
+ const timeButtonRef = useRef(null);
15558
+ const timeOverlayRef = useRef(null);
15559
+ useEffect(() => {
15560
+ calendarRef.current?.toggleAttribute('inert', timeOpen);
15561
+ if (!timeOpen)
15562
+ return;
15563
+ timeOverlayRef.current
15564
+ ?.querySelector('[role="spinbutton"]')
15565
+ ?.focus();
15566
+ }, [timeOpen]);
15567
+ const closeTime = useCallback(() => {
15568
+ const panel = panelRef.current;
15569
+ const active = getActiveElementInTree(panel);
15570
+ const movedOn = !!panel?.contains(active) && !timeOverlayRef.current?.contains(active);
15571
+ if (!movedOn)
15572
+ timeButtonRef.current?.focus();
15573
+ setTimeOpen(false);
15574
+ }, []);
15064
15575
  const commitValue = useCallback((next) => {
15065
15576
  if (!isControlled)
15066
15577
  setInternalValue(next);
@@ -15069,20 +15580,32 @@ const DateTimeInputBase = forwardRef((props, ref) => {
15069
15580
  setFocusedDate(next);
15070
15581
  onChange?.(next);
15071
15582
  }, [isControlled, onChange]);
15583
+ // A picked day takes the value's whole time of day, seconds and all, or
15584
+ // midnight in an empty field. The day handed over by the keyboard is the
15585
+ // calendar's focused date, which can carry the time of the clock it
15586
+ // started from, so none of its time is kept.
15072
15587
  const handleDateSelect = useCallback((d) => {
15073
15588
  const merged = setTimeOfDay(d, {
15074
15589
  hours: value?.getHours() ?? 0,
15075
15590
  minutes: value?.getMinutes() ?? 0,
15076
- seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
15591
+ seconds: value?.getSeconds() ?? 0,
15592
+ milliseconds: value?.getMilliseconds() ?? 0,
15077
15593
  });
15078
15594
  if (!isWithin(merged, lowerBound, max))
15079
15595
  return;
15080
15596
  commitValue(merged);
15081
15597
  setFocusedDate(d);
15082
- }, [value, enableSeconds, lowerBound, max, commitValue]);
15598
+ }, [value, lowerBound, max, commitValue]);
15599
+ // The wheels set the value's time. An empty field starts from the focused
15600
+ // day, at the whole minute or second the wheels show.
15083
15601
  const handleTimeChange = useCallback((parts) => {
15084
- const base = value ?? focusedDate;
15085
- const next = setTimeOfDay(base, parts);
15602
+ const next = value
15603
+ ? setTimeOfDay(value, parts)
15604
+ : setTimeOfDay(focusedDate, {
15605
+ ...parts,
15606
+ seconds: parts.seconds ?? 0,
15607
+ milliseconds: 0,
15608
+ });
15086
15609
  if (!isWithin(next, lowerBound, max))
15087
15610
  return;
15088
15611
  commitValue(next);
@@ -15096,8 +15619,12 @@ const DateTimeInputBase = forwardRef((props, ref) => {
15096
15619
  const fmt = typeof defaultFormat === 'string' ? defaultFormat : undefined;
15097
15620
  return parseDate(trimmed, fmt ?? DEFAULT_DATETIME_FORMAT);
15098
15621
  }, [parse, defaultFormat, locale]);
15099
- // The Date the user edits when starting without a current value (now).
15100
- const makeBaseDate = useCallback(() => new Date(), []);
15622
+ // The Date the user edits when starting without a current value: now, at
15623
+ // the whole minute, or the whole second with `enableSeconds`.
15624
+ const makeBaseDate = useCallback(() => setTimeOfDay(new Date(), {
15625
+ seconds: enableSeconds ? undefined : 0,
15626
+ milliseconds: 0,
15627
+ }), [enableSeconds]);
15101
15628
  const inputReadOnlyAttr = !!readOnly || !editable;
15102
15629
  const canOpen = !!popover && !disabled && !readOnly;
15103
15630
  // Blocking predicate for manual entry, combining the calendar's
@@ -15161,20 +15688,20 @@ const DateTimeInputBase = forwardRef((props, ref) => {
15161
15688
  minutes: value?.getMinutes() ?? 0,
15162
15689
  seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
15163
15690
  };
15164
- const panel = (jsxs("div", { className: panelClass, onKeyDown: e => {
15691
+ const panel = (jsxs("div", { ref: panelRef, className: panelClass, onKeyDown: e => {
15165
15692
  // First Escape collapses the floating time wheels; the popover's own
15166
15693
  // Escape handler (which closes the whole panel) only sees the second.
15167
15694
  if (timeOpen && e.key === 'Escape') {
15168
15695
  e.stopPropagation();
15169
- setTimeOpen(false);
15696
+ closeTime();
15170
15697
  }
15171
- }, children: [jsxs("div", { className: calendarWrapClass, children: [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 && (jsx("div", { className: timeOverlayClass, onClick: e => {
15698
+ }, children: [jsxs("div", { className: calendarWrapClass, children: [jsx("div", { ref: calendarRef, children: 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 && (jsx("div", { ref: timeOverlayRef, className: timeOverlayClass, onClick: e => {
15172
15699
  // Tap outside the wheel card (on the covered calendar area)
15173
15700
  // collapses the wheels without selecting a date, matching the
15174
15701
  // native behavior.
15175
15702
  if (e.target === e.currentTarget)
15176
- setTimeOpen(false);
15177
- }, children: jsx("div", { className: timeCardClass, children: 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) }) }) }))] }), jsxs("div", { className: footerClass, children: [jsxs("button", { type: "button", className: footerTimeClass, onClick: () => setTimeOpen(o => !o), "aria-expanded": timeOpen, disabled: disabled, children: [jsx("span", { children: t.time }), jsx("span", { className: footerTimePillClass, children: value ? formatTime(value, timeDisplayFormat, locale) : '—' })] }), jsxs("div", { className: footerActionsClass, children: [jsx("button", { type: "button", className: footerResetClass, onClick: () => commitValue(valueAtOpenRef.current), children: t.reset }), jsx("button", { type: "button", className: footerDoneClass, "aria-label": t.done, onClick: () => setOpen(false), children: jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", "aria-hidden": "true", children: jsx("path", { d: "M5 11l4 4 8-8", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] })] }));
15703
+ closeTime();
15704
+ }, children: jsx("div", { className: timeCardClass, children: 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) }) }) }))] }), jsxs("div", { className: footerClass, children: [jsxs("button", { ref: timeButtonRef, type: "button", className: footerTimeClass, onClick: () => (timeOpen ? closeTime() : setTimeOpen(true)), "aria-expanded": timeOpen, disabled: disabled, children: [jsx("span", { children: t.time }), jsx("span", { className: footerTimePillClass, children: value ? formatTime(value, timeDisplayFormat, locale) : '—' })] }), jsxs("div", { className: footerActionsClass, children: [jsx("button", { type: "button", className: footerResetClass, onClick: () => commitValue(valueAtOpenRef.current), children: t.reset }), jsx("button", { type: "button", className: footerDoneClass, "aria-label": t.done, onClick: () => setOpen(false), children: jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", "aria-hidden": "true", children: jsx("path", { d: "M5 11l4 4 8-8", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] })] }));
15178
15705
  if (inline) {
15179
15706
  return (jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [panel, name && (jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoDateTime(value, enableSeconds) : '', required: required }))] }));
15180
15707
  }
@@ -15218,13 +15745,14 @@ const DateTimeInput = forwardRef((props, ref) => {
15218
15745
  // icon-left container has nothing to anchor to, and it renders no
15219
15746
  // Control inside one or out. The left icon is checked as the caller
15220
15747
  // passed it: the default glyph is this component's own choice, not a
15221
- // prop the caller set.
15748
+ // prop the caller set, so it goes in only for the advice.
15222
15749
  const ownControl = rendersOwnControl('DateTimeInput', {
15223
15750
  insideControl,
15224
15751
  inline: baseProps.inline,
15225
15752
  isLoading,
15226
15753
  iconLeft,
15227
15754
  iconLeftName: props.iconLeftName,
15755
+ defaultIconLeftName: iconLeftName,
15228
15756
  iconLeftSize,
15229
15757
  iconRight,
15230
15758
  iconRightName,
@@ -15259,6 +15787,354 @@ const DateTimeInput = forwardRef((props, ref) => {
15259
15787
  });
15260
15788
  DateTimeInput.displayName = 'DateTimeInput';
15261
15789
 
15790
+ const EMPTY_RANGE = [null, null];
15791
+ /** A range's end as the form and the native inputs carry it. */
15792
+ const isoDay = (d) => (d ? toIsoValue(d, 'day') : '');
15793
+ /** Today at local midnight, where typing into an empty end starts. */
15794
+ const today = () => startOfDay(new Date());
15795
+ /**
15796
+ * Raw DateRangeInput: the two inputs and the popover calendar without
15797
+ * Field/Control wrapping. Use `DateRangeInput` for the convenience wrapper.
15798
+ *
15799
+ * @function
15800
+ * @param {DateRangeInputBaseProps} props
15801
+ * @returns {JSX.Element}
15802
+ */
15803
+ const DateRangeInputBase = forwardRef((props, ref) => {
15804
+ 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;
15805
+ // The launcher gives way to the loading spinner of a Control this sits in.
15806
+ const controlLoading = useControlLoading();
15807
+ const triggerIcon = triggerIconProp ?? !controlLoading;
15808
+ const t = mergeLabels(labels);
15809
+ const resolvedFormat = format ?? DEFAULT_DATE_FORMAT;
15810
+ const isControlled = controlledValue !== undefined;
15811
+ const [internalValue, setInternalValue] = useState(defaultValue ?? EMPTY_RANGE);
15812
+ const range = isControlled ? controlledValue : internalValue;
15813
+ const [start, end] = range;
15814
+ // Nothing before year 1 is in range, as in HTML's date inputs.
15815
+ const lowerBound = useMemo(() => floorMin(min), [min]);
15816
+ const [focusedDate, setFocusedDate] = useState(() => clampDate(start ?? end ?? new Date(), lowerBound, max));
15817
+ // Re-clamp the focused day if the bounds change after mount, so the
15818
+ // calendar never shows a month outside them.
15819
+ useEffect(() => {
15820
+ setFocusedDate(prev => clampDate(prev, lowerBound, max));
15821
+ }, [lowerBound, max]);
15822
+ const [open, setOpenState] = useState(false);
15823
+ const [startText, setStartText] = useState(() => start ? formatDate(start, resolvedFormat, locale) : '');
15824
+ const [endText, setEndText] = useState(() => end ? formatDate(end, resolvedFormat, locale) : '');
15825
+ // Each input shows its end of the range as it changes from outside.
15826
+ useEffect(() => {
15827
+ setStartText(start ? formatDate(start, resolvedFormat, locale) : '');
15828
+ }, [start, resolvedFormat, locale]);
15829
+ useEffect(() => {
15830
+ setEndText(end ? formatDate(end, resolvedFormat, locale) : '');
15831
+ }, [end, resolvedFormat, locale]);
15832
+ const containerRef = useRef(null);
15833
+ const startRef = useRef(null);
15834
+ const endRef = useRef(null);
15835
+ const reactId = useId();
15836
+ const popoverId = id ? `${id}-popover` : `picker-${reactId}`;
15837
+ const { bulmaHelperClasses, rest: cleanRest } = useBulmaClasses(rest);
15838
+ const { shouldUseNative } = useNativeMobilePicker({
15839
+ force: mobileNative === 'auto' ? undefined : mobileNative,
15840
+ });
15841
+ const useNative = !inline && shouldUseNative;
15842
+ // The root borrows DateInput's container class, which places the launcher
15843
+ // and sizes a Control's icons around the field.
15844
+ const containerClass = usePrefixedClassNames('daterangeinput', 'dateinput-container');
15845
+ const fieldClass = usePrefixedClassNames('input', 'daterangeinput-field', {
15846
+ [`is-${color}`]: !!color,
15847
+ [`is-${size}`]: !!size,
15848
+ 'is-rounded': isRounded,
15849
+ 'is-disabled': disabled,
15850
+ });
15851
+ const inputClass = usePrefixedClassNames('daterangeinput-input');
15852
+ const separatorClass = usePrefixedClassNames('daterangeinput-separator');
15853
+ const triggerClass = usePrefixedClassNames('dateinput-trigger');
15854
+ // What the latest request asked for, so a second request in the same event
15855
+ // sees it before React renders. The callbacks fire here rather than in a
15856
+ // state updater, which StrictMode calls twice.
15857
+ const requestedOpenRef = useRef(false);
15858
+ const setOpen = useCallback((next) => {
15859
+ if (requestedOpenRef.current === next)
15860
+ return;
15861
+ requestedOpenRef.current = next;
15862
+ setOpenState(next);
15863
+ if (next)
15864
+ onOpen?.();
15865
+ else
15866
+ onClose?.();
15867
+ }, [onOpen, onClose]);
15868
+ // Focus that moves inside the field, from one input to the other or from
15869
+ // the launcher, is not focus arriving, so it opens nothing. Set as the
15870
+ // focus is captured on the way in, and cleared once the input has handled
15871
+ // it, so a click that follows still opens the popover.
15872
+ const focusFromInsideRef = useRef(false);
15873
+ const handleFocusCapture = (e) => {
15874
+ const from = e.relatedTarget;
15875
+ focusFromInsideRef.current = !!from && e.currentTarget.contains(from);
15876
+ onFocusCapture?.(e);
15877
+ };
15878
+ const handleFocus = (e) => {
15879
+ focusFromInsideRef.current = false;
15880
+ onFocus?.(e);
15881
+ };
15882
+ const requestOpen = useCallback((next) => {
15883
+ if (next && focusFromInsideRef.current)
15884
+ return;
15885
+ setOpen(next);
15886
+ }, [setOpen]);
15887
+ const commitRange = useCallback((next) => {
15888
+ if (!isControlled)
15889
+ setInternalValue(next);
15890
+ onChange?.(next);
15891
+ }, [isControlled, onChange]);
15892
+ const constraints = useMemo(() => ({ shouldDisableDate, unselectableDates }), [shouldDisableDate, unselectableDates]);
15893
+ // A day the calendar disables: shouldDisableDate or unselectableDates.
15894
+ // Typing and the native inputs check the bounds alongside it.
15895
+ const isBlockedDay = useCallback((d) => !!shouldDisableDate?.(d) ||
15896
+ !!unselectableDates?.some(u => isSameDay(u, d)), [shouldDisableDate, unselectableDates]);
15897
+ // An end the range can take: it is not disabled, and it closes a range
15898
+ // from the start, which it can't when it comes first or, unless allowed,
15899
+ // reaches over a disabled day.
15900
+ const isBlockedEnd = useCallback((d) => isBlockedDay(d) ||
15901
+ (!!start && !canCloseRange(start, d, constraints, allowDisabledInRange)), [isBlockedDay, start, constraints, allowDisabledInRange]);
15902
+ // A new start keeps the end only while the two still make a range, so a
15903
+ // start moved past the end, or over a disabled day before it, clears it.
15904
+ const commitStart = useCallback((next) => {
15905
+ const keepsEnd = !next ||
15906
+ !end ||
15907
+ canCloseRange(next, end, constraints, allowDisabledInRange);
15908
+ commitRange([next, keepsEnd ? end : null]);
15909
+ if (next)
15910
+ setFocusedDate(next);
15911
+ }, [end, constraints, allowDisabledInRange, commitRange]);
15912
+ // Every digit typed into the start commits, so a start passes through
15913
+ // values on its way to the one being typed. The first digits of a year make
15914
+ // a start centuries back, which can't keep the end once a disabled day may
15915
+ // lie between. Such a value waits for the segment's next digit rather than
15916
+ // clearing an end the finished start may keep.
15917
+ const startCostsEnd = useCallback((d) => !!end && !canCloseRange(d, end, constraints, allowDisabledInRange), [end, constraints, allowDisabledInRange]);
15918
+ const commitEnd = useCallback((next) => {
15919
+ commitRange([start, next]);
15920
+ if (next)
15921
+ setFocusedDate(next);
15922
+ }, [start, commitRange]);
15923
+ const handleRangeSelect = useCallback((from, to) => {
15924
+ commitRange([from, to]);
15925
+ setFocusedDate(to);
15926
+ if (closeOnSelect)
15927
+ setOpen(false);
15928
+ }, [commitRange, closeOnSelect, setOpen]);
15929
+ const tryParse = useCallback((s) => {
15930
+ const trimmed = s.trim();
15931
+ if (!trimmed)
15932
+ return null;
15933
+ const fmt = typeof format === 'string' ? format : undefined;
15934
+ return parse
15935
+ ? parse(trimmed)
15936
+ : parseDate(trimmed, fmt ?? DEFAULT_DATE_FORMAT);
15937
+ }, [parse, format, locale]);
15938
+ // Typing into an empty end starts from the start, so only what differs
15939
+ // needs typing; an empty start starts from today.
15940
+ const makeEndBase = useCallback(() => (start ? startOfDay(start) : today()), [start]);
15941
+ // Where focus goes back as the popover closes: the input focused last,
15942
+ // which is the one that opened it unless the launcher did, or the start
15943
+ // input before either has had focus.
15944
+ const openerRef = useRef(null);
15945
+ const restoreFocusRef = useMemo(() => ({
15946
+ get current() {
15947
+ return openerRef.current ?? startRef.current;
15948
+ },
15949
+ }), []);
15950
+ const shared = {
15951
+ format: resolvedFormat,
15952
+ formatFn: formatDate,
15953
+ tryParse,
15954
+ locale,
15955
+ min: lowerBound,
15956
+ max,
15957
+ disabled,
15958
+ readOnly,
15959
+ editable,
15960
+ popover,
15961
+ openOnFocus,
15962
+ closeOnSelect,
15963
+ isOpen: open,
15964
+ setOpen: requestOpen,
15965
+ };
15966
+ // Each input is its own picker field. Its blur counts focus leaving it,
15967
+ // even for the other input, so text typed into one is read as focus moves
15968
+ // to the other.
15969
+ const { inputHandlers: startHandlers } = useSegmentedEntry({
15970
+ ...shared,
15971
+ value: start,
15972
+ commitValue: commitStart,
15973
+ text: startText,
15974
+ setText: setStartText,
15975
+ makeBaseDate: today,
15976
+ isBlocked: isBlockedDay,
15977
+ isUnfinishedBlocked: startCostsEnd,
15978
+ inputRef: startRef,
15979
+ containerRef: startRef,
15980
+ onFocus: () => {
15981
+ openerRef.current = startRef.current;
15982
+ },
15983
+ });
15984
+ const { inputHandlers: endHandlers } = useSegmentedEntry({
15985
+ ...shared,
15986
+ value: end,
15987
+ commitValue: commitEnd,
15988
+ text: endText,
15989
+ setText: setEndText,
15990
+ makeBaseDate: makeEndBase,
15991
+ isBlocked: isBlockedEnd,
15992
+ inputRef: endRef,
15993
+ containerRef: endRef,
15994
+ onFocus: () => {
15995
+ openerRef.current = endRef.current;
15996
+ },
15997
+ });
15998
+ const combinedRef = useCallback((node) => {
15999
+ startRef.current =
16000
+ node;
16001
+ if (typeof ref === 'function')
16002
+ ref(node);
16003
+ else if (ref)
16004
+ ref.current = node;
16005
+ }, [ref]);
16006
+ const handlePopoverClose = useCallback(() => setOpen(false), [setOpen]);
16007
+ const startField = startName ?? (name ? `${name}[start]` : undefined);
16008
+ const endField = endName ?? (name ? `${name}[end]` : undefined);
16009
+ const hiddenInputs = (jsxs(Fragment, { children: [startField && (jsx("input", { type: "hidden", name: startField, form: form, value: isoDay(start) })), endField && (jsx("input", { type: "hidden", name: endField, form: form, value: isoDay(end) }))] }));
16010
+ const separator = (jsx("span", { className: separatorClass, "aria-hidden": "true", children: t.rangeSeparator }));
16011
+ // Native mobile path: two <input type="date">s, held to the same rules as
16012
+ // typing. A change the rules refuse leaves the input showing the range.
16013
+ if (useNative) {
16014
+ const nativeChange = (commit, isBlocked) => (e) => {
16015
+ // An empty value is the end cleared. A date input gives nothing else
16016
+ // that doesn't read as a date.
16017
+ const parsed = fromIsoValue(e.target.value, 'day');
16018
+ if (!parsed)
16019
+ commit(null);
16020
+ else if (isWithin(parsed, lowerBound, max) && !isBlocked(parsed)) {
16021
+ commit(parsed);
16022
+ }
16023
+ };
16024
+ const minIso = min ? isoDay(lowerBound) : undefined;
16025
+ const maxIso = max ? isoDay(max) : undefined;
16026
+ return (jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: onFocus, onFocusCapture: onFocusCapture, children: [jsxs("div", { className: fieldClass, children: [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, 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] }));
16027
+ }
16028
+ const calendar = (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,
16029
+ // The popover panel takes `popoverId`, so the calendar inside it takes
16030
+ // one of its own. Inline there is no panel.
16031
+ id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }));
16032
+ if (inline) {
16033
+ return (jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: onFocus, onFocusCapture: onFocusCapture, children: [calendar, hiddenInputs] }));
16034
+ }
16035
+ const canOpen = !!popover && !disabled && !readOnly;
16036
+ const inputReadOnly = !!readOnly || !editable;
16037
+ // The field reads as one text box, so a press on it that misses both
16038
+ // inputs, on its padding, the gap or the separator, goes to the nearer
16039
+ // input and does what a press on that input does.
16040
+ const handleFieldMouseDown = (e) => {
16041
+ const startInput = startRef.current;
16042
+ const endInput = endRef.current;
16043
+ if (disabled || e.button !== 0)
16044
+ return;
16045
+ if (e.target === startInput || e.target === endInput)
16046
+ return;
16047
+ e.preventDefault();
16048
+ const middle = (startInput.getBoundingClientRect().right +
16049
+ endInput.getBoundingClientRect().left) /
16050
+ 2;
16051
+ (e.clientX < middle ? startInput : endInput).focus();
16052
+ if (openOnFocus && canOpen)
16053
+ requestOpen(true);
16054
+ };
16055
+ const comboboxProps = {
16056
+ type: 'text',
16057
+ role: 'combobox',
16058
+ 'aria-haspopup': 'dialog',
16059
+ 'aria-expanded': open,
16060
+ 'aria-controls': popoverId,
16061
+ autoComplete: 'off',
16062
+ className: inputClass,
16063
+ placeholder,
16064
+ disabled,
16065
+ readOnly: inputReadOnly,
16066
+ required,
16067
+ };
16068
+ return (jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: handleFocus, onFocusCapture: handleFocusCapture, children: [jsxs("div", { className: fieldClass, onMouseDown: handleFieldMouseDown, children: [jsx("input", { ...comboboxProps, ref: combinedRef, id: id, "aria-label": t.rangeStart, value: startText, ...startHandlers }), separator, jsx("input", { ...comboboxProps, ref: endRef, id: id ? `${id}-end` : undefined, "aria-label": t.rangeEnd, value: endText, ...endHandlers })] }), popover && triggerIcon && (jsx("button", { type: "button", className: triggerClass,
16069
+ // Disabled whenever the popover can't open, so a click can.
16070
+ onClick: () => setOpen(!open), disabled: !canOpen, "aria-label": t.chooseDateRange, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (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] }));
16071
+ });
16072
+ DateRangeInputBase.displayName = 'DateRangeInputBase';
16073
+
16074
+ /**
16075
+ * The `DateRangeInput` component is a form input for a start and end date, picked from one popover calendar or typed into two segmented inputs.
16076
+ *
16077
+ * @function
16078
+ * @param {DateRangeInputProps} props - Props for the DateRangeInput.
16079
+ * @returns {JSX.Element}
16080
+ *
16081
+ * @example
16082
+ * <DateRangeInput label="Stay" name="stay" min={new Date()} />
16083
+ */
16084
+ const DateRangeInput = forwardRef((props, ref) => {
16085
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
16086
+ const insideField = useInsideField();
16087
+ const insideControl = useInsideControl();
16088
+ const ownField = rendersOwnField('DateRangeInput', {
16089
+ insideField,
16090
+ insideControl,
16091
+ label,
16092
+ message,
16093
+ horizontal,
16094
+ fieldClassName,
16095
+ });
16096
+ // Inline mode renders a bare calendar with no input, so the Control's
16097
+ // icon containers have nothing to anchor to, and it renders no Control
16098
+ // inside one or out. The left icon is checked as the caller passed it:
16099
+ // the default glyph is this component's own choice, not a prop the
16100
+ // caller set, so it goes in only for the advice.
16101
+ const ownControl = rendersOwnControl('DateRangeInput', {
16102
+ insideControl,
16103
+ inline: baseProps.inline,
16104
+ isLoading,
16105
+ iconLeft,
16106
+ iconLeftName: props.iconLeftName,
16107
+ defaultIconLeftName: iconLeftName,
16108
+ iconLeftSize,
16109
+ iconRight,
16110
+ iconRightName,
16111
+ iconRightSize,
16112
+ hasIconsLeft,
16113
+ hasIconsRight,
16114
+ isExpanded,
16115
+ controlSize,
16116
+ controlClassName,
16117
+ });
16118
+ const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
16119
+ label,
16120
+ labelProps,
16121
+ rendersLabel: ownField,
16122
+ });
16123
+ const helpClass = usePrefixedClassNames('help', {
16124
+ [`is-${messageColor}`]: !!messageColor,
16125
+ });
16126
+ let content = (jsx(DateRangeInputBase, { ref: ref, "aria-labelledby": ariaLabelledBy, ...baseProps }));
16127
+ if (ownControl) {
16128
+ content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
16129
+ }
16130
+ const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
16131
+ if (ownField) {
16132
+ return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
16133
+ }
16134
+ return (jsxs(Fragment, { children: [content, messageEl] }));
16135
+ });
16136
+ DateRangeInput.displayName = 'DateRangeInput';
16137
+
15262
16138
  /**
15263
16139
  * The `Cell` component provides a single Bulma grid cell for use inside the [`Grid`](./grid.md) component.
15264
16140
  *
@@ -16638,5 +17514,5 @@ const Section = ({ size, className, children, color, bgColor, textColor, style,
16638
17514
  return (jsx("section", { className: sectionClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
16639
17515
  };
16640
17516
 
16641
- export { Autocomplete, Avatar, Avatars, Badge, Block, Box, Breadcrumb, Button, Buttons, Card, Carousel, CarouselItem, Cell, Checkbox, Checkboxes, ClientOnly, Code, Collapse, Collapses, Column, Columns, ConfigProvider, Container, Content, Control, DateInput, DateInputBase, DateTimeInput, DateTimeInputBase, Delete, Dialog, DialogContainer, Divider, Dropdown, DropdownDivider, DropdownItem, Emphasis, Field, FieldBody, FieldLabel, Figure, File, Footer, Grid, Hero, HeroBody, HeroFoot, HeroHead, Icon, IconText, Image, Input, InputBase, Level, LevelItem, LevelLeft, LevelRight, Link, LinkButton, ListItem, Loader, Loading, Media, MediaContent, MediaLeft, MediaRight, Menu, MenuItem, MenuLabel, MenuList, Message, Modal, Navbar, NavbarBrand, NavbarBurger, NavbarDivider, NavbarDropdown, NavbarDropdownMenu, NavbarEnd, NavbarItem, NavbarLink, NavbarMenu, NavbarStart, Notification, NotificationContainer, NumberInput, Numberinput, OrderedList, Pagination, PaginationEllipsis, PaginationLink, PaginationList, PaginationNext, PaginationPrevious, Panel, PanelBlock, PanelButtonBlock, PanelCheckboxBlock, PanelHeading, PanelIcon, PanelInputBlock, PanelTabs, Paragraph, Popover, PopoverBody, PopoverClose, PopoverFooter, PopoverHeader, Portal, Pre, Progress, Radio, Radios, Rate, Reveal, Section, Select, SelectBase, Sidebar, Skeleton, Slider, Span, Step, Steps, Strong, SubTitle, Switch, Tab, TabContentItem, TabItem, TabList, Table, Tabs, TabsContent, Tag, TagInput, Taginput, Tags, Tbody, Td, TextArea, TextAreaBase, Tfoot, Th, Thead, Theme, TimeInput, TimeInputBase, Title, Toast, ToastContainer, Tooltip, Tr, UnorderedList, checkboxColors, checkboxSizes, classNames, createPrefixedClassNames, dialog, isBrowser$1 as isBrowser, mergeBulmaStyles, notification, prefixedClassNames, radioColors, radioSizes, switchColors, switchSizes, toast, useBulmaClasses, useClassPrefix, useColorClasses, useColorStyles, useConfig, useFlexboxClasses, useFocusTrap, useIconLibrary, useInsideControl, useInsideField, useIsHydrated, useOtherClasses, usePrefixedClass, usePrefixedClassNames, useSpacingClasses, useTypographyClasses, useVisibilityClasses, validAlignContents, validAlignItems, validAlignSelfs, validAlignments$1 as validAlignments, validAspectRatios, validAxisOverflows, validColorShades, validColors, validCursors, validDisplays, validFlexDirections, validFlexGrowShrink, validFlexWraps, validFloats, validFontFamilies, validGaps, validInteractions, validJustifyContents, validOverflows, validPositions, validRadii, validResponsives, validSchemeColors, validShadows, validSizes$1 as validSizes, validTableColors, validTextSizes, validTextTransforms, validTextWeights, validViewports, validVisibilities };
17517
+ export { Autocomplete, Avatar, Avatars, Badge, Block, Box, Breadcrumb, Button, Buttons, Card, Carousel, CarouselItem, Cell, Checkbox, Checkboxes, ClientOnly, Code, Collapse, Collapses, Column, Columns, ConfigProvider, Container, Content, Control, DateInput, DateInputBase, DateRangeInput, DateRangeInputBase, DateTimeInput, DateTimeInputBase, Delete, Dialog, DialogContainer, Divider, Dropdown, DropdownDivider, DropdownItem, Emphasis, Field, FieldBody, FieldLabel, Figure, File, Footer, Grid, Hero, HeroBody, HeroFoot, HeroHead, Icon, IconText, Image, Input, InputBase, Level, LevelItem, LevelLeft, LevelRight, Link, LinkButton, ListItem, Loader, Loading, Media, MediaContent, MediaLeft, MediaRight, Menu, MenuItem, MenuLabel, MenuList, Message, Modal, Navbar, NavbarBrand, NavbarBurger, NavbarDivider, NavbarDropdown, NavbarDropdownMenu, NavbarEnd, NavbarItem, NavbarLink, NavbarMenu, NavbarStart, Notification, NotificationContainer, NumberInput, Numberinput, OrderedList, Pagination, PaginationEllipsis, PaginationLink, PaginationList, PaginationNext, PaginationPrevious, Panel, PanelBlock, PanelButtonBlock, PanelCheckboxBlock, PanelHeading, PanelIcon, PanelInputBlock, PanelTabs, Paragraph, Popover, PopoverBody, PopoverClose, PopoverFooter, PopoverHeader, Portal, Pre, Progress, Radio, Radios, Rate, Reveal, Section, Select, SelectBase, Sidebar, Skeleton, Slider, Span, Step, Steps, Strong, SubTitle, Switch, Tab, TabContentItem, TabItem, TabList, Table, Tabs, TabsContent, Tag, TagInput, Taginput, Tags, Tbody, Td, TextArea, TextAreaBase, Tfoot, Th, Thead, Theme, TimeInput, TimeInputBase, Title, Toast, ToastContainer, Tooltip, Tr, UnorderedList, checkboxColors, checkboxSizes, classNames, createPrefixedClassNames, dialog, isBrowser$1 as isBrowser, mergeBulmaStyles, notification, prefixedClassNames, radioColors, radioSizes, switchColors, switchSizes, toast, useBulmaClasses, useClassPrefix, useColorClasses, useColorStyles, useConfig, useFlexboxClasses, useFocusTrap, useIconLibrary, useInsideControl, useInsideField, useIsHydrated, useOtherClasses, usePrefixedClass, usePrefixedClassNames, useSpacingClasses, useTypographyClasses, useVisibilityClasses, validAlignContents, validAlignItems, validAlignSelfs, validAlignments$1 as validAlignments, validAspectRatios, validAxisOverflows, validColorShades, validColors, validCursors, validDisplays, validFlexDirections, validFlexGrowShrink, validFlexWraps, validFloats, validFontFamilies, validGaps, validInteractions, validJustifyContents, validOverflows, validPositions, validRadii, validResponsives, validSchemeColors, validShadows, validSizes$1 as validSizes, validTableColors, validTextSizes, validTextTransforms, validTextWeights, validViewports, validVisibilities };
16642
17518
  //# sourceMappingURL=index.esm.js.map