@octanejs/day-picker 0.0.1

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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -0
  3. package/UPSTREAM.md +24 -0
  4. package/package.json +77 -0
  5. package/src/DayPicker.tsrx +717 -0
  6. package/src/UI.ts +121 -0
  7. package/src/classes/CalendarDay.ts +82 -0
  8. package/src/classes/CalendarMonth.ts +20 -0
  9. package/src/classes/CalendarWeek.ts +19 -0
  10. package/src/classes/DateLib.ts +695 -0
  11. package/src/classes/index.ts +4 -0
  12. package/src/components/custom-components.tsrx +237 -0
  13. package/src/formatters/formatCaption.ts +18 -0
  14. package/src/formatters/formatDay.ts +17 -0
  15. package/src/formatters/formatMonthDropdown.ts +16 -0
  16. package/src/formatters/formatWeekNumber.ts +19 -0
  17. package/src/formatters/formatWeekNumberHeader.ts +11 -0
  18. package/src/formatters/formatWeekdayName.ts +17 -0
  19. package/src/formatters/formatYearDropdown.ts +15 -0
  20. package/src/formatters/index.ts +7 -0
  21. package/src/helpers/calculateFocusTarget.ts +79 -0
  22. package/src/helpers/createGetModifiers.ts +117 -0
  23. package/src/helpers/endOfBroadcastWeek.ts +22 -0
  24. package/src/helpers/getBroadcastWeeksInMonth.ts +32 -0
  25. package/src/helpers/getClassNamesForModifiers.ts +38 -0
  26. package/src/helpers/getComponents.ts +19 -0
  27. package/src/helpers/getDataAttributes.ts +17 -0
  28. package/src/helpers/getDates.ts +87 -0
  29. package/src/helpers/getDays.ts +19 -0
  30. package/src/helpers/getDefaultClassNames.ts +36 -0
  31. package/src/helpers/getDisplayMonths.ts +30 -0
  32. package/src/helpers/getFocusableDate.ts +66 -0
  33. package/src/helpers/getFormatters.ts +16 -0
  34. package/src/helpers/getInitialMonth.ts +35 -0
  35. package/src/helpers/getLabels.ts +93 -0
  36. package/src/helpers/getMonthOptions.ts +45 -0
  37. package/src/helpers/getMonths.ts +89 -0
  38. package/src/helpers/getNavMonth.ts +35 -0
  39. package/src/helpers/getNextFocus.ts +45 -0
  40. package/src/helpers/getNextMonth.ts +46 -0
  41. package/src/helpers/getPreviousMonth.ts +45 -0
  42. package/src/helpers/getStyleForModifiers.ts +31 -0
  43. package/src/helpers/getWeekdays.ts +33 -0
  44. package/src/helpers/getWeeks.ts +14 -0
  45. package/src/helpers/getYearOptions.ts +43 -0
  46. package/src/helpers/index.ts +2 -0
  47. package/src/helpers/startOfBroadcastWeek.ts +26 -0
  48. package/src/helpers/useControlledValue.ts +38 -0
  49. package/src/index.ts +13 -0
  50. package/src/labels/index.ts +12 -0
  51. package/src/labels/labelDayButton.ts +29 -0
  52. package/src/labels/labelGrid.ts +18 -0
  53. package/src/labels/labelGridcell.ts +26 -0
  54. package/src/labels/labelMonthDropdown.ts +14 -0
  55. package/src/labels/labelNav.ts +11 -0
  56. package/src/labels/labelNext.ts +17 -0
  57. package/src/labels/labelPrevious.ts +13 -0
  58. package/src/labels/labelWeekNumber.ts +15 -0
  59. package/src/labels/labelWeekNumberHeader.ts +14 -0
  60. package/src/labels/labelWeekday.ts +16 -0
  61. package/src/labels/labelYearDropdown.ts +14 -0
  62. package/src/locale/af.ts +48 -0
  63. package/src/locale/ar-DZ.ts +48 -0
  64. package/src/locale/ar-EG.ts +48 -0
  65. package/src/locale/ar-MA.ts +48 -0
  66. package/src/locale/ar-SA.ts +48 -0
  67. package/src/locale/ar-TN.ts +48 -0
  68. package/src/locale/ar.ts +48 -0
  69. package/src/locale/az.ts +48 -0
  70. package/src/locale/be-tarask.ts +51 -0
  71. package/src/locale/be.ts +48 -0
  72. package/src/locale/bg.ts +48 -0
  73. package/src/locale/bn.ts +48 -0
  74. package/src/locale/bs.ts +48 -0
  75. package/src/locale/ca.ts +48 -0
  76. package/src/locale/ckb.ts +51 -0
  77. package/src/locale/cs.ts +48 -0
  78. package/src/locale/cy.ts +48 -0
  79. package/src/locale/da.ts +48 -0
  80. package/src/locale/de-AT.ts +48 -0
  81. package/src/locale/de.ts +48 -0
  82. package/src/locale/el.ts +48 -0
  83. package/src/locale/en-AU.ts +48 -0
  84. package/src/locale/en-CA.ts +48 -0
  85. package/src/locale/en-GB.ts +51 -0
  86. package/src/locale/en-IE.ts +48 -0
  87. package/src/locale/en-IN.ts +48 -0
  88. package/src/locale/en-NZ.ts +48 -0
  89. package/src/locale/en-US.ts +72 -0
  90. package/src/locale/en-ZA.ts +48 -0
  91. package/src/locale/eo.ts +48 -0
  92. package/src/locale/es.ts +48 -0
  93. package/src/locale/et.ts +48 -0
  94. package/src/locale/eu.ts +48 -0
  95. package/src/locale/fa-IR.ts +48 -0
  96. package/src/locale/fi.ts +48 -0
  97. package/src/locale/fr-CA.ts +48 -0
  98. package/src/locale/fr-CH.ts +48 -0
  99. package/src/locale/fr.ts +48 -0
  100. package/src/locale/fy.ts +48 -0
  101. package/src/locale/gd.ts +48 -0
  102. package/src/locale/gl.ts +48 -0
  103. package/src/locale/gu.ts +48 -0
  104. package/src/locale/he.ts +48 -0
  105. package/src/locale/hi.ts +48 -0
  106. package/src/locale/hr.ts +48 -0
  107. package/src/locale/ht.ts +48 -0
  108. package/src/locale/hu.ts +48 -0
  109. package/src/locale/hy.ts +48 -0
  110. package/src/locale/id.ts +48 -0
  111. package/src/locale/is.ts +48 -0
  112. package/src/locale/it-CH.ts +48 -0
  113. package/src/locale/it.ts +48 -0
  114. package/src/locale/ja-Hira.ts +48 -0
  115. package/src/locale/ja.ts +48 -0
  116. package/src/locale/ka.ts +48 -0
  117. package/src/locale/kk.ts +48 -0
  118. package/src/locale/km.ts +48 -0
  119. package/src/locale/kn.ts +48 -0
  120. package/src/locale/ko.ts +48 -0
  121. package/src/locale/lb.ts +48 -0
  122. package/src/locale/lt.ts +48 -0
  123. package/src/locale/lv.ts +48 -0
  124. package/src/locale/mk.ts +48 -0
  125. package/src/locale/mn.ts +48 -0
  126. package/src/locale/ms.ts +48 -0
  127. package/src/locale/mt.ts +48 -0
  128. package/src/locale/nb.ts +48 -0
  129. package/src/locale/nl-BE.ts +48 -0
  130. package/src/locale/nl.ts +48 -0
  131. package/src/locale/nn.ts +48 -0
  132. package/src/locale/oc.ts +48 -0
  133. package/src/locale/pl.ts +48 -0
  134. package/src/locale/pt-BR.ts +48 -0
  135. package/src/locale/pt.ts +48 -0
  136. package/src/locale/ro.ts +48 -0
  137. package/src/locale/ru.ts +48 -0
  138. package/src/locale/se.ts +48 -0
  139. package/src/locale/sk.ts +48 -0
  140. package/src/locale/sl.ts +48 -0
  141. package/src/locale/sq.ts +48 -0
  142. package/src/locale/sr-Latn.ts +48 -0
  143. package/src/locale/sr.ts +48 -0
  144. package/src/locale/sv.ts +48 -0
  145. package/src/locale/ta.ts +48 -0
  146. package/src/locale/te.ts +48 -0
  147. package/src/locale/th.ts +48 -0
  148. package/src/locale/tr.ts +48 -0
  149. package/src/locale/ug.ts +48 -0
  150. package/src/locale/uk.ts +48 -0
  151. package/src/locale/uz-Cyrl.ts +48 -0
  152. package/src/locale/uz.ts +48 -0
  153. package/src/locale/vi.ts +48 -0
  154. package/src/locale/zh-CN.ts +48 -0
  155. package/src/locale/zh-HK.ts +51 -0
  156. package/src/locale/zh-TW.ts +51 -0
  157. package/src/locale.ts +108 -0
  158. package/src/noonDateLib.ts +202 -0
  159. package/src/selection/useMulti.ts +74 -0
  160. package/src/selection/useRange.ts +89 -0
  161. package/src/selection/useSingle.ts +77 -0
  162. package/src/style.css +451 -0
  163. package/src/style.css.d.ts +3 -0
  164. package/src/style.module.css +451 -0
  165. package/src/style.module.css.d.ts +29 -0
  166. package/src/types/index.ts +3 -0
  167. package/src/types/props.ts +671 -0
  168. package/src/types/selection.ts +97 -0
  169. package/src/types/shared.ts +346 -0
  170. package/src/useAnimation.ts +242 -0
  171. package/src/useCalendar.ts +233 -0
  172. package/src/useDayPicker.ts +91 -0
  173. package/src/useFocus.ts +109 -0
  174. package/src/useSelection.ts +36 -0
  175. package/src/utils/addToRange.ts +91 -0
  176. package/src/utils/convertMatchersToTimeZone.ts +88 -0
  177. package/src/utils/dateMatchModifiers.ts +72 -0
  178. package/src/utils/index.ts +7 -0
  179. package/src/utils/rangeContainsDayOfWeek.ts +33 -0
  180. package/src/utils/rangeContainsModifiers.ts +106 -0
  181. package/src/utils/rangeIncludesDate.ts +40 -0
  182. package/src/utils/rangeOverlaps.ts +26 -0
  183. package/src/utils/toTimeZone.ts +13 -0
  184. package/src/utils/typeguards.ts +71 -0
  185. package/status.json +13 -0
@@ -0,0 +1,717 @@
1
+ // @ts-nocheck -- React-compatible DOM props cross Octane's stricter event boundary here.
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'octane';
3
+ import type { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent } from 'react';
4
+ import type { CalendarDay } from './classes/CalendarDay.js';
5
+ import { DateLib, defaultLocale } from './classes/DateLib.js';
6
+ import { createGetModifiers } from './helpers/createGetModifiers.js';
7
+ import { getClassNamesForModifiers } from './helpers/getClassNamesForModifiers.js';
8
+ import { getComponents } from './helpers/getComponents.js';
9
+ import { getDataAttributes } from './helpers/getDataAttributes.js';
10
+ import { getDefaultClassNames } from './helpers/getDefaultClassNames.js';
11
+ import { getInitialMonth } from './helpers/getInitialMonth.js';
12
+ import { getFormatters } from './helpers/getFormatters.js';
13
+ import { getLabels } from './helpers/getLabels.js';
14
+ import { getMonthOptions } from './helpers/getMonthOptions.js';
15
+ import { getNavMonths } from './helpers/getNavMonth.js';
16
+ import { getStyleForModifiers } from './helpers/getStyleForModifiers.js';
17
+ import { getWeekdays } from './helpers/getWeekdays.js';
18
+ import { getYearOptions } from './helpers/getYearOptions.js';
19
+ import { createNoonOverrides } from './noonDateLib.js';
20
+ import type {
21
+ DayPickerProps,
22
+ Modifiers,
23
+ MoveFocusBy,
24
+ MoveFocusDir,
25
+ SelectedValue,
26
+ SelectHandler,
27
+ } from './types/index.js';
28
+ import { DayFlag, SelectionState, UI } from './UI.js';
29
+ import { useAnimation } from './useAnimation.js';
30
+ import { useCalendar } from './useCalendar.js';
31
+ import { type DayPickerContext, dayPickerContext } from './useDayPicker.js';
32
+ import { useFocus } from './useFocus.js';
33
+ import { useSelection } from './useSelection.js';
34
+ import { convertMatchersToTimeZone } from './utils/convertMatchersToTimeZone.js';
35
+ import { rangeIncludesDate } from './utils/rangeIncludesDate.js';
36
+ import { toTimeZone } from './utils/toTimeZone.js';
37
+ import { isDateRange } from './utils/typeguards.js';
38
+
39
+ const noDateSelected = () => false;
40
+
41
+ /**
42
+ * Renders the DayPicker calendar component.
43
+ *
44
+ * @param initialProps - The props for the DayPicker component.
45
+ * @returns The rendered DayPicker component.
46
+ * @group DayPicker
47
+ * @see https://daypicker.dev
48
+ */
49
+ export function DayPicker(initialProps: DayPickerProps) @{
50
+ let props = initialProps;
51
+ const timeZone = props.timeZone;
52
+
53
+ if (timeZone) {
54
+ props = {
55
+ ...initialProps,
56
+ timeZone,
57
+ };
58
+ if (props.today) {
59
+ props.today = toTimeZone(props.today, timeZone);
60
+ }
61
+ if (props.month) {
62
+ props.month = toTimeZone(props.month, timeZone);
63
+ }
64
+ if (props.defaultMonth) {
65
+ props.defaultMonth = toTimeZone(props.defaultMonth, timeZone);
66
+ }
67
+ if (props.startMonth) {
68
+ props.startMonth = toTimeZone(props.startMonth, timeZone);
69
+ }
70
+ if (props.endMonth) {
71
+ props.endMonth = toTimeZone(props.endMonth, timeZone);
72
+ }
73
+ if (props.mode === 'single' && props.selected) {
74
+ props.selected = toTimeZone(props.selected, timeZone);
75
+ } else if (props.mode === 'multiple' && props.selected) {
76
+ props.selected = props.selected?.map((date) => toTimeZone(date, timeZone));
77
+ } else if (props.mode === 'range' && props.selected) {
78
+ props.selected = {
79
+ from: props.selected.from ? toTimeZone(props.selected.from, timeZone) : props.selected.from,
80
+ to: props.selected.to ? toTimeZone(props.selected.to, timeZone) : props.selected.to,
81
+ };
82
+ }
83
+ if (props.disabled !== undefined) {
84
+ props.disabled = convertMatchersToTimeZone(props.disabled, timeZone);
85
+ }
86
+ if (props.hidden !== undefined) {
87
+ props.hidden = convertMatchersToTimeZone(props.hidden, timeZone);
88
+ }
89
+ if (props.modifiers) {
90
+ const nextModifiers: NonNullable<typeof props.modifiers> = {};
91
+ Object.keys(props.modifiers).forEach((key) => {
92
+ nextModifiers[key] = convertMatchersToTimeZone(props.modifiers?.[key], timeZone);
93
+ });
94
+ props.modifiers = nextModifiers;
95
+ }
96
+ }
97
+ const { components, formatters, labels, dateLib, locale, classNames } = useMemo(() => {
98
+ const locale = { ...defaultLocale, ...props.locale };
99
+ const weekStartsOn = props.broadcastCalendar ? 1 : props.weekStartsOn;
100
+
101
+ const noonOverrides =
102
+ props.noonSafe && props.timeZone
103
+ ? createNoonOverrides(props.timeZone, {
104
+ weekStartsOn,
105
+ locale,
106
+ })
107
+ : undefined;
108
+ const overrides =
109
+ props.dateLib && noonOverrides
110
+ ? { ...noonOverrides, ...props.dateLib }
111
+ : props.dateLib ?? noonOverrides;
112
+
113
+ const dateLib = new DateLib({
114
+ locale,
115
+ weekStartsOn,
116
+ firstWeekContainsDate: props.firstWeekContainsDate,
117
+ useAdditionalWeekYearTokens: props.useAdditionalWeekYearTokens,
118
+ useAdditionalDayOfYearTokens: props.useAdditionalDayOfYearTokens,
119
+ timeZone: props.timeZone,
120
+ numerals: props.numerals,
121
+ }, overrides);
122
+
123
+ return {
124
+ dateLib,
125
+ components: getComponents(props.components),
126
+ formatters: getFormatters(props.formatters),
127
+ labels: getLabels(props.labels, dateLib.options),
128
+ locale,
129
+ classNames: { ...getDefaultClassNames(), ...props.classNames },
130
+ };
131
+ }, [
132
+ props.locale,
133
+ props.broadcastCalendar,
134
+ props.weekStartsOn,
135
+ props.firstWeekContainsDate,
136
+ props.useAdditionalWeekYearTokens,
137
+ props.useAdditionalDayOfYearTokens,
138
+ props.timeZone,
139
+ props.numerals,
140
+ props.dateLib,
141
+ props.noonSafe,
142
+ props.components,
143
+ props.formatters,
144
+ props.labels,
145
+ props.classNames,
146
+ ]);
147
+
148
+ if (!props.today) {
149
+ props = { ...props, today: dateLib.today() };
150
+ }
151
+
152
+ // Keep uncontrolled navigation in the compiled component scope. Plain `.ts`
153
+ // custom hooks share their enclosing slot path, while this state cell receives
154
+ // a compiler-owned call-site slot and therefore schedules the DayPicker block
155
+ // directly when navigation changes the visible month.
156
+ const monthIsControlled = initialProps.month !== undefined;
157
+ const [uncontrolledMonth, setUncontrolledMonth] = useState(() => {
158
+ const [navStart, navEnd] = getNavMonths(props, dateLib);
159
+ return getInitialMonth(props, navStart, navEnd, dateLib);
160
+ });
161
+ const previousTimeZone = useRef(props.timeZone);
162
+ useEffect(() => {
163
+ if (previousTimeZone.current === props.timeZone) return;
164
+ previousTimeZone.current = props.timeZone;
165
+ if (monthIsControlled) return;
166
+ const [navStart, navEnd] = getNavMonths(props, dateLib);
167
+ setUncontrolledMonth(
168
+ getInitialMonth({ ...props, month: undefined }, navStart, navEnd, dateLib),
169
+ );
170
+ }, [props.timeZone]);
171
+
172
+ const {
173
+ captionLayout,
174
+ mode,
175
+ navLayout,
176
+ numberOfMonths = 1,
177
+ onDayBlur,
178
+ onDayClick,
179
+ onDayFocus,
180
+ onDayKeyDown,
181
+ onDayMouseEnter,
182
+ onDayMouseLeave,
183
+ onNextClick,
184
+ onPrevClick,
185
+ showWeekNumber,
186
+ styles,
187
+ } = props;
188
+
189
+ const {
190
+ formatCaption,
191
+ formatDay,
192
+ formatMonthDropdown,
193
+ formatWeekNumber,
194
+ formatWeekNumberHeader,
195
+ formatWeekdayName,
196
+ formatYearDropdown,
197
+ } = formatters;
198
+
199
+ const calendar = useCalendar(props, dateLib, monthIsControlled ? undefined : uncontrolledMonth, (
200
+ month,
201
+ ) => {
202
+ setUncontrolledMonth(month);
203
+ props.onMonthChange?.(month);
204
+ });
205
+
206
+ const { days, months, navStart, navEnd, previousMonth, nextMonth, goToMonth } = calendar;
207
+
208
+ const getModifiers = createGetModifiers(days, props, navStart, navEnd, dateLib);
209
+
210
+ const { isSelected, select, selected: selectedValue } = useSelection(props, dateLib) ?? {};
211
+ const isDateSelected = isSelected ?? noDateSelected;
212
+
213
+ const { blur, focused, isFocusTarget, moveFocus, setFocused } = useFocus(
214
+ props,
215
+ calendar,
216
+ getModifiers,
217
+ isDateSelected,
218
+ dateLib,
219
+ );
220
+
221
+ const {
222
+ labelDayButton,
223
+ labelGridcell,
224
+ labelGrid,
225
+ labelMonthDropdown,
226
+ labelNav,
227
+ labelPrevious,
228
+ labelNext,
229
+ labelWeekday,
230
+ labelWeekNumber,
231
+ labelWeekNumberHeader,
232
+ labelYearDropdown,
233
+ } = labels;
234
+
235
+ const weekdays = useMemo(
236
+ () => getWeekdays(dateLib, props.ISOWeek, props.broadcastCalendar, props.today),
237
+ [dateLib, props.ISOWeek, props.broadcastCalendar, props.today],
238
+ );
239
+
240
+ const isInteractive = mode !== undefined || onDayClick !== undefined;
241
+
242
+ const handlePreviousClick = useCallback(() => {
243
+ if (!previousMonth) return;
244
+ goToMonth(previousMonth);
245
+ onPrevClick?.(previousMonth);
246
+ }, [previousMonth, goToMonth, onPrevClick]);
247
+
248
+ const handleNextClick = useCallback(() => {
249
+ if (!nextMonth) return;
250
+ goToMonth(nextMonth);
251
+ onNextClick?.(nextMonth);
252
+ }, [goToMonth, nextMonth, onNextClick]);
253
+
254
+ const handleDayClick = useCallback(
255
+ (day: CalendarDay, m: Modifiers) => (e: MouseEvent) => {
256
+ e.preventDefault();
257
+ e.stopPropagation();
258
+ setFocused(day);
259
+ if (m.disabled) {
260
+ return;
261
+ }
262
+ select?.(day.date, m, e);
263
+ onDayClick?.(day.date, m, e);
264
+ },
265
+ [select, onDayClick, setFocused],
266
+ );
267
+
268
+ const handleDayFocus = useCallback(
269
+ (day: CalendarDay, m: Modifiers) => (e: FocusEvent) => {
270
+ setFocused(day);
271
+ onDayFocus?.(day.date, m, e);
272
+ },
273
+ [onDayFocus, setFocused],
274
+ );
275
+
276
+ const handleDayBlur = useCallback(
277
+ (day: CalendarDay, m: Modifiers) => (e: FocusEvent) => {
278
+ blur();
279
+ onDayBlur?.(day.date, m, e);
280
+ },
281
+ [blur, onDayBlur],
282
+ );
283
+
284
+ const handleDayKeyDown = useCallback(
285
+ (day: CalendarDay, modifiers: Modifiers) => (e: KeyboardEvent) => {
286
+ const keyMap: Record<string, [MoveFocusBy, MoveFocusDir]> = {
287
+ ArrowLeft: [e.shiftKey ? 'month' : 'day', props.dir === 'rtl' ? 'after' : 'before'],
288
+ ArrowRight: [e.shiftKey ? 'month' : 'day', props.dir === 'rtl' ? 'before' : 'after'],
289
+ ArrowDown: [e.shiftKey ? 'year' : 'week', 'after'],
290
+ ArrowUp: [e.shiftKey ? 'year' : 'week', 'before'],
291
+ PageUp: [e.shiftKey ? 'year' : 'month', 'before'],
292
+ PageDown: [e.shiftKey ? 'year' : 'month', 'after'],
293
+ Home: ['startOfWeek', 'before'],
294
+ End: ['endOfWeek', 'after'],
295
+ };
296
+ if (keyMap[e.key]) {
297
+ e.preventDefault();
298
+ e.stopPropagation();
299
+ const [moveBy, moveDir] = keyMap[e.key];
300
+ moveFocus(moveBy, moveDir);
301
+ }
302
+ onDayKeyDown?.(day.date, modifiers, e);
303
+ },
304
+ [moveFocus, onDayKeyDown, props.dir],
305
+ );
306
+
307
+ const handleDayMouseEnter = useCallback(
308
+ (day: CalendarDay, modifiers: Modifiers) => (e: MouseEvent) => {
309
+ onDayMouseEnter?.(day.date, modifiers, e);
310
+ },
311
+ [onDayMouseEnter],
312
+ );
313
+
314
+ const handleDayMouseLeave = useCallback(
315
+ (day: CalendarDay, modifiers: Modifiers) => (e: MouseEvent) => {
316
+ onDayMouseLeave?.(day.date, modifiers, e);
317
+ },
318
+ [onDayMouseLeave],
319
+ );
320
+
321
+ const handleMonthChange = useCallback(
322
+ (date: Date, monthOffset: number) => (e: ChangeEvent<HTMLSelectElement>) => {
323
+ const selectedMonth = Number(e.target.value);
324
+ const month = dateLib.setMonth(dateLib.startOfMonth(date), selectedMonth);
325
+ goToMonth(dateLib.addMonths(month, -monthOffset));
326
+ },
327
+ [dateLib, goToMonth],
328
+ );
329
+
330
+ const handleYearChange = useCallback(
331
+ (date: Date, monthOffset: number) => (e: ChangeEvent<HTMLSelectElement>) => {
332
+ const selectedYear = Number(e.target.value);
333
+ const month = dateLib.setYear(dateLib.startOfMonth(date), selectedYear);
334
+ goToMonth(dateLib.addMonths(month, -monthOffset));
335
+ },
336
+ [dateLib, goToMonth],
337
+ );
338
+
339
+ const { className, style } = useMemo(
340
+ () => ({
341
+ className: [classNames[UI.Root], props.className].filter(Boolean).join(' '),
342
+ style: { ...styles?.[UI.Root], ...props.style },
343
+ }),
344
+ [classNames, props.className, props.style, styles],
345
+ );
346
+
347
+ const dataAttributes = getDataAttributes(props);
348
+
349
+ const getDropdownStyle = (dropdown: UI.MonthsDropdown | UI.YearsDropdown) => {
350
+ const dropdownStyle = styles?.[UI.Dropdown];
351
+ const specificDropdownStyle = styles?.[dropdown];
352
+
353
+ if (!dropdownStyle && !specificDropdownStyle) {
354
+ return undefined;
355
+ }
356
+
357
+ return {
358
+ ...dropdownStyle,
359
+ ...specificDropdownStyle,
360
+ };
361
+ };
362
+
363
+ const rootElRef = useRef<HTMLDivElement | null>(null);
364
+ useAnimation(rootElRef, Boolean(props.animate), {
365
+ classNames,
366
+ months,
367
+ focused,
368
+ dateLib,
369
+ });
370
+
371
+ const contextValue: DayPickerContext<DayPickerProps> = {
372
+ dayPickerProps: props,
373
+ selected: selectedValue as SelectedValue<DayPickerProps>,
374
+ select: select as SelectHandler<DayPickerProps>,
375
+ isSelected,
376
+ months,
377
+ nextMonth,
378
+ previousMonth,
379
+ goToMonth,
380
+ getModifiers,
381
+ components,
382
+ classNames,
383
+ styles,
384
+ labels,
385
+ formatters,
386
+ };
387
+
388
+ <dayPickerContext.Provider value={contextValue}>
389
+ <components.Root
390
+ rootRef={props.animate ? rootElRef : undefined}
391
+ className={className}
392
+ style={style}
393
+ dir={props.dir}
394
+ id={props.id}
395
+ lang={props.lang ?? locale.code}
396
+ nonce={props.nonce}
397
+ title={props.title}
398
+ role={props.role}
399
+ aria-label={props['aria-label']}
400
+ aria-labelledby={props['aria-labelledby']}
401
+ {...dataAttributes}
402
+ >
403
+ <components.Months className={classNames[UI.Months]} style={styles?.[UI.Months]}>
404
+ {!props.hideNavigation && !navLayout &&
405
+ <components.Nav
406
+ data-animated-nav={props.animate ? 'true' : undefined}
407
+ className={classNames[UI.Nav]}
408
+ style={styles?.[UI.Nav]}
409
+ aria-label={labelNav()}
410
+ onPreviousClick={handlePreviousClick}
411
+ onNextClick={handleNextClick}
412
+ previousMonth={previousMonth}
413
+ nextMonth={nextMonth}
414
+ />}
415
+ {months.map((calendarMonth, displayIndex) => {
416
+ const monthOffset = props.reverseMonths ? months.length - 1 - displayIndex : displayIndex;
417
+
418
+ return <components.Month
419
+ data-animated-month={props.animate ? 'true' : undefined}
420
+ className={classNames[UI.Month]}
421
+ style={styles?.[UI.Month]}
422
+ // biome-ignore lint/suspicious/noArrayIndexKey: breaks animation
423
+ key={displayIndex}
424
+ displayIndex={displayIndex}
425
+ calendarMonth={calendarMonth}
426
+ >
427
+ {navLayout === 'around' && !props.hideNavigation && displayIndex === 0 &&
428
+ <components.PreviousMonthButton
429
+ type="button"
430
+ className={classNames[UI.PreviousMonthButton]}
431
+ style={styles?.[UI.PreviousMonthButton]}
432
+ tabIndex={previousMonth ? undefined : -1}
433
+ aria-disabled={previousMonth ? undefined : true}
434
+ aria-label={labelPrevious(previousMonth)}
435
+ onClick={handlePreviousClick}
436
+ data-animated-button={props.animate ? 'true' : undefined}
437
+ >
438
+ <components.Chevron
439
+ disabled={previousMonth ? undefined : true}
440
+ className={classNames[UI.Chevron]}
441
+ style={styles?.[UI.Chevron]}
442
+ orientation={props.dir === 'rtl' ? 'right' : 'left'}
443
+ />
444
+ </components.PreviousMonthButton>}
445
+ <components.MonthCaption
446
+ data-animated-caption={props.animate ? 'true' : undefined}
447
+ className={classNames[UI.MonthCaption]}
448
+ style={styles?.[UI.MonthCaption]}
449
+ calendarMonth={calendarMonth}
450
+ displayIndex={displayIndex}
451
+ >
452
+ {captionLayout?.startsWith('dropdown')
453
+ ? <components.DropdownNav
454
+ className={classNames[UI.Dropdowns]}
455
+ style={styles?.[UI.Dropdowns]}
456
+ >
457
+ {(() => {
458
+ const monthControl =
459
+ captionLayout === 'dropdown' || captionLayout === 'dropdown-months'
460
+ ? <components.MonthsDropdown
461
+ key="month"
462
+ className={classNames[UI.MonthsDropdown]}
463
+ aria-label={labelMonthDropdown()}
464
+ disabled={Boolean(props.disableNavigation)}
465
+ onChange={handleMonthChange(calendarMonth.date, monthOffset)}
466
+ options={getMonthOptions(
467
+ calendarMonth.date,
468
+ navStart,
469
+ navEnd,
470
+ formatters,
471
+ dateLib,
472
+ )}
473
+ style={getDropdownStyle(UI.MonthsDropdown)}
474
+ value={dateLib.getMonth(calendarMonth.date)}
475
+ />
476
+ : <span key="month">{formatMonthDropdown(
477
+ calendarMonth.date,
478
+ dateLib,
479
+ )}</span>;
480
+
481
+ const yearControl =
482
+ captionLayout === 'dropdown' || captionLayout === 'dropdown-years'
483
+ ? <components.YearsDropdown
484
+ key="year"
485
+ className={classNames[UI.YearsDropdown]}
486
+ aria-label={labelYearDropdown(dateLib.options)}
487
+ disabled={Boolean(props.disableNavigation)}
488
+ onChange={handleYearChange(calendarMonth.date, monthOffset)}
489
+ options={getYearOptions(
490
+ navStart,
491
+ navEnd,
492
+ formatters,
493
+ dateLib,
494
+ Boolean(props.reverseYears),
495
+ )}
496
+ style={getDropdownStyle(UI.YearsDropdown)}
497
+ value={dateLib.getYear(calendarMonth.date)}
498
+ />
499
+ : <span key="year">{formatYearDropdown(
500
+ calendarMonth.date,
501
+ dateLib,
502
+ )}</span>;
503
+
504
+ const controls =
505
+ dateLib.getMonthYearOrder() === 'year-first'
506
+ ? [yearControl, monthControl]
507
+ : [monthControl, yearControl];
508
+
509
+ return controls;
510
+ })()}
511
+ <span
512
+ role="status"
513
+ aria-live="polite"
514
+ style={{
515
+ border: 0,
516
+ clip: 'rect(0 0 0 0)',
517
+ height: '1px',
518
+ margin: '-1px',
519
+ overflow: 'hidden',
520
+ padding: 0,
521
+ position: 'absolute',
522
+ width: '1px',
523
+ whiteSpace: 'nowrap',
524
+ wordWrap: 'normal',
525
+ }}
526
+ >{formatCaption(calendarMonth.date, dateLib.options, dateLib)}</span>
527
+ </components.DropdownNav>
528
+ : <components.CaptionLabel
529
+ className={classNames[UI.CaptionLabel]}
530
+ style={styles?.[UI.CaptionLabel]}
531
+ role="status"
532
+ aria-live="polite"
533
+ >{formatCaption(
534
+ calendarMonth.date,
535
+ dateLib.options,
536
+ dateLib,
537
+ )}</components.CaptionLabel>}
538
+ </components.MonthCaption>
539
+ {navLayout === 'around' && !props.hideNavigation &&
540
+ displayIndex === numberOfMonths - 1 &&
541
+ <components.NextMonthButton
542
+ type="button"
543
+ className={classNames[UI.NextMonthButton]}
544
+ style={styles?.[UI.NextMonthButton]}
545
+ tabIndex={nextMonth ? undefined : -1}
546
+ aria-disabled={nextMonth ? undefined : true}
547
+ aria-label={labelNext(nextMonth)}
548
+ onClick={handleNextClick}
549
+ data-animated-button={props.animate ? 'true' : undefined}
550
+ >
551
+ <components.Chevron
552
+ disabled={nextMonth ? undefined : true}
553
+ className={classNames[UI.Chevron]}
554
+ style={styles?.[UI.Chevron]}
555
+ orientation={props.dir === 'rtl' ? 'left' : 'right'}
556
+ />
557
+ </components.NextMonthButton>}
558
+ {displayIndex === numberOfMonths - 1 && navLayout === 'after' &&
559
+ !props.hideNavigation &&
560
+ <components.Nav
561
+ data-animated-nav={props.animate ? 'true' : undefined}
562
+ className={classNames[UI.Nav]}
563
+ style={styles?.[UI.Nav]}
564
+ aria-label={labelNav()}
565
+ onPreviousClick={handlePreviousClick}
566
+ onNextClick={handleNextClick}
567
+ previousMonth={previousMonth}
568
+ nextMonth={nextMonth}
569
+ />}
570
+
571
+ <components.MonthGrid
572
+ role="grid"
573
+ aria-multiselectable={mode === 'multiple' || mode === 'range'}
574
+ aria-label={labelGrid(calendarMonth.date, dateLib.options, dateLib) || undefined}
575
+ className={classNames[UI.MonthGrid]}
576
+ style={styles?.[UI.MonthGrid]}
577
+ >
578
+ {!props.hideWeekdays &&
579
+ <components.Weekdays
580
+ data-animated-weekdays={props.animate ? 'true' : undefined}
581
+ className={classNames[UI.Weekdays]}
582
+ style={styles?.[UI.Weekdays]}
583
+ >
584
+ {showWeekNumber &&
585
+ <components.WeekNumberHeader
586
+ aria-label={labelWeekNumberHeader(dateLib.options)}
587
+ className={classNames[UI.WeekNumberHeader]}
588
+ style={styles?.[UI.WeekNumberHeader]}
589
+ scope="col"
590
+ >{formatWeekNumberHeader()}</components.WeekNumberHeader>}
591
+ {weekdays.map(
592
+ (weekday) =>
593
+ <components.Weekday
594
+ aria-label={labelWeekday(weekday, dateLib.options, dateLib)}
595
+ className={classNames[UI.Weekday]}
596
+ key={String(weekday)}
597
+ style={styles?.[UI.Weekday]}
598
+ scope="col"
599
+ >{formatWeekdayName(weekday, dateLib.options, dateLib)}</components.Weekday>,
600
+ )}
601
+ </components.Weekdays>}
602
+ <components.Weeks
603
+ data-animated-weeks={props.animate ? 'true' : undefined}
604
+ className={classNames[UI.Weeks]}
605
+ style={styles?.[UI.Weeks]}
606
+ >{calendarMonth.weeks.map((week) => {
607
+ return <components.Week
608
+ className={classNames[UI.Week]}
609
+ key={week.weekNumber}
610
+ style={styles?.[UI.Week]}
611
+ week={week}
612
+ >
613
+ {showWeekNumber &&
614
+ <components.WeekNumber
615
+ week={week}
616
+ style={styles?.[UI.WeekNumber]}
617
+ aria-label={labelWeekNumber(week.weekNumber, {
618
+ locale,
619
+ })}
620
+ className={classNames[UI.WeekNumber]}
621
+ scope="row"
622
+ role="rowheader"
623
+ >{formatWeekNumber(week.weekNumber, dateLib)}</components.WeekNumber>}
624
+ {week.days.map((day: CalendarDay) => {
625
+ const { date } = day;
626
+ const modifiers = getModifiers(day);
627
+
628
+ modifiers[DayFlag.focused] = !modifiers.hidden &&
629
+ Boolean(focused?.isEqualTo(day));
630
+
631
+ modifiers[SelectionState.selected] = isSelected?.(date) || modifiers.selected;
632
+
633
+ if (isDateRange(selectedValue)) {
634
+ // add range modifiers
635
+ const { from, to } = selectedValue;
636
+ modifiers[SelectionState.range_start] = Boolean(
637
+ from && to && dateLib.isSameDay(date, from),
638
+ );
639
+ modifiers[SelectionState.range_end] = Boolean(
640
+ from && to && dateLib.isSameDay(date, to),
641
+ );
642
+ modifiers[SelectionState.range_middle] = rangeIncludesDate(
643
+ selectedValue,
644
+ date,
645
+ true,
646
+ dateLib,
647
+ );
648
+ }
649
+
650
+ const style = getStyleForModifiers(modifiers, styles, props.modifiersStyles);
651
+
652
+ const className = getClassNamesForModifiers(
653
+ modifiers,
654
+ classNames,
655
+ props.modifiersClassNames,
656
+ );
657
+
658
+ const ariaLabel =
659
+ !isInteractive && !modifiers.hidden
660
+ ? labelGridcell(date, modifiers, dateLib.options, dateLib)
661
+ : undefined;
662
+
663
+ return <components.Day
664
+ key={`${day.isoDate}_${day.displayMonthId}`}
665
+ day={day}
666
+ modifiers={modifiers}
667
+ className={className.join(' ')}
668
+ style={style}
669
+ role="gridcell"
670
+ aria-selected={modifiers.selected || undefined}
671
+ aria-label={ariaLabel}
672
+ data-day={day.isoDate}
673
+ data-month={day.outside ? day.dateMonthId : undefined}
674
+ data-selected={modifiers.selected || undefined}
675
+ data-disabled={modifiers.disabled || undefined}
676
+ data-hidden={modifiers.hidden || undefined}
677
+ data-outside={day.outside || undefined}
678
+ data-focused={modifiers.focused || undefined}
679
+ data-today={modifiers.today || undefined}
680
+ >
681
+ {!modifiers.hidden && isInteractive
682
+ ? <components.DayButton
683
+ className={classNames[UI.DayButton]}
684
+ style={styles?.[UI.DayButton]}
685
+ type="button"
686
+ day={day}
687
+ modifiers={modifiers}
688
+ disabled={!modifiers.focused && modifiers.disabled || undefined}
689
+ aria-disabled={modifiers.focused && modifiers.disabled || undefined}
690
+ tabIndex={isFocusTarget(day) ? 0 : -1}
691
+ aria-label={labelDayButton(date, modifiers, dateLib.options, dateLib)}
692
+ onClick={handleDayClick(day, modifiers)}
693
+ onBlur={handleDayBlur(day, modifiers)}
694
+ onFocus={handleDayFocus(day, modifiers)}
695
+ onKeyDown={handleDayKeyDown(day, modifiers)}
696
+ onMouseEnter={handleDayMouseEnter(day, modifiers)}
697
+ onMouseLeave={handleDayMouseLeave(day, modifiers)}
698
+ >{formatDay(date, dateLib.options, dateLib)}</components.DayButton>
699
+ : !modifiers.hidden && formatDay(day.date, dateLib.options, dateLib)}
700
+ </components.Day>;
701
+ })}
702
+ </components.Week>;
703
+ })}</components.Weeks>
704
+ </components.MonthGrid>
705
+ </components.Month>;
706
+ })}
707
+ </components.Months>
708
+ {props.footer &&
709
+ <components.Footer
710
+ className={classNames[UI.Footer]}
711
+ style={styles?.[UI.Footer]}
712
+ role="status"
713
+ aria-live="polite"
714
+ >{props.footer}</components.Footer>}
715
+ </components.Root>
716
+ </dayPickerContext.Provider>
717
+ }