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