@scalewing/react 1.39.0 → 1.40.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 (95) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +102 -0
  3. package/dist/calendar-month.d.ts +6 -0
  4. package/dist/calendar-month.js +15 -0
  5. package/dist/components/DateRangeButton.d.ts +65 -0
  6. package/dist/components/DateRangeButton.js +54 -0
  7. package/dist/components/ListGroup.d.ts +33 -0
  8. package/dist/components/ListGroup.js +19 -0
  9. package/dist/components/ListRow.d.ts +51 -0
  10. package/dist/components/ListRow.js +59 -0
  11. package/dist/components/NavCount.d.ts +21 -0
  12. package/dist/components/NavCount.js +23 -0
  13. package/dist/components/SectionNav.js +1 -8
  14. package/dist/components/StatTile.d.ts +11 -4
  15. package/dist/components/StatTile.js +11 -4
  16. package/dist/components/{date-field → calendar}/CalendarGrid.d.ts +17 -2
  17. package/dist/components/{date-field → calendar}/CalendarGrid.js +11 -3
  18. package/dist/components/calendar/CalendarPopover.d.ts +25 -0
  19. package/dist/components/calendar/CalendarPopover.js +22 -0
  20. package/dist/components/calendar/MonthStep.d.ts +16 -0
  21. package/dist/components/calendar/MonthStep.js +17 -0
  22. package/dist/components/calendar/use-grid-focus.d.ts +18 -0
  23. package/dist/components/calendar/use-grid-focus.js +35 -0
  24. package/dist/components/calendar/wrap-tab.d.ts +7 -0
  25. package/dist/components/calendar/wrap-tab.js +29 -0
  26. package/dist/components/date-field/CalendarDialog.js +9 -67
  27. package/dist/components/date-field/CalendarHeader.js +2 -11
  28. package/dist/components/date-range/DateRangePopover.d.ts +7 -0
  29. package/dist/components/date-range/DateRangePopover.js +17 -0
  30. package/dist/components/date-range/DateRangeSheet.d.ts +7 -0
  31. package/dist/components/date-range/DateRangeSheet.js +15 -0
  32. package/dist/components/date-range/RangeMonths.d.ts +28 -0
  33. package/dist/components/date-range/RangeMonths.js +50 -0
  34. package/dist/components/date-range/RangePresets.d.ts +16 -0
  35. package/dist/components/date-range/RangePresets.js +10 -0
  36. package/dist/components/date-range/RangeSummary.d.ts +15 -0
  37. package/dist/components/date-range/RangeSummary.js +11 -0
  38. package/dist/components/date-range/range-panel-props.d.ts +31 -0
  39. package/dist/components/date-range/range-panel-props.js +1 -0
  40. package/dist/components/date-range/use-range-popup.d.ts +21 -0
  41. package/dist/components/date-range/use-range-popup.js +53 -0
  42. package/dist/components/link-press.d.ts +7 -0
  43. package/dist/components/link-press.js +13 -0
  44. package/dist/components/list/ListRowAccessoryMark.d.ts +5 -0
  45. package/dist/components/list/ListRowAccessoryMark.js +7 -0
  46. package/dist/components/list/ListRowContent.d.ts +20 -0
  47. package/dist/components/list/ListRowContent.js +10 -0
  48. package/dist/components/list/list-row-container.d.ts +5 -0
  49. package/dist/components/list/list-row-container.js +6 -0
  50. package/dist/components/use-media-query.d.ts +5 -0
  51. package/dist/components/use-media-query.js +18 -0
  52. package/dist/css/chevron.d.ts +6 -0
  53. package/dist/css/chevron.js +14 -0
  54. package/dist/css/css-button-group-attached.js +8 -4
  55. package/dist/css/css-button-group-track.js +1 -1
  56. package/dist/css/css-checkbox.js +2 -2
  57. package/dist/css/css-components.js +9 -0
  58. package/dist/css/css-count-pill.d.ts +2 -0
  59. package/dist/css/css-count-pill.js +54 -0
  60. package/dist/css/css-date-field.js +3 -2
  61. package/dist/css/css-date-range-presets.d.ts +2 -0
  62. package/dist/css/css-date-range-presets.js +109 -0
  63. package/dist/css/css-date-range.d.ts +2 -0
  64. package/dist/css/css-date-range.js +197 -0
  65. package/dist/css/css-list-row.d.ts +2 -0
  66. package/dist/css/css-list-row.js +193 -0
  67. package/dist/css/css-list.d.ts +3 -0
  68. package/dist/css/css-list.js +85 -0
  69. package/dist/css/css-scroll-region.d.ts +18 -0
  70. package/dist/css/css-scroll-region.js +41 -0
  71. package/dist/css/css-section-nav-filled.js +2 -25
  72. package/dist/css/css-stat-tile.d.ts +7 -0
  73. package/dist/css/css-stat-tile.js +16 -0
  74. package/dist/css/css-steps.js +2 -2
  75. package/dist/css/css-table-bounded-edges.js +4 -27
  76. package/dist/css/range-band.d.ts +2 -0
  77. package/dist/css/range-band.js +7 -0
  78. package/dist/css/stacking.js +2 -2
  79. package/dist/css/stylesheet.js +6 -0
  80. package/dist/date-range-draft.d.ts +26 -0
  81. package/dist/date-range-draft.js +36 -0
  82. package/dist/date-range-input.d.ts +26 -0
  83. package/dist/date-range-input.js +58 -0
  84. package/dist/date-range-labels.d.ts +22 -0
  85. package/dist/date-range-labels.js +26 -0
  86. package/dist/date-range-view.d.ts +12 -0
  87. package/dist/date-range-view.js +29 -0
  88. package/dist/date-range-words.d.ts +11 -0
  89. package/dist/date-range-words.js +40 -0
  90. package/dist/index.d.ts +6 -1
  91. package/dist/index.js +6 -1
  92. package/dist/list-options.d.ts +70 -0
  93. package/dist/list-options.js +77 -0
  94. package/dist/styles.css +507 -26
  95. package/package.json +1 -1
@@ -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
  }
@@ -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,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;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef, useState } from 'react';
3
+ import { monthTitle } from '../../calendar-labels.js';
4
+ import { firstOfMonth, lastOfMonth, monthsBetween, sameDayIn, shiftMonth, } from '../../calendar-month.js';
5
+ import { firstShownMonth, shownMonths, } from '../../date-range-view.js';
6
+ import { clampDateOnly } from '../../date-only.js';
7
+ import { CalendarGrid } from '../calendar/CalendarGrid.js';
8
+ import { MonthStep } from '../calendar/MonthStep.js';
9
+ import { useGridFocus } from '../calendar/use-grid-focus.js';
10
+ /** `focusDate` moved into the shown months, after a month step. */
11
+ function keepInView(focusDate, first, count) {
12
+ const months = shownMonths(first, count);
13
+ const last = months[months.length - 1] ?? first;
14
+ if (focusDate < firstOfMonth(first))
15
+ return sameDayIn(first, focusDate);
16
+ if (focusDate > lastOfMonth(last))
17
+ return sameDayIn(last, focusDate);
18
+ return focusDate;
19
+ }
20
+ /**
21
+ * One month, or two side by side, of the range being picked: each a
22
+ * labelled grid with one roving day across them, the previous step before
23
+ * the first month and the next step after the last. It opens on the
24
+ * range's end, and the months follow the focus day.
25
+ */
26
+ export function RangeMonths({ count, draft, onPress, latest, min, max, locale, today, weekStartsOn, labels, focusOnMount, }) {
27
+ const id = useId();
28
+ const containerRef = useRef(null);
29
+ const [rawFocusDate, setFocusDate] = useState(draft.range.end);
30
+ // Clamped on every render, so new bounds while open move it inside.
31
+ const focusDate = clampDateOnly(rawFocusDate, min, max);
32
+ const [first, setFirst] = useState(() => firstShownMonth(focusDate, count, undefined, min));
33
+ const shown = firstShownMonth(focusDate, count, first, min);
34
+ if (monthsBetween(first, shown) !== 0)
35
+ setFirst(shown);
36
+ const { requestFocus, focusHandlers } = useGridFocus(containerRef, focusDate, focusOnMount);
37
+ function stepBy(delta) {
38
+ const nextFirst = shiftMonth(shown, delta);
39
+ setFirst(nextFirst);
40
+ setFocusDate(clampDateOnly(keepInView(focusDate, nextFirst, count), min, max));
41
+ }
42
+ const months = shownMonths(shown, count);
43
+ return (_jsxs("div", { className: "sw-date-range-months", ref: containerRef, ...focusHandlers, children: [_jsx("div", { "aria-live": "polite", className: "sw-sr-only", children: months.map((month) => (_jsx("div", { children: monthTitle(month, locale) }, `${month.year}-${month.month}`))) }), months.map((month, index) => {
44
+ const titleId = `${id}-${index}`;
45
+ return (_jsxs("div", { className: "sw-date-range-month", children: [_jsxs("div", { className: "sw-date-range-month-header", children: [index === 0 ? (_jsx(MonthStep, { delta: -1, label: labels.previousMonth, max: max, min: min, month: month, onMonthChange: () => stepBy(-1) })) : null, _jsx("div", { className: "sw-date-range-month-title", id: titleId, children: monthTitle(month, locale) }), index === months.length - 1 ? (_jsx(MonthStep, { delta: 1, label: labels.nextMonth, max: max, min: min, month: month, onMonthChange: () => stepBy(1) })) : null] }), _jsx(CalendarGrid, { blankOutside: true, focusDate: focusDate, latest: latest, locale: locale, max: max, min: min, month: month, onFocusDate: setFocusDate, onMoveFocus: (next) => {
46
+ requestFocus();
47
+ setFocusDate(next);
48
+ }, onSelect: onPress, selected: draft.range, titleId: titleId, today: today, weekStartsOn: weekStartsOn })] }, titleId));
49
+ })] }));
50
+ }
@@ -0,0 +1,16 @@
1
+ import { type DateRangePreset } from '../../date-range-input.js';
2
+ export type RangePresetsProps = {
3
+ presets: readonly DateRangePreset[];
4
+ /** Names the group, such as "Quick ranges". */
5
+ label: string;
6
+ /** The preset whose range is the one shown, if any. */
7
+ pressedId: string | undefined;
8
+ onChoose: (preset: DateRangePreset) => void;
9
+ /** `list` beside the months; `row` above one month, scrolling sideways. */
10
+ layout: 'list' | 'row';
11
+ };
12
+ /**
13
+ * DateRangeButton's presets: a named group of buttons, the one matching
14
+ * the range shown `aria-pressed`. A press applies its range at once.
15
+ */
16
+ export declare function RangePresets({ presets, label, pressedId, onChoose, layout, }: RangePresetsProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ import { tabStop } from '../../tab-stop.js';
4
+ /**
5
+ * DateRangeButton's presets: a named group of buttons, the one matching
6
+ * the range shown `aria-pressed`. A press applies its range at once.
7
+ */
8
+ export function RangePresets({ presets, label, pressedId, onChoose, layout, }) {
9
+ return (_jsx("div", { "aria-label": label, className: cx('sw-date-range-presets', layout === 'row' && 'sw-date-range-presets-row'), role: "group", children: presets.map((preset) => (_jsx("button", { "aria-pressed": preset.id === pressedId, className: "sw-date-range-preset", onClick: () => onChoose(preset), tabIndex: tabStop, type: "button", children: preset.label }, preset.id))) }));
10
+ }
@@ -0,0 +1,15 @@
1
+ export type RangeSummaryProps = {
2
+ /** The range being picked, with its weekdays. */
3
+ line: string;
4
+ /** Its length, such as "7 days". */
5
+ count: string;
6
+ /** How to pick, after the length; left out where the page shows it elsewhere. */
7
+ hint?: string;
8
+ /** Keep the words for assistive technology only, where the page shows them elsewhere. */
9
+ visuallyHidden?: boolean;
10
+ };
11
+ /**
12
+ * The range being picked, in a polite live region so each press is heard:
13
+ * the range on its own line, then its length and the hint.
14
+ */
15
+ export declare function RangeSummary({ line, count, hint, visuallyHidden, }: RangeSummaryProps): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * The range being picked, in a polite live region so each press is heard:
4
+ * the range on its own line, then its length and the hint.
5
+ */
6
+ export function RangeSummary({ line, count, hint, visuallyHidden = false, }) {
7
+ const status = (_jsxs("div", { "aria-atomic": "true", className: "sw-date-range-status", role: "status", children: [_jsx("span", { className: "sw-date-range-line", children: line }), ' ', _jsx("span", { className: "sw-date-range-count", children: count })] }));
8
+ if (visuallyHidden)
9
+ return _jsx("div", { className: "sw-sr-only", children: status });
10
+ return (_jsxs("div", { className: "sw-date-range-summary", children: [status, hint === undefined ? null : (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "sw-date-range-separator", children: ' · ' }), _jsx("span", { className: "sw-date-range-hint", children: hint })] }))] }));
11
+ }
@@ -0,0 +1,31 @@
1
+ import { type RefObject } from 'react';
2
+ import { type WeekStart } from '../../calendar-month.js';
3
+ import { type RangeDraft } from '../../date-range-draft.js';
4
+ import { type DateRangePreset } from '../../date-range-input.js';
5
+ import { type DateRangeButtonLabels } from '../../date-range-labels.js';
6
+ /** What both of DateRangeButton's presentations, popover and sheet, draw. */
7
+ export type RangePanelProps = {
8
+ id: string;
9
+ /** Names the popover or titles the sheet. */
10
+ label: string;
11
+ labels: DateRangeButtonLabels;
12
+ draft: RangeDraft;
13
+ onPress: (day: string) => void;
14
+ presets: readonly DateRangePreset[];
15
+ pressedPresetId: string | undefined;
16
+ onChoosePreset: (preset: DateRangePreset) => void;
17
+ /** Applies the draft. */
18
+ onApply: () => void;
19
+ /** Keeps the value; `true` returns focus to the button. */
20
+ onCancel: (restoreFocus: boolean) => void;
21
+ latest: string | undefined;
22
+ min?: string;
23
+ max?: string;
24
+ locale: string;
25
+ today: string;
26
+ weekStartsOn: WeekStart;
27
+ buttonRef: RefObject<HTMLButtonElement | null>;
28
+ /** The draft's words: the range with weekdays, and its length. */
29
+ line: string;
30
+ count: string;
31
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { type RefObject } from 'react';
2
+ import { type RangeDraft } from '../../date-range-draft.js';
3
+ import { type DateOnlyRange } from '../../date-only.js';
4
+ export type RangePopup = {
5
+ buttonRef: RefObject<HTMLButtonElement | null>;
6
+ id: string;
7
+ shown: boolean;
8
+ toggle: () => void;
9
+ draft: RangeDraft;
10
+ press: (day: string) => void;
11
+ /** Closes and gives `range` to `onChange` when it is a new range. */
12
+ apply: (range: DateOnlyRange) => void;
13
+ /** Closes and keeps the value; `true` returns focus to the button. */
14
+ cancel: (restoreFocus: boolean) => void;
15
+ };
16
+ /**
17
+ * DateRangeButton's open state and the range being picked. Opening starts
18
+ * from the value; a new value while open starts over from it. Disabling
19
+ * closes it for good.
20
+ */
21
+ export declare function useRangePopup(value: DateOnlyRange, disabled: boolean, onChange: (value: DateOnlyRange) => void): RangePopup;