react-day-picker 9.6.2 → 9.6.4

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 (181) hide show
  1. package/dist/cjs/DayPicker.js +15 -3
  2. package/dist/cjs/DayPicker.js.map +1 -1
  3. package/dist/cjs/classes/DateLib.js +4 -4
  4. package/dist/cjs/classes/DateLib.js.map +1 -1
  5. package/dist/cjs/useAnimation.js +74 -42
  6. package/dist/cjs/useAnimation.js.map +1 -1
  7. package/dist/esm/DayPicker.js +15 -3
  8. package/dist/esm/DayPicker.js.map +1 -1
  9. package/dist/esm/classes/DateLib.js +1 -1
  10. package/dist/esm/classes/DateLib.js.map +1 -1
  11. package/dist/esm/useAnimation.js +74 -42
  12. package/dist/esm/useAnimation.js.map +1 -1
  13. package/package.json +2 -4
  14. package/src/.eslintignore +1 -0
  15. package/src/.eslintrc.cjs +27 -0
  16. package/src/DayPicker.test.tsx +199 -0
  17. package/src/DayPicker.tsx +630 -0
  18. package/src/UI.ts +365 -0
  19. package/src/classes/CalendarDay.test.ts +17 -0
  20. package/src/classes/CalendarDay.ts +61 -0
  21. package/src/classes/CalendarMonth.test.ts +28 -0
  22. package/src/classes/CalendarMonth.ts +15 -0
  23. package/src/classes/CalendarWeek.test.ts +21 -0
  24. package/src/classes/CalendarWeek.ts +13 -0
  25. package/src/classes/DateLib.ts +615 -0
  26. package/src/classes/index.ts +4 -0
  27. package/src/components/Button.tsx +13 -0
  28. package/src/components/CaptionLabel.tsx +13 -0
  29. package/src/components/Chevron.tsx +42 -0
  30. package/src/components/Day.tsx +28 -0
  31. package/src/components/DayButton.tsx +29 -0
  32. package/src/components/Dropdown.tsx +71 -0
  33. package/src/components/DropdownNav.tsx +13 -0
  34. package/src/components/Footer.tsx +13 -0
  35. package/src/components/Month.tsx +24 -0
  36. package/src/components/MonthCaption.tsx +23 -0
  37. package/src/components/MonthGrid.tsx +13 -0
  38. package/src/components/Months.tsx +13 -0
  39. package/src/components/MonthsDropdown.tsx +16 -0
  40. package/src/components/Nav.tsx +90 -0
  41. package/src/components/NextMonthButton.tsx +18 -0
  42. package/src/components/Option.tsx +13 -0
  43. package/src/components/PreviousMonthButton.tsx +20 -0
  44. package/src/components/Root.tsx +19 -0
  45. package/src/components/Select.tsx +13 -0
  46. package/src/components/Week.tsx +20 -0
  47. package/src/components/WeekNumber.tsx +21 -0
  48. package/src/components/WeekNumberHeader.tsx +15 -0
  49. package/src/components/Weekday.tsx +13 -0
  50. package/src/components/Weekdays.tsx +17 -0
  51. package/src/components/Weeks.tsx +13 -0
  52. package/src/components/YearsDropdown.tsx +16 -0
  53. package/src/components/custom-components.tsx +26 -0
  54. package/src/formatters/formatCaption.test.ts +27 -0
  55. package/src/formatters/formatCaption.ts +23 -0
  56. package/src/formatters/formatDay.test.ts +7 -0
  57. package/src/formatters/formatDay.ts +16 -0
  58. package/src/formatters/formatMonthDropdown.test.ts +19 -0
  59. package/src/formatters/formatMonthDropdown.ts +15 -0
  60. package/src/formatters/formatWeekNumber.test.ts +5 -0
  61. package/src/formatters/formatWeekNumber.ts +13 -0
  62. package/src/formatters/formatWeekNumberHeader.ts +10 -0
  63. package/src/formatters/formatWeekdayName.test.ts +15 -0
  64. package/src/formatters/formatWeekdayName.ts +16 -0
  65. package/src/formatters/formatYearDropdown.test.ts +7 -0
  66. package/src/formatters/formatYearDropdown.ts +21 -0
  67. package/src/formatters/index.ts +7 -0
  68. package/src/helpers/broadcastCalendar.test.ts +43 -0
  69. package/src/helpers/calculateFocusTarget.ts +51 -0
  70. package/src/helpers/endOfBroadcastWeek.test.ts +25 -0
  71. package/src/helpers/endOfBroadcastWeek.ts +16 -0
  72. package/src/helpers/getBroadcastWeeksInMonth.test.ts +23 -0
  73. package/src/helpers/getBroadcastWeeksInMonth.ts +31 -0
  74. package/src/helpers/getClassNamesForModifiers.ts +26 -0
  75. package/src/helpers/getComponents.ts +11 -0
  76. package/src/helpers/getDataAttributes.test.tsx +48 -0
  77. package/src/helpers/getDataAttributes.tsx +21 -0
  78. package/src/helpers/getDates.test.ts +190 -0
  79. package/src/helpers/getDates.ts +64 -0
  80. package/src/helpers/getDays.test.ts +30 -0
  81. package/src/helpers/getDays.ts +16 -0
  82. package/src/helpers/getDefaultClassNames.test.ts +47 -0
  83. package/src/helpers/getDefaultClassNames.ts +33 -0
  84. package/src/helpers/getDisplayMonths.test.ts +44 -0
  85. package/src/helpers/getDisplayMonths.ts +20 -0
  86. package/src/helpers/getFocusableDate.ts +59 -0
  87. package/src/helpers/getFormatters.test.ts +48 -0
  88. package/src/helpers/getFormatters.ts +19 -0
  89. package/src/helpers/getInitialMonth.test.ts +79 -0
  90. package/src/helpers/getInitialMonth.ts +41 -0
  91. package/src/helpers/getLabels.ts +10 -0
  92. package/src/helpers/getMonthOptions.test.ts +226 -0
  93. package/src/helpers/getMonthOptions.ts +37 -0
  94. package/src/helpers/getMonths.test.ts +88 -0
  95. package/src/helpers/getMonths.ts +90 -0
  96. package/src/helpers/getNavMonth.test.ts +253 -0
  97. package/src/helpers/getNavMonth.ts +70 -0
  98. package/src/helpers/getNextFocus.test.tsx +99 -0
  99. package/src/helpers/getNextFocus.tsx +67 -0
  100. package/src/helpers/getNextMonth.test.ts +101 -0
  101. package/src/helpers/getNextMonth.ts +45 -0
  102. package/src/helpers/getPossibleFocusDate.test.ts +144 -0
  103. package/src/helpers/getPreviousMonth.test.ts +77 -0
  104. package/src/helpers/getPreviousMonth.ts +40 -0
  105. package/src/helpers/getStyleForModifiers.test.ts +92 -0
  106. package/src/helpers/getStyleForModifiers.ts +21 -0
  107. package/src/helpers/getWeekdays.test.ts +44 -0
  108. package/src/helpers/getWeekdays.ts +29 -0
  109. package/src/helpers/getWeeks.test.ts +30 -0
  110. package/src/helpers/getWeeks.ts +9 -0
  111. package/src/helpers/getYearOptions.test.ts +46 -0
  112. package/src/helpers/getYearOptions.ts +34 -0
  113. package/src/helpers/index.ts +2 -0
  114. package/src/helpers/startOfBroadcastWeek.test.ts +24 -0
  115. package/src/helpers/startOfBroadcastWeek.ts +19 -0
  116. package/src/helpers/useControlledValue.test.ts +45 -0
  117. package/src/helpers/useControlledValue.ts +33 -0
  118. package/src/index.ts +15 -0
  119. package/src/jalali.tsx +2 -0
  120. package/src/labels/index.ts +12 -0
  121. package/src/labels/labelDayButton.test.ts +41 -0
  122. package/src/labels/labelDayButton.ts +31 -0
  123. package/src/labels/labelGrid.test.ts +7 -0
  124. package/src/labels/labelGrid.ts +23 -0
  125. package/src/labels/labelGridcell.test.ts +7 -0
  126. package/src/labels/labelGridcell.ts +22 -0
  127. package/src/labels/labelMonthDropdown.test.ts +5 -0
  128. package/src/labels/labelMonthDropdown.ts +12 -0
  129. package/src/labels/labelNav.test.ts +5 -0
  130. package/src/labels/labelNav.ts +10 -0
  131. package/src/labels/labelNext.test.ts +5 -0
  132. package/src/labels/labelNext.ts +13 -0
  133. package/src/labels/labelPrevious.test.ts +5 -0
  134. package/src/labels/labelPrevious.ts +13 -0
  135. package/src/labels/labelWeekNumber.test.ts +5 -0
  136. package/src/labels/labelWeekNumber.ts +15 -0
  137. package/src/labels/labelWeekNumberHeader.test.ts +5 -0
  138. package/src/labels/labelWeekNumberHeader.ts +12 -0
  139. package/src/labels/labelWeekday.test.ts +15 -0
  140. package/src/labels/labelWeekday.ts +16 -0
  141. package/src/labels/labelYearDropdown.test.ts +5 -0
  142. package/src/labels/labelYearDropdown.ts +12 -0
  143. package/src/locale.ts +1 -0
  144. package/src/persian.tsx +86 -0
  145. package/src/selection/useMulti.test.tsx +41 -0
  146. package/src/selection/useMulti.tsx +74 -0
  147. package/src/selection/useRange.test.tsx +154 -0
  148. package/src/selection/useRange.tsx +73 -0
  149. package/src/selection/useSingle.test.tsx +38 -0
  150. package/src/selection/useSingle.tsx +69 -0
  151. package/src/types/deprecated.ts +230 -0
  152. package/src/types/index.ts +4 -0
  153. package/src/types/props.test.tsx +71 -0
  154. package/src/types/props.ts +675 -0
  155. package/src/types/selection.ts +57 -0
  156. package/src/types/shared.ts +442 -0
  157. package/src/useAnimation.test.tsx +190 -0
  158. package/src/useAnimation.ts +236 -0
  159. package/src/useCalendar.ts +178 -0
  160. package/src/useDayPicker.test.tsx +142 -0
  161. package/src/useDayPicker.ts +93 -0
  162. package/src/useFocus.ts +87 -0
  163. package/src/useGetModifiers.test.tsx +154 -0
  164. package/src/useGetModifiers.tsx +122 -0
  165. package/src/useSelection.ts +26 -0
  166. package/src/utc.tsx +10 -0
  167. package/src/utils/addToRange.test.ts +117 -0
  168. package/src/utils/addToRange.ts +87 -0
  169. package/src/utils/dateMatchModifiers.test.ts +120 -0
  170. package/src/utils/dateMatchModifiers.ts +88 -0
  171. package/src/utils/index.ts +7 -0
  172. package/src/utils/rangeContainsDayOfWeek.test.ts +48 -0
  173. package/src/utils/rangeContainsDayOfWeek.ts +35 -0
  174. package/src/utils/rangeContainsModifiers.test.ts +230 -0
  175. package/src/utils/rangeContainsModifiers.ts +125 -0
  176. package/src/utils/rangeIncludesDate.test.ts +46 -0
  177. package/src/utils/rangeIncludesDate.ts +43 -0
  178. package/src/utils/rangeOverlaps.test.ts +60 -0
  179. package/src/utils/rangeOverlaps.ts +22 -0
  180. package/src/utils/typeguards.test.ts +83 -0
  181. package/src/utils/typeguards.ts +70 -0
@@ -0,0 +1,236 @@
1
+ import React, { useLayoutEffect, useRef } from "react";
2
+
3
+ import { Animation } from "./UI.js";
4
+ import type { CalendarDay } from "./classes/CalendarDay.js";
5
+ import { CalendarMonth } from "./classes/CalendarMonth.js";
6
+ import type { DateLib } from "./classes/DateLib.js";
7
+ import { ClassNames } from "./types/shared.js";
8
+
9
+ const asHtmlElement = (element: Element | null): HTMLElement | null => {
10
+ if (element instanceof HTMLElement) return element;
11
+ return null;
12
+ };
13
+
14
+ const queryMonthEls = (element: HTMLElement) => [
15
+ ...(element.querySelectorAll("[data-animated-month]") ?? [])
16
+ ];
17
+ const queryMonthEl = (element: HTMLElement) =>
18
+ asHtmlElement(element.querySelector("[data-animated-month]"));
19
+ const queryCaptionEl = (element: HTMLElement) =>
20
+ asHtmlElement(element.querySelector("[data-animated-caption]"));
21
+ const queryWeeksEl = (element: HTMLElement) =>
22
+ asHtmlElement(element.querySelector("[data-animated-weeks]"));
23
+ const queryNavEl = (element: HTMLElement) =>
24
+ asHtmlElement(element.querySelector("[data-animated-nav]"));
25
+ const queryWeekdaysEl = (element: HTMLElement) =>
26
+ asHtmlElement(element.querySelector("[data-animated-weekdays]"));
27
+
28
+ /** @private */
29
+ export function useAnimation(
30
+ rootElRef: React.RefObject<HTMLDivElement | null>,
31
+ enabled: boolean,
32
+ {
33
+ classNames,
34
+ months,
35
+ focused,
36
+ dateLib
37
+ }: {
38
+ classNames: ClassNames;
39
+ months: CalendarMonth[];
40
+ focused: CalendarDay | undefined;
41
+ dateLib: DateLib;
42
+ }
43
+ ): void {
44
+ const previousRootElSnapshotRef = useRef<HTMLElement>(null);
45
+ const previousMonthsRef = useRef(months);
46
+ const animatingRef = useRef(false);
47
+
48
+ useLayoutEffect(() => {
49
+ // get previous months before updating the previous months ref
50
+ const previousMonths = previousMonthsRef.current;
51
+ // update previous months ref for next effect trigger
52
+ previousMonthsRef.current = months;
53
+
54
+ if (
55
+ !enabled ||
56
+ !rootElRef.current ||
57
+ // safety check because the ref can be set to anything by consumers
58
+ !(rootElRef.current instanceof HTMLElement) ||
59
+ // validation required for the animation to work as expected
60
+ months.length === 0 ||
61
+ previousMonths.length === 0 ||
62
+ months.length !== previousMonths.length
63
+ ) {
64
+ return;
65
+ }
66
+
67
+ const isSameMonth = dateLib.isSameMonth(
68
+ months[0].date,
69
+ previousMonths[0].date
70
+ );
71
+
72
+ const isAfterPreviousMonth = dateLib.isAfter(
73
+ months[0].date,
74
+ previousMonths[0].date
75
+ );
76
+
77
+ const captionAnimationClass = isAfterPreviousMonth
78
+ ? classNames[Animation.caption_after_enter]
79
+ : classNames[Animation.caption_before_enter];
80
+
81
+ const weeksAnimationClass = isAfterPreviousMonth
82
+ ? classNames[Animation.weeks_after_enter]
83
+ : classNames[Animation.weeks_before_enter];
84
+
85
+ // get previous root element snapshot before updating the snapshot ref
86
+ const previousRootElSnapshot = previousRootElSnapshotRef.current;
87
+
88
+ // update snapshot for next effect trigger
89
+ const rootElSnapshot = rootElRef.current.cloneNode(true);
90
+ if (rootElSnapshot instanceof HTMLElement) {
91
+ // if this effect is triggered while animating, we need to clean up the new root snapshot
92
+ // to put it in the same state as when not animating, to correctly animate the next month change
93
+ const currentMonthElsSnapshot = queryMonthEls(rootElSnapshot);
94
+ currentMonthElsSnapshot.forEach((currentMonthElSnapshot) => {
95
+ if (!(currentMonthElSnapshot instanceof HTMLElement)) return;
96
+
97
+ // remove the old month snapshots from the new root snapshot
98
+ const previousMonthElSnapshot = queryMonthEl(currentMonthElSnapshot);
99
+ if (
100
+ previousMonthElSnapshot &&
101
+ currentMonthElSnapshot.contains(previousMonthElSnapshot)
102
+ ) {
103
+ currentMonthElSnapshot.removeChild(previousMonthElSnapshot);
104
+ }
105
+
106
+ // remove animation classes from the new month snapshots
107
+ const captionEl = queryCaptionEl(currentMonthElSnapshot);
108
+ if (captionEl) {
109
+ captionEl.classList.remove(captionAnimationClass);
110
+ }
111
+
112
+ const weeksEl = queryWeeksEl(currentMonthElSnapshot);
113
+ if (weeksEl) {
114
+ weeksEl.classList.remove(weeksAnimationClass);
115
+ }
116
+ });
117
+
118
+ previousRootElSnapshotRef.current = rootElSnapshot;
119
+ } else {
120
+ previousRootElSnapshotRef.current = null;
121
+ }
122
+
123
+ if (
124
+ animatingRef.current ||
125
+ isSameMonth ||
126
+ // skip animation if a day is focused because it can cause issues to the animation and is better for a11y
127
+ focused
128
+ ) {
129
+ return;
130
+ }
131
+
132
+ const previousMonthEls =
133
+ previousRootElSnapshot instanceof HTMLElement
134
+ ? queryMonthEls(previousRootElSnapshot)
135
+ : [];
136
+
137
+ const currentMonthEls = queryMonthEls(rootElRef.current);
138
+
139
+ if (
140
+ currentMonthEls &&
141
+ currentMonthEls.every((el) => el instanceof HTMLElement) &&
142
+ previousMonthEls &&
143
+ previousMonthEls.every((el) => el instanceof HTMLElement)
144
+ ) {
145
+ animatingRef.current = true;
146
+ const cleanUpFunctions: (() => void)[] = [];
147
+
148
+ // set isolation to isolate to isolate the stacking context during animation
149
+ rootElRef.current.style.isolation = "isolate";
150
+ // set z-index to 1 to ensure the nav is clickable over the other elements being animated
151
+ const navEl = queryNavEl(rootElRef.current);
152
+ if (navEl) {
153
+ navEl.style.zIndex = "1";
154
+ }
155
+
156
+ currentMonthEls.forEach((currentMonthEl, index) => {
157
+ const previousMonthEl = previousMonthEls[index];
158
+
159
+ if (!previousMonthEl) {
160
+ return;
161
+ }
162
+
163
+ // animate new displayed month
164
+ currentMonthEl.style.position = "relative";
165
+ currentMonthEl.style.overflow = "hidden";
166
+ const captionEl = queryCaptionEl(currentMonthEl);
167
+ if (captionEl) {
168
+ captionEl.classList.add(captionAnimationClass);
169
+ }
170
+
171
+ const weeksEl = queryWeeksEl(currentMonthEl);
172
+ if (weeksEl) {
173
+ weeksEl.classList.add(weeksAnimationClass);
174
+ }
175
+ // animate new displayed month end
176
+
177
+ const cleanUp = () => {
178
+ animatingRef.current = false;
179
+
180
+ if (rootElRef.current) {
181
+ rootElRef.current.style.isolation = "";
182
+ }
183
+ if (navEl) {
184
+ navEl.style.zIndex = "";
185
+ }
186
+
187
+ if (captionEl) {
188
+ captionEl.classList.remove(captionAnimationClass);
189
+ }
190
+ if (weeksEl) {
191
+ weeksEl.classList.remove(weeksAnimationClass);
192
+ }
193
+ currentMonthEl.style.position = "";
194
+ currentMonthEl.style.overflow = "";
195
+ if (currentMonthEl.contains(previousMonthEl)) {
196
+ currentMonthEl.removeChild(previousMonthEl);
197
+ }
198
+ };
199
+ cleanUpFunctions.push(cleanUp);
200
+
201
+ // animate old displayed month
202
+ previousMonthEl.style.pointerEvents = "none";
203
+ previousMonthEl.style.position = "absolute";
204
+ previousMonthEl.style.overflow = "hidden";
205
+ previousMonthEl.setAttribute("aria-hidden", "true");
206
+
207
+ // hide the weekdays container of the old month and only the new one
208
+ const previousWeekdaysEl = queryWeekdaysEl(previousMonthEl);
209
+ if (previousWeekdaysEl) {
210
+ previousWeekdaysEl.style.opacity = "0";
211
+ }
212
+
213
+ const previousCaptionEl = queryCaptionEl(previousMonthEl);
214
+ if (previousCaptionEl) {
215
+ previousCaptionEl.classList.add(
216
+ isAfterPreviousMonth
217
+ ? classNames[Animation.caption_before_exit]
218
+ : classNames[Animation.caption_after_exit]
219
+ );
220
+ previousCaptionEl.addEventListener("animationend", cleanUp);
221
+ }
222
+
223
+ const previousWeeksEl = queryWeeksEl(previousMonthEl);
224
+ if (previousWeeksEl) {
225
+ previousWeeksEl.classList.add(
226
+ isAfterPreviousMonth
227
+ ? classNames[Animation.weeks_before_exit]
228
+ : classNames[Animation.weeks_after_exit]
229
+ );
230
+ }
231
+
232
+ currentMonthEl.insertBefore(previousMonthEl, currentMonthEl.firstChild);
233
+ });
234
+ }
235
+ });
236
+ }
@@ -0,0 +1,178 @@
1
+ import { useEffect } from "react";
2
+
3
+ import type {
4
+ CalendarWeek,
5
+ CalendarDay,
6
+ CalendarMonth,
7
+ DateLib
8
+ } from "./classes/index.js";
9
+ import { getDates } from "./helpers/getDates.js";
10
+ import { getDays } from "./helpers/getDays.js";
11
+ import { getDisplayMonths } from "./helpers/getDisplayMonths.js";
12
+ import { getInitialMonth } from "./helpers/getInitialMonth.js";
13
+ import { getMonths } from "./helpers/getMonths.js";
14
+ import { getNavMonths } from "./helpers/getNavMonth.js";
15
+ import { getNextMonth } from "./helpers/getNextMonth.js";
16
+ import { getPreviousMonth } from "./helpers/getPreviousMonth.js";
17
+ import { getWeeks } from "./helpers/getWeeks.js";
18
+ import { useControlledValue } from "./helpers/useControlledValue.js";
19
+ import type { DayPickerProps } from "./types/props.js";
20
+
21
+ /**
22
+ * Return the calendar object to work with the calendar in custom components.
23
+ *
24
+ * @see https://daypicker.dev/guides/custom-components
25
+ */
26
+ export interface Calendar {
27
+ /**
28
+ * All the days displayed in the calendar. As opposite from
29
+ * {@link CalendarContext.dates}, it may return duplicated dates when shown
30
+ * outside the month.
31
+ */
32
+ days: CalendarDay[];
33
+ /** The months displayed in the calendar. */
34
+ weeks: CalendarWeek[];
35
+ /** The months displayed in the calendar. */
36
+ months: CalendarMonth[];
37
+
38
+ /** The next month to display. */
39
+ nextMonth: Date | undefined;
40
+ /** The previous month to display. */
41
+ previousMonth: Date | undefined;
42
+
43
+ /**
44
+ * The month where the navigation starts. `undefined` if the calendar can be
45
+ * navigated indefinitely to the past.
46
+ */
47
+ navStart: Date | undefined;
48
+ /**
49
+ * The month where the navigation ends. `undefined` if the calendar can be
50
+ * navigated indefinitely to the past.
51
+ */
52
+ navEnd: Date | undefined;
53
+
54
+ /** Navigate to the specified month. Will fire the `onMonthChange` callback. */
55
+ goToMonth: (month: Date) => void;
56
+ /**
57
+ * Navigate to the specified date. If the second parameter (refDate) is
58
+ * provided and the date is before the refDate, then the month is set to one
59
+ * month before the date.
60
+ *
61
+ * @param day - The date to navigate to.
62
+ * @param dateToCompare - Optional. If `date` is before `dateToCompare`, the
63
+ * month is set to one month before the date.
64
+ */
65
+ goToDay: (day: CalendarDay) => void;
66
+ }
67
+
68
+ /** @private */
69
+ export function useCalendar(
70
+ props: Pick<
71
+ DayPickerProps,
72
+ | "captionLayout"
73
+ | "endMonth"
74
+ | "startMonth"
75
+ | "today"
76
+ | "fixedWeeks"
77
+ | "ISOWeek"
78
+ | "numberOfMonths"
79
+ | "disableNavigation"
80
+ | "onMonthChange"
81
+ | "month"
82
+ | "defaultMonth"
83
+ | "timeZone"
84
+ | "broadcastCalendar"
85
+ // Deprecated:
86
+ | "fromMonth"
87
+ | "fromYear"
88
+ | "toMonth"
89
+ | "toYear"
90
+ >,
91
+ dateLib: DateLib
92
+ ): Calendar {
93
+ const [navStart, navEnd] = getNavMonths(props, dateLib);
94
+
95
+ const { startOfMonth, endOfMonth } = dateLib;
96
+ const initialMonth = getInitialMonth(props, dateLib);
97
+ const [firstMonth, setFirstMonth] = useControlledValue(
98
+ initialMonth,
99
+ // initialMonth is always computed from props.month if provided
100
+ props.month ? initialMonth : undefined
101
+ );
102
+
103
+ useEffect(() => {
104
+ const newInitialMonth = getInitialMonth(props, dateLib);
105
+ setFirstMonth(newInitialMonth);
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
+ }, [props.timeZone]);
108
+
109
+ /** The months displayed in the calendar. */
110
+ const displayMonths = getDisplayMonths(firstMonth, navEnd, props, dateLib);
111
+
112
+ /** The dates displayed in the calendar. */
113
+ const dates = getDates(
114
+ displayMonths,
115
+ props.endMonth ? endOfMonth(props.endMonth) : undefined,
116
+ props,
117
+ dateLib
118
+ );
119
+
120
+ /** The Months displayed in the calendar. */
121
+ const months = getMonths(displayMonths, dates, props, dateLib);
122
+
123
+ /** The Weeks displayed in the calendar. */
124
+ const weeks = getWeeks(months);
125
+
126
+ /** The Days displayed in the calendar. */
127
+ const days = getDays(months);
128
+
129
+ const previousMonth = getPreviousMonth(firstMonth, navStart, props, dateLib);
130
+ const nextMonth = getNextMonth(firstMonth, navEnd, props, dateLib);
131
+
132
+ const { disableNavigation, onMonthChange } = props;
133
+
134
+ const isDayInCalendar = (day: CalendarDay) =>
135
+ weeks.some((week: CalendarWeek) => week.days.some((d) => d.isEqualTo(day)));
136
+
137
+ const goToMonth = (date: Date) => {
138
+ if (disableNavigation) {
139
+ return;
140
+ }
141
+ let newMonth = startOfMonth(date);
142
+ // if month is before start, use the first month instead
143
+ if (navStart && newMonth < startOfMonth(navStart)) {
144
+ newMonth = startOfMonth(navStart);
145
+ }
146
+ // if month is after endMonth, use the last month instead
147
+ if (navEnd && newMonth > startOfMonth(navEnd)) {
148
+ newMonth = startOfMonth(navEnd);
149
+ }
150
+ setFirstMonth(newMonth);
151
+ onMonthChange?.(newMonth);
152
+ };
153
+
154
+ const goToDay = (day: CalendarDay) => {
155
+ // is this check necessary?
156
+ if (isDayInCalendar(day)) {
157
+ return;
158
+ }
159
+ goToMonth(day.date);
160
+ };
161
+
162
+ const calendar = {
163
+ months,
164
+ weeks,
165
+ days,
166
+
167
+ navStart,
168
+ navEnd,
169
+
170
+ previousMonth,
171
+ nextMonth,
172
+
173
+ goToMonth,
174
+ goToDay
175
+ };
176
+
177
+ return calendar;
178
+ }
@@ -0,0 +1,142 @@
1
+ import React from "react";
2
+
3
+ import { renderHook } from "@testing-library/react";
4
+
5
+ import { Animation, DayFlag, SelectionState, UI } from "./UI";
6
+ import { CalendarDay } from "./classes/CalendarDay";
7
+ import { CalendarMonth } from "./classes/CalendarMonth";
8
+ import { DayPickerProps } from "./types/props";
9
+ import { Modifiers } from "./types/shared";
10
+ import {
11
+ DayPickerContext,
12
+ dayPickerContext,
13
+ useDayPicker
14
+ } from "./useDayPicker";
15
+
16
+ describe("useDayPicker", () => {
17
+ const mockContextValue: DayPickerContext<{
18
+ required: false;
19
+ mode: "single";
20
+ }> = {
21
+ months: [new CalendarMonth(new Date(), [])],
22
+ nextMonth: new Date(),
23
+ previousMonth: new Date(),
24
+ goToMonth: jest.fn(),
25
+ getModifiers: jest.fn((day: CalendarDay) => ({}) as Modifiers),
26
+ selected: undefined,
27
+ select: jest.fn(),
28
+ isSelected: jest.fn((date: Date) => false),
29
+ components: {
30
+ Button: jest.fn(),
31
+ Chevron: jest.fn(),
32
+ CaptionLabel: jest.fn(),
33
+ Day: jest.fn(),
34
+ DayButton: jest.fn(),
35
+ Dropdown: jest.fn(),
36
+ DropdownNav: jest.fn(),
37
+ Footer: jest.fn(),
38
+ Month: jest.fn(),
39
+ MonthCaption: jest.fn(),
40
+ MonthGrid: jest.fn(),
41
+ Months: jest.fn(),
42
+ Nav: jest.fn(),
43
+ Option: jest.fn(),
44
+ PreviousMonthButton: jest.fn(),
45
+ NextMonthButton: jest.fn(),
46
+ Root: jest.fn(),
47
+ Select: jest.fn(),
48
+ Weeks: jest.fn(),
49
+ Week: jest.fn(),
50
+ Weekday: jest.fn(),
51
+ Weekdays: jest.fn(),
52
+ WeekNumber: jest.fn(),
53
+ WeekNumberHeader: jest.fn(),
54
+ MonthsDropdown: jest.fn(),
55
+ YearsDropdown: jest.fn()
56
+ },
57
+ classNames: {
58
+ [UI.Root]: "",
59
+ [UI.Chevron]: "",
60
+ [UI.Day]: "",
61
+ [UI.DayButton]: "",
62
+ [UI.CaptionLabel]: "",
63
+ [UI.Dropdowns]: "",
64
+ [UI.Dropdown]: "",
65
+ [UI.DropdownRoot]: "",
66
+ [UI.Footer]: "",
67
+ [UI.MonthGrid]: "",
68
+ [UI.MonthCaption]: "",
69
+ [UI.MonthsDropdown]: "",
70
+ [UI.Month]: "",
71
+ [UI.Months]: "",
72
+ [UI.Nav]: "",
73
+ [UI.NextMonthButton]: "",
74
+ [UI.PreviousMonthButton]: "",
75
+ [UI.Week]: "",
76
+ [UI.Weeks]: "",
77
+ [UI.Weekday]: "",
78
+ [UI.Weekdays]: "",
79
+ [UI.WeekNumber]: "",
80
+ [UI.WeekNumberHeader]: "",
81
+ [UI.YearsDropdown]: "",
82
+ [SelectionState.range_end]: "",
83
+ [SelectionState.range_middle]: "",
84
+ [SelectionState.range_start]: "",
85
+ [SelectionState.selected]: "",
86
+ [DayFlag.disabled]: "",
87
+ [DayFlag.hidden]: "",
88
+ [DayFlag.outside]: "",
89
+ [DayFlag.focused]: "",
90
+ [DayFlag.today]: "",
91
+ [Animation.weeks_after_enter]: "",
92
+ [Animation.weeks_before_exit]: "",
93
+ [Animation.weeks_before_enter]: "",
94
+ [Animation.weeks_after_exit]: "",
95
+ [Animation.caption_after_enter]: "",
96
+ [Animation.caption_before_exit]: "",
97
+ [Animation.caption_before_enter]: "",
98
+ [Animation.caption_after_exit]: ""
99
+ },
100
+ styles: {},
101
+ labels: {
102
+ labelNav: jest.fn(),
103
+ labelGrid: jest.fn(),
104
+ labelGridcell: jest.fn(),
105
+ labelMonthDropdown: jest.fn(),
106
+ labelYearDropdown: jest.fn(),
107
+ labelNext: jest.fn(),
108
+ labelPrevious: jest.fn(),
109
+ labelDayButton: jest.fn(),
110
+ labelDay: jest.fn(),
111
+ labelWeekday: jest.fn(),
112
+ labelWeekNumber: jest.fn(),
113
+ labelWeekNumberHeader: jest.fn()
114
+ },
115
+ formatters: {
116
+ formatCaption: jest.fn(),
117
+ formatDay: jest.fn(),
118
+ formatMonthDropdown: jest.fn(),
119
+ formatMonthCaption: jest.fn(),
120
+ formatWeekNumber: jest.fn(),
121
+ formatWeekNumberHeader: jest.fn(),
122
+ formatWeekdayName: jest.fn(),
123
+ formatYearDropdown: jest.fn(),
124
+ formatYearCaption: jest.fn()
125
+ },
126
+ dayPickerProps: {
127
+ mode: "single",
128
+ required: false
129
+ } as DayPickerProps
130
+ };
131
+
132
+ it("should return the context value when used within a DayPicker provider", () => {
133
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
134
+ <dayPickerContext.Provider value={mockContextValue}>
135
+ {children}
136
+ </dayPickerContext.Provider>
137
+ );
138
+
139
+ const { result } = renderHook(() => useDayPicker(), { wrapper });
140
+ expect(result.current).toEqual(mockContextValue);
141
+ });
142
+ });
@@ -0,0 +1,93 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ import { CalendarDay } from "./classes/CalendarDay.js";
4
+ import { CalendarMonth } from "./classes/CalendarMonth.js";
5
+ import { DayPickerProps } from "./types/props.js";
6
+ import type { SelectedValue, SelectHandler } from "./types/selection.js";
7
+ import {
8
+ ClassNames,
9
+ CustomComponents,
10
+ Formatters,
11
+ Labels,
12
+ Mode,
13
+ Modifiers,
14
+ Styles
15
+ } from "./types/shared.js";
16
+
17
+ /** @ignore */
18
+ export const dayPickerContext = createContext<
19
+ | DayPickerContext<{
20
+ mode?: Mode | undefined;
21
+ required?: boolean | undefined;
22
+ }>
23
+ | undefined
24
+ >(undefined);
25
+
26
+ /**
27
+ * Represents the context for the DayPicker component, providing various
28
+ * properties and methods to interact with the calendar.
29
+ *
30
+ * @template T - The type of the DayPicker props, which must optionally include
31
+ * `mode` and `required` properties. This type can be used to refine the type
32
+ * returned by the hook.
33
+ */
34
+ export type DayPickerContext<
35
+ T extends { mode?: Mode | undefined; required?: boolean | undefined }
36
+ > = {
37
+ /** The months displayed in the calendar. */
38
+ months: CalendarMonth[];
39
+ /** The next month to display. */
40
+ nextMonth: Date | undefined;
41
+ /** The previous month to display. */
42
+ previousMonth: Date | undefined;
43
+ /** Navigate to the specified month. Will fire the `onMonthChange` callback. */
44
+ goToMonth: (month: Date) => void;
45
+ /** Returns the modifiers for the given day. */
46
+ getModifiers: (day: CalendarDay) => Modifiers;
47
+ /** The selected date(s). */
48
+ selected: SelectedValue<T> | undefined;
49
+ /** Set a selection. */
50
+ select: SelectHandler<T> | undefined;
51
+ /** Whether the given date is selected. */
52
+ isSelected: ((date: Date) => boolean) | undefined;
53
+ /** The components used internally by DayPicker. */
54
+ components: CustomComponents;
55
+ /** The class names for the UI elements. */
56
+ classNames: ClassNames;
57
+ /** The styles for the UI elements. */
58
+ styles: Partial<Styles> | undefined;
59
+ /** The labels used in the user interface. */
60
+ labels: Labels;
61
+ /** The formatters used to format the UI elements. */
62
+ formatters: Formatters;
63
+ /**
64
+ * The props as passed to the DayPicker component.
65
+ *
66
+ * @since 9.3.0
67
+ */
68
+ dayPickerProps: DayPickerProps;
69
+ };
70
+
71
+ /**
72
+ * Returns the context to work with `<DayPicker />` inside custom components.
73
+ *
74
+ * This hook provides access to the DayPicker context, which includes various
75
+ * properties and methods to interact with the DayPicker component. It must be
76
+ * used within a custom component.
77
+ *
78
+ * @template T - Use this type to refine the returned context type with a
79
+ * specific selection mode.
80
+ * @returns {DayPickerContext<T>} The context to work with DayPicker.
81
+ * @throws {Error} If the hook is used outside of a DayPicker provider.
82
+ * @group Hooks
83
+ * @see https://daypicker.dev/guides/custom-components
84
+ */
85
+ export function useDayPicker<
86
+ T extends { mode?: Mode | undefined; required?: boolean | undefined }
87
+ >(): DayPickerContext<T> {
88
+ const context = useContext(dayPickerContext);
89
+ if (context === undefined) {
90
+ throw new Error("useDayPicker() must be used within a custom component.");
91
+ }
92
+ return context;
93
+ }