@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.
@@ -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
- // TODO: This test times out. We'll come back to fix later
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
- // TODO: This test times out. We'll come back to fix later
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
+ });