@sproutsocial/seeds-react-datepicker 1.0.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/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +7 -0
- package/dist/esm/index.js +411 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +451 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +53 -0
- package/src/DateRangePicker/DateRangePicker.stories.tsx +125 -0
- package/src/DateRangePicker/DateRangePicker.tsx +83 -0
- package/src/DateRangePicker/DateRangePickerTypes.ts +35 -0
- package/src/DateRangePicker/StatefulDateRangePicker.tsx +47 -0
- package/src/DateRangePicker/__tests__/DateRangePicker.test.tsx +257 -0
- package/src/DateRangePicker/__tests__/DateRangePicker.typetest.tsx +34 -0
- package/src/DateRangePicker/index.ts +6 -0
- package/src/SingleDatePicker/SingleDatePicker.stories.tsx +92 -0
- package/src/SingleDatePicker/SingleDatePicker.tsx +73 -0
- package/src/SingleDatePicker/SingleDatePickerTypes.ts +25 -0
- package/src/SingleDatePicker/StatefulSingleDatePicker.tsx +25 -0
- package/src/SingleDatePicker/__tests__/SingleDatePicker.test.tsx +155 -0
- package/src/SingleDatePicker/__tests__/SingleDatePicker.typetest.tsx +24 -0
- package/src/SingleDatePicker/index.ts +6 -0
- package/src/common.tsx +60 -0
- package/src/index.ts +2 -0
- package/src/styled.d.ts +7 -0
- package/src/styles.ts +224 -0
- package/src/types.ts +24 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import moment, { type Moment } from "moment";
|
|
3
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
4
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
5
|
+
import Popout from "@sproutsocial/seeds-react-popout";
|
|
6
|
+
import DateRangePicker from "./DateRangePicker";
|
|
7
|
+
import type { EnumFocusedInput } from "./DateRangePickerTypes";
|
|
8
|
+
import { StatefulDateRangePicker } from "./StatefulDateRangePicker";
|
|
9
|
+
import { formatDateAsCalendarHeader } from "../common";
|
|
10
|
+
|
|
11
|
+
const START_DATE = "startDate";
|
|
12
|
+
const END_DATE = "endDate";
|
|
13
|
+
|
|
14
|
+
const today = moment("2023-01-06");
|
|
15
|
+
|
|
16
|
+
const dateProps = {
|
|
17
|
+
startDate: today,
|
|
18
|
+
endDate: today.clone().add(3, "days"),
|
|
19
|
+
initialVisibleMonth: () => today,
|
|
20
|
+
setStatusText: (dates: moment.Moment[]) =>
|
|
21
|
+
`Now showing: ${dates.map(formatDateAsCalendarHeader).join(" and ")}`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const meta = {
|
|
25
|
+
title: "Components/DatePickers/DateRangePicker",
|
|
26
|
+
component: StatefulDateRangePicker,
|
|
27
|
+
};
|
|
28
|
+
export default meta;
|
|
29
|
+
|
|
30
|
+
export const Default = {
|
|
31
|
+
args: dateProps,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const NoStartingDate = {
|
|
35
|
+
args: {
|
|
36
|
+
...dateProps,
|
|
37
|
+
startDate: null,
|
|
38
|
+
endDate: null,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const StartOnPreviousMonth = {
|
|
43
|
+
args: {
|
|
44
|
+
...dateProps,
|
|
45
|
+
initialVisibleMonth: () => dateProps.endDate.clone().subtract(1, "month"),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const BlockedRanges = {
|
|
50
|
+
render: () => {
|
|
51
|
+
const endOfPreviousMonth = today
|
|
52
|
+
.clone()
|
|
53
|
+
.subtract(1, "month")
|
|
54
|
+
.endOf("month");
|
|
55
|
+
const startOfNextMonth = today.clone().add(1, "month").startOf("month");
|
|
56
|
+
return (
|
|
57
|
+
<StatefulDateRangePicker
|
|
58
|
+
{...dateProps}
|
|
59
|
+
isOutsideRange={(day) =>
|
|
60
|
+
day.isSameOrBefore(endOfPreviousMonth) ||
|
|
61
|
+
day.isSameOrAfter(startOfNextMonth)
|
|
62
|
+
}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const FocusControlledPopoutDatePicker = {
|
|
69
|
+
render: () => {
|
|
70
|
+
const [focusedInput, setFocusedInput] = useState<EnumFocusedInput>(null);
|
|
71
|
+
const [dates, pickDate] = useState<{
|
|
72
|
+
startDate: Moment | null;
|
|
73
|
+
endDate: Moment | null;
|
|
74
|
+
}>({
|
|
75
|
+
startDate: null,
|
|
76
|
+
endDate: null,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const onFocusChange = (nextFocusedInput: typeof focusedInput) => {
|
|
80
|
+
setFocusedInput(nextFocusedInput);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Popout
|
|
85
|
+
isOpen={focusedInput !== null}
|
|
86
|
+
content={
|
|
87
|
+
<Popout.Content pt={400} p={0}>
|
|
88
|
+
<DateRangePicker
|
|
89
|
+
{...dates}
|
|
90
|
+
onDatesChange={pickDate}
|
|
91
|
+
focusedInput={focusedInput}
|
|
92
|
+
onFocusChange={onFocusChange}
|
|
93
|
+
setStatusText={dateProps.setStatusText}
|
|
94
|
+
/>
|
|
95
|
+
<Box display="flex" justifyContent="space-between" p={400}>
|
|
96
|
+
<Button
|
|
97
|
+
appearance="secondary"
|
|
98
|
+
onClick={() => setFocusedInput(START_DATE)}
|
|
99
|
+
>
|
|
100
|
+
{dates.startDate
|
|
101
|
+
? dates.startDate.format("DD MMM YYYY")
|
|
102
|
+
: "Choose Start Date"}
|
|
103
|
+
</Button>
|
|
104
|
+
<Button
|
|
105
|
+
appearance="secondary"
|
|
106
|
+
onClick={() => setFocusedInput(END_DATE)}
|
|
107
|
+
>
|
|
108
|
+
{dates.endDate
|
|
109
|
+
? dates.endDate.format("DD MMM YYYY")
|
|
110
|
+
: "Choose End Date"}
|
|
111
|
+
</Button>
|
|
112
|
+
</Box>
|
|
113
|
+
</Popout.Content>
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<Button
|
|
117
|
+
appearance="secondary"
|
|
118
|
+
onClick={() => setFocusedInput(START_DATE)}
|
|
119
|
+
>
|
|
120
|
+
Open Date Picker
|
|
121
|
+
</Button>
|
|
122
|
+
</Popout>
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useCallback, useState, type KeyboardEvent } from "react";
|
|
2
|
+
import moment from "moment";
|
|
3
|
+
import DayPickerRangeController from "react-dates/lib/components/DayPickerRangeController";
|
|
4
|
+
import { ReactDatesCssOverrides } from "../styles";
|
|
5
|
+
import {
|
|
6
|
+
commonDatePickerProps,
|
|
7
|
+
DefaultSetStatusText,
|
|
8
|
+
getVisibleMonths,
|
|
9
|
+
} from "../common";
|
|
10
|
+
import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
|
|
11
|
+
import type { TypeDateRangePickerProps } from "./DateRangePickerTypes";
|
|
12
|
+
|
|
13
|
+
const DateRangePicker = ({
|
|
14
|
+
startDate = null,
|
|
15
|
+
endDate = null,
|
|
16
|
+
onDatesChange,
|
|
17
|
+
setStatusText = DefaultSetStatusText,
|
|
18
|
+
initialVisibleMonth,
|
|
19
|
+
numberOfMonths = 2,
|
|
20
|
+
onFocusChange,
|
|
21
|
+
onBlur,
|
|
22
|
+
focusedInput,
|
|
23
|
+
...rest
|
|
24
|
+
}: TypeDateRangePickerProps) => {
|
|
25
|
+
const [statusText, updateStatusText] = useState(() =>
|
|
26
|
+
setStatusText(
|
|
27
|
+
getVisibleMonths(
|
|
28
|
+
moment(initialVisibleMonth?.() ?? startDate ?? undefined),
|
|
29
|
+
numberOfMonths
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
interface HandleMonthClick {
|
|
34
|
+
(month: moment.Moment): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handleMonthClick: HandleMonthClick = useCallback(
|
|
38
|
+
(month) => {
|
|
39
|
+
updateStatusText(setStatusText(getVisibleMonths(month, numberOfMonths)));
|
|
40
|
+
},
|
|
41
|
+
[numberOfMonths, setStatusText]
|
|
42
|
+
);
|
|
43
|
+
const wrappedOnBlur = useCallback<
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
45
|
+
(event: undefined | KeyboardEvent<HTMLDivElement>) => void
|
|
46
|
+
>(
|
|
47
|
+
(event) => {
|
|
48
|
+
// for some reason onBlur is called with no event on day selection 🤷
|
|
49
|
+
if (!event) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
onFocusChange?.(null);
|
|
53
|
+
onBlur?.(event);
|
|
54
|
+
},
|
|
55
|
+
[onBlur, onFocusChange]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<ReactDatesCssOverrides />
|
|
61
|
+
<VisuallyHidden>
|
|
62
|
+
<div role="status">{statusText}</div>
|
|
63
|
+
</VisuallyHidden>
|
|
64
|
+
<DayPickerRangeController
|
|
65
|
+
{...commonDatePickerProps}
|
|
66
|
+
startDate={startDate}
|
|
67
|
+
endDate={endDate}
|
|
68
|
+
onDatesChange={onDatesChange}
|
|
69
|
+
numberOfMonths={numberOfMonths}
|
|
70
|
+
initialVisibleMonth={initialVisibleMonth || null}
|
|
71
|
+
focusedInput={focusedInput}
|
|
72
|
+
isFocused={focusedInput !== null}
|
|
73
|
+
onBlur={wrappedOnBlur}
|
|
74
|
+
onFocusChange={onFocusChange}
|
|
75
|
+
onPrevMonthClick={handleMonthClick}
|
|
76
|
+
onNextMonthClick={handleMonthClick}
|
|
77
|
+
{...rest}
|
|
78
|
+
/>
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default DateRangePicker;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Moment } from "moment";
|
|
2
|
+
import type { TypeCommonDatePickerProps } from "../types";
|
|
3
|
+
|
|
4
|
+
export type EnumFocusedInput = null | "startDate" | "endDate";
|
|
5
|
+
|
|
6
|
+
export interface TypeStatefulDateRangePickerProps
|
|
7
|
+
extends TypeCommonDatePickerProps {
|
|
8
|
+
startDate?: Moment | null;
|
|
9
|
+
endDate?: Moment | null;
|
|
10
|
+
focusedInput?: EnumFocusedInput;
|
|
11
|
+
onDatesChange?: (arg0: {
|
|
12
|
+
startDate: Moment | null;
|
|
13
|
+
endDate: Moment | null;
|
|
14
|
+
}) => void;
|
|
15
|
+
onFocusChange?: (arg0: EnumFocusedInput) => void;
|
|
16
|
+
setStatusText?: (dates: Moment[]) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TypeDateRangePickerProps
|
|
20
|
+
extends Required<
|
|
21
|
+
Pick<
|
|
22
|
+
TypeStatefulDateRangePickerProps,
|
|
23
|
+
"focusedInput" | "onDatesChange" | "onFocusChange"
|
|
24
|
+
>
|
|
25
|
+
>,
|
|
26
|
+
Pick<
|
|
27
|
+
TypeStatefulDateRangePickerProps,
|
|
28
|
+
| "startDate"
|
|
29
|
+
| "endDate"
|
|
30
|
+
| "setStatusText"
|
|
31
|
+
| "initialVisibleMonth"
|
|
32
|
+
| "numberOfMonths"
|
|
33
|
+
> {
|
|
34
|
+
onBlur?: React.KeyboardEventHandler<HTMLDivElement> | undefined;
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
// @ts-ignore unknown types
|
|
3
|
+
import { START_DATE } from "react-dates/constants";
|
|
4
|
+
import type { TypeStatefulDateRangePickerProps } from "./DateRangePickerTypes";
|
|
5
|
+
import DateRangePicker from "./DateRangePicker";
|
|
6
|
+
|
|
7
|
+
export const StatefulDateRangePicker = ({
|
|
8
|
+
startDate,
|
|
9
|
+
endDate,
|
|
10
|
+
onDatesChange,
|
|
11
|
+
onFocusChange,
|
|
12
|
+
...rest
|
|
13
|
+
}: TypeStatefulDateRangePickerProps) => {
|
|
14
|
+
const [dates, setDate] = useState({
|
|
15
|
+
startDate,
|
|
16
|
+
endDate,
|
|
17
|
+
});
|
|
18
|
+
const [focusedInput, setFocusedInput] = React.useState(START_DATE);
|
|
19
|
+
|
|
20
|
+
// @ts-ignore unknown types
|
|
21
|
+
const handleDatesChange = (nextDates) => {
|
|
22
|
+
onDatesChange && onDatesChange(nextDates);
|
|
23
|
+
setDate(nextDates);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// @ts-ignore unknown types
|
|
27
|
+
const handleFocusChange = (nextFocusedInput) => {
|
|
28
|
+
onFocusChange && onFocusChange(nextFocusedInput);
|
|
29
|
+
setFocusedInput(
|
|
30
|
+
// null means that we've selected an end date. we want to go back to START_DATE
|
|
31
|
+
// so the user can modify their selection. if focusedInput === null then it won't
|
|
32
|
+
// respond to click or keyboard events
|
|
33
|
+
nextFocusedInput === null ? START_DATE : nextFocusedInput
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<DateRangePicker
|
|
39
|
+
startDate={dates.startDate}
|
|
40
|
+
endDate={dates.endDate}
|
|
41
|
+
focusedInput={focusedInput}
|
|
42
|
+
onDatesChange={handleDatesChange}
|
|
43
|
+
onFocusChange={handleFocusChange}
|
|
44
|
+
{...rest}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import moment, { type Moment } from "moment";
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
fireEvent,
|
|
6
|
+
screen,
|
|
7
|
+
waitFor,
|
|
8
|
+
act,
|
|
9
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
10
|
+
import { formatDateAsCalendarDay } from "../../common";
|
|
11
|
+
import {
|
|
12
|
+
DateRangePicker,
|
|
13
|
+
type EnumFocusedInput,
|
|
14
|
+
StatefulDateRangePicker,
|
|
15
|
+
} from "../index";
|
|
16
|
+
import Popout from "@sproutsocial/seeds-react-popout";
|
|
17
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
18
|
+
|
|
19
|
+
// @ts-ignore Unknown types
|
|
20
|
+
const getStartDate = (options, date) =>
|
|
21
|
+
// eslint-disable-next-line testing-library/prefer-screen-queries
|
|
22
|
+
options.queryByLabelText(
|
|
23
|
+
`Selected as start date. ${formatDateAsCalendarDay(date)}`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// @ts-ignore Unknown types
|
|
27
|
+
const getEndDate = (options, date) =>
|
|
28
|
+
// eslint-disable-next-line testing-library/prefer-screen-queries
|
|
29
|
+
options.queryByLabelText(
|
|
30
|
+
`Selected as end date. ${formatDateAsCalendarDay(date)}`
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// @ts-ignore Unknown types
|
|
34
|
+
const clickOnStartDate = (options, date) =>
|
|
35
|
+
fireEvent.click(
|
|
36
|
+
// eslint-disable-next-line testing-library/prefer-screen-queries
|
|
37
|
+
options.getByLabelText(
|
|
38
|
+
`Choose ${formatDateAsCalendarDay(
|
|
39
|
+
date
|
|
40
|
+
)} as your check-in date. It’s available.`
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// @ts-ignore Unknown types
|
|
45
|
+
const clickOnEndDate = (options, date) =>
|
|
46
|
+
fireEvent.click(
|
|
47
|
+
// eslint-disable-next-line testing-library/prefer-screen-queries
|
|
48
|
+
options.getByLabelText(
|
|
49
|
+
`Choose ${formatDateAsCalendarDay(
|
|
50
|
+
date
|
|
51
|
+
)} as your check-out date. It’s available.`
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
describe("date range picker", () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.useFakeTimers();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("should start and end on a date range", async () => {
|
|
61
|
+
const today = moment();
|
|
62
|
+
const oneWeekFromToday = today.clone().add(1, "week");
|
|
63
|
+
render(
|
|
64
|
+
<StatefulDateRangePicker
|
|
65
|
+
startDate={today}
|
|
66
|
+
endDate={oneWeekFromToday}
|
|
67
|
+
setStatusText={() => ""}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
await waitFor(() => {
|
|
71
|
+
expect(getStartDate(screen, today)).toBeInTheDocument();
|
|
72
|
+
expect(getEndDate(screen, oneWeekFromToday)).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("should select a new start and end date", async () => {
|
|
77
|
+
const today = moment();
|
|
78
|
+
const oneWeekFromToday = today.clone().add(1, "week");
|
|
79
|
+
const oneMonthFromNow = today.clone().add(1, "month");
|
|
80
|
+
const oneMonthPlusOneWeekFromNow = oneMonthFromNow.clone().add(1, "week");
|
|
81
|
+
render(
|
|
82
|
+
<StatefulDateRangePicker
|
|
83
|
+
startDate={today}
|
|
84
|
+
endDate={oneWeekFromToday}
|
|
85
|
+
setStatusText={() => ""}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
await waitFor(() => {
|
|
89
|
+
expect(getStartDate(screen, today)).toBeInTheDocument();
|
|
90
|
+
expect(getEndDate(screen, oneWeekFromToday)).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
clickOnStartDate(screen, oneMonthFromNow);
|
|
93
|
+
clickOnEndDate(screen, oneMonthPlusOneWeekFromNow);
|
|
94
|
+
await waitFor(() => {
|
|
95
|
+
expect(getStartDate(screen, today)).not.toBeInTheDocument();
|
|
96
|
+
expect(getEndDate(screen, oneWeekFromToday)).not.toBeInTheDocument();
|
|
97
|
+
expect(getStartDate(screen, oneMonthFromNow)).toBeInTheDocument();
|
|
98
|
+
expect(
|
|
99
|
+
getEndDate(screen, oneMonthPlusOneWeekFromNow)
|
|
100
|
+
).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("should call onDateChange", async () => {
|
|
105
|
+
const onDatesChange = jest.fn();
|
|
106
|
+
const today = moment();
|
|
107
|
+
const oneWeekFromToday = today.clone().add(1, "week");
|
|
108
|
+
const twoWeeksFromToday = today.clone().add(2, "week");
|
|
109
|
+
const threeWeeksFromToday = today.clone().add(3, "week");
|
|
110
|
+
render(
|
|
111
|
+
<StatefulDateRangePicker
|
|
112
|
+
startDate={today}
|
|
113
|
+
endDate={oneWeekFromToday}
|
|
114
|
+
onDatesChange={onDatesChange}
|
|
115
|
+
setStatusText={() => ""}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
clickOnStartDate(screen, twoWeeksFromToday);
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(onDatesChange).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(onDatesChange.mock.calls[0][0].startDate.isValid()).toEqual(true);
|
|
122
|
+
});
|
|
123
|
+
clickOnEndDate(screen, threeWeeksFromToday);
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(onDatesChange).toHaveBeenCalledTimes(2);
|
|
126
|
+
expect(onDatesChange.mock.calls[1][0].endDate.isValid()).toEqual(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("should call onFocusChange", async () => {
|
|
131
|
+
const onFocusChange = jest.fn();
|
|
132
|
+
const today = moment();
|
|
133
|
+
const oneWeekFromToday = today.clone().add(1, "week");
|
|
134
|
+
const oneMonthFromNow = today.clone().add(1, "month");
|
|
135
|
+
const oneMonthPlusOneWeekFromNow = oneMonthFromNow.clone().add(1, "week");
|
|
136
|
+
render(
|
|
137
|
+
<StatefulDateRangePicker
|
|
138
|
+
startDate={today}
|
|
139
|
+
endDate={oneWeekFromToday}
|
|
140
|
+
onFocusChange={onFocusChange}
|
|
141
|
+
setStatusText={() => ""}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
clickOnStartDate(screen, oneMonthFromNow);
|
|
145
|
+
await waitFor(() => {
|
|
146
|
+
expect(onFocusChange).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(onFocusChange).toHaveBeenCalledWith("endDate");
|
|
148
|
+
});
|
|
149
|
+
clickOnEndDate(screen, oneMonthPlusOneWeekFromNow);
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(onFocusChange).toHaveBeenCalledTimes(2);
|
|
152
|
+
expect(onFocusChange).toHaveBeenCalledWith(null);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// TODO: This test times out. We'll come back to fix later
|
|
157
|
+
xtest("should close on escape key press", async () => {
|
|
158
|
+
const buttonText = "Date Picker";
|
|
159
|
+
const initialDate = moment("2020-01-01");
|
|
160
|
+
|
|
161
|
+
const FocusControlledPopoutDatePicker = () => {
|
|
162
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
163
|
+
const [focusedInput, setFocusedInput] = useState<EnumFocusedInput>(null);
|
|
164
|
+
const [dates, pickDate] = useState<{
|
|
165
|
+
startDate: Moment | null;
|
|
166
|
+
endDate: Moment | null;
|
|
167
|
+
}>({
|
|
168
|
+
startDate: null,
|
|
169
|
+
endDate: null,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const onFocusChange = (nextFocusedInput: typeof focusedInput) => {
|
|
173
|
+
if (nextFocusedInput) {
|
|
174
|
+
setIsOpen(true);
|
|
175
|
+
} else {
|
|
176
|
+
setIsOpen(false);
|
|
177
|
+
}
|
|
178
|
+
setFocusedInput(nextFocusedInput);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Popout
|
|
183
|
+
isOpen={isOpen}
|
|
184
|
+
setIsOpen={setIsOpen}
|
|
185
|
+
content={
|
|
186
|
+
<Popout.Content pt={400} p={0}>
|
|
187
|
+
<DateRangePicker
|
|
188
|
+
{...dates}
|
|
189
|
+
onDatesChange={pickDate}
|
|
190
|
+
focusedInput={focusedInput}
|
|
191
|
+
onFocusChange={onFocusChange}
|
|
192
|
+
setStatusText={() => ""}
|
|
193
|
+
initialVisibleMonth={() => initialDate}
|
|
194
|
+
/>
|
|
195
|
+
<Button
|
|
196
|
+
appearance="secondary"
|
|
197
|
+
onClick={() => {
|
|
198
|
+
setIsOpen(true);
|
|
199
|
+
setFocusedInput("startDate");
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{dates.startDate
|
|
203
|
+
? dates.startDate.format("DD MMM YYYY")
|
|
204
|
+
: "Choose Start Date"}
|
|
205
|
+
</Button>
|
|
206
|
+
<Button
|
|
207
|
+
appearance="secondary"
|
|
208
|
+
onClick={() => {
|
|
209
|
+
setIsOpen(true);
|
|
210
|
+
setFocusedInput("endDate");
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
{dates.endDate
|
|
214
|
+
? dates.endDate.format("DD MMM YYYY")
|
|
215
|
+
: "Choose End Date"}
|
|
216
|
+
</Button>
|
|
217
|
+
</Popout.Content>
|
|
218
|
+
}
|
|
219
|
+
>
|
|
220
|
+
<Button
|
|
221
|
+
appearance="secondary"
|
|
222
|
+
onClick={() => {
|
|
223
|
+
setIsOpen(true);
|
|
224
|
+
setFocusedInput("startDate");
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{buttonText}
|
|
228
|
+
</Button>
|
|
229
|
+
</Popout>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
render(<FocusControlledPopoutDatePicker />);
|
|
234
|
+
|
|
235
|
+
expect(screen.getByText(buttonText)).toBeInTheDocument();
|
|
236
|
+
|
|
237
|
+
act(() => {
|
|
238
|
+
fireEvent.click(screen.getByText(buttonText));
|
|
239
|
+
jest.runAllTimers();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await waitFor(() => {
|
|
243
|
+
expect(screen.getByText("January 2020")).toBeInTheDocument();
|
|
244
|
+
expect(screen.getByText("February 2020")).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
act(() => {
|
|
248
|
+
fireEvent.keyDown(screen.getByText("January 2020"), { key: "Escape" });
|
|
249
|
+
jest.runAllTimers();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await waitFor(() => {
|
|
253
|
+
expect(screen.queryByText("January 2020")).not.toBeInTheDocument();
|
|
254
|
+
expect(screen.queryByText("February 2020")).not.toBeInTheDocument();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { DateRangePicker, StatefulDateRangePicker } from "../../index";
|
|
3
|
+
|
|
4
|
+
const mockedRequiredProps = {
|
|
5
|
+
focusedInput: null,
|
|
6
|
+
onDatesChange: jest.fn(),
|
|
7
|
+
onFocusChange: jest.fn(),
|
|
8
|
+
setStatusText: jest.fn(),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
12
|
+
function DateRangePickerTypes() {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<StatefulDateRangePicker setStatusText={() => ""} />
|
|
16
|
+
<StatefulDateRangePicker
|
|
17
|
+
startDate={null}
|
|
18
|
+
endDate={null}
|
|
19
|
+
setStatusText={() => ""}
|
|
20
|
+
/>
|
|
21
|
+
{/* @ts-expect-error - test that invalid type is rejected */}
|
|
22
|
+
<StatefulDateRangePicker startDate="invalid" />
|
|
23
|
+
|
|
24
|
+
<DateRangePicker {...mockedRequiredProps} />
|
|
25
|
+
<DateRangePicker
|
|
26
|
+
{...mockedRequiredProps}
|
|
27
|
+
startDate={null}
|
|
28
|
+
endDate={null}
|
|
29
|
+
/>
|
|
30
|
+
{/* @ts-expect-error - test missing required props is rejected */}
|
|
31
|
+
<DateRangePicker />
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import moment, { type Moment } from "moment";
|
|
3
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
4
|
+
import Popout from "@sproutsocial/seeds-react-popout";
|
|
5
|
+
import SingleDatePicker from "./SingleDatePicker";
|
|
6
|
+
import { StatefulSingleDatePicker } from "./StatefulSingleDatePicker";
|
|
7
|
+
import { formatDateAsCalendarHeader } from "../common";
|
|
8
|
+
|
|
9
|
+
const today = moment("2023-01-06");
|
|
10
|
+
interface DateProps {
|
|
11
|
+
date: Moment | null;
|
|
12
|
+
focused: boolean;
|
|
13
|
+
initialVisibleMonth: () => Moment;
|
|
14
|
+
setStatusText: (dates: Moment[]) => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dateProps: DateProps = {
|
|
18
|
+
date: today,
|
|
19
|
+
focused: true,
|
|
20
|
+
initialVisibleMonth: () => today,
|
|
21
|
+
setStatusText: (dates: Moment[]) =>
|
|
22
|
+
`Now showing: ${dates.map(formatDateAsCalendarHeader).join(" and ")}`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
title: "Components/DatePickers/SingleDatePicker",
|
|
27
|
+
component: StatefulSingleDatePicker,
|
|
28
|
+
};
|
|
29
|
+
export default meta;
|
|
30
|
+
|
|
31
|
+
export const Default = {
|
|
32
|
+
args: dateProps,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const NoStartingDate = {
|
|
36
|
+
args: {
|
|
37
|
+
...dateProps,
|
|
38
|
+
date: null,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const TwoMonthDatePicker = {
|
|
43
|
+
args: {
|
|
44
|
+
...dateProps,
|
|
45
|
+
numberOfMonths: 2,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const DatePickerInPopout = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<Popout
|
|
52
|
+
content={
|
|
53
|
+
<Popout.Content p={0} py={400}>
|
|
54
|
+
<StatefulSingleDatePicker {...dateProps} />
|
|
55
|
+
</Popout.Content>
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
<Button appearance="secondary">Open Date Picker</Button>
|
|
59
|
+
</Popout>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const FocusControlledPopoutDatePicker = {
|
|
64
|
+
render: () => {
|
|
65
|
+
const [focused, setIsFocused] = useState(false);
|
|
66
|
+
const [date, pickDate] = useState<Moment | null>(moment());
|
|
67
|
+
|
|
68
|
+
const onFocusChange = ({ focused }: { focused: boolean }) =>
|
|
69
|
+
setIsFocused(focused);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Popout
|
|
73
|
+
isOpen={focused}
|
|
74
|
+
content={
|
|
75
|
+
<Popout.Content p={0} py={400}>
|
|
76
|
+
<SingleDatePicker
|
|
77
|
+
date={date}
|
|
78
|
+
onDateChange={pickDate}
|
|
79
|
+
focused={focused}
|
|
80
|
+
onFocusChange={onFocusChange}
|
|
81
|
+
setStatusText={dateProps.setStatusText}
|
|
82
|
+
/>
|
|
83
|
+
</Popout.Content>
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
<Button appearance="secondary" onClick={() => setIsFocused(true)}>
|
|
87
|
+
Open Date Picker
|
|
88
|
+
</Button>
|
|
89
|
+
</Popout>
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
};
|