@sproutsocial/seeds-react-datepicker 1.0.51 → 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 +54 -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,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
|
+
});
|