@scalewing/react 1.39.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +255 -5
  3. package/dist/calendar-labels.d.ts +2 -0
  4. package/dist/calendar-labels.js +7 -0
  5. package/dist/calendar-month.d.ts +6 -0
  6. package/dist/calendar-month.js +15 -0
  7. package/dist/components/BarChart.d.ts +20 -4
  8. package/dist/components/BarChart.js +13 -6
  9. package/dist/components/ButtonGroup.d.ts +8 -1
  10. package/dist/components/ButtonGroup.js +11 -2
  11. package/dist/components/DateField.d.ts +14 -2
  12. package/dist/components/DateField.js +14 -11
  13. package/dist/components/DateRangeButton.d.ts +65 -0
  14. package/dist/components/DateRangeButton.js +54 -0
  15. package/dist/components/DenominationGrid.d.ts +3 -2
  16. package/dist/components/DenominationGrid.js +21 -3
  17. package/dist/components/Dialog.d.ts +29 -13
  18. package/dist/components/Dialog.js +22 -20
  19. package/dist/components/Field.d.ts +13 -2
  20. package/dist/components/Field.js +14 -38
  21. package/dist/components/FilterChips.d.ts +7 -37
  22. package/dist/components/FilterChips.js +32 -24
  23. package/dist/components/ListGroup.d.ts +33 -0
  24. package/dist/components/ListGroup.js +19 -0
  25. package/dist/components/ListRow.d.ts +60 -0
  26. package/dist/components/ListRow.js +60 -0
  27. package/dist/components/NavCount.d.ts +21 -0
  28. package/dist/components/NavCount.js +23 -0
  29. package/dist/components/SectionNav.js +1 -8
  30. package/dist/components/SegmentedControl.d.ts +17 -1
  31. package/dist/components/SegmentedControl.js +11 -3
  32. package/dist/components/StatTile.d.ts +11 -4
  33. package/dist/components/StatTile.js +11 -4
  34. package/dist/components/Switch.d.ts +16 -1
  35. package/dist/components/Switch.js +17 -6
  36. package/dist/components/action-bar/ActionBarSummary.d.ts +10 -2
  37. package/dist/components/action-bar/ActionBarSummary.js +11 -2
  38. package/dist/components/{date-field → calendar}/CalendarGrid.d.ts +17 -2
  39. package/dist/components/{date-field → calendar}/CalendarGrid.js +11 -3
  40. package/dist/components/calendar/CalendarPopover.d.ts +25 -0
  41. package/dist/components/calendar/CalendarPopover.js +22 -0
  42. package/dist/components/calendar/MonthStep.d.ts +16 -0
  43. package/dist/components/calendar/MonthStep.js +17 -0
  44. package/dist/components/calendar/use-grid-focus.d.ts +18 -0
  45. package/dist/components/calendar/use-grid-focus.js +35 -0
  46. package/dist/components/calendar/wrap-tab.d.ts +7 -0
  47. package/dist/components/calendar/wrap-tab.js +29 -0
  48. package/dist/components/date-field/CalendarDialog.js +9 -67
  49. package/dist/components/date-field/CalendarEntryButton.d.ts +24 -0
  50. package/dist/components/date-field/CalendarEntryButton.js +16 -0
  51. package/dist/components/date-field/CalendarHeader.js +2 -11
  52. package/dist/components/date-field/TypedEntry.d.ts +26 -0
  53. package/dist/components/date-field/TypedEntry.js +13 -0
  54. package/dist/components/date-range/DateRangePopover.d.ts +7 -0
  55. package/dist/components/date-range/DateRangePopover.js +17 -0
  56. package/dist/components/date-range/DateRangeSheet.d.ts +7 -0
  57. package/dist/components/date-range/DateRangeSheet.js +15 -0
  58. package/dist/components/date-range/RangeMonths.d.ts +28 -0
  59. package/dist/components/date-range/RangeMonths.js +50 -0
  60. package/dist/components/date-range/RangePresets.d.ts +16 -0
  61. package/dist/components/date-range/RangePresets.js +10 -0
  62. package/dist/components/date-range/RangeSummary.d.ts +15 -0
  63. package/dist/components/date-range/RangeSummary.js +11 -0
  64. package/dist/components/date-range/range-panel-props.d.ts +31 -0
  65. package/dist/components/date-range/range-panel-props.js +1 -0
  66. package/dist/components/date-range/use-range-popup.d.ts +21 -0
  67. package/dist/components/date-range/use-range-popup.js +53 -0
  68. package/dist/components/denomination-grid/CountCell.d.ts +16 -0
  69. package/dist/components/denomination-grid/CountCell.js +14 -0
  70. package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -1
  71. package/dist/components/denomination-grid/DenominationStrip.js +2 -2
  72. package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
  73. package/dist/components/denomination-grid/EntryCell.js +2 -2
  74. package/dist/components/denomination-grid/StripRow.js +1 -10
  75. package/dist/components/denomination-grid/TransposedStrip.d.ts +17 -0
  76. package/dist/components/denomination-grid/TransposedStrip.js +43 -0
  77. package/dist/components/denomination-grid/types.d.ts +31 -0
  78. package/dist/components/denomination-grid/types.js +7 -1
  79. package/dist/components/denomination-grid/validate-options.d.ts +7 -0
  80. package/dist/components/denomination-grid/validate-options.js +27 -0
  81. package/dist/components/dialog/DialogFooter.d.ts +6 -5
  82. package/dist/components/dialog/DialogFooter.js +6 -6
  83. package/dist/components/dialog/validate.d.ts +24 -0
  84. package/dist/components/dialog/validate.js +23 -0
  85. package/dist/components/field/field-checks.d.ts +34 -0
  86. package/dist/components/field/field-checks.js +42 -0
  87. package/dist/components/filter-chips/FilterChip.d.ts +16 -0
  88. package/dist/components/filter-chips/FilterChip.js +20 -0
  89. package/dist/components/filter-chips/options.d.ts +75 -0
  90. package/dist/components/filter-chips/options.js +11 -0
  91. package/dist/components/filter-chips/validate.d.ts +9 -0
  92. package/dist/components/filter-chips/validate.js +48 -0
  93. package/dist/components/link-press.d.ts +7 -0
  94. package/dist/components/link-press.js +13 -0
  95. package/dist/components/list/ListRowAccessoryMark.d.ts +5 -0
  96. package/dist/components/list/ListRowAccessoryMark.js +7 -0
  97. package/dist/components/list/ListRowContent.d.ts +22 -0
  98. package/dist/components/list/ListRowContent.js +11 -0
  99. package/dist/components/list/list-row-container.d.ts +5 -0
  100. package/dist/components/list/list-row-container.js +6 -0
  101. package/dist/components/use-media-query.d.ts +5 -0
  102. package/dist/components/use-media-query.js +18 -0
  103. package/dist/css/chart-tones.d.ts +2 -0
  104. package/dist/css/chart-tones.js +5 -1
  105. package/dist/css/chevron.d.ts +6 -0
  106. package/dist/css/chevron.js +14 -0
  107. package/dist/css/css-bar-chart.d.ts +17 -2
  108. package/dist/css/css-bar-chart.js +38 -7
  109. package/dist/css/css-button-group-attached.d.ts +3 -0
  110. package/dist/css/css-button-group-attached.js +26 -5
  111. package/dist/css/css-button-group-track.js +1 -1
  112. package/dist/css/css-canvas-grouped.d.ts +17 -0
  113. package/dist/css/css-canvas-grouped.js +40 -0
  114. package/dist/css/css-checkbox.js +2 -2
  115. package/dist/css/css-components.js +15 -0
  116. package/dist/css/css-count-pill.d.ts +2 -0
  117. package/dist/css/css-count-pill.js +54 -0
  118. package/dist/css/css-date-field-calendar-entry.d.ts +11 -0
  119. package/dist/css/css-date-field-calendar-entry.js +50 -0
  120. package/dist/css/css-date-field.js +7 -2
  121. package/dist/css/css-date-range-presets.d.ts +2 -0
  122. package/dist/css/css-date-range-presets.js +109 -0
  123. package/dist/css/css-date-range.d.ts +2 -0
  124. package/dist/css/css-date-range.js +197 -0
  125. package/dist/css/css-denomination-grid.js +4 -0
  126. package/dist/css/css-denomination-transposed.d.ts +4 -0
  127. package/dist/css/css-denomination-transposed.js +178 -0
  128. package/dist/css/css-dialog-canvas.d.ts +3 -0
  129. package/dist/css/css-dialog-canvas.js +20 -0
  130. package/dist/css/css-dialog-frame.js +19 -2
  131. package/dist/css/css-dialog-grabber.d.ts +4 -0
  132. package/dist/css/css-dialog-grabber.js +56 -0
  133. package/dist/css/css-dialog.js +3 -2
  134. package/dist/css/css-document.js +3 -0
  135. package/dist/css/css-field-width.d.ts +12 -0
  136. package/dist/css/css-field-width.js +49 -0
  137. package/dist/css/css-field.js +4 -0
  138. package/dist/css/css-filter-chips-layout.d.ts +2 -0
  139. package/dist/css/css-filter-chips-layout.js +42 -0
  140. package/dist/css/css-filter-chips.js +4 -0
  141. package/dist/css/css-list-row.d.ts +8 -0
  142. package/dist/css/css-list-row.js +209 -0
  143. package/dist/css/css-list.d.ts +3 -0
  144. package/dist/css/css-list.js +85 -0
  145. package/dist/css/css-scroll-region.d.ts +18 -0
  146. package/dist/css/css-scroll-region.js +41 -0
  147. package/dist/css/css-section-nav-filled.js +2 -25
  148. package/dist/css/css-segmented-sizing.d.ts +13 -0
  149. package/dist/css/css-segmented-sizing.js +66 -0
  150. package/dist/css/css-segmented.js +4 -0
  151. package/dist/css/css-stat-tile.d.ts +7 -0
  152. package/dist/css/css-stat-tile.js +16 -0
  153. package/dist/css/css-steps.js +2 -2
  154. package/dist/css/css-switch.d.ts +6 -0
  155. package/dist/css/css-switch.js +39 -0
  156. package/dist/css/css-table-bounded-edges.js +4 -27
  157. package/dist/css/range-band.d.ts +2 -0
  158. package/dist/css/range-band.js +7 -0
  159. package/dist/css/stacking.d.ts +1 -0
  160. package/dist/css/stacking.js +6 -3
  161. package/dist/css/stylesheet.js +10 -0
  162. package/dist/date-field-labels.d.ts +5 -0
  163. package/dist/date-field-labels.js +1 -0
  164. package/dist/date-range-draft.d.ts +26 -0
  165. package/dist/date-range-draft.js +36 -0
  166. package/dist/date-range-input.d.ts +26 -0
  167. package/dist/date-range-input.js +58 -0
  168. package/dist/date-range-labels.d.ts +22 -0
  169. package/dist/date-range-labels.js +26 -0
  170. package/dist/date-range-view.d.ts +12 -0
  171. package/dist/date-range-view.js +29 -0
  172. package/dist/date-range-words.d.ts +11 -0
  173. package/dist/date-range-words.js +40 -0
  174. package/dist/index.d.ts +16 -9
  175. package/dist/index.js +14 -7
  176. package/dist/list-options.d.ts +71 -0
  177. package/dist/list-options.js +84 -0
  178. package/dist/styles.css +863 -41
  179. package/dist/theme/ThemeProvider.d.ts +11 -1
  180. package/dist/theme/ThemeProvider.js +4 -2
  181. package/package.json +1 -1
@@ -5,18 +5,26 @@ export type ActionBarSummaryTone = (typeof actionBarSummaryTones)[number];
5
5
  export type ActionBarSummary = {
6
6
  /** What the figure is, such as "Counted". */
7
7
  label: string;
8
- /** The figure, formatted by the consumer, such as "$33,790.00". */
8
+ /**
9
+ * The figure, formatted by the consumer, such as "$33,790.00". An empty
10
+ * string (or only spaces) means there is no figure yet, such as before
11
+ * anything is counted: the summary line is left out and the live region
12
+ * stays, so the first figure is announced when it arrives.
13
+ */
9
14
  value: string;
10
15
  /** A short line after the figure, such as "short $4,000.00". */
11
16
  detail?: string;
12
17
  /** Colors the value only; the words say why. Defaults to `default`. */
13
18
  tone?: ActionBarSummaryTone;
14
19
  };
20
+ /** Whether the summary has a figure to show; an empty value has none yet. */
21
+ export declare function hasActionBarSummaryFigure(summary: ActionBarSummary): boolean;
15
22
  export declare function assertActionBarSummary(summary: ActionBarSummary): void;
16
23
  /**
17
24
  * The bar's figure: its label, the value in the heading variant at tabular
18
25
  * figures, and its detail, on one baseline, wrapping as one phrase.
26
+ * Nothing while the value is empty.
19
27
  */
20
28
  export declare function ActionBarSummaryLine({ summary, }: {
21
29
  summary: ActionBarSummary;
22
- }): import("react").JSX.Element;
30
+ }): import("react").JSX.Element | null;
@@ -4,9 +4,15 @@ export const actionBarSummaryTones = ['default', 'danger', 'success'];
4
4
  function isBlank(text) {
5
5
  return typeof text !== 'string' || !text.trim();
6
6
  }
7
+ /** Whether the summary has a figure to show; an empty value has none yet. */
8
+ export function hasActionBarSummaryFigure(summary) {
9
+ return !isBlank(summary.value);
10
+ }
7
11
  export function assertActionBarSummary(summary) {
8
- if (isBlank(summary.label) || isBlank(summary.value))
9
- throw new RangeError('ActionBar summary needs a label and a value');
12
+ if (isBlank(summary.label))
13
+ throw new RangeError('ActionBar summary needs a label');
14
+ if (typeof summary.value !== 'string')
15
+ throw new RangeError('ActionBar summary value must be a string');
10
16
  if (summary.detail !== undefined && isBlank(summary.detail))
11
17
  throw new RangeError('ActionBar summary detail must not be empty');
12
18
  if (summary.tone !== undefined &&
@@ -16,8 +22,11 @@ export function assertActionBarSummary(summary) {
16
22
  /**
17
23
  * The bar's figure: its label, the value in the heading variant at tabular
18
24
  * figures, and its detail, on one baseline, wrapping as one phrase.
25
+ * Nothing while the value is empty.
19
26
  */
20
27
  export function ActionBarSummaryLine({ summary, }) {
28
+ if (!hasActionBarSummaryFigure(summary))
29
+ return null;
21
30
  const tone = summary.tone ?? 'default';
22
31
  return (_jsxs("span", { className: cx('sw-action-bar-summary', tone !== 'default' && `sw-action-bar-summary-${tone}`), children: [_jsx("span", { className: "sw-action-bar-summary-label", children: summary.label }), ' ', _jsx("span", { className: "sw-action-bar-summary-value sw-text-heading sw-tabular", children: summary.value }), summary.detail ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-action-bar-summary-detail", children: summary.detail })] })) : null] }));
23
32
  }
@@ -13,7 +13,22 @@ export type CalendarGridProps = {
13
13
  onSelect: (value: string) => void;
14
14
  /** Days to tint as a span, such as the period a page shows. */
15
15
  range?: DateOnlyRange;
16
- selected: string;
16
+ /**
17
+ * The selected day; `''` for none. A range instead selects every day in
18
+ * it (the grid is then multiselectable) and tints it as `range` does.
19
+ */
20
+ selected: string | DateOnlyRange;
21
+ /**
22
+ * The last day a press may pick when it is earlier than `max`, such as a
23
+ * range's longest end. Later days are `aria-disabled`; keys still reach
24
+ * them up to `max`, for orientation.
25
+ */
26
+ latest?: string;
27
+ /**
28
+ * Leave the neighbouring months' days blank, so months shown side by
29
+ * side hold each day once. By default they are shown, muted.
30
+ */
31
+ blankOutside?: boolean;
17
32
  titleId: string;
18
33
  today: string;
19
34
  weekStartsOn: WeekStart;
@@ -23,4 +38,4 @@ export type CalendarGridProps = {
23
38
  * `[min, max]` stay focusable for orientation but are `aria-disabled`;
24
39
  * padding past the four-digit years is blank and inert.
25
40
  */
26
- export declare function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, range, selected, titleId, today, weekStartsOn, }: CalendarGridProps): import("react").JSX.Element;
41
+ export declare function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, range, selected, latest, blankOutside, titleId, today, weekStartsOn, }: CalendarGridProps): import("react").JSX.Element;
@@ -12,14 +12,22 @@ function rangeClassNames(place) {
12
12
  (place === 'end' || place === 'only') && 'sw-date-field-day-range-end',
13
13
  ];
14
14
  }
15
+ function isSelectedDay(value, selected) {
16
+ return typeof selected === 'string'
17
+ ? value === selected
18
+ : dayInRange(value, selected) !== null;
19
+ }
15
20
  /**
16
21
  * The month as a WAI-ARIA grid with one roving tab stop. Days outside
17
22
  * `[min, max]` stay focusable for orientation but are `aria-disabled`;
18
23
  * padding past the four-digit years is blank and inert.
19
24
  */
20
- export function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, range, selected, titleId, today, weekStartsOn, }) {
25
+ export function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, range, selected, latest, blankOutside = false, titleId, today, weekStartsOn, }) {
26
+ const pickMax = latest !== undefined && (max === undefined || latest < max) ? latest : max;
27
+ const tint = typeof selected === 'string' ? range : selected;
28
+ const unavailable = (value) => isOutsideDateRange(value, min, pickMax);
21
29
  function choose(value) {
22
- if (!isOutsideDateRange(value, min, max))
30
+ if (!unavailable(value))
23
31
  onSelect(value);
24
32
  }
25
33
  function onKeyDown(event) {
@@ -35,7 +43,7 @@ export function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate,
35
43
  event.preventDefault();
36
44
  onMoveFocus(target);
37
45
  }
38
- return (_jsxs("table", { "aria-labelledby": titleId, className: "sw-date-field-grid", onKeyDown: onKeyDown, role: "grid", children: [_jsx("thead", { children: _jsx("tr", { children: weekdayLabels(locale, weekStartsOn).map((weekday) => (_jsx("th", { abbr: weekday.long, scope: "col", children: weekday.short }, weekday.long))) }) }), _jsx("tbody", { children: monthGrid(month, weekStartsOn).map((row, rowIndex) => (_jsx("tr", { children: row.map((cell, column) => cell === null ? (_jsx("td", { "aria-disabled": "true", className: "sw-date-field-day sw-date-field-day-outside", role: "gridcell" }, `blank-${column}`)) : (_jsx("td", { "aria-current": cell.value === today ? 'date' : undefined, "aria-disabled": isOutsideDateRange(cell.value, min, max) || undefined, "aria-label": formatDateLabel(cell.value, locale), "aria-selected": cell.value === selected, className: cx('sw-date-field-day', !cell.inMonth && 'sw-date-field-day-outside', ...rangeClassNames(dayInRange(cell.value, range))), "data-date": cell.value, onClick: () => choose(cell.value), onFocus: () => {
46
+ return (_jsxs("table", { "aria-labelledby": titleId, "aria-multiselectable": typeof selected === 'string' ? undefined : true, className: "sw-date-field-grid", onKeyDown: onKeyDown, role: "grid", children: [_jsx("thead", { children: _jsx("tr", { children: weekdayLabels(locale, weekStartsOn).map((weekday) => (_jsx("th", { abbr: weekday.long, scope: "col", children: weekday.short }, weekday.long))) }) }), _jsx("tbody", { children: monthGrid(month, weekStartsOn).map((row, rowIndex) => (_jsx("tr", { children: row.map((cell, column) => cell === null || (blankOutside && !cell.inMonth) ? (_jsx("td", { "aria-disabled": "true", className: "sw-date-field-day sw-date-field-day-outside", role: "gridcell" }, `blank-${column}`)) : (_jsx("td", { "aria-current": cell.value === today ? 'date' : undefined, "aria-disabled": unavailable(cell.value) || undefined, "aria-label": formatDateLabel(cell.value, locale), "aria-selected": isSelectedDay(cell.value, selected), className: cx('sw-date-field-day', !cell.inMonth && 'sw-date-field-day-outside', ...rangeClassNames(dayInRange(cell.value, tint))), "data-date": cell.value, onClick: () => choose(cell.value), onFocus: () => {
39
47
  // A press on a neighboring month's day selects it; moving
40
48
  // the grid first would pull the day out from under it.
41
49
  if (cell.inMonth && cell.value !== focusDate)
@@ -0,0 +1,25 @@
1
+ import { type ReactNode, type RefObject } from 'react';
2
+ import { type GridFocus } from './use-grid-focus.js';
3
+ export type CalendarPopoverProps = {
4
+ /** The element the popover sits beside, such as the field or button. */
5
+ anchorRef: RefObject<HTMLElement | null>;
6
+ /** The button that toggles it; a press there is not a press outside. */
7
+ buttonRef: RefObject<HTMLElement | null>;
8
+ popoverRef: RefObject<HTMLDivElement | null>;
9
+ id: string;
10
+ /** Names the dialog by an element, or by `aria-label`. */
11
+ 'aria-labelledby'?: string;
12
+ 'aria-label'?: string;
13
+ /** `true` returns focus to the button: Escape does, a press outside does not. */
14
+ onClose: (restoreFocus: boolean) => void;
15
+ /** Follows focus in the grid when the popover itself holds it. */
16
+ focusHandlers?: GridFocus['focusHandlers'];
17
+ className?: string;
18
+ children: ReactNode;
19
+ };
20
+ /**
21
+ * A calendar's popover: a modal dialog on the top layer beside its anchor,
22
+ * as the WAI-ARIA date picker dialog pattern asks. Escape closes it and
23
+ * returns focus; Tab stays inside; a press outside closes it.
24
+ */
25
+ export declare function CalendarPopover({ anchorRef, buttonRef, popoverRef, id, 'aria-labelledby': labelledBy, 'aria-label': label, onClose, focusHandlers, className, children, }: CalendarPopoverProps): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ import { useAnchoredPopover } from '../use-anchored-popover.js';
4
+ import { wrapTab } from './wrap-tab.js';
5
+ /**
6
+ * A calendar's popover: a modal dialog on the top layer beside its anchor,
7
+ * as the WAI-ARIA date picker dialog pattern asks. Escape closes it and
8
+ * returns focus; Tab stays inside; a press outside closes it.
9
+ */
10
+ export function CalendarPopover({ anchorRef, buttonRef, popoverRef, id, 'aria-labelledby': labelledBy, 'aria-label': label, onClose, focusHandlers, className, children, }) {
11
+ useAnchoredPopover(popoverRef, anchorRef, buttonRef, () => onClose(false));
12
+ function onKeyDown(event) {
13
+ if (event.key === 'Escape') {
14
+ event.preventDefault();
15
+ event.stopPropagation();
16
+ onClose(true);
17
+ }
18
+ else if (event.key === 'Tab')
19
+ wrapTab(event);
20
+ }
21
+ return (_jsx("div", { ...focusHandlers, "aria-label": label, "aria-labelledby": labelledBy, "aria-modal": "true", className: cx('sw-date-field-calendar', className), id: id, onKeyDown: onKeyDown, ref: popoverRef, role: "dialog", children: children }));
22
+ }
@@ -0,0 +1,16 @@
1
+ import { type CalendarMonth } from '../../calendar-month.js';
2
+ export type MonthStepProps = {
3
+ /** The month the step leaves from: the first or the last month shown. */
4
+ month: CalendarMonth;
5
+ delta: 1 | -1;
6
+ label: string;
7
+ min?: string;
8
+ max?: string;
9
+ onMonthChange: (month: CalendarMonth) => void;
10
+ };
11
+ /**
12
+ * A calendar's previous or next month step. Past the four-digit years or
13
+ * outside `[min, max]` it is `aria-disabled`, so it keeps focus and its
14
+ * press does nothing.
15
+ */
16
+ export declare function MonthStep({ delta, label, max, min, month, onMonthChange, }: MonthStepProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { monthInRange, shiftMonth, } from '../../calendar-month.js';
3
+ import { tabStop } from '../../tab-stop.js';
4
+ import { DateFieldGlyph } from '../date-field/DateFieldGlyph.js';
5
+ /**
6
+ * A calendar's previous or next month step. Past the four-digit years or
7
+ * outside `[min, max]` it is `aria-disabled`, so it keeps focus and its
8
+ * press does nothing.
9
+ */
10
+ export function MonthStep({ delta, label, max, min, month, onMonthChange, }) {
11
+ const target = shiftMonth(month, delta);
12
+ const unavailable = target.year < 1 || target.year > 9999 || !monthInRange(target, min, max);
13
+ return (_jsx("button", { "aria-disabled": unavailable || undefined, "aria-label": label, className: "sw-date-field-step", onClick: () => {
14
+ if (!unavailable)
15
+ onMonthChange(target);
16
+ }, tabIndex: tabStop, type: "button", children: _jsx(DateFieldGlyph, { name: delta < 0 ? 'previous' : 'next' }) }));
17
+ }
@@ -0,0 +1,18 @@
1
+ import { type FocusEvent, type RefObject } from 'react';
2
+ export type GridFocus = {
3
+ /** The next focus date takes DOM focus, as after a grid key. */
4
+ requestFocus: () => void;
5
+ /** Spread on the element holding the grids, to follow where focus is. */
6
+ focusHandlers: {
7
+ onBlur: () => void;
8
+ onFocus: (event: FocusEvent<HTMLElement>) => void;
9
+ };
10
+ };
11
+ /**
12
+ * Moves DOM focus to a calendar's focus date when asked (on mount when
13
+ * `focusOnMount`, and after a grid key) and while a day already holds
14
+ * focus, so a new value or new bounds never strand focus on a removed day.
15
+ * Header controls and presses change the date without taking focus from
16
+ * where it is. Shared by DateField's calendar and DateRangeButton.
17
+ */
18
+ export declare function useGridFocus(containerRef: RefObject<HTMLElement | null>, focusDate: string, focusOnMount: boolean): GridFocus;
@@ -0,0 +1,35 @@
1
+ import { useLayoutEffect, useRef, } from 'react';
2
+ /**
3
+ * Moves DOM focus to a calendar's focus date when asked (on mount when
4
+ * `focusOnMount`, and after a grid key) and while a day already holds
5
+ * focus, so a new value or new bounds never strand focus on a removed day.
6
+ * Header controls and presses change the date without taking focus from
7
+ * where it is. Shared by DateField's calendar and DateRangeButton.
8
+ */
9
+ export function useGridFocus(containerRef, focusDate, focusOnMount) {
10
+ const focusGrid = useRef(focusOnMount);
11
+ // Whether a day holds DOM focus. Removing that day fires no blur, so this
12
+ // stays true when a new value or bounds move the grid out from under it.
13
+ const gridFocused = useRef(false);
14
+ useLayoutEffect(() => {
15
+ if (!focusGrid.current && !gridFocused.current)
16
+ return;
17
+ focusGrid.current = false;
18
+ containerRef.current
19
+ ?.querySelector(`[role="grid"] [data-date="${focusDate}"]`)
20
+ ?.focus({ preventScroll: true });
21
+ }, [containerRef, focusDate]);
22
+ return {
23
+ requestFocus: () => {
24
+ focusGrid.current = true;
25
+ },
26
+ focusHandlers: {
27
+ onBlur: () => {
28
+ gridFocused.current = false;
29
+ },
30
+ onFocus: (event) => {
31
+ gridFocused.current = event.target.closest('[role="grid"]') !== null;
32
+ },
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,7 @@
1
+ import { type KeyboardEvent } from 'react';
2
+ /**
3
+ * Keeps Tab and Shift+Tab inside a calendar popover, as a modal dialog
4
+ * does: from the last stop to the first, and back. Shared by DateField's
5
+ * calendar and DateRangeButton's popover.
6
+ */
7
+ export declare function wrapTab(event: KeyboardEvent<HTMLElement>): void;
@@ -0,0 +1,29 @@
1
+ /** A calendar popover's tab stops: enabled buttons and a grid's roving day. */
2
+ function isTabStop(node) {
3
+ if (node instanceof HTMLButtonElement)
4
+ return !node.disabled;
5
+ return node.getAttribute('tabindex') === '0';
6
+ }
7
+ /**
8
+ * Keeps Tab and Shift+Tab inside a calendar popover, as a modal dialog
9
+ * does: from the last stop to the first, and back. Shared by DateField's
10
+ * calendar and DateRangeButton's popover.
11
+ */
12
+ export function wrapTab(event) {
13
+ // One universal selector keeps document order in every DOM implementation.
14
+ const items = [
15
+ ...event.currentTarget.querySelectorAll('*'),
16
+ ].filter(isTabStop);
17
+ const first = items[0];
18
+ const last = items[items.length - 1];
19
+ if (!first || !last)
20
+ return;
21
+ if (event.shiftKey && document.activeElement === first) {
22
+ event.preventDefault();
23
+ last.focus();
24
+ }
25
+ else if (!event.shiftKey && document.activeElement === last) {
26
+ event.preventDefault();
27
+ first.focus();
28
+ }
29
+ }
@@ -1,43 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useId, useLayoutEffect, useRef, useState, } from 'react';
3
- import { monthOf, } from '../../calendar-month.js';
4
- import { clampDateOnly, daysInMonth, formatDateOnly, isOutsideDateRange, parseDateOnly, } from '../../date-only.js';
2
+ import { useId, useRef, useState } from 'react';
3
+ import { monthOf, sameDayIn } from '../../calendar-month.js';
4
+ import { clampDateOnly, isOutsideDateRange, } from '../../date-only.js';
5
5
  import { Button } from '../Button.js';
6
- import { CalendarGrid } from './CalendarGrid.js';
6
+ import { CalendarGrid } from '../calendar/CalendarGrid.js';
7
7
  import { CalendarHeader } from './CalendarHeader.js';
8
- import { useAnchoredPopover } from '../use-anchored-popover.js';
9
- /** The dialog's tab stops: enabled buttons and the grid's roving day. */
10
- function isTabStop(node) {
11
- if (node instanceof HTMLButtonElement)
12
- return !node.disabled;
13
- return node.getAttribute('tabindex') === '0';
14
- }
15
- /** Keeps Tab and Shift+Tab inside the dialog, as a modal dialog does. */
16
- function wrapTab(event) {
17
- // One universal selector keeps document order in every DOM implementation.
18
- const items = [
19
- ...event.currentTarget.querySelectorAll('*'),
20
- ].filter(isTabStop);
21
- const first = items[0];
22
- const last = items[items.length - 1];
23
- if (!first || !last)
24
- return;
25
- if (event.shiftKey && document.activeElement === first) {
26
- event.preventDefault();
27
- last.focus();
28
- }
29
- else if (!event.shiftKey && document.activeElement === last) {
30
- event.preventDefault();
31
- first.focus();
32
- }
33
- }
34
- function sameDayIn(month, focused) {
35
- const day = parseDateOnly(focused)?.day ?? 1;
36
- return formatDateOnly({
37
- ...month,
38
- day: Math.min(day, daysInMonth(month.year, month.month)),
39
- });
40
- }
8
+ import { CalendarPopover } from '../calendar/CalendarPopover.js';
9
+ import { useGridFocus } from '../calendar/use-grid-focus.js';
41
10
  /**
42
11
  * The calendar popover: a labelled modal dialog anchored under the field,
43
12
  * following the WAI-ARIA date picker dialog pattern. It opens on the
@@ -57,37 +26,10 @@ export function CalendarDialog({ anchorRef, buttonRef, id, labelId, labels, loca
57
26
  // Clamped on every render, so new bounds while open move it inside.
58
27
  const focusDate = clampDateOnly(rawFocusDate, min, max);
59
28
  const month = monthOf(focusDate);
60
- // Opening and grid keys move DOM focus to the focus date; header controls
61
- // and presses change the date without taking focus from where it is.
62
- const focusGrid = useRef(true);
63
- useAnchoredPopover(dialogRef, anchorRef, buttonRef, () => onClose(false));
64
- // Whether a day holds DOM focus. Removing that day fires no blur, so this
65
- // stays true when a new value or bounds move the grid out from under it.
66
- const gridFocused = useRef(false);
67
- useLayoutEffect(() => {
68
- if (!focusGrid.current && !gridFocused.current)
69
- return;
70
- focusGrid.current = false;
71
- dialogRef.current
72
- ?.querySelector(`[role="grid"] [data-date="${focusDate}"]`)
73
- ?.focus({ preventScroll: true });
74
- }, [focusDate]);
75
- function onKeyDown(event) {
76
- if (event.key === 'Escape') {
77
- event.preventDefault();
78
- event.stopPropagation();
79
- onClose(true);
80
- }
81
- else if (event.key === 'Tab')
82
- wrapTab(event);
83
- }
29
+ const { requestFocus, focusHandlers } = useGridFocus(dialogRef, focusDate, true);
84
30
  const todayUnavailable = isOutsideDateRange(today, min, max);
85
- return (_jsxs("div", { "aria-labelledby": labelId, "aria-modal": "true", className: "sw-date-field-calendar", id: id, onBlur: () => {
86
- gridFocused.current = false;
87
- }, onFocus: (event) => {
88
- gridFocused.current = event.target.closest('[role="grid"]') !== null;
89
- }, onKeyDown: onKeyDown, ref: dialogRef, role: "dialog", children: [_jsx(CalendarHeader, { labels: labels, locale: locale, max: max, min: min, month: month, onMonthChange: (next) => setFocusDate(sameDayIn(next, focusDate)), titleId: titleId, todayYear: monthOf(today).year }), _jsx(CalendarGrid, { focusDate: focusDate, locale: locale, max: max, min: min, month: month, onFocusDate: setFocusDate, onMoveFocus: (next) => {
90
- focusGrid.current = true;
31
+ return (_jsxs(CalendarPopover, { anchorRef: anchorRef, "aria-labelledby": labelId, buttonRef: buttonRef, focusHandlers: focusHandlers, id: id, onClose: onClose, popoverRef: dialogRef, children: [_jsx(CalendarHeader, { labels: labels, locale: locale, max: max, min: min, month: month, onMonthChange: (next) => setFocusDate(sameDayIn(next, focusDate)), titleId: titleId, todayYear: monthOf(today).year }), _jsx(CalendarGrid, { focusDate: focusDate, locale: locale, max: max, min: min, month: month, onFocusDate: setFocusDate, onMoveFocus: (next) => {
32
+ requestFocus();
91
33
  setFocusDate(next);
92
34
  }, onSelect: onSelect, range: range, selected: value, titleId: titleId, today: today, weekStartsOn: weekStartsOn }), _jsxs("div", { className: "sw-date-field-footer", children: [required ? null : (_jsx(Button, { disabled: value === '', onPress: () => onSelect(''), size: "sm", variant: "ghost", children: labels.clear })), _jsx(Button, { disabled: todayUnavailable, onPress: () => onSelect(today), size: "sm", variant: "ghost", children: labels.today })] })] }));
93
35
  }
@@ -0,0 +1,24 @@
1
+ import { type Ref } from 'react';
2
+ import { type CalendarPopup } from './use-calendar-popup.js';
3
+ export type CalendarEntryButtonProps = {
4
+ id: string;
5
+ /** The field's label element, which names the button with its words. */
6
+ labelId: string;
7
+ describedBy: string | undefined;
8
+ value: string;
9
+ locale: string;
10
+ /** What an empty field shows, such as "Choose a day". */
11
+ emptyText: string;
12
+ disabled: boolean;
13
+ invalid: boolean;
14
+ buttonRef: Ref<HTMLButtonElement>;
15
+ trigger: CalendarPopup['trigger'];
16
+ onPress: () => void;
17
+ };
18
+ /**
19
+ * DateField's calendar entry: a neutral Button showing the calendar glyph
20
+ * and the chosen day, or the words that ask for one, that opens the
21
+ * calendar. It is named by the field's label and its own words ("Hire date
22
+ * Choose a day", "Hire date Oct 6, 2026") and described as the field is.
23
+ */
24
+ export declare function CalendarEntryButton({ id, labelId, describedBy, value, locale, emptyText, disabled, invalid, buttonRef, trigger, onPress, }: CalendarEntryButtonProps): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ import { formatDateMedium } from '../../calendar-labels.js';
4
+ import { cx } from '../../class-names.js';
5
+ import { Button } from '../Button.js';
6
+ import { DateFieldGlyph } from './DateFieldGlyph.js';
7
+ /**
8
+ * DateField's calendar entry: a neutral Button showing the calendar glyph
9
+ * and the chosen day, or the words that ask for one, that opens the
10
+ * calendar. It is named by the field's label and its own words ("Hire date
11
+ * Choose a day", "Hire date Oct 6, 2026") and described as the field is.
12
+ */
13
+ export function CalendarEntryButton({ id, labelId, describedBy, value, locale, emptyText, disabled, invalid, buttonRef, trigger, onPress, }) {
14
+ const textId = useId();
15
+ return (_jsxs(Button, { ...trigger, "aria-describedby": describedBy, "aria-labelledby": `${labelId} ${textId}`, className: cx('sw-date-field-calendar-entry', invalid && 'sw-date-field-calendar-entry-invalid'), disabled: disabled, id: id, onPress: onPress, ref: buttonRef, variant: "neutral", children: [_jsx(DateFieldGlyph, { name: "calendar" }), _jsx("span", { className: "sw-date-field-calendar-entry-text", id: textId, children: value ? formatDateMedium(value, locale) : emptyText })] }));
16
+ }
@@ -1,18 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { monthNames, monthTitle } from '../../calendar-labels.js';
3
- import { monthInRange, monthsInRange, shiftMonth, } from '../../calendar-month.js';
3
+ import { monthsInRange } from '../../calendar-month.js';
4
4
  import { calendarYearRange, yearsIn } from '../../calendar-years.js';
5
5
  import { Select } from '../Select.js';
6
- import { DateFieldGlyph } from './DateFieldGlyph.js';
7
- import { tabStop } from '../../tab-stop.js';
8
- function MonthStep({ delta, label, max, min, month, onMonthChange, }) {
9
- const target = shiftMonth(month, delta);
10
- const unavailable = target.year < 1 || target.year > 9999 || !monthInRange(target, min, max);
11
- return (_jsx("button", { "aria-disabled": unavailable || undefined, "aria-label": label, className: "sw-date-field-step", onClick: () => {
12
- if (!unavailable)
13
- onMonthChange(target);
14
- }, tabIndex: tabStop, type: "button", children: _jsx(DateFieldGlyph, { name: delta < 0 ? 'previous' : 'next' }) }));
15
- }
6
+ import { MonthStep } from '../calendar/MonthStep.js';
16
7
  /**
17
8
  * Month stepping plus month and year selectors for jumping far, both cut to
18
9
  * `min` and `max`, over a visually hidden title that announces the shown
@@ -0,0 +1,26 @@
1
+ import { type Ref } from 'react';
2
+ import { type CalendarPopup } from './use-calendar-popup.js';
3
+ import { type DateEntry } from './use-date-entry.js';
4
+ export type TypedEntryProps = {
5
+ id: string;
6
+ /** The field's label element, which describes the calendar button. */
7
+ labelId: string;
8
+ describedBy: string | undefined;
9
+ entry: DateEntry;
10
+ placeholder: string;
11
+ /** The calendar button's name, `labels.chooseDate`. */
12
+ toggleName: string;
13
+ disabled: boolean;
14
+ required: boolean;
15
+ invalid: boolean;
16
+ controlRef: Ref<HTMLDivElement>;
17
+ inputRef: Ref<HTMLInputElement>;
18
+ buttonRef: Ref<HTMLButtonElement>;
19
+ trigger: CalendarPopup['trigger'];
20
+ onToggle: () => void;
21
+ };
22
+ /**
23
+ * DateField's typed entry: the text input in the locale's order, checked
24
+ * on blur and Enter, and the calendar button inside its end.
25
+ */
26
+ export declare function TypedEntry({ id, labelId, describedBy, entry, placeholder, toggleName, disabled, required, invalid, controlRef, inputRef, buttonRef, trigger, onToggle, }: TypedEntryProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { tabStop } from '../../tab-stop.js';
3
+ import { DateFieldGlyph } from './DateFieldGlyph.js';
4
+ /**
5
+ * DateField's typed entry: the text input in the locale's order, checked
6
+ * on blur and Enter, and the calendar button inside its end.
7
+ */
8
+ export function TypedEntry({ id, labelId, describedBy, entry, placeholder, toggleName, disabled, required, invalid, controlRef, inputRef, buttonRef, trigger, onToggle, }) {
9
+ return (_jsxs("div", { className: "sw-date-field-control", ref: controlRef, children: [_jsx("input", { "aria-describedby": describedBy, "aria-invalid": invalid, autoComplete: "off", className: "sw-date-field-input", disabled: disabled, id: id, onBlur: entry.check, onChange: (event) => entry.change(event.currentTarget.value), onKeyDown: (event) => {
10
+ if (event.key === 'Enter')
11
+ entry.check();
12
+ }, placeholder: placeholder, ref: inputRef, required: required, spellCheck: false, type: "text", value: entry.text }), _jsxs("button", { ...trigger, "aria-describedby": labelId, className: "sw-date-field-button", disabled: disabled, onClick: onToggle, ref: buttonRef, tabIndex: tabStop, type: "button", children: [_jsx("span", { className: "sw-sr-only", children: toggleName }), _jsx(DateFieldGlyph, { name: "calendar" })] })] }));
13
+ }
@@ -0,0 +1,7 @@
1
+ import { type RangePanelProps } from './range-panel-props.js';
2
+ /**
3
+ * DateRangeButton from `md` up: a popover beside the button with the
4
+ * presets, two months, the range being picked, Cancel and Apply. Escape
5
+ * and a press outside cancel.
6
+ */
7
+ export declare function DateRangePopover(props: RangePanelProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
3
+ import { Button } from '../Button.js';
4
+ import { CalendarPopover } from '../calendar/CalendarPopover.js';
5
+ import { RangeMonths } from './RangeMonths.js';
6
+ import { RangePresets } from './RangePresets.js';
7
+ import { RangeSummary } from './RangeSummary.js';
8
+ /**
9
+ * DateRangeButton from `md` up: a popover beside the button with the
10
+ * presets, two months, the range being picked, Cancel and Apply. Escape
11
+ * and a press outside cancel.
12
+ */
13
+ export function DateRangePopover(props) {
14
+ const popoverRef = useRef(null);
15
+ const { labels, presets } = props;
16
+ return (_jsxs(CalendarPopover, { anchorRef: props.buttonRef, "aria-label": props.label, buttonRef: props.buttonRef, className: "sw-date-range-popover", id: props.id, onClose: props.onCancel, popoverRef: popoverRef, children: [_jsxs("div", { className: "sw-date-range-body", children: [presets.length > 0 ? (_jsx(RangePresets, { label: labels.presets, layout: "list", onChoose: props.onChoosePreset, presets: presets, pressedId: props.pressedPresetId })) : null, _jsx(RangeMonths, { count: 2, draft: props.draft, focusOnMount: true, labels: labels, latest: props.latest, locale: props.locale, max: props.max, min: props.min, onPress: props.onPress, today: props.today, weekStartsOn: props.weekStartsOn })] }), _jsxs("div", { className: "sw-date-range-footer", children: [_jsx(RangeSummary, { count: props.count, hint: labels.hint, line: props.line }), _jsxs("div", { className: "sw-date-range-actions", children: [_jsx(Button, { onPress: () => props.onCancel(true), size: "sm", variant: "neutral", children: labels.cancel }), _jsx(Button, { onPress: props.onApply, size: "sm", children: labels.apply })] })] })] }));
17
+ }
@@ -0,0 +1,7 @@
1
+ import { type RangePanelProps } from './range-panel-props.js';
2
+ /**
3
+ * DateRangeButton below `md`: a bottom sheet titled by its label, the
4
+ * range being picked under the title, the presets in a row, one month,
5
+ * and the hint over Cancel and Apply. Closing it any other way cancels.
6
+ */
7
+ export declare function DateRangeSheet(props: RangePanelProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from '../Button.js';
3
+ import { Dialog } from '../Dialog.js';
4
+ import { RangeMonths } from './RangeMonths.js';
5
+ import { RangePresets } from './RangePresets.js';
6
+ import { RangeSummary } from './RangeSummary.js';
7
+ /**
8
+ * DateRangeButton below `md`: a bottom sheet titled by its label, the
9
+ * range being picked under the title, the presets in a row, one month,
10
+ * and the hint over Cancel and Apply. Closing it any other way cancels.
11
+ */
12
+ export function DateRangeSheet(props) {
13
+ const { labels, presets } = props;
14
+ return (_jsxs(Dialog, { className: "sw-date-range-sheet", closeLabel: labels.close, description: `${props.line} · ${props.count}`, footer: _jsxs("div", { className: "sw-date-range-sheet-footer", children: [_jsx("span", { className: "sw-date-range-hint", children: labels.hint }), _jsxs("div", { className: "sw-date-range-actions", children: [_jsx(Button, { onPress: () => props.onCancel(true), variant: "neutral", children: labels.cancel }), _jsx(Button, { onPress: props.onApply, children: labels.apply })] })] }), id: props.id, onClose: () => props.onCancel(true), open: true, sheetBelow: "md", title: props.label, children: [_jsx(RangeSummary, { count: props.count, line: props.line, visuallyHidden: true }), presets.length > 0 ? (_jsx(RangePresets, { label: labels.presets, layout: "row", onChoose: props.onChoosePreset, presets: presets, pressedId: props.pressedPresetId })) : null, _jsx(RangeMonths, { count: 1, draft: props.draft, focusOnMount: false, labels: labels, latest: props.latest, locale: props.locale, max: props.max, min: props.min, onPress: props.onPress, today: props.today, weekStartsOn: props.weekStartsOn })] }));
15
+ }
@@ -0,0 +1,28 @@
1
+ import { type WeekStart } from '../../calendar-month.js';
2
+ import { type RangeDraft } from '../../date-range-draft.js';
3
+ import { type RangeMonthCount } from '../../date-range-view.js';
4
+ export type RangeMonthsProps = {
5
+ count: RangeMonthCount;
6
+ draft: RangeDraft;
7
+ onPress: (day: string) => void;
8
+ /** The last day a press may pick (`max`, or `maxDays` from the start). */
9
+ latest: string | undefined;
10
+ min?: string;
11
+ max?: string;
12
+ locale: string;
13
+ today: string;
14
+ weekStartsOn: WeekStart;
15
+ labels: {
16
+ previousMonth: string;
17
+ nextMonth: string;
18
+ };
19
+ /** Take DOM focus on the focus day when shown, as a popover does. */
20
+ focusOnMount: boolean;
21
+ };
22
+ /**
23
+ * One month, or two side by side, of the range being picked: each a
24
+ * labelled grid with one roving day across them, the previous step before
25
+ * the first month and the next step after the last. It opens on the
26
+ * range's end, and the months follow the focus day.
27
+ */
28
+ export declare function RangeMonths({ count, draft, onPress, latest, min, max, locale, today, weekStartsOn, labels, focusOnMount, }: RangeMonthsProps): import("react").JSX.Element;