@allxsmith/bestax-bulma 5.26.4 → 5.26.6

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 (45) 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 +445 -55
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.esm.js +445 -55
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/types/form/Autocomplete.d.ts +6 -1
  10. package/dist/types/form/DateInput.d.ts +8 -4
  11. package/dist/types/form/DateTimeInput.d.ts +8 -4
  12. package/dist/types/form/FormContext.d.ts +47 -0
  13. package/dist/types/form/Input.d.ts +4 -3
  14. package/dist/types/form/Numberinput.d.ts +11 -1
  15. package/dist/types/form/Select.d.ts +4 -3
  16. package/dist/types/form/TextArea.d.ts +4 -3
  17. package/dist/types/form/TimeInput.d.ts +8 -4
  18. package/dist/types/form/_pickerInternals/Calendar.d.ts +45 -4
  19. package/dist/types/form/_pickerInternals/dateUtils.d.ts +17 -0
  20. package/dist/types/form/_pickerInternals/pickerTypes.d.ts +14 -0
  21. package/dist/types/helpers/statusRegion.d.ts +1 -0
  22. package/dist/types-cjs/form/Autocomplete.d.ts +6 -1
  23. package/dist/types-cjs/form/DateInput.d.ts +8 -4
  24. package/dist/types-cjs/form/DateTimeInput.d.ts +8 -4
  25. package/dist/types-cjs/form/FormContext.d.ts +47 -0
  26. package/dist/types-cjs/form/Input.d.ts +4 -3
  27. package/dist/types-cjs/form/Numberinput.d.ts +11 -1
  28. package/dist/types-cjs/form/Select.d.ts +4 -3
  29. package/dist/types-cjs/form/TextArea.d.ts +4 -3
  30. package/dist/types-cjs/form/TimeInput.d.ts +8 -4
  31. package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +45 -4
  32. package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +17 -0
  33. package/dist/types-cjs/form/_pickerInternals/pickerTypes.d.ts +14 -0
  34. package/dist/types-cjs/helpers/statusRegion.d.ts +1 -0
  35. package/dist/versions/bestax-no-dark-mode.css +1 -1
  36. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  37. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  38. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  39. package/dist/versions/bestax-no-helpers.css +1 -1
  40. package/dist/versions/bestax-no-helpers.css.map +1 -1
  41. package/dist/versions/bestax-prefixed.css +1 -1
  42. package/dist/versions/bestax-prefixed.css.map +1 -1
  43. package/package.json +1 -1
  44. package/src/scss/form/_dateinput.scss +8 -1
  45. package/src/scss/form/_taginput.scss +8 -0
package/dist/index.cjs CHANGED
@@ -7691,7 +7691,8 @@ const noAnnouncementsYet = {
7691
7691
  // the accessibility tree. The declarations match the `extras-sr-only` mixin,
7692
7692
  // plus `clip-path`, which replaces the deprecated `clip`. The negative margin
7693
7693
  // pulls the 1px box back inside the page, so it doesn't add scrollable
7694
- // overflow.
7694
+ // overflow. Exported for other live regions that must work without a
7695
+ // stylesheet, such as a range calendar's.
7695
7696
  const visuallyHidden = {
7696
7697
  position: 'absolute',
7697
7698
  width: '1px',
@@ -8821,6 +8822,58 @@ const rendersOwnField = (component, options) => {
8821
8822
  (moved.length > 0 ? `, and set ${listOf(moved)} on that <Field>.` : '.'));
8822
8823
  return true;
8823
8824
  };
8825
+ // Each Control-level prop, with what takes its place on the `Control` the
8826
+ // caller wrapped around the wrapper. `bare-control.test.tsx` holds this to
8827
+ // what each wrapper hands its own `Control`, so a new Control-level prop
8828
+ // fails there until it is added here and passed to `rendersOwnControl`.
8829
+ const ON_CONTROL = {
8830
+ isLoading: 'isLoading',
8831
+ iconLeft: 'iconLeft',
8832
+ iconLeftName: 'iconLeftName',
8833
+ iconLeftSize: 'iconLeftSize',
8834
+ iconRight: 'iconRight',
8835
+ iconRightName: 'iconRightName',
8836
+ iconRightSize: 'iconRightSize',
8837
+ hasIconsLeft: 'hasIconsLeft',
8838
+ hasIconsRight: 'hasIconsRight',
8839
+ isExpanded: 'isExpanded',
8840
+ controlSize: 'size (for controlSize)',
8841
+ controlClassName: 'className (for controlClassName)',
8842
+ };
8843
+ /**
8844
+ * Whether a convenience form wrapper renders a `Control` of its own (#921):
8845
+ * only outside one. Inside a `Control` it renders none, so the props it
8846
+ * would have handed its own do nothing there, and it warns in development,
8847
+ * naming them and saying to set them on that `Control`. An `inline` picker
8848
+ * renders none in either place, so it drops them inside a `Control` or not,
8849
+ * and its warning says so without pointing at a `Control`. A wrapper passes
8850
+ * only the props it hands its own `Control`, as the caller gave them, so a
8851
+ * default the wrapper fills in itself never warns. A prop counts when it is
8852
+ * truthy, since a falsy one would change nothing on a `Control` either.
8853
+ * Internal; not part of the public API.
8854
+ */
8855
+ const rendersOwnControl = (component, options) => {
8856
+ if (!options.inline && !options.insideControl)
8857
+ return true;
8858
+ const held = Object.keys(ON_CONTROL).filter(prop => options[prop]);
8859
+ if (held.length === 0)
8860
+ return false;
8861
+ const names = held.join(' ');
8862
+ const doNothing = held.length > 1 ? 'those props do' : 'that prop does';
8863
+ if (options.inline) {
8864
+ warnOnce(`${component}:Control-props-inline:${held.join('+')}`, `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
8865
+ `in inline mode, inside a <Control> or not, so ${doNothing} ` +
8866
+ `nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
8867
+ `inline picker.`);
8868
+ }
8869
+ else {
8870
+ warnOnce(`${component}:Control-props-in-Control:${held.join('+')}`, `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
8871
+ `no <Control> of its own, so ${doNothing} nothing there. Set ` +
8872
+ `${listOf(held.map(prop => ON_CONTROL[prop]))} on that <Control> ` +
8873
+ `instead.`);
8874
+ }
8875
+ return false;
8876
+ };
8824
8877
  const FieldLabelIdContext = React.createContext(undefined);
8825
8878
  /**
8826
8879
  * The id a labeled Field wants its single composed control to adopt (#495).
@@ -8955,6 +9008,7 @@ Checkbox.displayName = 'Checkbox';
8955
9008
  // picker bases read it so their right-side launcher gives way to that
8956
9009
  // spinner, which sits at the same edge, whether the `Control` is the one a
8957
9010
  // convenience input renders or one the caller wrapped around the input.
9011
+ // Autocomplete reads it for its clear button, which sits there too.
8958
9012
  // Kept apart from `FormContext`, whose contexts only report whether a `Field`
8959
9013
  // or `Control` is present. Internal; not part of the public API.
8960
9014
  const ControlLoadingContext = React.createContext(false);
@@ -9284,7 +9338,7 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
9284
9338
  }), [name, groupActive, currentValue, handleChange]);
9285
9339
  const checkboxesElement = (jsxRuntime.jsx("div", { className: wrapperClass, role: "group", "aria-labelledby": ariaLabelledBy, ...rest, children: jsxRuntime.jsx(CheckboxesProvider, { value: ctx, children: children }) }));
9286
9340
  let content = checkboxesElement;
9287
- if (!insideControl) {
9341
+ if (rendersOwnControl('Checkboxes', { insideControl })) {
9288
9342
  content = jsxRuntime.jsx(Control, { children: content });
9289
9343
  }
9290
9344
  if (ownField) {
@@ -9524,7 +9578,7 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
9524
9578
  }), [name, groupActive, currentValue, handleChange]);
9525
9579
  const radiosElement = (jsxRuntime.jsx("div", { className: wrapperClass, role: "radiogroup", "aria-labelledby": ariaLabelledBy, ...rest, children: jsxRuntime.jsx(RadiosProvider, { value: ctx, children: children }) }));
9526
9580
  let content = radiosElement;
9527
- if (!insideControl) {
9581
+ if (rendersOwnControl('Radios', { insideControl })) {
9528
9582
  content = jsxRuntime.jsx(Control, { children: content });
9529
9583
  }
9530
9584
  if (ownField) {
@@ -10058,7 +10112,7 @@ const Slider = React.forwardRef((props, ref) => {
10058
10112
  '--slider-output-arrow-offset': `calc(50% - ${nudgeXHigh}px)`,
10059
10113
  }, children: formatValue(currentRange[1]) })), renderTicks()] }));
10060
10114
  let content = sliderElement;
10061
- if (!insideControl) {
10115
+ if (rendersOwnControl('Slider', { insideControl })) {
10062
10116
  content = jsxRuntime.jsx(Control, { children: content });
10063
10117
  }
10064
10118
  if (ownField) {
@@ -10089,7 +10143,11 @@ const Numberinput = React.forwardRef(({
10089
10143
  // Field props
10090
10144
  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) => {
10091
10145
  const insideField = useInsideField();
10092
- const effectiveBare = bare ?? insideField;
10146
+ // Bare controls join their parent's row, which a `.control` does not lay
10147
+ // out. `useInsideControl()` is true anywhere below a `Control`, even past
10148
+ // a `Field` nested in it, so there they keep their own row as well.
10149
+ const insideControl = useInsideControl();
10150
+ const effectiveBare = bare ?? (insideField && !insideControl);
10093
10151
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
10094
10152
  const [internalValue, setInternalValue] = React.useState(defaultValue);
10095
10153
  const inputRef = React.useRef(null);
@@ -10502,7 +10560,7 @@ const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, messa
10502
10560
  const rateCustomTextClass = usePrefixedClassNames('rate-custom-text');
10503
10561
  const rateElement = (jsxRuntime.jsxs("div", { ref: ref, className: combinedClasses, role: "radiogroup", "aria-label": ariaLabelledBy ? undefined : 'Rating', "aria-labelledby": ariaLabelledBy, "aria-valuenow": currentValue, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuetext": ariaValueText, tabIndex: disabled ? -1 : 0, onKeyDown: handleKeyDown, ...rest, children: [jsxRuntime.jsx("div", { className: rateItemsClass, children: renderIcons() }), showScore && (jsxRuntime.jsx("span", { className: rateScoreClass, children: getScoreDisplay() })), text && jsxRuntime.jsx("span", { className: rateTextClass, children: text }), customText && (jsxRuntime.jsx("span", { className: rateCustomTextClass, children: customText })), name && (jsxRuntime.jsx("input", { type: "hidden", name: name, value: currentValue, form: form }))] }));
10504
10562
  let content = rateElement;
10505
- if (!insideControl) {
10563
+ if (rendersOwnControl('Rate', { insideControl })) {
10506
10564
  content = jsxRuntime.jsx(Control, { children: content });
10507
10565
  }
10508
10566
  if (ownField) {
@@ -10741,7 +10799,11 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
10741
10799
  [`is-${messageColor}`]: !!messageColor,
10742
10800
  });
10743
10801
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
10744
- const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": isActive, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), clearable && inputValue && !disabled && (jsxRuntime.jsx("span", { className: iconRightClickableClass, onClick: handleClear, role: "button", "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), isActive && (filteredData.length > 0 || empty) && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, role: "listbox", onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), filteredData.length > 0 ? (filteredData.map((item, index) => {
10802
+ // The clear button gives way to a spinner at its spot: the `loading` one,
10803
+ // or that of a loading Control this sits in.
10804
+ const controlLoading = useControlLoading();
10805
+ const showClear = clearable && !!inputValue && !disabled && !loading && !controlLoading;
10806
+ const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": isActive, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), showClear && (jsxRuntime.jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", "aria-hidden": "true", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), isActive && (filteredData.length > 0 || empty) && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, role: "listbox", onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), filteredData.length > 0 ? (filteredData.map((item, index) => {
10745
10807
  const isDisabled = typeof item !== 'string' && item.disabled;
10746
10808
  const isHighlighted = index === highlightedIndex;
10747
10809
  const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
@@ -11249,6 +11311,21 @@ fieldClassName, controlClassName,
11249
11311
  horizontal,
11250
11312
  fieldClassName,
11251
11313
  });
11314
+ const ownControl = rendersOwnControl('Input', {
11315
+ insideControl,
11316
+ isLoading,
11317
+ iconLeft,
11318
+ iconLeftName,
11319
+ iconLeftSize,
11320
+ iconRight,
11321
+ iconRightName,
11322
+ iconRightSize,
11323
+ hasIconsLeft,
11324
+ hasIconsRight,
11325
+ isExpanded,
11326
+ controlSize,
11327
+ controlClassName,
11328
+ });
11252
11329
  const { controlId, fieldLabelProps } = useAutoLabelId({
11253
11330
  label,
11254
11331
  id: inputProps.id,
@@ -11259,7 +11336,7 @@ fieldClassName, controlClassName,
11259
11336
  [`is-${messageColor}`]: !!messageColor,
11260
11337
  });
11261
11338
  let content = jsxRuntime.jsx(InputBase, { ref: ref, id: controlId, ...inputProps });
11262
- if (!insideControl) {
11339
+ if (ownControl) {
11263
11340
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11264
11341
  }
11265
11342
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -11307,6 +11384,17 @@ fieldClassName, controlClassName,
11307
11384
  horizontal,
11308
11385
  fieldClassName,
11309
11386
  });
11387
+ // Not isLoading, which the select draws itself and so keeps in a Control.
11388
+ const ownControl = rendersOwnControl('Select', {
11389
+ insideControl,
11390
+ iconLeft,
11391
+ iconLeftName,
11392
+ iconLeftSize,
11393
+ hasIconsLeft,
11394
+ isExpanded,
11395
+ controlSize,
11396
+ controlClassName,
11397
+ });
11310
11398
  const { controlId, fieldLabelProps } = useAutoLabelId({
11311
11399
  label,
11312
11400
  id: selectProps.id,
@@ -11317,7 +11405,7 @@ fieldClassName, controlClassName,
11317
11405
  [`is-${messageColor}`]: !!messageColor,
11318
11406
  });
11319
11407
  let content = jsxRuntime.jsx(SelectBase, { ref: ref, id: controlId, ...selectProps });
11320
- if (!insideControl) {
11408
+ if (ownControl) {
11321
11409
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
11322
11410
  }
11323
11411
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -11366,6 +11454,12 @@ fieldClassName, controlClassName,
11366
11454
  horizontal,
11367
11455
  fieldClassName,
11368
11456
  });
11457
+ const ownControl = rendersOwnControl('TextArea', {
11458
+ insideControl,
11459
+ isLoading: controlIsLoading,
11460
+ controlSize,
11461
+ controlClassName,
11462
+ });
11369
11463
  const { controlId, fieldLabelProps } = useAutoLabelId({
11370
11464
  label,
11371
11465
  id: textAreaProps.id,
@@ -11376,7 +11470,7 @@ fieldClassName, controlClassName,
11376
11470
  [`is-${messageColor}`]: !!messageColor,
11377
11471
  });
11378
11472
  let content = jsxRuntime.jsx(TextAreaBase, { ref: ref, id: controlId, ...textAreaProps });
11379
- if (!insideControl) {
11473
+ if (ownControl) {
11380
11474
  content = (jsxRuntime.jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
11381
11475
  }
11382
11476
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -11395,6 +11489,9 @@ const DEFAULT_PICKER_LABELS = {
11395
11489
  chooseDate: 'Choose date',
11396
11490
  chooseMonth: 'Choose month',
11397
11491
  chooseYear: 'Choose year',
11492
+ rangeStart: 'Start date',
11493
+ rangeEnd: 'End date',
11494
+ rangePreviewEnd: 'Choose as end date',
11398
11495
  hours: 'hours',
11399
11496
  minutes: 'minutes',
11400
11497
  seconds: 'seconds',
@@ -11547,6 +11644,43 @@ function isPeriodUnselectable(d, granularity, c) {
11547
11644
  }
11548
11645
  return true;
11549
11646
  }
11647
+ /**
11648
+ * The most days {@link canCloseRange} walks between a range's ends, about
11649
+ * eleven years. A span it would have to walk further is refused, so the check
11650
+ * stays cheap enough to run on every pointer move over the calendar.
11651
+ */
11652
+ const RANGE_WALK_LIMIT = 4000;
11653
+ /**
11654
+ * Whether `end` can close a range that opens on `start`: it falls on `start`'s
11655
+ * day or later, and, unless `allowDisabled`, no day between the two is one
11656
+ * that `shouldDisableDate` or `unselectableDates` disables. Checking that
11657
+ * walks the days between, so with either rule set and `allowDisabled` off,
11658
+ * an `end` more than {@link RANGE_WALK_LIMIT} days after `start` is refused.
11659
+ * The ends' own constraints, `min` and `max` among them, are the caller's to
11660
+ * check. Bounds can't fall between two days inside them, so they are not
11661
+ * walked.
11662
+ */
11663
+ function canCloseRange(start, end, c, allowDisabled = false) {
11664
+ const first = startOfDay(start);
11665
+ const last = startOfDay(end).getTime();
11666
+ if (last < first.getTime())
11667
+ return false;
11668
+ if (allowDisabled)
11669
+ return true;
11670
+ const blocks = {
11671
+ shouldDisableDate: c.shouldDisableDate,
11672
+ unselectableDates: c.unselectableDates,
11673
+ };
11674
+ if (!blocks.shouldDisableDate && !blocks.unselectableDates?.length) {
11675
+ return true;
11676
+ }
11677
+ for (let d = addDays(first, 1), walked = 1; d.getTime() < last; d = addDays(d, 1), walked++) {
11678
+ if (walked >= RANGE_WALK_LIMIT || isDayUnselectable(d, blocks)) {
11679
+ return false;
11680
+ }
11681
+ }
11682
+ return true;
11683
+ }
11550
11684
  function addDays(d, n) {
11551
11685
  const r = new Date(d);
11552
11686
  r.setDate(r.getDate() + n);
@@ -12007,11 +12141,34 @@ function nearestEnabled(i, disabled) {
12007
12141
  }
12008
12142
  return i;
12009
12143
  }
12144
+ /** The long, spoken form of a date in a range's announcement. */
12145
+ const LONG_DATE = {
12146
+ year: 'numeric',
12147
+ month: 'long',
12148
+ day: 'numeric',
12149
+ };
12150
+ /** A range's pending start: its start, when it has no end. */
12151
+ const pendingStartOf = (range) => range?.[0] && !range[1] ? startOfDay(range[0]) : null;
12010
12152
  /**
12011
- * The year list's keyboard when it is the selection surface: arrows move a
12012
- * year or a row, Home/End go to the list's ends. Returns the year to start
12013
- * from and the direction to search past disabled years, or `null` for a key
12014
- * the list doesn't handle.
12153
+ * The day grid's cells in rows of a week. A grid owns rows and rows own
12154
+ * cells, so assistive technology can walk the grid, and the selection it
12155
+ * reports, row by row.
12156
+ */
12157
+ const inWeeks = (days, className) => {
12158
+ const rows = [];
12159
+ for (let i = 0; i < days.length; i += 7) {
12160
+ rows.push(jsxRuntime.jsx("div", { role: "row", className: className, children: days.slice(i, i + 7) }, i));
12161
+ }
12162
+ return rows;
12163
+ };
12164
+ /** A date's time, or `null` for none, for comparing ranges by value. */
12165
+ const timeOf = (d) => d ? d.getTime() : null;
12166
+ /**
12167
+ * The year list's keyboard, as the selection surface and as navigation:
12168
+ * arrows move a year or a row, Home/End go to the list's ends. Returns the
12169
+ * year to start from and the direction to search past disabled years, or
12170
+ * `null` for a key the list doesn't handle. As navigation no year is
12171
+ * disabled, so only the year is used.
12015
12172
  */
12016
12173
  function yearStep(key, focusedYear, years) {
12017
12174
  switch (key) {
@@ -12031,7 +12188,7 @@ function yearStep(key, focusedYear, years) {
12031
12188
  return null;
12032
12189
  }
12033
12190
  }
12034
- 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', }) => {
12191
+ 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, }) => {
12035
12192
  const gridRef = React.useRef(null);
12036
12193
  const monthGridRef = React.useRef(null);
12037
12194
  const yearGridRef = React.useRef(null);
@@ -12088,6 +12245,80 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12088
12245
  const i = nearestEnabled(focusedDate.getDate() - 1, days.map(isDateUnselectable));
12089
12246
  return days[i];
12090
12247
  }, [cells, focusedDate, isDateUnselectable]);
12248
+ // ----- Range mode -----
12249
+ // Range props come only with the day grid.
12250
+ const rangeMode = !!range;
12251
+ const valuePending = pendingStartOf(range);
12252
+ // The start of a range being picked, which nothing outside the calendar
12253
+ // sees until its end is picked too. It starts as the range's own pending
12254
+ // start, and starts there again whenever the range changes from outside.
12255
+ const [anchor, setAnchor] = React.useState(valuePending);
12256
+ const rangeKey = `${timeOf(range?.[0])}|${timeOf(range?.[1])}`;
12257
+ const [anchorRangeKey, setAnchorRangeKey] = React.useState(rangeKey);
12258
+ if (anchorRangeKey !== rangeKey) {
12259
+ setAnchorRangeKey(rangeKey);
12260
+ setAnchor(valuePending);
12261
+ }
12262
+ // The day under the pointer while a start is pending, which the preview
12263
+ // runs to ahead of the focused day.
12264
+ const [hoverDate, setHoverDate] = React.useState(null);
12265
+ // Whether `day` can end a range that starts on `start`. The start is
12266
+ // checked as well as the end: one the range brought, or one picked before
12267
+ // the bounds or the disabled days changed, can rule itself out.
12268
+ const canEndAt = React.useCallback((start, day) => !isDateUnselectable(start) &&
12269
+ !isDateUnselectable(day) &&
12270
+ canCloseRange(start, day, { shouldDisableDate, unselectableDates }, allowDisabledInRange), [
12271
+ isDateUnselectable,
12272
+ shouldDisableDate,
12273
+ unselectableDates,
12274
+ allowDisabledInRange,
12275
+ ]);
12276
+ // A pick in range mode: the end of the pending range when it can be one,
12277
+ // and otherwise the start of a new one.
12278
+ const pickRangeDay = React.useCallback((picked) => {
12279
+ const day = startOfDay(picked);
12280
+ // The pointer is on the picked day, which the focused day now is too.
12281
+ setHoverDate(null);
12282
+ if (anchor && canEndAt(anchor, day)) {
12283
+ setAnchor(null);
12284
+ onRangeSelect?.(anchor, day);
12285
+ }
12286
+ else {
12287
+ setAnchor(day);
12288
+ }
12289
+ }, [anchor, canEndAt, onRangeSelect]);
12290
+ // What the grid shows: while a start is pending, the range from it to the
12291
+ // hovered or focused day, when that day can end it; otherwise the
12292
+ // committed range.
12293
+ let shownStart;
12294
+ let shownEnd;
12295
+ if (anchor) {
12296
+ shownStart = anchor;
12297
+ const previewEnd = startOfDay(hoverDate ?? focusedDate);
12298
+ shownEnd = canEndAt(anchor, previewEnd) ? previewEnd : null;
12299
+ }
12300
+ else {
12301
+ shownStart = range?.[0] ? startOfDay(range[0]) : null;
12302
+ shownEnd = range?.[1] ? startOfDay(range[1]) : null;
12303
+ }
12304
+ const previewing = !!anchor;
12305
+ // Ids for the words that name a range's ends and state the whole range.
12306
+ const reactId = React.useId();
12307
+ const rangeIdBase = id ?? `calendar-${reactId}`;
12308
+ const rangeStartId = `${rangeIdBase}-range-start`;
12309
+ const rangeEndId = `${rangeIdBase}-range-end`;
12310
+ const rangePreviewEndId = `${rangeIdBase}-range-preview-end`;
12311
+ const rangeSummaryId = `${rangeIdBase}-range-summary`;
12312
+ // The selection in words: the pending start alone, or the committed ends.
12313
+ const selectedEnd = previewing ? null : shownEnd;
12314
+ const rangeSummary = [
12315
+ shownStart &&
12316
+ `${t.rangeStart}: ${formatDate(shownStart, LONG_DATE, locale)}`,
12317
+ selectedEnd &&
12318
+ `${t.rangeEnd}: ${formatDate(selectedEnd, LONG_DATE, locale)}`,
12319
+ ]
12320
+ .filter(Boolean)
12321
+ .join(', ');
12091
12322
  // Whether the grid on show holds focus, or held it last before focus went
12092
12323
  // nowhere: the cell that had it left the page or became disabled. A blur
12093
12324
  // that takes focus out of the grid, to nowhere included, is the user
@@ -12125,6 +12356,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12125
12356
  }
12126
12357
  }, [min, max, isDateUnselectable, onFocusedDateChange]);
12127
12358
  const handleKeyDown = React.useCallback((e) => {
12359
+ // A key takes the range preview back from the pointer.
12360
+ setHoverDate(null);
12128
12361
  switch (e.key) {
12129
12362
  case 'ArrowLeft':
12130
12363
  e.preventDefault();
@@ -12167,11 +12400,35 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12167
12400
  case 'Enter':
12168
12401
  case ' ':
12169
12402
  e.preventDefault();
12170
- if (!isDateUnselectable(focusedDate))
12171
- onSelect(focusedDate);
12403
+ if (isDateUnselectable(focusedDate))
12404
+ break;
12405
+ if (rangeMode)
12406
+ pickRangeDay(focusedDate);
12407
+ else
12408
+ onSelect?.(focusedDate);
12409
+ break;
12410
+ case 'Escape':
12411
+ // A pending pick is taken back, and the key stops there so a
12412
+ // popover around the calendar stays open. The range's own pending
12413
+ // start is no pick, so Escape goes on to close the popover.
12414
+ if (anchor && !(valuePending && isSameDay(anchor, valuePending))) {
12415
+ e.preventDefault();
12416
+ e.stopPropagation();
12417
+ setAnchor(valuePending);
12418
+ }
12172
12419
  break;
12173
12420
  }
12174
- }, [focusedDate, firstDayOfWeek, isDateUnselectable, moveFocus, onSelect]);
12421
+ }, [
12422
+ focusedDate,
12423
+ firstDayOfWeek,
12424
+ isDateUnselectable,
12425
+ moveFocus,
12426
+ onSelect,
12427
+ rangeMode,
12428
+ pickRangeDay,
12429
+ anchor,
12430
+ valuePending,
12431
+ ]);
12175
12432
  // The month grid's counterpart to `moveFocus`: step by `direction` months
12176
12433
  // until a month with a selectable day turns up, stopping at the min/max
12177
12434
  // months. The cap guards against a range with nothing selectable.
@@ -12194,7 +12451,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12194
12451
  if (e.key === 'Enter' || e.key === ' ') {
12195
12452
  e.preventDefault();
12196
12453
  if (!isMonthUnselectable(focusedDate)) {
12197
- onSelect(startOfMonth(focusedDate));
12454
+ onSelect?.(startOfMonth(focusedDate));
12198
12455
  }
12199
12456
  return;
12200
12457
  }
@@ -12220,6 +12477,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12220
12477
  const dayNamesRowClass = usePrefixedClassNames('dateinput-day-names');
12221
12478
  const dayNameClass = usePrefixedClassNames('dateinput-day-name');
12222
12479
  const gridClass = usePrefixedClassNames('dateinput-grid');
12480
+ const weekClass = usePrefixedClassNames('dateinput-week');
12223
12481
  const monthsGridClass = usePrefixedClassNames('dateinput-months-grid');
12224
12482
  const monthsRowClass = usePrefixedClassNames('dateinput-months-row');
12225
12483
  const yearsGridClass = usePrefixedClassNames('dateinput-years-grid');
@@ -12272,8 +12530,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12272
12530
  // year is disabled the stop moves to the nearest enabled year in the list,
12273
12531
  // and focusing it makes it the focused year. As the selection surface a
12274
12532
  // year outside a non-empty list starts from the end it fell past, so the
12275
- // list keeps a stop. As navigation the stop marks the current year, so a
12276
- // year outside the list marks no option.
12533
+ // list keeps a stop. As navigation it marks the current year, so a year
12534
+ // outside the list marks no option, and the stop itself follows focus.
12277
12535
  const tabStopYear = React.useMemo(() => {
12278
12536
  let i = yearList.indexOf(focusedYear);
12279
12537
  if (i < 0) {
@@ -12283,6 +12541,12 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12283
12541
  }
12284
12542
  return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
12285
12543
  }, [yearList, focusedYear, disabledYears, isYearGranularity]);
12544
+ // As navigation the keys move focus and leave the focused year alone, so
12545
+ // the tab stop goes to the year that last had focus while it is listed,
12546
+ // and Tab leaves the list rather than landing on the current year. It
12547
+ // starts over each time the list opens.
12548
+ const [navYear, setNavYear] = React.useState(null);
12549
+ const listTabStop = navYear !== null && yearList.includes(navYear) ? navYear : tabStopYear;
12286
12550
  // Which of the focused year's months have no selectable day.
12287
12551
  const disabledMonths = React.useMemo(() => granularity === 'month'
12288
12552
  ? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
@@ -12303,7 +12567,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12303
12567
  // pick and `onSelect` gets the year's first day.
12304
12568
  const handleYearPick = React.useCallback((year) => {
12305
12569
  onFocusedDateChange(clampDate(makeDate(year), min, max));
12306
- onSelect(makeDate(year));
12570
+ onSelect?.(makeDate(year));
12307
12571
  }, [min, max, onFocusedDateChange, onSelect]);
12308
12572
  // Step through the listed years from `year` by `direction`, skipping
12309
12573
  // disabled ones, and focus the first selectable year found.
@@ -12318,19 +12582,39 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12318
12582
  }
12319
12583
  }, [yearList, disabledYears, onFocusedDateChange, min, max]);
12320
12584
  const handleYearListKeyDown = React.useCallback((e) => {
12321
- if (!isYearGranularity) {
12322
- // As navigation, Escape returns to the grid the list was opened from.
12323
- if (e.key === 'Escape') {
12324
- e.preventDefault();
12325
- setView(baseView);
12326
- }
12585
+ if (isYearGranularity) {
12586
+ const step = yearStep(e.key, focusedYear, yearList);
12587
+ if (!step)
12588
+ return;
12589
+ e.preventDefault();
12590
+ moveYearFocus(step[0], step[1]);
12327
12591
  return;
12328
12592
  }
12329
- const step = yearStep(e.key, focusedYear, yearList);
12593
+ // As navigation, Escape returns to the grid the list was opened from.
12594
+ // It stops there, so a popover around the calendar stays open and only
12595
+ // a second Escape closes it.
12596
+ if (e.key === 'Escape') {
12597
+ e.preventDefault();
12598
+ e.stopPropagation();
12599
+ setView(baseView);
12600
+ return;
12601
+ }
12602
+ // The other keys move DOM focus from the year that has it and leave
12603
+ // the focused date alone. The list is centred on that date, so moving
12604
+ // it would reflow the list under the keyboard, and it would turn the
12605
+ // grid behind the list before anything is picked. Enter or Space
12606
+ // clicks the year with focus, which picks it.
12607
+ const options = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
12608
+ const from = options.indexOf(e.target);
12609
+ if (from < 0)
12610
+ return;
12611
+ const step = yearStep(e.key, yearList[from], yearList);
12330
12612
  if (!step)
12331
12613
  return;
12332
12614
  e.preventDefault();
12333
- moveYearFocus(step[0], step[1]);
12615
+ // The listed years run one apart, so a year's index is its distance
12616
+ // from the first. A step past either end finds no option.
12617
+ options[step[0] - yearList[0]]?.focus();
12334
12618
  }, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
12335
12619
  // The day or month grid takes focus as the popover opens on it, or comes
12336
12620
  // back to it from the year list.
@@ -12401,32 +12685,72 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12401
12685
  list.querySelector('[data-focused-year="true"]')?.focus();
12402
12686
  }, [isYearGranularity, focusedYear]);
12403
12687
  const navArrow = (d) => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
12404
- return (jsxRuntime.jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxRuntime.jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxRuntime.jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsxRuntime.jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxRuntime.jsxs("div", { className: navGroupClass, children: [jsxRuntime.jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsxRuntime.jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsxRuntime.jsx("div", { className: dayNameClass, children: name }, i))) }), jsxRuntime.jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
12688
+ return (jsxRuntime.jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxRuntime.jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxRuntime.jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => {
12689
+ setNavYear(null);
12690
+ setView(v => (v === 'years' ? baseView : 'years'));
12691
+ }, children: [jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsxRuntime.jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxRuntime.jsxs("div", { className: navGroupClass, children: [jsxRuntime.jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsxRuntime.jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsxRuntime.jsx("div", { className: dayNameClass, children: name }, i))) }), jsxRuntime.jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, "aria-multiselectable": rangeMode || undefined, "aria-describedby": rangeMode ? rangeSummaryId : undefined, className: gridClass, onKeyDown: handleKeyDown, onMouseLeave: anchor ? () => setHoverDate(null) : undefined, ...gridFocusHandlers, children: inWeeks(cells.map(cell => {
12405
12692
  const disabled = isDateUnselectable(cell.date);
12406
- const isSelected = !!value && isSameDay(value, cell.date);
12693
+ const time = cell.date.getTime();
12694
+ // Range mode. The range on show has its ends filled. Every day
12695
+ // of a committed range is selected, and of a preview only the
12696
+ // start.
12697
+ const isStart = time === timeOf(shownStart);
12698
+ const isEnd = time === timeOf(shownEnd);
12699
+ const inRange = !!shownStart &&
12700
+ !!shownEnd &&
12701
+ time > shownStart.getTime() &&
12702
+ time < shownEnd.getTime();
12703
+ // The day a pending range would end on, which isn't picked
12704
+ // yet, so its description says what picking it does.
12705
+ const isPreviewEnd = previewing && isEnd && !isStart;
12706
+ const isPreview = (previewing && inRange) || isPreviewEnd;
12707
+ const isFilled = rangeMode
12708
+ ? isStart || (isEnd && !previewing)
12709
+ : !!value && isSameDay(value, cell.date);
12710
+ const isSelected = isFilled || (rangeMode && !previewing && inRange);
12711
+ const describedBy = rangeMode
12712
+ ? [
12713
+ isStart && rangeStartId,
12714
+ isEnd && !previewing && rangeEndId,
12715
+ isPreviewEnd && rangePreviewEndId,
12716
+ ]
12717
+ .filter(Boolean)
12718
+ .join(' ') || undefined
12719
+ : undefined;
12407
12720
  const isFocused = isSameDay(cell.date, tabStopDay);
12408
12721
  const otherMonth = !cell.inCurrentMonth;
12409
12722
  const cellClass = prefixedClassNames(classPrefix, 'dateinput-cell', {
12410
- 'is-selected': isSelected,
12723
+ 'is-selected': isFilled,
12724
+ 'is-range-start': isStart,
12725
+ 'is-range-end': isEnd,
12726
+ 'is-in-range': inRange,
12727
+ 'is-preview': isPreview,
12411
12728
  'is-today': cell.isToday,
12412
12729
  'is-disabled': disabled,
12413
12730
  'is-other-month': otherMonth,
12414
12731
  });
12415
12732
  const display = !otherMonth || nearbyMonthDays;
12416
- return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
12733
+ return (jsxRuntime.jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "aria-describedby": describedBy, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
12417
12734
  // Reached by Tab while the focused day is disabled, or
12418
12735
  // by pointer: keys move on from here. A nearby month's
12419
12736
  // day waits for its click, which turns the grid.
12420
12737
  if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
12421
12738
  onFocusedDateChange(cell.date);
12422
12739
  }
12423
- }, onClick: () => {
12740
+ },
12741
+ // Mouseover rather than mouseenter: browsers send it to a
12742
+ // disabled day as well, where React holds mouseenter back,
12743
+ // so the preview leaves the last enabled day behind.
12744
+ onMouseOver: anchor ? () => setHoverDate(cell.date) : undefined, onClick: () => {
12424
12745
  if (disabled)
12425
12746
  return;
12426
12747
  onFocusedDateChange(cell.date);
12427
- onSelect(cell.date);
12748
+ if (rangeMode)
12749
+ pickRangeDay(cell.date);
12750
+ else
12751
+ onSelect?.(cell.date);
12428
12752
  }, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
12429
- }) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
12753
+ }), weekClass) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
12430
12754
  const date = makeDate(focusedYear, month);
12431
12755
  const disabled = disabledMonths[month];
12432
12756
  const isSelected = !!value && isSameMonth(value, date);
@@ -12445,7 +12769,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12445
12769
  }
12446
12770
  }, onClick: () => {
12447
12771
  onFocusedDateChange(clampDate(date, min, max));
12448
- onSelect(date);
12772
+ onSelect?.(date);
12449
12773
  }, children: monthCellNames[month] }, month));
12450
12774
  }) }, row[0]))) })), view === 'years' && (jsxRuntime.jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
12451
12775
  const isFocused = year === tabStopYear;
@@ -12461,16 +12785,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
12461
12785
  'is-today': isToday,
12462
12786
  'is-disabled': disabled,
12463
12787
  });
12464
- return (jsxRuntime.jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: isFocused ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
12465
- // Reached by Tab while the focused year is disabled, or by
12466
- // pointer: keys move on from here.
12467
- if (isYearGranularity && year !== focusedYear) {
12788
+ return (jsxRuntime.jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
12789
+ // As navigation, focusing a year by key or pointer only
12790
+ // moves the tab stop: nothing is committed until a year is
12791
+ // picked. As the selection surface it is reached by Tab
12792
+ // while the focused year is disabled, or by pointer, and
12793
+ // keys move on from here.
12794
+ if (!isYearGranularity) {
12795
+ setNavYear(year);
12796
+ }
12797
+ else if (year !== focusedYear) {
12468
12798
  onFocusedDateChange(clampDate(makeDate(year), min, max));
12469
12799
  }
12470
12800
  }, onClick: () => isYearGranularity
12471
12801
  ? handleYearPick(year)
12472
12802
  : handleYearSelect(year), children: year }, year));
12473
- }) }))] }));
12803
+ }) })), rangeMode && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { id: rangeStartId, hidden: true, children: t.rangeStart }), jsxRuntime.jsx("span", { id: rangeEndId, hidden: true, children: t.rangeEnd }), jsxRuntime.jsx("span", { id: rangePreviewEndId, hidden: true, children: t.rangePreviewEnd }), jsxRuntime.jsx("div", { id: rangeSummaryId, "aria-live": "polite", style: visuallyHidden, children: rangeSummary })] }))] }));
12474
12804
  };
12475
12805
  Calendar.displayName = 'Calendar';
12476
12806
 
@@ -13610,7 +13940,8 @@ DateInputBase.displayName = 'DateInputBase';
13610
13940
  * shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
13611
13941
  * />
13612
13942
  */
13613
- const DateInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
13943
+ const DateInput = React.forwardRef((props, ref) => {
13944
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
13614
13945
  const insideField = useInsideField();
13615
13946
  const insideControl = useInsideControl();
13616
13947
  const ownField = rendersOwnField('DateInput', {
@@ -13621,6 +13952,27 @@ const DateInput = React.forwardRef(({ label, labelSize, labelProps, horizontal,
13621
13952
  horizontal,
13622
13953
  fieldClassName,
13623
13954
  });
13955
+ // Inline mode renders a bare calendar with no input, so the Control's
13956
+ // icon-left container has nothing to anchor to, and it renders no
13957
+ // Control inside one or out. The left icon is checked as the caller
13958
+ // passed it: the default glyph is this component's own choice, not a
13959
+ // prop the caller set.
13960
+ const ownControl = rendersOwnControl('DateInput', {
13961
+ insideControl,
13962
+ inline: baseProps.inline,
13963
+ isLoading,
13964
+ iconLeft,
13965
+ iconLeftName: props.iconLeftName,
13966
+ iconLeftSize,
13967
+ iconRight,
13968
+ iconRightName,
13969
+ iconRightSize,
13970
+ hasIconsLeft,
13971
+ hasIconsRight,
13972
+ isExpanded,
13973
+ controlSize,
13974
+ controlClassName,
13975
+ });
13624
13976
  const { controlId, fieldLabelProps } = useAutoLabelId({
13625
13977
  label,
13626
13978
  id: baseProps.id,
@@ -13634,9 +13986,7 @@ const DateInput = React.forwardRef(({ label, labelSize, labelProps, horizontal,
13634
13986
  // The base hides its launcher while the Control it sits in is loading,
13635
13987
  // whether that is the one rendered below or an enclosing one.
13636
13988
  let content = (jsxRuntime.jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps }));
13637
- // Inline mode renders a bare calendar with no input, so the Control's
13638
- // icon-left container has nothing to anchor to. Skip the Control wrap.
13639
- if (!insideControl && !baseProps.inline) {
13989
+ if (ownControl) {
13640
13990
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
13641
13991
  }
13642
13992
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -14718,7 +15068,8 @@ TimeInputBase.displayName = 'TimeInputBase';
14718
15068
  * @example
14719
15069
  * <TimeInput label="Slot" hourFormat="12" incrementMinutes={15} />
14720
15070
  */
14721
- const TimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
15071
+ const TimeInput = React.forwardRef((props, ref) => {
15072
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
14722
15073
  const insideField = useInsideField();
14723
15074
  const insideControl = useInsideControl();
14724
15075
  const ownField = rendersOwnField('TimeInput', {
@@ -14729,6 +15080,27 @@ const TimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal,
14729
15080
  horizontal,
14730
15081
  fieldClassName,
14731
15082
  });
15083
+ // Inline mode renders a bare panel with no input, so the Control's
15084
+ // icon-left container has nothing to anchor to, and it renders no
15085
+ // Control inside one or out. The left icon is checked as the caller
15086
+ // passed it: the default glyph is this component's own choice, not a
15087
+ // prop the caller set.
15088
+ const ownControl = rendersOwnControl('TimeInput', {
15089
+ insideControl,
15090
+ inline: baseProps.inline,
15091
+ isLoading,
15092
+ iconLeft,
15093
+ iconLeftName: props.iconLeftName,
15094
+ iconLeftSize,
15095
+ iconRight,
15096
+ iconRightName,
15097
+ iconRightSize,
15098
+ hasIconsLeft,
15099
+ hasIconsRight,
15100
+ isExpanded,
15101
+ controlSize,
15102
+ controlClassName,
15103
+ });
14732
15104
  const { controlId, fieldLabelProps } = useAutoLabelId({
14733
15105
  label,
14734
15106
  id: baseProps.id,
@@ -14742,9 +15114,7 @@ const TimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal,
14742
15114
  // The base hides its launcher while the Control it sits in is loading,
14743
15115
  // whether that is the one rendered below or an enclosing one.
14744
15116
  let content = (jsxRuntime.jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps }));
14745
- // Inline mode renders a bare panel with no input, so the Control's
14746
- // icon-left container has nothing to anchor to. Skip the Control wrap.
14747
- if (!insideControl && !baseProps.inline) {
15117
+ if (ownControl) {
14748
15118
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
14749
15119
  }
14750
15120
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
@@ -15039,7 +15409,8 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
15039
15409
  * shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
15040
15410
  * />
15041
15411
  */
15042
- const DateTimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
15412
+ const DateTimeInput = React.forwardRef((props, ref) => {
15413
+ const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
15043
15414
  const insideField = useInsideField();
15044
15415
  const insideControl = useInsideControl();
15045
15416
  const ownField = rendersOwnField('DateTimeInput', {
@@ -15050,6 +15421,27 @@ const DateTimeInput = React.forwardRef(({ label, labelSize, labelProps, horizont
15050
15421
  horizontal,
15051
15422
  fieldClassName,
15052
15423
  });
15424
+ // Inline mode renders a bare picker with no input, so the Control's
15425
+ // icon-left container has nothing to anchor to, and it renders no
15426
+ // Control inside one or out. The left icon is checked as the caller
15427
+ // passed it: the default glyph is this component's own choice, not a
15428
+ // prop the caller set.
15429
+ const ownControl = rendersOwnControl('DateTimeInput', {
15430
+ insideControl,
15431
+ inline: baseProps.inline,
15432
+ isLoading,
15433
+ iconLeft,
15434
+ iconLeftName: props.iconLeftName,
15435
+ iconLeftSize,
15436
+ iconRight,
15437
+ iconRightName,
15438
+ iconRightSize,
15439
+ hasIconsLeft,
15440
+ hasIconsRight,
15441
+ isExpanded,
15442
+ controlSize,
15443
+ controlClassName,
15444
+ });
15053
15445
  const { controlId, fieldLabelProps } = useAutoLabelId({
15054
15446
  label,
15055
15447
  id: baseProps.id,
@@ -15063,9 +15455,7 @@ const DateTimeInput = React.forwardRef(({ label, labelSize, labelProps, horizont
15063
15455
  // The base hides its launcher while the Control it sits in is loading,
15064
15456
  // whether that is the one rendered below or an enclosing one.
15065
15457
  let content = (jsxRuntime.jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps }));
15066
- // Inline mode renders a bare picker with no input, so the Control's
15067
- // icon-left container has nothing to anchor to. Skip the Control wrap.
15068
- if (!insideControl && !baseProps.inline) {
15458
+ if (ownControl) {
15069
15459
  content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
15070
15460
  }
15071
15461
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;