@superdispatch/dates 0.21.6 → 0.21.13

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 (62) hide show
  1. package/.babelrc.js +5 -0
  2. package/.turbo/turbo-version.log +26 -0
  3. package/package.json +58 -32
  4. package/pkg/README.md +10 -0
  5. package/{dist-types → pkg/dist-types}/index.d.ts +237 -237
  6. package/pkg/package.json +32 -0
  7. package/playroom.ts +10 -0
  8. package/src/__tests__/index.spec.ts +48 -0
  9. package/src/base-date-picker/BaseDatePicker.tsx +145 -0
  10. package/src/calendar/Calendar.playroom.tsx +28 -0
  11. package/src/calendar/Calendar.spec.tsx +531 -0
  12. package/src/calendar/Calendar.stories.tsx +50 -0
  13. package/src/calendar/Calendar.tsx +534 -0
  14. package/src/calendar/CalendarQuickSelection.tsx +34 -0
  15. package/src/calendar/InternalCalendarComponents.tsx +79 -0
  16. package/src/date-config/DateConfig.spec.tsx +23 -0
  17. package/src/date-config/DateConfig.tsx +60 -0
  18. package/src/date-field/DateField.playroom.tsx +21 -0
  19. package/src/date-field/DateField.spec.tsx +350 -0
  20. package/src/date-field/DateField.stories.tsx +47 -0
  21. package/src/date-field/DateField.tsx +155 -0
  22. package/src/date-range-field/DateRangeField.playroom.tsx +24 -0
  23. package/src/date-range-field/DateRangeField.spec.tsx +318 -0
  24. package/src/date-range-field/DateRangeField.stories.tsx +51 -0
  25. package/src/date-range-field/DateRangeField.tsx +277 -0
  26. package/src/date-time-utils/DateTimeUtils.spec.ts +652 -0
  27. package/src/date-time-utils/DateTimeUtils.ts +339 -0
  28. package/src/date-utils/DateUtils.spec.ts +234 -0
  29. package/src/date-utils/DateUtils.ts +333 -0
  30. package/src/formatted-date/FormattedDate.spec.tsx +103 -0
  31. package/src/formatted-date/FormattedDate.ts +42 -0
  32. package/src/formatted-relative-time/FormattedRelativeTime.spec.tsx +93 -0
  33. package/src/formatted-relative-time/FormattedRelativeTime.ts +60 -0
  34. package/src/index.ts +12 -0
  35. package/src/time-field/TimeField.playroom.tsx +21 -0
  36. package/src/time-field/TimeField.stories.tsx +35 -0
  37. package/src/time-field/TimeField.tsx +221 -0
  38. package/src/use-date-time/useDateTime.spec.ts +45 -0
  39. package/src/use-date-time/useDateTime.ts +31 -0
  40. package/src/use-date-time-range/useDateTimeRange.spec.ts +53 -0
  41. package/src/use-date-time-range/useDateTimeRange.ts +24 -0
  42. package/tsconfig.json +19 -0
  43. package/LICENSE +0 -21
  44. /package/{dist-node → pkg/dist-node}/index.js +0 -0
  45. /package/{dist-node → pkg/dist-node}/index.js.map +0 -0
  46. /package/{dist-src → pkg/dist-src}/base-date-picker/BaseDatePicker.js +0 -0
  47. /package/{dist-src → pkg/dist-src}/calendar/Calendar.js +0 -0
  48. /package/{dist-src → pkg/dist-src}/calendar/CalendarQuickSelection.js +0 -0
  49. /package/{dist-src → pkg/dist-src}/calendar/InternalCalendarComponents.js +0 -0
  50. /package/{dist-src → pkg/dist-src}/date-config/DateConfig.js +0 -0
  51. /package/{dist-src → pkg/dist-src}/date-field/DateField.js +0 -0
  52. /package/{dist-src → pkg/dist-src}/date-range-field/DateRangeField.js +0 -0
  53. /package/{dist-src → pkg/dist-src}/date-time-utils/DateTimeUtils.js +0 -0
  54. /package/{dist-src → pkg/dist-src}/date-utils/DateUtils.js +0 -0
  55. /package/{dist-src → pkg/dist-src}/formatted-date/FormattedDate.js +0 -0
  56. /package/{dist-src → pkg/dist-src}/formatted-relative-time/FormattedRelativeTime.js +0 -0
  57. /package/{dist-src → pkg/dist-src}/index.js +0 -0
  58. /package/{dist-src → pkg/dist-src}/time-field/TimeField.js +0 -0
  59. /package/{dist-src → pkg/dist-src}/use-date-time/useDateTime.js +0 -0
  60. /package/{dist-src → pkg/dist-src}/use-date-time-range/useDateTimeRange.js +0 -0
  61. /package/{dist-web → pkg/dist-web}/index.js +0 -0
  62. /package/{dist-web → pkg/dist-web}/index.js.map +0 -0
@@ -0,0 +1,318 @@
1
+ import {
2
+ mockDate,
3
+ renderComponent,
4
+ renderCSS,
5
+ } from '@superdispatch/ui-testutils';
6
+ import { fireEvent, Matcher, screen } from '@testing-library/react';
7
+ import userEvent from '@testing-library/user-event';
8
+ import { DateTime } from 'luxon';
9
+ import { useState } from 'react';
10
+ import { defaultDateConfig } from '../date-config/DateConfig';
11
+ import {
12
+ DateStringRange,
13
+ DateTimeRange,
14
+ } from '../date-time-utils/DateTimeUtils';
15
+ import { DateRangeField, DateRangeFieldProps } from './DateRangeField';
16
+
17
+ function queryByClassName(classNames: string): Element[] {
18
+ return Array.from(document.getElementsByClassName(classNames));
19
+ }
20
+
21
+ function UncontrolledDateRangeField(props: DateRangeFieldProps) {
22
+ const [value, setValue] = useState<DateTimeRange>();
23
+
24
+ return (
25
+ <DateRangeField
26
+ {...props}
27
+ value={value}
28
+ onChange={({ dateValue }) => {
29
+ setValue(dateValue);
30
+ }}
31
+ />
32
+ );
33
+ }
34
+
35
+ beforeEach(() => {
36
+ mockDate();
37
+ });
38
+
39
+ test('basic', () => {
40
+ const onChange = jest.fn();
41
+ renderComponent(<DateRangeField label="Range" onChange={onChange} />);
42
+
43
+ expect(screen.queryByRole('grid')).toBeNull();
44
+ expect(screen.getByLabelText('Range')).toHaveValue('');
45
+
46
+ userEvent.click(screen.getByLabelText('Range'));
47
+
48
+ expect(onChange).not.toHaveBeenCalled();
49
+
50
+ expect(screen.queryAllByRole('grid')).toHaveLength(2);
51
+
52
+ userEvent.click(screen.getByRole('gridcell', { name: /May 24/ }));
53
+
54
+ expect(onChange).toHaveBeenCalledTimes(1);
55
+ expect(onChange).toHaveBeenLastCalledWith({
56
+ config: defaultDateConfig,
57
+ dateValue: [expect.any(DateTime), null],
58
+ stringValue: ['2019-05-24T00:00:00.000-05:00', null],
59
+ });
60
+
61
+ expect(screen.queryAllByRole('grid')).toHaveLength(2);
62
+ expect(screen.getByLabelText('Range')).toHaveValue('');
63
+ });
64
+
65
+ test('uncontrolled', () => {
66
+ renderComponent(<UncontrolledDateRangeField label="Range" />);
67
+
68
+ expect(screen.getByLabelText('Range')).toHaveValue('');
69
+
70
+ userEvent.click(screen.getByLabelText('Range'));
71
+
72
+ userEvent.click(screen.getByRole('gridcell', { name: /May 24/ }));
73
+
74
+ expect(screen.getByLabelText('Range')).toHaveValue('May 24, 2019 - …');
75
+
76
+ userEvent.click(screen.getByRole('gridcell', { name: /May 30/ }));
77
+
78
+ expect(screen.queryByRole('grid')).toBeNull();
79
+ expect(screen.getByLabelText('Range')).toHaveValue('May 24 - May 30, 2019');
80
+ });
81
+
82
+ test('close on select', () => {
83
+ renderComponent(<DateRangeField value={[Date.now()]} />);
84
+
85
+ expect(screen.queryByRole('grid')).toBeNull();
86
+
87
+ userEvent.click(screen.getByRole('textbox'));
88
+ userEvent.click(screen.getByRole('gridcell', { name: /May 25/ }));
89
+
90
+ expect(screen.queryByRole('grid')).toBeNull();
91
+ });
92
+
93
+ test('selected days', () => {
94
+ renderComponent(<DateRangeField value={['2019-05-24T00:00:00.000-05:00']} />);
95
+
96
+ function assertSelection(startDay: number, finishDay?: number): void {
97
+ const selected = queryByClassName('SD-DateRangeField-selected').filter(
98
+ (element) => !element.classList.contains('SD-DateRangeField-outside'),
99
+ );
100
+
101
+ const startDays = selected.filter((element) =>
102
+ element.classList.contains('SD-DateRangeField-rangeStart'),
103
+ );
104
+ const finishDays = selected.filter((element) =>
105
+ element.classList.contains('SD-DateRangeField-rangeFinish'),
106
+ );
107
+
108
+ const [startDate] = selected;
109
+ const finishDate = selected[selected.length - 1];
110
+
111
+ expect(startDays).toHaveLength(1);
112
+ expect(startDays[0]).toHaveTextContent(String(startDay));
113
+
114
+ if (finishDay == null) {
115
+ // eslint-disable-next-line jest/no-conditional-expect
116
+ expect(selected).toHaveLength(1);
117
+ // eslint-disable-next-line jest/no-conditional-expect
118
+ expect(startDate).toBe(finishDate);
119
+ // eslint-disable-next-line jest/no-conditional-expect
120
+ expect(finishDays).toHaveLength(0);
121
+ } else {
122
+ // eslint-disable-next-line jest/no-conditional-expect
123
+ expect(selected).toHaveLength(finishDay - startDay + 1);
124
+
125
+ // eslint-disable-next-line jest/no-conditional-expect
126
+ expect(finishDays).toHaveLength(1);
127
+ // eslint-disable-next-line jest/no-conditional-expect
128
+ expect(finishDays[0]).toHaveTextContent(String(finishDay));
129
+
130
+ for (let i = 0; i < selected.length; i++) {
131
+ // eslint-disable-next-line jest/no-conditional-expect
132
+ expect(selected[i]).toHaveTextContent(String(startDay + i));
133
+ }
134
+ }
135
+ }
136
+
137
+ userEvent.click(screen.getByRole('textbox'));
138
+
139
+ assertSelection(24);
140
+
141
+ fireEvent.mouseEnter(screen.getByRole('gridcell', { name: /May 24/ }));
142
+
143
+ assertSelection(24, 24);
144
+
145
+ fireEvent.mouseEnter(screen.getByRole('gridcell', { name: /May 26/ }));
146
+
147
+ assertSelection(24, 26);
148
+
149
+ fireEvent.mouseEnter(screen.getByRole('gridcell', { name: /May 20/ }));
150
+
151
+ assertSelection(20, 24);
152
+ });
153
+
154
+ test('disabledDays', () => {
155
+ const onChange = jest.fn();
156
+ const onDayClick = jest.fn();
157
+
158
+ renderComponent(
159
+ <DateRangeField
160
+ onChange={onChange}
161
+ CalendarProps={{
162
+ onDayClick,
163
+ disabledDays: ({ dateValue }) => dateValue.day >= 24,
164
+ }}
165
+ />,
166
+ );
167
+
168
+ userEvent.click(screen.getByRole('textbox'));
169
+
170
+ expect(onChange).not.toHaveBeenCalled();
171
+ expect(onDayClick).not.toHaveBeenCalled();
172
+
173
+ expect(screen.getByRole('gridcell', { name: /May 24/ })).toHaveClass(
174
+ 'SD-Calendar-disabled',
175
+ );
176
+
177
+ userEvent.click(screen.getByRole('gridcell', { name: /May 24/ }));
178
+
179
+ expect(onChange).not.toHaveBeenCalled();
180
+ expect(onDayClick).toHaveBeenCalledTimes(1);
181
+ });
182
+
183
+ test('enableClearable', () => {
184
+ const onChange = jest.fn();
185
+ const view = renderComponent(
186
+ <DateRangeField onChange={onChange} enableClearable={true} />,
187
+ );
188
+
189
+ expect(screen.queryByRole('button', { name: 'clear' })).toBeNull();
190
+
191
+ view.rerender(
192
+ <DateRangeField
193
+ onChange={onChange}
194
+ enableClearable={true}
195
+ value={[Date.now(), undefined]}
196
+ />,
197
+ );
198
+ expect(screen.queryByRole('button', { name: 'clear' })).toBeNull();
199
+
200
+ view.rerender(
201
+ <DateRangeField
202
+ onChange={onChange}
203
+ enableClearable={true}
204
+ value={[Date.now(), Date.now()]}
205
+ />,
206
+ );
207
+
208
+ expect(screen.getByRole('button', { name: 'clear' })).toBeInTheDocument();
209
+
210
+ expect(onChange).not.toHaveBeenCalled();
211
+
212
+ userEvent.click(screen.getByRole('button', { name: 'clear' }));
213
+
214
+ expect(onChange).toHaveBeenCalledTimes(1);
215
+ expect(onChange).toHaveBeenLastCalledWith({
216
+ config: defaultDateConfig,
217
+ dateValue: [null, null],
218
+ stringValue: [null, null],
219
+ });
220
+
221
+ view.rerender(
222
+ <DateRangeField
223
+ label="Custom Label"
224
+ onChange={onChange}
225
+ enableClearable={true}
226
+ value={[Date.now(), Date.now()]}
227
+ />,
228
+ );
229
+
230
+ expect(screen.queryByRole('button', { name: 'clear' })).toBeNull();
231
+
232
+ userEvent.click(screen.getByRole('button', { name: 'clear Custom Label' }));
233
+
234
+ expect(onChange).toHaveBeenCalledTimes(2);
235
+ expect(onChange).toHaveBeenLastCalledWith({
236
+ config: defaultDateConfig,
237
+ dateValue: [null, null],
238
+ stringValue: [null, null],
239
+ });
240
+ });
241
+
242
+ test('time normalization', () => {
243
+ const view = renderComponent(<DateRangeField />);
244
+
245
+ const variants: Array<
246
+ [
247
+ input: undefined | DateStringRange,
248
+ matcher: Matcher,
249
+ result: DateStringRange,
250
+ ]
251
+ > = [
252
+ [undefined, /May 24/, ['2019-05-24T00:00:00.000-05:00', null]],
253
+
254
+ [
255
+ ['2019-05-29T00:00:00.000-05:00', null],
256
+ /May 24/,
257
+ ['2019-05-24T00:00:00.000-05:00', '2019-05-29T23:59:59.999-05:00'],
258
+ ],
259
+
260
+ [
261
+ ['2019-05-29T10:11:12.134-05:00', null],
262
+ /May 24/,
263
+ ['2019-05-24T10:11:12.134-05:00', '2019-05-29T23:59:59.999-05:00'],
264
+ ],
265
+ ];
266
+
267
+ for (const [input, labelMatcher, stringValue] of variants) {
268
+ const onChange = jest.fn();
269
+
270
+ view.rerender(
271
+ <DateRangeField
272
+ id="range"
273
+ label="Range"
274
+ value={input}
275
+ onChange={onChange}
276
+ />,
277
+ );
278
+
279
+ userEvent.click(screen.getByLabelText('Range'));
280
+ userEvent.click(screen.getByLabelText(labelMatcher));
281
+
282
+ expect(onChange).toHaveBeenCalledTimes(1);
283
+ expect(onChange).toHaveBeenLastCalledWith({
284
+ stringValue,
285
+ config: defaultDateConfig,
286
+ dateValue: stringValue.map((x) => x && expect.any(DateTime)),
287
+ });
288
+ }
289
+ });
290
+
291
+ test('css', () => {
292
+ expect(renderCSS(<DateRangeField />, ['SD-DateRangeField']))
293
+ .toMatchInlineSnapshot(`
294
+ .SD-DateRangeField-day.SD-DateRangeField-selected:not(.SD-DateRangeField-outside).SD-DateRangeField-rangeStart:before {
295
+ left: 4px;
296
+ }
297
+
298
+ .SD-DateRangeField-day.SD-DateRangeField-selected:not(.SD-DateRangeField-outside).SD-DateRangeField-rangeFinish:before {
299
+ right: 4px;
300
+ }
301
+
302
+ .SD-DateRangeField-day.SD-DateRangeField-selected:not(.SD-DateRangeField-outside):not(.SD-DateRangeField-rangeStart):not(.SD-DateRangeField-rangeFinish):after {
303
+ background-color: Color.Transparent;
304
+ }
305
+
306
+ .SD-DateRangeField-day.SD-DateRangeField-selected:not(.SD-DateRangeField-outside):not(.SD-DateRangeField-rangeStart):not(.SD-DateRangeField-rangeFinish):not(.SD-DateRangeField-disabled) {
307
+ color: Color.Blue500;
308
+ }
309
+
310
+ .SD-DateRangeField-day.SD-DateRangeField-selected:not(.SD-DateRangeField-outside):not(.SD-DateRangeField-rangeStart):not(.SD-DateRangeField-rangeFinish):not(.SD-DateRangeField-disabled):before {
311
+ background-color: Color.Blue50;
312
+ }
313
+
314
+ .SD-DateRangeField-day.SD-DateRangeField-selected:not(.SD-DateRangeField-outside):not(.SD-DateRangeField-rangeStart):not(.SD-DateRangeField-rangeFinish).SD-DateRangeField-disabled:before {
315
+ background-color: Color.Silver100;
316
+ }
317
+ `);
318
+ });
@@ -0,0 +1,51 @@
1
+ import { InputAdornment } from '@material-ui/core';
2
+ import { Meta } from '@storybook/react';
3
+ import { DateRangeField } from './DateRangeField.playroom';
4
+
5
+ export default {
6
+ title: 'Dates/DateRangeField',
7
+ component: DateRangeField,
8
+ } as Meta;
9
+
10
+ export const basic = () => <DateRangeField />;
11
+
12
+ export const advanced = () => (
13
+ <DateRangeField
14
+ label="Label"
15
+ placeholder="Placeholder"
16
+ helperText="Helper Text"
17
+ />
18
+ );
19
+
20
+ export const errorState = () => (
21
+ <DateRangeField
22
+ label="Label"
23
+ error={true}
24
+ placeholder="Placeholder"
25
+ helperText="Error Text"
26
+ />
27
+ );
28
+
29
+ export const adornment = () => (
30
+ <DateRangeField
31
+ InputProps={{
32
+ startAdornment: (
33
+ <InputAdornment position="start">Start Adornment:</InputAdornment>
34
+ ),
35
+ }}
36
+ />
37
+ );
38
+
39
+ export const fullWidth = () => <DateRangeField fullWidth={true} />;
40
+
41
+ export const disabled = () => <DateRangeField disabled={true} />;
42
+
43
+ export const enableClearable = () => <DateRangeField enableClearable={true} />;
44
+
45
+ export const disableCloseOnSelect = () => (
46
+ <DateRangeField disableCloseOnSelect={true} />
47
+ );
48
+
49
+ export const customEmptyText = () => (
50
+ <DateRangeField fallback="Never" enableClearable={true} />
51
+ );
@@ -0,0 +1,277 @@
1
+ import { BaseTextFieldProps, InputBaseProps } from '@material-ui/core';
2
+ import { makeStyles } from '@material-ui/styles';
3
+ import { Color, SuperDispatchTheme } from '@superdispatch/ui';
4
+ import { forwardRef, ReactNode, useMemo, useRef, useState } from 'react';
5
+ import {
6
+ BaseDatePicker,
7
+ InternalBaseDateFieldAPI,
8
+ } from '../base-date-picker/BaseDatePicker';
9
+ import {
10
+ Calendar,
11
+ CalendarClassNames,
12
+ CalendarProps,
13
+ } from '../calendar/Calendar';
14
+ import { DateFormat, useDateConfig } from '../date-config/DateConfig';
15
+ import {
16
+ DateRangePayload,
17
+ formatDateRange,
18
+ NullableDateRangeInput,
19
+ parseDateRange,
20
+ stringifyDateRange,
21
+ toDateRangePayload,
22
+ } from '../date-time-utils/DateTimeUtils';
23
+ import { useDateTimeRange } from '../use-date-time-range/useDateTimeRange';
24
+
25
+ const useStyles = makeStyles<
26
+ SuperDispatchTheme,
27
+ CalendarProps,
28
+ | 'rangeStart'
29
+ | 'rangeFinish'
30
+ | Extract<CalendarClassNames, 'outside' | 'disabled' | 'selected' | 'day'>
31
+ >(
32
+ (theme) => ({
33
+ rangeStart: {},
34
+ rangeFinish: {},
35
+
36
+ outside: {},
37
+ disabled: {},
38
+ selected: {},
39
+
40
+ day: {
41
+ '&$selected:not($outside)': {
42
+ '&$rangeStart:before': {
43
+ left: theme.spacing(0.5),
44
+ },
45
+
46
+ '&$rangeFinish:before': {
47
+ right: theme.spacing(0.5),
48
+ },
49
+
50
+ '&:not($rangeStart):not($rangeFinish)': {
51
+ '&:after': {
52
+ backgroundColor: Color.Transparent,
53
+ },
54
+
55
+ '&$disabled': {
56
+ '&:before': {
57
+ backgroundColor: Color.Silver100,
58
+ },
59
+ },
60
+
61
+ '&:not($disabled)': {
62
+ color: Color.Blue500,
63
+
64
+ '&:before': {
65
+ backgroundColor: Color.Blue50,
66
+ },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ }),
72
+ { name: 'SD-DateRangeField' },
73
+ );
74
+
75
+ interface DateRangeFieldAPI extends DateRangePayload {
76
+ close: () => void;
77
+ change: (value: NullableDateRangeInput) => void;
78
+ }
79
+
80
+ export interface DateRangeFieldProps
81
+ extends Pick<
82
+ BaseTextFieldProps,
83
+ | 'disabled'
84
+ | 'error'
85
+ | 'fullWidth'
86
+ | 'helperText'
87
+ | 'id'
88
+ | 'label'
89
+ | 'name'
90
+ | 'required'
91
+ | 'placeholder'
92
+ > {
93
+ fallback?: string;
94
+ enableClearable?: boolean;
95
+ disableCloseOnSelect?: boolean;
96
+
97
+ format?: DateFormat;
98
+ value?: NullableDateRangeInput;
99
+
100
+ onBlur?: () => void;
101
+ onFocus?: () => void;
102
+ onChange?: (value: DateRangePayload) => void;
103
+
104
+ renderFooter?: (api: DateRangeFieldAPI) => ReactNode;
105
+ renderQuickSelection?: (api: DateRangeFieldAPI) => ReactNode;
106
+
107
+ InputProps?: Pick<
108
+ InputBaseProps,
109
+ 'aria-label' | 'aria-labelledby' | 'startAdornment'
110
+ >;
111
+ CalendarProps?: Omit<
112
+ CalendarProps,
113
+ 'footer' | 'classes' | 'selectedDays' | 'quickSelection' | 'numberOfMonths'
114
+ >;
115
+ }
116
+
117
+ export const DateRangeField = forwardRef<HTMLDivElement, DateRangeFieldProps>(
118
+ (
119
+ {
120
+ onBlur,
121
+ onFocus,
122
+ onChange,
123
+ renderFooter,
124
+ renderQuickSelection,
125
+
126
+ value: valueProp,
127
+ format: formatProp,
128
+
129
+ fallback = '',
130
+
131
+ enableClearable,
132
+ disableCloseOnSelect,
133
+
134
+ CalendarProps: {
135
+ modifiers,
136
+ onDayClick,
137
+ onDayMouseEnter,
138
+ ...calendarProps
139
+ } = {} as const,
140
+
141
+ ...textFieldProps
142
+ },
143
+ ref,
144
+ ) => {
145
+ const apiRef = useRef<InternalBaseDateFieldAPI>(null);
146
+ const { rangeStart, rangeFinish, ...styles } = useStyles({});
147
+
148
+ const config = useDateConfig({ format: formatProp });
149
+ const [startDate, finishDate] = useDateTimeRange(valueProp, config);
150
+ const [startDateString, finishDateString] = useMemo(
151
+ () => stringifyDateRange([startDate, finishDate], config),
152
+ [config, startDate, finishDate],
153
+ );
154
+ const displayValue = useMemo(
155
+ () => formatDateRange([startDate, finishDate], { fallback }, config),
156
+ [config, fallback, startDate, finishDate],
157
+ );
158
+
159
+ const [hoveredDate, setHoveredDate] = useState<number>();
160
+ const [calendarStartDate, calendarFinishDate] = useMemo(() => {
161
+ const [nextCalendarStartDate, nextCalendarFinishDate] = parseDateRange(
162
+ [startDate, finishDate || hoveredDate],
163
+ config,
164
+ );
165
+
166
+ return [
167
+ nextCalendarStartDate?.startOf('day'),
168
+ nextCalendarFinishDate?.endOf('day'),
169
+ ];
170
+ }, [config, startDate, finishDate, hoveredDate]);
171
+
172
+ function handleClose(): void {
173
+ apiRef.current?.close();
174
+ }
175
+
176
+ function handleChange(nextValue: NullableDateRangeInput): void {
177
+ let [nextStartDate, nextFinishDate] = parseDateRange(nextValue, config);
178
+
179
+ if (onChange) {
180
+ if (nextStartDate) {
181
+ if (startDate) {
182
+ nextStartDate = nextStartDate.set({
183
+ hour: startDate.hour,
184
+ minute: startDate.minute,
185
+ second: startDate.second,
186
+ millisecond: startDate.millisecond,
187
+ });
188
+ } else {
189
+ nextStartDate = nextStartDate.startOf('day');
190
+ }
191
+ }
192
+
193
+ if (nextFinishDate) {
194
+ nextFinishDate = nextFinishDate.endOf('day');
195
+ }
196
+
197
+ onChange(toDateRangePayload([nextStartDate, nextFinishDate], config));
198
+ }
199
+
200
+ if (!disableCloseOnSelect && nextFinishDate?.isValid) {
201
+ handleClose();
202
+ }
203
+ }
204
+
205
+ const api: DateRangeFieldAPI = {
206
+ config,
207
+ close: handleClose,
208
+ change: handleChange,
209
+ dateValue: [startDate, finishDate],
210
+ stringValue: [startDateString, finishDateString],
211
+ };
212
+
213
+ return (
214
+ <BaseDatePicker
215
+ {...textFieldProps}
216
+ ref={ref}
217
+ api={apiRef}
218
+ value={displayValue || fallback}
219
+ enableClearable={enableClearable && !!startDate && !!finishDate}
220
+ onClear={() => {
221
+ handleChange([undefined, undefined]);
222
+ }}
223
+ onClose={() => {
224
+ onBlur?.();
225
+ setHoveredDate(undefined);
226
+ }}
227
+ >
228
+ <Calendar
229
+ numberOfMonths={2}
230
+ {...calendarProps}
231
+ classes={styles}
232
+ initialMonth={startDateString}
233
+ modifiers={{
234
+ ...modifiers,
235
+ [rangeStart]: ({ dateValue }) =>
236
+ !!calendarStartDate?.hasSame(dateValue, 'day'),
237
+ [rangeFinish]: ({ dateValue }) =>
238
+ !!calendarFinishDate?.hasSame(dateValue, 'day'),
239
+ }}
240
+ selectedDays={({ dateValue }) => {
241
+ if (calendarStartDate) {
242
+ if (!calendarFinishDate) {
243
+ return calendarStartDate.hasSame(dateValue, 'day');
244
+ }
245
+
246
+ return (
247
+ calendarStartDate <= dateValue &&
248
+ dateValue <= calendarFinishDate
249
+ );
250
+ }
251
+
252
+ return false;
253
+ }}
254
+ footer={renderFooter?.(api)}
255
+ quickSelection={renderQuickSelection?.(api)}
256
+ onDayMouseEnter={(event) => {
257
+ onDayMouseEnter?.(event);
258
+ setHoveredDate(
259
+ !event.disabled ? event.dateValue.valueOf() : undefined,
260
+ );
261
+ }}
262
+ onDayClick={(event) => {
263
+ onDayClick?.(event);
264
+
265
+ if (!event.disabled) {
266
+ if (startDate && !finishDate) {
267
+ handleChange([startDateString, event.stringValue]);
268
+ } else {
269
+ handleChange([event.stringValue, undefined]);
270
+ }
271
+ }
272
+ }}
273
+ />
274
+ </BaseDatePicker>
275
+ );
276
+ },
277
+ );