react-day-picker 8.0.3 → 8.0.6

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 (207) hide show
  1. package/README.md +1 -1
  2. package/dist/hooks/useDayRender/useDayRender.d.ts +1 -1
  3. package/dist/hooks/useInput/useInput.d.ts +2 -2
  4. package/dist/index.esm.js +3 -3
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/index.js.map +1 -1
  8. package/dist/react-day-picker.min.js +1 -0
  9. package/dist/style.css +11 -10
  10. package/dist/style.module.css +11 -10
  11. package/dist/types/Styles.d.ts +8 -2
  12. package/package.json +15 -13
  13. package/src/DayPicker.tsx +113 -0
  14. package/src/components/Button/Button.test.tsx +47 -0
  15. package/src/components/Button/Button.tsx +36 -0
  16. package/src/components/Button/index.ts +1 -0
  17. package/src/components/Caption/Caption.test.tsx +86 -0
  18. package/src/components/Caption/Caption.tsx +54 -0
  19. package/src/components/Caption/index.ts +1 -0
  20. package/src/components/CaptionDropdowns/CaptionDropdowns.test.tsx +123 -0
  21. package/src/components/CaptionDropdowns/CaptionDropdowns.tsx +43 -0
  22. package/src/components/CaptionDropdowns/index.ts +1 -0
  23. package/src/components/CaptionLabel/CaptionLabel.test.tsx +29 -0
  24. package/src/components/CaptionLabel/CaptionLabel.tsx +32 -0
  25. package/src/components/CaptionLabel/index.ts +1 -0
  26. package/src/components/CaptionNavigation/CaptionNavigation.test.tsx +172 -0
  27. package/src/components/CaptionNavigation/CaptionNavigation.tsx +63 -0
  28. package/src/components/CaptionNavigation/index.ts +1 -0
  29. package/src/components/Day/Day.test.tsx +84 -0
  30. package/src/components/Day/Day.tsx +30 -0
  31. package/src/components/Day/index.ts +1 -0
  32. package/src/components/DayContent/DayContent.test.tsx +51 -0
  33. package/src/components/DayContent/DayContent.tsx +36 -0
  34. package/src/components/DayContent/index.ts +1 -0
  35. package/src/components/Dropdown/Dropdown.test.tsx +73 -0
  36. package/src/components/Dropdown/Dropdown.tsx +56 -0
  37. package/src/components/Dropdown/index.ts +1 -0
  38. package/src/components/Footer/Footer.test.tsx +29 -0
  39. package/src/components/Footer/Footer.tsx +20 -0
  40. package/src/components/Footer/index.ts +1 -0
  41. package/src/components/Head/Head.test.tsx +117 -0
  42. package/src/components/Head/Head.tsx +51 -0
  43. package/src/components/Head/index.ts +1 -0
  44. package/src/components/Head/utils/getWeekdays.test.ts +36 -0
  45. package/src/components/Head/utils/getWeekdays.ts +22 -0
  46. package/src/components/Head/utils/index.ts +1 -0
  47. package/src/components/IconDropdown/IconDropdown.test.tsx +20 -0
  48. package/src/components/IconDropdown/IconDropdown.tsx +24 -0
  49. package/src/components/IconDropdown/index.ts +1 -0
  50. package/src/components/IconLeft/IconLeft.test.tsx +20 -0
  51. package/src/components/IconLeft/IconLeft.tsx +18 -0
  52. package/src/components/IconLeft/index.ts +1 -0
  53. package/src/components/IconRight/IconRight.test.tsx +20 -0
  54. package/src/components/IconRight/IconRight.tsx +17 -0
  55. package/src/components/IconRight/index.ts +1 -0
  56. package/src/components/Month/Month.test.tsx +216 -0
  57. package/src/components/Month/Month.tsx +53 -0
  58. package/src/components/Month/index.ts +1 -0
  59. package/src/components/MonthsDropdown/MonthsDropdown.test.tsx +99 -0
  60. package/src/components/MonthsDropdown/MonthsDropdown.tsx +75 -0
  61. package/src/components/MonthsDropdown/index.ts +1 -0
  62. package/src/components/Navigation/Navigation.test.tsx +129 -0
  63. package/src/components/Navigation/Navigation.tsx +102 -0
  64. package/src/components/Navigation/index.ts +1 -0
  65. package/src/components/Root/Root.test.tsx +123 -0
  66. package/src/components/Root/Root.tsx +58 -0
  67. package/src/components/Root/index.ts +1 -0
  68. package/src/components/Row/Row.test.tsx +69 -0
  69. package/src/components/Row/Row.tsx +51 -0
  70. package/src/components/Row/index.ts +1 -0
  71. package/src/components/Table/Table.test.tsx +42 -0
  72. package/src/components/Table/Table.tsx +60 -0
  73. package/src/components/Table/__snapshots__/Table.test.tsx.snap +1453 -0
  74. package/src/components/Table/index.ts +1 -0
  75. package/src/components/Table/utils/daysToMonthWeeks.ts +47 -0
  76. package/src/components/Table/utils/getMonthWeeks.test.ts +68 -0
  77. package/src/components/Table/utils/getMonthWeeks.ts +55 -0
  78. package/src/components/WeekNumber/WeekNumber.test.tsx +46 -0
  79. package/src/components/WeekNumber/WeekNumber.tsx +58 -0
  80. package/src/components/WeekNumber/__snapshots__/WeekNumber.test.tsx.snap +11 -0
  81. package/src/components/WeekNumber/index.ts +1 -0
  82. package/src/components/YearsDropdown/YearsDropdown.test.tsx +98 -0
  83. package/src/components/YearsDropdown/YearsDropdown.tsx +76 -0
  84. package/src/components/YearsDropdown/index.ts +1 -0
  85. package/src/contexts/DayPicker/DayPickerContext.tsx +156 -0
  86. package/src/contexts/DayPicker/defaultClassNames.ts +58 -0
  87. package/src/contexts/DayPicker/defaultContextValue.ts +37 -0
  88. package/src/contexts/DayPicker/formatters/formatCaption.test.ts +15 -0
  89. package/src/contexts/DayPicker/formatters/formatCaption.ts +12 -0
  90. package/src/contexts/DayPicker/formatters/formatDay.test.ts +7 -0
  91. package/src/contexts/DayPicker/formatters/formatDay.ts +9 -0
  92. package/src/contexts/DayPicker/formatters/formatMonthCaption.test.ts +15 -0
  93. package/src/contexts/DayPicker/formatters/formatMonthCaption.ts +12 -0
  94. package/src/contexts/DayPicker/formatters/formatWeekNumber.test.ts +5 -0
  95. package/src/contexts/DayPicker/formatters/formatWeekNumber.ts +6 -0
  96. package/src/contexts/DayPicker/formatters/formatWeekdayName.test.ts +15 -0
  97. package/src/contexts/DayPicker/formatters/formatWeekdayName.ts +12 -0
  98. package/src/contexts/DayPicker/formatters/formatYearCaption.test.ts +7 -0
  99. package/src/contexts/DayPicker/formatters/formatYearCaption.ts +11 -0
  100. package/src/contexts/DayPicker/formatters/index.ts +6 -0
  101. package/src/contexts/DayPicker/index.ts +2 -0
  102. package/src/contexts/DayPicker/labels/index.ts +7 -0
  103. package/src/contexts/DayPicker/labels/labelDay.test.ts +7 -0
  104. package/src/contexts/DayPicker/labels/labelDay.ts +10 -0
  105. package/src/contexts/DayPicker/labels/labelMonthDropdown.test.ts +5 -0
  106. package/src/contexts/DayPicker/labels/labelMonthDropdown.ts +6 -0
  107. package/src/contexts/DayPicker/labels/labelNext.test.ts +5 -0
  108. package/src/contexts/DayPicker/labels/labelNext.ts +8 -0
  109. package/src/contexts/DayPicker/labels/labelPrevious.test.ts +5 -0
  110. package/src/contexts/DayPicker/labels/labelPrevious.ts +8 -0
  111. package/src/contexts/DayPicker/labels/labelWeekNumber.test.ts +5 -0
  112. package/src/contexts/DayPicker/labels/labelWeekNumber.ts +8 -0
  113. package/src/contexts/DayPicker/labels/labelWeekday.test.ts +15 -0
  114. package/src/contexts/DayPicker/labels/labelWeekday.ts +10 -0
  115. package/src/contexts/DayPicker/labels/labelYearDropdown.test.ts +5 -0
  116. package/src/contexts/DayPicker/labels/labelYearDropdown.ts +6 -0
  117. package/src/contexts/DayPicker/useDayPicker.test.ts +297 -0
  118. package/src/contexts/DayPicker/useDayPicker.ts +17 -0
  119. package/src/contexts/DayPicker/utils/index.ts +1 -0
  120. package/src/contexts/DayPicker/utils/parseFromToProps.test.ts +47 -0
  121. package/src/contexts/DayPicker/utils/parseFromToProps.ts +32 -0
  122. package/src/contexts/Focus/FocusContext.tsx +174 -0
  123. package/src/contexts/Focus/index.ts +2 -0
  124. package/src/contexts/Focus/useFocusContext.test.ts +183 -0
  125. package/src/contexts/Focus/useFocusContext.ts +12 -0
  126. package/src/contexts/Focus/utils/getInitialFocusTarget.test.tsx +12 -0
  127. package/src/contexts/Focus/utils/getInitialFocusTarget.tsx +44 -0
  128. package/src/contexts/Modifiers/ModifiersContext.tsx +44 -0
  129. package/src/contexts/Modifiers/index.ts +2 -0
  130. package/src/contexts/Modifiers/useModifiers.test.ts +46 -0
  131. package/src/contexts/Modifiers/useModifiers.ts +17 -0
  132. package/src/contexts/Modifiers/utils/getActiveModifiers.test.ts +53 -0
  133. package/src/contexts/Modifiers/utils/getActiveModifiers.ts +33 -0
  134. package/src/contexts/Modifiers/utils/getCustomModifiers.test.ts +14 -0
  135. package/src/contexts/Modifiers/utils/getCustomModifiers.ts +14 -0
  136. package/src/contexts/Modifiers/utils/getInternalModifiers.test.ts +146 -0
  137. package/src/contexts/Modifiers/utils/getInternalModifiers.ts +58 -0
  138. package/src/contexts/Modifiers/utils/isDateInRange.test.ts +28 -0
  139. package/src/contexts/Modifiers/utils/isDateInRange.ts +27 -0
  140. package/src/contexts/Modifiers/utils/isMatch.test.ts +92 -0
  141. package/src/contexts/Modifiers/utils/isMatch.ts +76 -0
  142. package/src/contexts/Modifiers/utils/matcherToArray.test.ts +22 -0
  143. package/src/contexts/Modifiers/utils/matcherToArray.ts +14 -0
  144. package/src/contexts/Navigation/NavigationContext.tsx +84 -0
  145. package/src/contexts/Navigation/index.ts +2 -0
  146. package/src/contexts/Navigation/useNavigation.test.ts +126 -0
  147. package/src/contexts/Navigation/useNavigation.ts +12 -0
  148. package/src/contexts/Navigation/useNavigationState.test.ts +36 -0
  149. package/src/contexts/Navigation/useNavigationState.ts +25 -0
  150. package/src/contexts/Navigation/utils/getDisplayMonths.ts +31 -0
  151. package/src/contexts/Navigation/utils/getInitialMonth.test.ts +56 -0
  152. package/src/contexts/Navigation/utils/getInitialMonth.ts +24 -0
  153. package/src/contexts/Navigation/utils/getNextMonth.test.ts +75 -0
  154. package/src/contexts/Navigation/utils/getNextMonth.ts +45 -0
  155. package/src/contexts/Navigation/utils/getPreviousMonth.test.ts +55 -0
  156. package/src/contexts/Navigation/utils/getPreviousMonth.ts +44 -0
  157. package/src/contexts/RootProvider.tsx +37 -0
  158. package/src/contexts/SelectMultiple/SelectMultipleContext.tsx +135 -0
  159. package/src/contexts/SelectMultiple/index.ts +2 -0
  160. package/src/contexts/SelectMultiple/useSelectMultiple.test.ts +191 -0
  161. package/src/contexts/SelectMultiple/useSelectMultiple.ts +17 -0
  162. package/src/contexts/SelectRange/SelectRangeContext.tsx +158 -0
  163. package/src/contexts/SelectRange/index.ts +2 -0
  164. package/src/contexts/SelectRange/useSelectRange.test.ts +282 -0
  165. package/src/contexts/SelectRange/useSelectRange.ts +15 -0
  166. package/src/contexts/SelectRange/utils/addToRange.test.ts +119 -0
  167. package/src/contexts/SelectRange/utils/addToRange.ts +43 -0
  168. package/src/contexts/SelectSingle/SelectSingleContext.tsx +80 -0
  169. package/src/contexts/SelectSingle/index.ts +2 -0
  170. package/src/contexts/SelectSingle/useSelectSingle.test.ts +81 -0
  171. package/src/contexts/SelectSingle/useSelectSingle.ts +17 -0
  172. package/src/hooks/useActiveModifiers/index.ts +1 -0
  173. package/src/hooks/useActiveModifiers/useActiveModifiers.test.tsx +36 -0
  174. package/src/hooks/useActiveModifiers/useActiveModifiers.tsx +18 -0
  175. package/src/hooks/useControlledValue/index.ts +1 -0
  176. package/src/hooks/useControlledValue/useControlledValue.test.ts +68 -0
  177. package/src/hooks/useControlledValue/useControlledValue.ts +24 -0
  178. package/src/hooks/useDayEventHandlers/index.ts +1 -0
  179. package/src/hooks/useDayEventHandlers/useDayEventHandlers.test.tsx +213 -0
  180. package/src/hooks/useDayEventHandlers/useDayEventHandlers.tsx +195 -0
  181. package/src/hooks/useDayRender/index.ts +1 -0
  182. package/src/hooks/useDayRender/useDayRender.test.tsx +304 -0
  183. package/src/hooks/useDayRender/useDayRender.tsx +123 -0
  184. package/src/hooks/useDayRender/utils/getDayClassNames.test.ts +63 -0
  185. package/src/hooks/useDayRender/utils/getDayClassNames.ts +32 -0
  186. package/src/hooks/useDayRender/utils/getDayStyle.ts +19 -0
  187. package/src/hooks/useInput/index.ts +1 -0
  188. package/src/hooks/useInput/useInput.ts +175 -0
  189. package/src/hooks/useInput/utils/isValidDate.tsx +4 -0
  190. package/src/hooks/useSelectedDays/index.ts +1 -0
  191. package/src/hooks/useSelectedDays/useSelectedDays.test.ts +72 -0
  192. package/src/hooks/useSelectedDays/useSelectedDays.ts +32 -0
  193. package/src/index.ts +43 -0
  194. package/src/style.css +311 -0
  195. package/src/style.css.d.ts +38 -0
  196. package/src/types/DayPickerBase.ts +267 -0
  197. package/src/types/DayPickerDefault.ts +15 -0
  198. package/src/types/DayPickerMultiple.ts +26 -0
  199. package/src/types/DayPickerRange.ts +27 -0
  200. package/src/types/DayPickerSingle.ts +24 -0
  201. package/src/types/EventHandlers.ts +87 -0
  202. package/src/types/Formatters.ts +29 -0
  203. package/src/types/Labels.ts +36 -0
  204. package/src/types/Matchers.ts +106 -0
  205. package/src/types/Modifiers.ts +62 -0
  206. package/src/types/Styles.ts +125 -0
  207. package/tsconfig.json +24 -0
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+
3
+ import { useId } from '@reach/auto-id';
4
+
5
+ import { Caption } from 'components/Caption';
6
+ import { Table } from 'components/Table';
7
+ import { useDayPicker } from 'contexts/DayPicker';
8
+ import { useNavigation } from 'contexts/Navigation';
9
+
10
+ /** The props for the [[Month]] component. */
11
+ export interface MonthProps {
12
+ displayIndex: number;
13
+ displayMonth: Date;
14
+ }
15
+
16
+ /** Render a month. */
17
+ export function Month(props: MonthProps) {
18
+ const dayPicker = useDayPicker();
19
+ const { dir, classNames, styles, components } = dayPicker;
20
+ const { displayMonths } = useNavigation();
21
+ const captionId = useId();
22
+ const className = [classNames.month];
23
+ let style = styles.month;
24
+
25
+ let isStart = props.displayIndex === 0;
26
+ let isEnd = props.displayIndex === displayMonths.length - 1;
27
+ const isCenter = !isStart && !isEnd;
28
+ if (dir === 'rtl') {
29
+ [isEnd, isStart] = [isStart, isEnd];
30
+ }
31
+
32
+ if (isStart) {
33
+ className.push(classNames.caption_start);
34
+ style = { ...style, ...styles.caption_start };
35
+ }
36
+ if (isEnd) {
37
+ className.push(classNames.caption_end);
38
+ style = { ...style, ...styles.caption_end };
39
+ }
40
+ if (isCenter) {
41
+ className.push(classNames.caption_between);
42
+ style = { ...style, ...styles.caption_between };
43
+ }
44
+
45
+ const CaptionComponent = components?.Caption ?? Caption;
46
+
47
+ return (
48
+ <div key={props.displayIndex} className={className.join(' ')} style={style}>
49
+ <CaptionComponent id={captionId} displayMonth={props.displayMonth} />
50
+ <Table aria-labelledby={captionId} displayMonth={props.displayMonth} />
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1 @@
1
+ export * from './Month';
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+
3
+ import { screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { differenceInMonths } from 'date-fns';
6
+ import { DayPickerProps } from 'DayPicker';
7
+
8
+ import { customRender } from 'test/render';
9
+ import { freezeBeforeAll } from 'test/utils';
10
+
11
+ import { MonthsDropdown, MonthsDropdownProps } from './MonthsDropdown';
12
+
13
+ const today = new Date(2020, 12, 22);
14
+
15
+ freezeBeforeAll(today);
16
+
17
+ let root: HTMLDivElement;
18
+ let options: HTMLCollectionOf<HTMLOptionElement> | undefined;
19
+ let select: HTMLSelectElement | null;
20
+
21
+ function setup(props: MonthsDropdownProps, dayPickerProps?: DayPickerProps) {
22
+ const view = customRender(<MonthsDropdown {...props} />, dayPickerProps);
23
+ root = view.container.firstChild as HTMLDivElement;
24
+ select = screen.queryByRole('combobox', { name: 'Month:' });
25
+ options = select?.getElementsByTagName('option');
26
+ }
27
+
28
+ const props: MonthsDropdownProps = {
29
+ displayMonth: today,
30
+ onChange: jest.fn()
31
+ };
32
+
33
+ describe('when "fromDate" is not set', () => {
34
+ beforeEach(() => {
35
+ setup(props, { fromDate: undefined });
36
+ });
37
+ test('should return nothing', () => {
38
+ expect(root).toBeNull();
39
+ });
40
+ });
41
+
42
+ describe('when "toDate" is not set', () => {
43
+ beforeEach(() => {
44
+ setup(props, { toDate: undefined });
45
+ });
46
+ test('should return nothing', () => {
47
+ expect(root).toBeNull();
48
+ });
49
+ });
50
+
51
+ describe('when "fromDate" and "toDate" are in the same year', () => {
52
+ const fromDate = new Date(2012, 0, 22);
53
+ const toDate = new Date(2012, 10, 22);
54
+ beforeEach(() => {
55
+ setup(props, { fromDate, toDate });
56
+ });
57
+ test('should display the months included between the two dates', () => {
58
+ expect(options).toHaveLength(differenceInMonths(toDate, fromDate) + 1);
59
+ });
60
+ test('the first month should be the fromDate month', () => {
61
+ expect(options?.[0]).toHaveValue(String(fromDate.getMonth()));
62
+ });
63
+ test('the last month should be the toMonth month', () => {
64
+ expect(options?.[options.length - 1]).toHaveValue(
65
+ String(toDate.getMonth())
66
+ );
67
+ });
68
+ });
69
+
70
+ describe('when "fromDate" and "toDate" are not in the same year', () => {
71
+ const fromDate = new Date(2012, 0, 22);
72
+ const toDate = new Date(2015, 10, 22);
73
+ const displayMonth = new Date(2015, 7, 0);
74
+ beforeEach(() => {
75
+ setup({ ...props, displayMonth }, { fromDate, toDate });
76
+ });
77
+ test('should display the 12 months', () => {
78
+ expect(options).toHaveLength(12);
79
+ });
80
+ test('the first month should be January', () => {
81
+ expect(options?.[0]).toHaveValue('0');
82
+ });
83
+ test('the last month should be December', () => {
84
+ expect(options?.[options.length - 1]).toHaveValue('11');
85
+ });
86
+ test('should select the displayed month', () => {
87
+ expect(select).toHaveValue(`${displayMonth.getMonth()}`);
88
+ });
89
+
90
+ describe('when the dropdown changes', () => {
91
+ beforeEach(() => {
92
+ if (select) userEvent.selectOptions(select, 'February');
93
+ });
94
+ test('should fire the "onChange" event handler', () => {
95
+ const expectedMonth = new Date(2015, 1, 1);
96
+ expect(props.onChange).toHaveBeenCalledWith(expectedMonth);
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+
3
+ import isSameYear from 'date-fns/isSameYear';
4
+ import setMonth from 'date-fns/setMonth';
5
+ import startOfMonth from 'date-fns/startOfMonth';
6
+
7
+ import { Dropdown } from 'components/Dropdown';
8
+ import { useDayPicker } from 'contexts/DayPicker';
9
+ import { MonthChangeEventHandler } from 'types/EventHandlers';
10
+
11
+ /** The props for the [[MonthsDropdown]] component. */
12
+ export interface MonthsDropdownProps {
13
+ /** The month where the dropdown is displayed. */
14
+ displayMonth: Date;
15
+ onChange: MonthChangeEventHandler;
16
+ }
17
+
18
+ /** Render the dropdown to navigate between months. */
19
+ export function MonthsDropdown(props: MonthsDropdownProps): JSX.Element {
20
+ const {
21
+ fromDate,
22
+ toDate,
23
+ styles,
24
+ locale,
25
+ formatters: { formatMonthCaption },
26
+ classNames,
27
+ components,
28
+ labels: { labelMonthDropdown }
29
+ } = useDayPicker();
30
+
31
+ // Dropdown should appear only when both from/toDate is set
32
+ if (!fromDate) return <></>;
33
+ if (!toDate) return <></>;
34
+
35
+ const dropdownMonths: Date[] = [];
36
+
37
+ if (isSameYear(fromDate, toDate)) {
38
+ // only display the months included in the range
39
+ const date = startOfMonth(fromDate);
40
+ for (let month = fromDate.getMonth(); month <= toDate.getMonth(); month++) {
41
+ dropdownMonths.push(setMonth(date, month));
42
+ }
43
+ } else {
44
+ // display all the 12 months
45
+ const date = startOfMonth(new Date()); // Any date should be OK, as we just need the year
46
+ for (let month = 0; month <= 11; month++) {
47
+ dropdownMonths.push(setMonth(date, month));
48
+ }
49
+ }
50
+
51
+ const handleChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
52
+ const selectedMonth = Number(e.target.value);
53
+ const newMonth = setMonth(startOfMonth(props.displayMonth), selectedMonth);
54
+ props.onChange(newMonth);
55
+ };
56
+
57
+ const DropdownComponent = components?.Dropdown ?? Dropdown;
58
+
59
+ return (
60
+ <DropdownComponent
61
+ aria-label={labelMonthDropdown()}
62
+ className={classNames.dropdown_month}
63
+ style={styles.dropdown_month}
64
+ onChange={handleChange}
65
+ value={props.displayMonth.getMonth()}
66
+ caption={formatMonthCaption(props.displayMonth, { locale })}
67
+ >
68
+ {dropdownMonths.map((m) => (
69
+ <option key={m.getMonth()} value={m.getMonth()}>
70
+ {formatMonthCaption(m, { locale })}
71
+ </option>
72
+ ))}
73
+ </DropdownComponent>
74
+ );
75
+ }
@@ -0,0 +1 @@
1
+ export * from './MonthsDropdown';
@@ -0,0 +1,129 @@
1
+ import React from 'react';
2
+
3
+ import userEvent from '@testing-library/user-event';
4
+ import { DayPickerProps } from 'DayPicker';
5
+
6
+ import { getNextButton, getPrevButton } from 'test/po';
7
+ import { customRender } from 'test/render';
8
+
9
+ import { Navigation, NavigationProps } from './Navigation';
10
+
11
+ let root: HTMLElement;
12
+
13
+ function setup(props: NavigationProps, dayPickerProps?: DayPickerProps) {
14
+ const view = customRender(<Navigation {...props} />, dayPickerProps);
15
+ root = view.container.firstChild as HTMLElement;
16
+ }
17
+
18
+ const props: NavigationProps = {
19
+ previousMonth: new Date(2021, 3),
20
+ nextMonth: new Date(2021, 5),
21
+ displayMonth: new Date(2021, 4),
22
+ hidePrevious: false,
23
+ hideNext: false,
24
+ onNextClick: jest.fn(),
25
+ onPreviousClick: jest.fn()
26
+ };
27
+
28
+ const dayPickerProps = {
29
+ classNames: {
30
+ nav: 'foo'
31
+ },
32
+ styles: {
33
+ nav: { color: 'red' }
34
+ },
35
+ components: {
36
+ IconRight: () => <svg>IconRight</svg>,
37
+ IconLeft: () => <svg>IconLeft</svg>
38
+ }
39
+ };
40
+
41
+ describe('when rendered', () => {
42
+ beforeEach(() => {
43
+ setup(props, dayPickerProps);
44
+ });
45
+ test('should add the class name', () => {
46
+ expect(root).toHaveClass(dayPickerProps.classNames.nav);
47
+ });
48
+ test('should apply the style', () => {
49
+ expect(root).toHaveStyle(dayPickerProps.styles.nav);
50
+ });
51
+ test('the previous button should display the left icon', () => {
52
+ const icons = root.getElementsByTagName('svg');
53
+ expect(icons[0]).toHaveTextContent('IconLeft');
54
+ });
55
+ test('the next button should display the right icon', () => {
56
+ const icons = root.getElementsByTagName('svg');
57
+ expect(icons[1]).toHaveTextContent('IconRight');
58
+ });
59
+ beforeEach(() => {
60
+ userEvent.click(getPrevButton());
61
+ });
62
+ test('should call "onPreviousClick"', () => {
63
+ expect(props.onPreviousClick).toHaveBeenCalled();
64
+ });
65
+
66
+ describe('when clicking the next button', () => {
67
+ beforeEach(() => {
68
+ userEvent.click(getNextButton());
69
+ });
70
+ test('should call "onNextClick"', () => {
71
+ expect(props.onNextClick).toHaveBeenCalled();
72
+ });
73
+ });
74
+ });
75
+
76
+ describe('when in right-to-left direction', () => {
77
+ beforeEach(() => {
78
+ setup(props, { ...dayPickerProps, dir: 'rtl' });
79
+ });
80
+ test('the previous button should display the right icon', () => {
81
+ const icons = root.getElementsByTagName('svg');
82
+ expect(icons[0]).toHaveTextContent('IconRight');
83
+ });
84
+ test('the next button should display the left icon', () => {
85
+ const icons = root.getElementsByTagName('svg');
86
+ expect(icons[1]).toHaveTextContent('IconLeft');
87
+ });
88
+
89
+ describe('when clicking the previous button', () => {
90
+ beforeEach(() => {
91
+ userEvent.click(getPrevButton());
92
+ });
93
+ test('should call "onPreviousClick"', () => {
94
+ expect(props.onPreviousClick).toHaveBeenCalled();
95
+ });
96
+ });
97
+ describe('when clicking the next button', () => {
98
+ beforeEach(() => {
99
+ userEvent.click(getNextButton());
100
+ });
101
+ test('should call "onNextClick"', () => {
102
+ expect(props.onNextClick).toHaveBeenCalled();
103
+ });
104
+ });
105
+ });
106
+
107
+ describe('when the previous month is undefined', () => {
108
+ beforeEach(() => {
109
+ setup({ ...props, previousMonth: undefined }, dayPickerProps);
110
+ });
111
+ test('the previous button should be aria-disabled', () => {
112
+ expect(getPrevButton()).toHaveAttribute('aria-disabled', 'true');
113
+ });
114
+ test('the next button should not be aria-disabled', () => {
115
+ expect(getNextButton()).not.toHaveAttribute('aria-disabled', 'true');
116
+ });
117
+ });
118
+
119
+ describe('when the next month is undefined', () => {
120
+ beforeEach(() => {
121
+ setup({ ...props, nextMonth: undefined }, dayPickerProps);
122
+ });
123
+ test('the previous button should be enabled', () => {
124
+ expect(getPrevButton()).not.toHaveAttribute('aria-disabled', 'true');
125
+ });
126
+ test('the next button should be disabled', () => {
127
+ expect(getNextButton()).toHaveAttribute('aria-disabled', 'true');
128
+ });
129
+ });
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+
3
+ import { IconLeft } from 'components/IconLeft';
4
+ import { IconRight } from 'components/IconRight';
5
+ import { useDayPicker } from 'contexts/DayPicker';
6
+
7
+ import { Button } from '../Button';
8
+
9
+ /** The props for the [[Navigation]] component. */
10
+ export interface NavigationProps {
11
+ /** The month where the caption is displayed. */
12
+ displayMonth: Date;
13
+ /** The previous month. */
14
+ previousMonth?: Date;
15
+ /** The next month. */
16
+ nextMonth?: Date;
17
+ /** Hide the previous button. */
18
+ hidePrevious: boolean;
19
+ /** Hide the next button. */
20
+ hideNext: boolean;
21
+ /** Event handler when the next button is clicked. */
22
+ onNextClick: React.MouseEventHandler<HTMLButtonElement>;
23
+ /** Event handler when the previous button is clicked. */
24
+ onPreviousClick: React.MouseEventHandler<HTMLButtonElement>;
25
+ }
26
+
27
+ /** A component rendering the navigation buttons or the drop-downs. */
28
+ export function Navigation(props: NavigationProps): JSX.Element {
29
+ const {
30
+ dir,
31
+ locale,
32
+ classNames,
33
+ styles,
34
+ labels: { labelPrevious, labelNext },
35
+ components
36
+ } = useDayPicker();
37
+
38
+ if (!props.nextMonth && !props.previousMonth) {
39
+ return <></>;
40
+ }
41
+
42
+ const previousLabel = labelPrevious(props.previousMonth, { locale });
43
+ const previousClassName = [
44
+ classNames.nav_button,
45
+ classNames.nav_button_previous
46
+ ].join(' ');
47
+
48
+ const nextLabel = labelNext(props.nextMonth, { locale });
49
+ const nextClassName = [
50
+ classNames.nav_button,
51
+ classNames.nav_button_next
52
+ ].join(' ');
53
+
54
+ const IconRightComponent = components?.IconRight ?? IconRight;
55
+ const IconLeftComponent = components?.IconLeft ?? IconLeft;
56
+ return (
57
+ <div className={classNames.nav} style={styles.nav}>
58
+ {!props.hidePrevious && (
59
+ <Button
60
+ aria-label={previousLabel}
61
+ className={previousClassName}
62
+ style={styles.nav_button_previous}
63
+ aria-disabled={!props.previousMonth}
64
+ onClick={props.onPreviousClick}
65
+ >
66
+ {dir === 'rtl' ? (
67
+ <IconRightComponent
68
+ className={classNames.nav_icon}
69
+ style={styles.nav_icon}
70
+ />
71
+ ) : (
72
+ <IconLeftComponent
73
+ className={classNames.nav_icon}
74
+ style={styles.nav_icon}
75
+ />
76
+ )}
77
+ </Button>
78
+ )}
79
+ {!props.hideNext && (
80
+ <Button
81
+ aria-label={nextLabel}
82
+ className={nextClassName}
83
+ style={styles.nav_button_next}
84
+ aria-disabled={!props.nextMonth}
85
+ onClick={props.onNextClick}
86
+ >
87
+ {dir === 'rtl' ? (
88
+ <IconLeftComponent
89
+ className={classNames.nav_icon}
90
+ style={styles.nav_icon}
91
+ />
92
+ ) : (
93
+ <IconRightComponent
94
+ className={classNames.nav_icon}
95
+ style={styles.nav_icon}
96
+ />
97
+ )}
98
+ </Button>
99
+ )}
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1 @@
1
+ export * from './Navigation';
@@ -0,0 +1,123 @@
1
+ import React from 'react';
2
+
3
+ import { RenderResult } from '@testing-library/react';
4
+ import { addDays } from 'date-fns';
5
+ import { DayPickerProps } from 'DayPicker';
6
+
7
+ import { focusDay } from 'test/actions';
8
+ import { getDayButton, queryMonthGrids } from 'test/po';
9
+ import { customRender } from 'test/render';
10
+ import { freezeBeforeAll } from 'test/utils';
11
+
12
+ import { defaultClassNames } from 'contexts/DayPicker/defaultClassNames';
13
+ import { ClassNames } from 'types/Styles';
14
+
15
+ import { Root } from './Root';
16
+
17
+ const today = new Date(2020, 10, 4);
18
+ freezeBeforeAll(today);
19
+
20
+ let container: HTMLElement;
21
+ let view: RenderResult;
22
+
23
+ function setup(dayPickerProps: DayPickerProps = {}) {
24
+ view = customRender(<Root />, dayPickerProps);
25
+ container = view.container;
26
+ }
27
+
28
+ describe('when the number of months is 1', () => {
29
+ const props: DayPickerProps = { numberOfMonths: 1 };
30
+ beforeEach(() => {
31
+ setup(props);
32
+ });
33
+ test('should display one month grid', () => {
34
+ expect(queryMonthGrids()).toHaveLength(1);
35
+ });
36
+ });
37
+
38
+ describe('when the number of months is greater than 1', () => {
39
+ const props: DayPickerProps = { numberOfMonths: 3 };
40
+ beforeEach(() => {
41
+ setup(props);
42
+ });
43
+ test('should display the specified number of month grids', () => {
44
+ expect(queryMonthGrids()).toHaveLength(3);
45
+ });
46
+ });
47
+
48
+ describe('when using the "classNames" prop', () => {
49
+ const classNames: ClassNames = {
50
+ root: 'foo'
51
+ };
52
+ beforeEach(() => {
53
+ setup({ classNames });
54
+ });
55
+ test('should display the specified number of month grids', () => {
56
+ expect(container.firstChild).toHaveClass('foo');
57
+ });
58
+ });
59
+
60
+ describe('when using the "className" prop', () => {
61
+ const props: DayPickerProps = { className: 'foo' };
62
+ beforeEach(() => {
63
+ setup(props);
64
+ });
65
+ test('should append the class name to the root element', () => {
66
+ expect(container.firstChild).toHaveClass('foo');
67
+ });
68
+ });
69
+
70
+ describe('when the "numberOfMonths" is greater than 1', () => {
71
+ const props: DayPickerProps = { numberOfMonths: 3 };
72
+ const expectedClassName = defaultClassNames.multiple_months;
73
+ beforeEach(() => {
74
+ setup(props);
75
+ });
76
+ test(`should have the ${expectedClassName} class name`, () => {
77
+ expect(container.firstChild).toHaveClass(expectedClassName);
78
+ });
79
+ });
80
+
81
+ describe('when showing the week numbers', () => {
82
+ const props: DayPickerProps = { showWeekNumber: true };
83
+ const expectedClassName = defaultClassNames.with_weeknumber;
84
+ beforeEach(() => {
85
+ setup(props);
86
+ });
87
+ test(`should have the ${expectedClassName} class name`, () => {
88
+ expect(container.firstChild).toHaveClass(expectedClassName);
89
+ });
90
+ });
91
+
92
+ describe('when "initialFocus" is set', () => {
93
+ const baseProps: DayPickerProps = {
94
+ initialFocus: true,
95
+ mode: 'single'
96
+ };
97
+ describe('when a day is not selected', () => {
98
+ beforeEach(() => {
99
+ setup(baseProps);
100
+ });
101
+ test('should focus today', () => {
102
+ expect(getDayButton(today)).toHaveFocus();
103
+ });
104
+ describe('when a new day is focused', () => {
105
+ beforeEach(() => {
106
+ focusDay(addDays(today, 1));
107
+ });
108
+ describe('and the calendar is rerendered', () => {
109
+ test.todo('should focus the new day');
110
+ });
111
+ });
112
+ });
113
+ describe('when a day is selected', () => {
114
+ const selected = addDays(today, 1);
115
+ const props: DayPickerProps = { ...baseProps, selected };
116
+ beforeEach(() => {
117
+ setup(props);
118
+ });
119
+ test('should focus the selected day', () => {
120
+ expect(getDayButton(selected)).toHaveFocus();
121
+ });
122
+ });
123
+ });
@@ -0,0 +1,58 @@
1
+ import React, { useEffect, useState } from 'react';
2
+
3
+ import { Month } from 'components/Month';
4
+ import { useDayPicker } from 'contexts/DayPicker';
5
+ import { useFocusContext } from 'contexts/Focus';
6
+ import { useNavigation } from 'contexts/Navigation';
7
+
8
+ /** Render the container with the months according to the number of months to display. */
9
+ export function Root(): JSX.Element {
10
+ const dayPicker = useDayPicker();
11
+ const focusContext = useFocusContext();
12
+ const navigation = useNavigation();
13
+
14
+ const [hasInitialFocus, setHasInitialFocus] = useState(false);
15
+
16
+ // Focus the focus target when initialFocus is passed in
17
+ useEffect(() => {
18
+ if (!dayPicker.initialFocus) return;
19
+ if (!focusContext.focusTarget) return;
20
+ if (hasInitialFocus) return;
21
+
22
+ focusContext.focus(focusContext.focusTarget);
23
+ setHasInitialFocus(true);
24
+ }, [
25
+ dayPicker.initialFocus,
26
+ hasInitialFocus,
27
+ focusContext.focus,
28
+ focusContext.focusTarget,
29
+ focusContext
30
+ ]);
31
+
32
+ // Apply classnames according to props
33
+ const classNames = [dayPicker.className ?? dayPicker.classNames.root];
34
+ if (dayPicker.numberOfMonths > 1) {
35
+ classNames.push(dayPicker.classNames.multiple_months);
36
+ }
37
+ if (dayPicker.showWeekNumber) {
38
+ classNames.push(dayPicker.classNames.with_weeknumber);
39
+ }
40
+
41
+ const style = {
42
+ ...dayPicker.styles.root,
43
+ ...dayPicker.style
44
+ };
45
+
46
+ return (
47
+ <div className={classNames.join(' ')} style={style} dir={dayPicker.dir}>
48
+ <div
49
+ className={dayPicker.classNames.months}
50
+ style={dayPicker.styles.months}
51
+ >
52
+ {navigation.displayMonths.map((month, i) => (
53
+ <Month key={i} displayIndex={i} displayMonth={month} />
54
+ ))}
55
+ </div>
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1 @@
1
+ export * from './Root';