@sproutsocial/seeds-react-datepicker 1.0.52 → 1.1.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.
- package/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +43 -0
- package/dist/datepicker.css +262 -0
- package/dist/esm/index.js +76 -174
- package/dist/esm/index.js.map +1 -1
- package/dist/index.js +74 -172
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/CalendarDayTailwind.tsx +76 -0
- package/src/DateRangePicker/DateRangePicker.tsx +6 -2
- package/src/DateRangePicker/__tests__/DateRangePicker.test.tsx +1 -2
- package/src/SingleDatePicker/SingleDatePicker.tsx +6 -2
- package/src/SingleDatePicker/__tests__/SingleDatePicker.test.tsx +1 -2
- package/src/__tests__/DatepickerContract.test.tsx +269 -0
- package/src/__tests__/DatepickerRouting.test.tsx +501 -0
- package/src/cn.ts +28 -0
- package/src/common.tsx +1 -1
- package/src/datepicker.css +262 -0
- package/src/dayModifiers.ts +71 -0
- package/src/styles.ts +21 -54
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type moment from "moment";
|
|
3
|
+
import type { ModifiersShape } from "react-dates";
|
|
4
|
+
import { cn } from "./cn";
|
|
5
|
+
import {
|
|
6
|
+
isHoveredAndInRange,
|
|
7
|
+
isOutOfRange,
|
|
8
|
+
isSelected,
|
|
9
|
+
shouldHaveLeftPill,
|
|
10
|
+
shouldHaveRightPill,
|
|
11
|
+
} from "./dayModifiers";
|
|
12
|
+
|
|
13
|
+
export interface TypeCalendarDayProps {
|
|
14
|
+
modifiers: ModifiersShape;
|
|
15
|
+
day: moment.Moment;
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Static day cell. Renders the semantic `seeds-datepicker-day*` classes from
|
|
22
|
+
* datepicker.css with no runtime CSS-in-JS.
|
|
23
|
+
*
|
|
24
|
+
* The legacy cell was `styled(Box)`, and Box ships no CSS of its own, so
|
|
25
|
+
* `.seeds-datepicker-day` reproduces Box's baseline (box-sizing + font-family)
|
|
26
|
+
* and this component reproduces Box's forwardRef.
|
|
27
|
+
*
|
|
28
|
+
* The visual states are mutually exclusive, exactly as in the styled version's
|
|
29
|
+
* if / else-if chain: a selected day never also picks up the hovered or
|
|
30
|
+
* out-of-range treatment. Pill edges are modifier classes rather than values,
|
|
31
|
+
* so an absent edge emits no declaration at all.
|
|
32
|
+
*/
|
|
33
|
+
const dayStateClasses = (
|
|
34
|
+
modifiers: ModifiersShape,
|
|
35
|
+
day: moment.Moment
|
|
36
|
+
): Record<string, boolean> => {
|
|
37
|
+
if (isSelected(modifiers)) {
|
|
38
|
+
return {
|
|
39
|
+
"seeds-datepicker-day-selected": true,
|
|
40
|
+
"seeds-datepicker-day-pill-left": shouldHaveLeftPill(modifiers, day),
|
|
41
|
+
"seeds-datepicker-day-pill-right": shouldHaveRightPill(modifiers, day),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isHoveredAndInRange(modifiers)) {
|
|
46
|
+
return { "seeds-datepicker-day-hovered": true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isOutOfRange(modifiers)) {
|
|
50
|
+
return { "seeds-datepicker-day-out-of-range": true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const CalendarDayTailwind = React.forwardRef<
|
|
57
|
+
HTMLDivElement,
|
|
58
|
+
TypeCalendarDayProps
|
|
59
|
+
>(({ modifiers, day, className, children }, ref) => (
|
|
60
|
+
<div
|
|
61
|
+
ref={ref}
|
|
62
|
+
// Semantic classes come before the consumer className so consumer utilities
|
|
63
|
+
// can override the layered base styles.
|
|
64
|
+
className={cn(
|
|
65
|
+
"seeds-datepicker-day",
|
|
66
|
+
dayStateClasses(modifiers, day),
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
</div>
|
|
72
|
+
));
|
|
73
|
+
|
|
74
|
+
CalendarDayTailwind.displayName = "CalendarDay";
|
|
75
|
+
|
|
76
|
+
export default CalendarDayTailwind;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useState, type KeyboardEvent } from "react";
|
|
2
2
|
import moment from "moment";
|
|
3
3
|
import DayPickerRangeController from "react-dates/lib/components/DayPickerRangeController";
|
|
4
|
-
import { ReactDatesCssOverrides } from "../styles";
|
|
5
4
|
import {
|
|
6
5
|
commonDatePickerProps,
|
|
7
6
|
DefaultSetStatusText,
|
|
@@ -55,9 +54,14 @@ const DateRangePicker = ({
|
|
|
55
54
|
[onBlur, onFocusChange]
|
|
56
55
|
);
|
|
57
56
|
|
|
57
|
+
/*
|
|
58
|
+
* The react-dates overrides that ReactDatesCssOverrides used to inject at
|
|
59
|
+
* runtime now ship statically in datepicker.css, aggregated into
|
|
60
|
+
* @sproutsocial/racine/css/components. styles.ts keeps the styled source for
|
|
61
|
+
* Phase A, but nothing imports it, so it is tree-shaken out of the bundle.
|
|
62
|
+
*/
|
|
58
63
|
return (
|
|
59
64
|
<>
|
|
60
|
-
<ReactDatesCssOverrides />
|
|
61
65
|
<VisuallyHidden>
|
|
62
66
|
<div role="status">{statusText}</div>
|
|
63
67
|
</VisuallyHidden>
|
|
@@ -153,8 +153,7 @@ describe("date range picker", () => {
|
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
xtest("should close on escape key press", async () => {
|
|
156
|
+
test("should close on escape key press", async () => {
|
|
158
157
|
const buttonText = "Date Picker";
|
|
159
158
|
const initialDate = moment("2020-01-01");
|
|
160
159
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useState } from "react";
|
|
2
2
|
import moment from "moment";
|
|
3
3
|
import DayPickerSingleDateController from "react-dates/lib/components/DayPickerSingleDateController";
|
|
4
|
-
import { ReactDatesCssOverrides } from "../styles";
|
|
5
4
|
import {
|
|
6
5
|
commonDatePickerProps,
|
|
7
6
|
DefaultSetStatusText,
|
|
@@ -47,9 +46,14 @@ const SingleDatePicker = ({
|
|
|
47
46
|
[onBlur, onFocusChange]
|
|
48
47
|
);
|
|
49
48
|
|
|
49
|
+
/*
|
|
50
|
+
* The react-dates overrides that ReactDatesCssOverrides used to inject at
|
|
51
|
+
* runtime now ship statically in datepicker.css, aggregated into
|
|
52
|
+
* @sproutsocial/racine/css/components. styles.ts keeps the styled source for
|
|
53
|
+
* Phase A, but nothing imports it, so it is tree-shaken out of the bundle.
|
|
54
|
+
*/
|
|
50
55
|
return (
|
|
51
56
|
<>
|
|
52
|
-
<ReactDatesCssOverrides />
|
|
53
57
|
<VisuallyHidden>
|
|
54
58
|
<div role="status">{statusText}</div>
|
|
55
59
|
</VisuallyHidden>
|
|
@@ -101,8 +101,7 @@ describe("SingleDatePicker", () => {
|
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
xtest("should close on escape key press", async () => {
|
|
104
|
+
test("should close on escape key press", async () => {
|
|
106
105
|
const buttonText = "Date Picker";
|
|
107
106
|
const initialDate = moment("2020-01-01");
|
|
108
107
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import moment, { type Moment } from "moment";
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
screen,
|
|
6
|
+
within,
|
|
7
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
import { theme } from "@sproutsocial/seeds-react-theme";
|
|
9
|
+
import { CalendarDay } from "../styles";
|
|
10
|
+
import {
|
|
11
|
+
SingleDatePicker,
|
|
12
|
+
StatefulSingleDatePicker,
|
|
13
|
+
} from "../SingleDatePicker";
|
|
14
|
+
import { DateRangePicker, StatefulDateRangePicker } from "../DateRangePicker";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Permanent contract tests for the Datepicker package.
|
|
18
|
+
*
|
|
19
|
+
* These freeze the observable public behavior of SingleDatePicker,
|
|
20
|
+
* DateRangePicker, and the day-cell styling contract, and must stay GREEN
|
|
21
|
+
* across the styled-components -> Tailwind hybrid migration.
|
|
22
|
+
*
|
|
23
|
+
* Determinism: the visible month, the day-of-week modifiers that decide the
|
|
24
|
+
* pill radii, and react-dates' English aria phrases all depend on ambient
|
|
25
|
+
* state. Pin a mid-month noon-UTC instant (no timezone offset can move it out
|
|
26
|
+
* of June 2026, and +/- a week stays inside the visible month) and the "en"
|
|
27
|
+
* locale.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const FIXED_NOW = new Date("2026-06-15T12:00:00Z");
|
|
31
|
+
|
|
32
|
+
// A Wednesday in the middle of June 2026: not the first day of the week, not
|
|
33
|
+
// the first or last day of the month. Every pill-edge predicate is false for
|
|
34
|
+
// it unless a modifier forces one, which is what makes it the right probe for
|
|
35
|
+
// the "no declaration emitted" contract below.
|
|
36
|
+
const MID_WEEK_DAY = moment("2026-06-17");
|
|
37
|
+
|
|
38
|
+
const noop = () => {};
|
|
39
|
+
|
|
40
|
+
const renderCalendarDay = (modifiers: string[], day = MID_WEEK_DAY) => {
|
|
41
|
+
const { container } = render(
|
|
42
|
+
<CalendarDay day={day} modifiers={new Set(modifiers)}>
|
|
43
|
+
{day.format("D")}
|
|
44
|
+
</CalendarDay>
|
|
45
|
+
);
|
|
46
|
+
return container.firstChild as HTMLElement;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
jest.useFakeTimers();
|
|
51
|
+
jest.setSystemTime(FIXED_NOW);
|
|
52
|
+
moment.locale("en");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("CalendarDay styling contract", () => {
|
|
56
|
+
it("renders the day contents in a single element", () => {
|
|
57
|
+
const day = renderCalendarDay([]);
|
|
58
|
+
expect(day.tagName).toBe("DIV");
|
|
59
|
+
expect(day).toHaveTextContent("17");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not paint an unmodified day", () => {
|
|
63
|
+
const day = renderCalendarDay([]);
|
|
64
|
+
expect(day).toHaveStyleRule("background-color", undefined);
|
|
65
|
+
expect(day).toHaveStyleRule("color", undefined);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("paints a selected day and gives it both pill edges", () => {
|
|
69
|
+
const day = renderCalendarDay(["selected"]);
|
|
70
|
+
expect(day).toHaveStyleRule(
|
|
71
|
+
"background-color",
|
|
72
|
+
theme.colors.container.background.selected
|
|
73
|
+
);
|
|
74
|
+
expect(day).toHaveStyleRule("color", theme.colors.text.inverse);
|
|
75
|
+
expect(day).toHaveStyleRule("margin-left", theme.space[200]);
|
|
76
|
+
expect(day).toHaveStyleRule("margin-right", theme.space[200]);
|
|
77
|
+
expect(day).toHaveStyleRule("border-top-left-radius", theme.radii.pill);
|
|
78
|
+
expect(day).toHaveStyleRule("border-bottom-left-radius", theme.radii.pill);
|
|
79
|
+
expect(day).toHaveStyleRule("border-top-right-radius", theme.radii.pill);
|
|
80
|
+
expect(day).toHaveStyleRule("border-bottom-right-radius", theme.radii.pill);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The interior of a selection emits NO margin/radius declaration at all —
|
|
85
|
+
* styled-components drops `margin-left: ${false && ...}` rather than writing
|
|
86
|
+
* `margin-left: 0`. A port that writes a zero fallback is a silent
|
|
87
|
+
* regression, so this asserts absence, not a zero value.
|
|
88
|
+
*/
|
|
89
|
+
it("emits no edge declarations for the interior of a selected span", () => {
|
|
90
|
+
const day = renderCalendarDay(["selected-span"]);
|
|
91
|
+
expect(day).toHaveStyleRule(
|
|
92
|
+
"background-color",
|
|
93
|
+
theme.colors.container.background.selected
|
|
94
|
+
);
|
|
95
|
+
for (const property of [
|
|
96
|
+
"margin-left",
|
|
97
|
+
"margin-right",
|
|
98
|
+
"border-top-left-radius",
|
|
99
|
+
"border-bottom-left-radius",
|
|
100
|
+
"border-top-right-radius",
|
|
101
|
+
"border-bottom-right-radius",
|
|
102
|
+
]) {
|
|
103
|
+
expect(day).toHaveStyleRule(property, undefined);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("gives a selected span a left pill on the first day of the week only", () => {
|
|
108
|
+
const day = renderCalendarDay(["selected-span", "first-day-of-week"]);
|
|
109
|
+
expect(day).toHaveStyleRule("margin-left", theme.space[200]);
|
|
110
|
+
expect(day).toHaveStyleRule("margin-right", undefined);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("gives a selected span a right pill on the last day of the week only", () => {
|
|
114
|
+
const day = renderCalendarDay(["selected-span", "last-day-of-week"]);
|
|
115
|
+
expect(day).toHaveStyleRule("margin-right", theme.space[200]);
|
|
116
|
+
expect(day).toHaveStyleRule("margin-left", undefined);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("pills the first and last day of the month inside a span", () => {
|
|
120
|
+
expect(
|
|
121
|
+
renderCalendarDay(["selected-span"], moment("2026-06-01"))
|
|
122
|
+
).toHaveStyleRule("margin-left", theme.space[200]);
|
|
123
|
+
expect(
|
|
124
|
+
renderCalendarDay(["selected-span"], moment("2026-06-30"))
|
|
125
|
+
).toHaveStyleRule("margin-right", theme.space[200]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("outlines a hovered in-range day", () => {
|
|
129
|
+
const day = renderCalendarDay(["hovered"]);
|
|
130
|
+
expect(day).toHaveStyleRule("margin", `0 ${theme.space[200]}`);
|
|
131
|
+
expect(day).toHaveStyleRule("border-radius", theme.radii.pill);
|
|
132
|
+
expect(day).toHaveStyleRule(
|
|
133
|
+
"border",
|
|
134
|
+
`1px solid ${theme.colors.container.border.selected}`
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("does not outline a hovered day that is out of range", () => {
|
|
139
|
+
const day = renderCalendarDay(["hovered", "blocked-out-of-range"]);
|
|
140
|
+
expect(day).toHaveStyleRule("border-radius", undefined);
|
|
141
|
+
expect(day).toHaveStyleRule("color", theme.colors.text.subtext);
|
|
142
|
+
expect(day).toHaveStyleRule("opacity", "0.4");
|
|
143
|
+
expect(day).toHaveStyleRule("pointer-events", "none");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("dims an out-of-range day", () => {
|
|
147
|
+
const day = renderCalendarDay(["blocked-out-of-range"]);
|
|
148
|
+
expect(day).toHaveStyleRule("color", theme.colors.text.subtext);
|
|
149
|
+
expect(day).toHaveStyleRule("opacity", "0.4");
|
|
150
|
+
expect(day).toHaveStyleRule("pointer-events", "none");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("prefers the selected treatment over out-of-range", () => {
|
|
154
|
+
const day = renderCalendarDay(["selected", "blocked-out-of-range"]);
|
|
155
|
+
expect(day).toHaveStyleRule("color", theme.colors.text.inverse);
|
|
156
|
+
expect(day).toHaveStyleRule("pointer-events", undefined);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("SingleDatePicker contract", () => {
|
|
161
|
+
it("announces the visible month in a live status region", () => {
|
|
162
|
+
render(<SingleDatePicker date={moment()} onDateChange={noop} />);
|
|
163
|
+
expect(screen.getByRole("status")).toHaveTextContent("June 2026");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("renders one month of day cells carrying the day number", () => {
|
|
167
|
+
const { container } = render(
|
|
168
|
+
<SingleDatePicker date={moment()} onDateChange={noop} />
|
|
169
|
+
);
|
|
170
|
+
expect(container.querySelectorAll("[data-visible=true]")).toHaveLength(1);
|
|
171
|
+
expect(
|
|
172
|
+
within(screen.getByLabelText("Wednesday, June 17, 2026")).getByText("17")
|
|
173
|
+
).toBeInTheDocument();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("renders both month navigation controls with decorative icons", () => {
|
|
177
|
+
const { container } = render(
|
|
178
|
+
<SingleDatePicker date={moment()} onDateChange={noop} />
|
|
179
|
+
);
|
|
180
|
+
const previous = screen.getByLabelText(
|
|
181
|
+
"Move backward to switch to the previous month."
|
|
182
|
+
);
|
|
183
|
+
const next = screen.getByLabelText(
|
|
184
|
+
"Move forward to switch to the next month."
|
|
185
|
+
);
|
|
186
|
+
expect(previous).toHaveClass("DayPickerNavigation_button__horizontal");
|
|
187
|
+
expect(next).toHaveClass("DayPickerNavigation_button__horizontal");
|
|
188
|
+
expect(container.querySelectorAll(".seeds-icon[aria-hidden]")).toHaveLength(
|
|
189
|
+
2
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("hides the keyboard shortcuts panel by default", () => {
|
|
194
|
+
render(<SingleDatePicker date={moment()} onDateChange={noop} />);
|
|
195
|
+
expect(
|
|
196
|
+
screen.queryByLabelText("Open the keyboard shortcuts panel.")
|
|
197
|
+
).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* `commonDatePickerProps` is spread BEFORE `...rest`, so every default it
|
|
202
|
+
* supplies is overridable by the caller. That spread order is observable
|
|
203
|
+
* contract, not an implementation detail.
|
|
204
|
+
*/
|
|
205
|
+
it("lets a caller override a default supplied by commonDatePickerProps", () => {
|
|
206
|
+
render(
|
|
207
|
+
<StatefulSingleDatePicker
|
|
208
|
+
date={moment()}
|
|
209
|
+
hideKeyboardShortcutsPanel={false}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
expect(
|
|
213
|
+
screen.getByLabelText("Open the keyboard shortcuts panel.")
|
|
214
|
+
).toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("lets a caller replace renderDayContents entirely", () => {
|
|
218
|
+
render(
|
|
219
|
+
<StatefulSingleDatePicker
|
|
220
|
+
date={moment()}
|
|
221
|
+
renderDayContents={(day: Moment) => <b>{`d${day.format("D")}`}</b>}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
const cell = screen.getByLabelText("Wednesday, June 17, 2026");
|
|
225
|
+
expect(within(cell).getByText("d17")).toBeInTheDocument();
|
|
226
|
+
expect(cell.querySelector("b")).toBeInTheDocument();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("DateRangePicker contract", () => {
|
|
231
|
+
it("announces both visible months in a live status region", () => {
|
|
232
|
+
render(
|
|
233
|
+
<DateRangePicker
|
|
234
|
+
startDate={moment()}
|
|
235
|
+
focusedInput="startDate"
|
|
236
|
+
onDatesChange={noop}
|
|
237
|
+
onFocusChange={noop}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
expect(screen.getByRole("status")).toHaveTextContent(
|
|
241
|
+
"June 2026, July 2026"
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("renders two months by default", () => {
|
|
246
|
+
const { container } = render(
|
|
247
|
+
<DateRangePicker
|
|
248
|
+
startDate={moment()}
|
|
249
|
+
focusedInput="startDate"
|
|
250
|
+
onDatesChange={noop}
|
|
251
|
+
onFocusChange={noop}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
expect(container.querySelectorAll("[data-visible=true]")).toHaveLength(2);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("lets a caller override the month count", () => {
|
|
258
|
+
const { container } = render(
|
|
259
|
+
<DateRangePicker
|
|
260
|
+
startDate={moment()}
|
|
261
|
+
focusedInput="startDate"
|
|
262
|
+
onDatesChange={noop}
|
|
263
|
+
onFocusChange={noop}
|
|
264
|
+
numberOfMonths={1}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
expect(container.querySelectorAll("[data-visible=true]")).toHaveLength(1);
|
|
268
|
+
});
|
|
269
|
+
});
|