@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,233 @@
1
+ import { useEffect, useMemo } from 'octane';
2
+
3
+ import type { CalendarDay, CalendarMonth, CalendarWeek, DateLib } from './classes/index.js';
4
+ import { getDates } from './helpers/getDates.js';
5
+ import { getDays } from './helpers/getDays.js';
6
+ import { getDisplayMonths } from './helpers/getDisplayMonths.js';
7
+ import { getInitialMonth } from './helpers/getInitialMonth.js';
8
+ import { getMonths } from './helpers/getMonths.js';
9
+ import { getNavMonths } from './helpers/getNavMonth.js';
10
+ import { getNextMonth } from './helpers/getNextMonth.js';
11
+ import { getPreviousMonth } from './helpers/getPreviousMonth.js';
12
+ import { getWeeks } from './helpers/getWeeks.js';
13
+ import { useControlledValue } from './helpers/useControlledValue.js';
14
+ import type { DayPickerProps } from './types/props.js';
15
+
16
+ const calendarStateSlot = Symbol.for('@octanejs/day-picker/useCalendar/state');
17
+ const calendarTimeZoneEffectSlot = Symbol.for('@octanejs/day-picker/useCalendar/time-zone');
18
+ const calendarMemoSlot = Symbol.for('@octanejs/day-picker/useCalendar/months');
19
+
20
+ /**
21
+ * Returns the calendar object used by DayPicker custom components.
22
+ *
23
+ * @see https://daypicker.dev/guides/custom-components
24
+ */
25
+ export interface Calendar {
26
+ /**
27
+ * All the days displayed in the calendar. Unlike
28
+ * {@link CalendarContext.dates}, it may contain duplicated dates when shown
29
+ * outside the month.
30
+ */
31
+ days: CalendarDay[];
32
+ /** The weeks displayed in the calendar. */
33
+ weeks: CalendarWeek[];
34
+ /** The months displayed in the calendar. */
35
+ months: CalendarMonth[];
36
+
37
+ /** The next month to display. */
38
+ nextMonth: Date | undefined;
39
+ /** The previous month to display. */
40
+ previousMonth: Date | undefined;
41
+
42
+ /**
43
+ * The month where the navigation starts. `undefined` if the calendar can be
44
+ * navigated indefinitely to the past.
45
+ */
46
+ navStart: Date | undefined;
47
+ /**
48
+ * The month where the navigation ends. `undefined` if the calendar can be
49
+ * navigated indefinitely to the future.
50
+ */
51
+ navEnd: Date | undefined;
52
+
53
+ /** Navigate to the specified month. Will fire the `onMonthChange` callback. */
54
+ goToMonth: (month: Date) => void;
55
+ /**
56
+ * Navigate to the month containing the specified day when it falls outside
57
+ * the currently displayed calendar.
58
+ *
59
+ * @param day - The date to navigate to.
60
+ */
61
+ goToDay: (day: CalendarDay) => void;
62
+ }
63
+
64
+ /**
65
+ * Provides the calendar object to work with the calendar in custom components.
66
+ *
67
+ * @private
68
+ * @param props - The DayPicker props related to calendar configuration.
69
+ * @param dateLib - The date utility library instance.
70
+ * @returns The calendar object containing displayed days, weeks, months, and
71
+ * navigation methods.
72
+ */
73
+ export function useCalendar(
74
+ props: Pick<
75
+ DayPickerProps,
76
+ | 'captionLayout'
77
+ | 'endMonth'
78
+ | 'startMonth'
79
+ | 'today'
80
+ | 'fixedWeeks'
81
+ | 'ISOWeek'
82
+ | 'numberOfMonths'
83
+ | 'pagedNavigation'
84
+ | 'reverseMonths'
85
+ | 'disableNavigation'
86
+ | 'onMonthChange'
87
+ | 'month'
88
+ | 'defaultMonth'
89
+ | 'timeZone'
90
+ | 'broadcastCalendar'
91
+ >,
92
+ dateLib: DateLib,
93
+ uncontrolledMonth?: Date,
94
+ onUncontrolledMonthChange?: (month: Date) => void,
95
+ ): Calendar {
96
+ const [navStart, navEnd] = getNavMonths(props, dateLib);
97
+
98
+ const { startOfMonth, endOfMonth } = dateLib;
99
+ const initialMonth = getInitialMonth(props, navStart, navEnd, dateLib);
100
+ const [firstMonth, setFirstMonth] = useControlledValue(
101
+ initialMonth,
102
+ // A lifted uncontrolled month must retain its identity when unrelated
103
+ // navigation bounds change. Controlled props still use the clamped month.
104
+ props.month ? initialMonth : uncontrolledMonth,
105
+ calendarStateSlot,
106
+ );
107
+
108
+ // biome-ignore lint/correctness/useExhaustiveDependencies: change the initial month when the time zone changes.
109
+ useEffect(
110
+ () => {
111
+ const newInitialMonth = getInitialMonth(props, navStart, navEnd, dateLib);
112
+ setFirstMonth(newInitialMonth);
113
+ },
114
+ [props.timeZone],
115
+ calendarTimeZoneEffectSlot,
116
+ );
117
+
118
+ /** The months displayed in the calendar. */
119
+ // biome-ignore lint/correctness/useExhaustiveDependencies: We want to recompute only when specific props change.
120
+ const { months, weeks, days, previousMonth, nextMonth } = useMemo(
121
+ () => {
122
+ const displayMonths = getDisplayMonths(
123
+ firstMonth,
124
+ navEnd,
125
+ { numberOfMonths: props.numberOfMonths },
126
+ dateLib,
127
+ );
128
+
129
+ const dates = getDates(
130
+ displayMonths,
131
+ props.endMonth ? endOfMonth(props.endMonth) : undefined,
132
+ {
133
+ ISOWeek: props.ISOWeek,
134
+ fixedWeeks: props.fixedWeeks,
135
+ broadcastCalendar: props.broadcastCalendar,
136
+ },
137
+ dateLib,
138
+ );
139
+
140
+ const months = getMonths(
141
+ displayMonths,
142
+ dates,
143
+ {
144
+ broadcastCalendar: props.broadcastCalendar,
145
+ fixedWeeks: props.fixedWeeks,
146
+ ISOWeek: props.ISOWeek,
147
+ reverseMonths: props.reverseMonths,
148
+ },
149
+ dateLib,
150
+ );
151
+
152
+ const weeks = getWeeks(months);
153
+ const days = getDays(months);
154
+
155
+ const previousMonth = getPreviousMonth(firstMonth, navStart, props, dateLib);
156
+ const nextMonth = getNextMonth(firstMonth, navEnd, props, dateLib);
157
+
158
+ return {
159
+ months,
160
+ weeks,
161
+ days,
162
+ previousMonth,
163
+ nextMonth,
164
+ };
165
+ },
166
+ [
167
+ dateLib,
168
+ firstMonth.getTime(),
169
+ navEnd?.getTime(),
170
+ navStart?.getTime(),
171
+ props.disableNavigation,
172
+ props.broadcastCalendar,
173
+ props.endMonth?.getTime(),
174
+ props.fixedWeeks,
175
+ props.ISOWeek,
176
+ props.numberOfMonths,
177
+ props.pagedNavigation,
178
+ props.reverseMonths,
179
+ ],
180
+ calendarMemoSlot,
181
+ );
182
+
183
+ const { disableNavigation, onMonthChange } = props;
184
+
185
+ const isDayInCalendar = (day: CalendarDay) =>
186
+ weeks.some((week: CalendarWeek) => week.days.some((d) => d.isEqualTo(day)));
187
+
188
+ const goToMonth = (date: Date) => {
189
+ if (disableNavigation) {
190
+ return;
191
+ }
192
+ let newMonth = startOfMonth(date);
193
+ // if month is before start, use the first month instead
194
+ if (navStart && newMonth < startOfMonth(navStart)) {
195
+ newMonth = startOfMonth(navStart);
196
+ }
197
+ // if month is after endMonth, use the last month instead
198
+ if (navEnd && newMonth > startOfMonth(navEnd)) {
199
+ newMonth = startOfMonth(navEnd);
200
+ }
201
+ setFirstMonth(newMonth);
202
+ if (uncontrolledMonth !== undefined) {
203
+ onUncontrolledMonthChange?.(newMonth);
204
+ } else {
205
+ onMonthChange?.(newMonth);
206
+ }
207
+ };
208
+
209
+ const goToDay = (day: CalendarDay) => {
210
+ // is this check necessary?
211
+ if (isDayInCalendar(day)) {
212
+ return;
213
+ }
214
+ goToMonth(day.date);
215
+ };
216
+
217
+ const calendar = {
218
+ months,
219
+ weeks,
220
+ days,
221
+
222
+ navStart,
223
+ navEnd,
224
+
225
+ previousMonth,
226
+ nextMonth,
227
+
228
+ goToMonth,
229
+ goToDay,
230
+ };
231
+
232
+ return calendar;
233
+ }
@@ -0,0 +1,91 @@
1
+ import { createContext, useContext } from 'octane';
2
+
3
+ import type { CalendarDay } from './classes/CalendarDay.js';
4
+ import type { CalendarMonth } from './classes/CalendarMonth.js';
5
+ import type { DayPickerProps } from './types/props.js';
6
+ import type { SelectedValue, SelectHandler } from './types/selection.js';
7
+ import type {
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
+ * Provides access to the DayPicker context, which includes properties and
73
+ * methods to interact with the DayPicker component. This hook must be used
74
+ * within a custom component.
75
+ *
76
+ * @template T - Use this type to refine the returned context type with a
77
+ * specific selection mode.
78
+ * @returns The context to work with DayPicker.
79
+ * @throws {Error} If the hook is used outside of a DayPicker provider.
80
+ * @group Hooks
81
+ * @see https://daypicker.dev/guides/custom-components
82
+ */
83
+ export function useDayPicker<
84
+ T extends { mode?: Mode | undefined; required?: boolean | undefined },
85
+ >(): DayPickerContext<T> {
86
+ const context = useContext(dayPickerContext);
87
+ if (context === undefined) {
88
+ throw new Error('useDayPicker() must be used within a custom component.');
89
+ }
90
+ return context;
91
+ }
@@ -0,0 +1,109 @@
1
+ import { useState } from 'octane';
2
+
3
+ import type { CalendarDay, DateLib } from './classes/index.js';
4
+ import { calculateFocusTarget } from './helpers/calculateFocusTarget.js';
5
+ import { getNextFocus } from './helpers/getNextFocus.js';
6
+ import type { DayPickerProps, Modifiers, MoveFocusBy, MoveFocusDir } from './types/index.js';
7
+ import type { Calendar } from './useCalendar.js';
8
+
9
+ const lastFocusedSlot = Symbol.for('@octanejs/day-picker/useFocus/last-focused');
10
+ const focusedDaySlot = Symbol.for('@octanejs/day-picker/useFocus/focused-day');
11
+
12
+ export type UseFocus = {
13
+ /** The date that is currently focused. */
14
+ focused: CalendarDay | undefined;
15
+
16
+ /** Check if the given day is the focus target when entering the calendar. */
17
+ isFocusTarget: (day: CalendarDay) => boolean;
18
+
19
+ /** Focus the given day. */
20
+ setFocused: (day: CalendarDay | undefined) => void;
21
+
22
+ /** Blur the focused day. */
23
+ blur: () => void;
24
+
25
+ /** Move the current focus to the next day according to the given direction. */
26
+ moveFocus: (moveBy: MoveFocusBy, moveDir: MoveFocusDir) => void;
27
+ };
28
+
29
+ /**
30
+ * Manages focus behavior for the DayPicker component, including setting,
31
+ * moving, and blurring focus on calendar days.
32
+ *
33
+ * @template T - The type of DayPicker props.
34
+ * @param props - The DayPicker props.
35
+ * @param calendar - The calendar object containing the displayed days and
36
+ * months.
37
+ * @param getModifiers - A function to retrieve modifiers for a given day.
38
+ * @param isSelected - A function to check if a date is selected.
39
+ * @param dateLib - The date utility library instance.
40
+ * @returns An object containing focus-related methods and the currently focused
41
+ * day.
42
+ */
43
+ export function useFocus<T extends DayPickerProps>(
44
+ props: T,
45
+ calendar: Calendar,
46
+ getModifiers: (day: CalendarDay) => Modifiers,
47
+ isSelected: (date: Date) => boolean,
48
+ dateLib: DateLib,
49
+ ): UseFocus {
50
+ const { autoFocus } = props;
51
+ const [lastFocused, setLastFocused] = useState<CalendarDay | undefined>(
52
+ undefined,
53
+ lastFocusedSlot,
54
+ );
55
+
56
+ const focusTarget = calculateFocusTarget(
57
+ calendar.days,
58
+ getModifiers,
59
+ isSelected || (() => false),
60
+ lastFocused,
61
+ );
62
+ const [focusedDay, setFocused] = useState<CalendarDay | undefined>(
63
+ autoFocus ? focusTarget : undefined,
64
+ focusedDaySlot,
65
+ );
66
+
67
+ const blur = () => {
68
+ setLastFocused(focusedDay);
69
+ setFocused(undefined);
70
+ };
71
+
72
+ const moveFocus = (moveBy: MoveFocusBy, moveDir: MoveFocusDir) => {
73
+ if (!focusedDay) return;
74
+ const nextFocus = getNextFocus(
75
+ moveBy,
76
+ moveDir,
77
+ focusedDay,
78
+ calendar.navStart,
79
+ calendar.navEnd,
80
+ props,
81
+ dateLib,
82
+ );
83
+ if (!nextFocus) return;
84
+
85
+ if (props.disableNavigation) {
86
+ const isNextInCalendar = calendar.days.some((day) => day.isEqualTo(nextFocus));
87
+ if (!isNextInCalendar) {
88
+ return;
89
+ }
90
+ }
91
+
92
+ calendar.goToDay(nextFocus);
93
+ setFocused(nextFocus);
94
+ };
95
+
96
+ const isFocusTarget = (day: CalendarDay) => {
97
+ return Boolean(focusTarget?.isEqualTo(day));
98
+ };
99
+
100
+ const useFocus: UseFocus = {
101
+ isFocusTarget,
102
+ setFocused,
103
+ focused: focusedDay,
104
+ blur,
105
+ moveFocus,
106
+ };
107
+
108
+ return useFocus;
109
+ }
@@ -0,0 +1,36 @@
1
+ import type { DateLib } from './classes/DateLib.js';
2
+ import { useMulti } from './selection/useMulti.js';
3
+ import { useRange } from './selection/useRange.js';
4
+ import { useSingle } from './selection/useSingle.js';
5
+ import type { DayPickerProps } from './types/index.js';
6
+ import type { Selection } from './types/selection.js';
7
+
8
+ /**
9
+ * Determines the appropriate selection hook to use based on the selection mode
10
+ * and returns the corresponding selection object.
11
+ *
12
+ * @template T - The type of DayPicker props.
13
+ * @param props - The DayPicker props.
14
+ * @param dateLib - The date utility library instance.
15
+ * @returns The selection object for the specified mode, or `undefined` if no
16
+ * mode is set.
17
+ */
18
+ export function useSelection<T extends DayPickerProps>(
19
+ props: T,
20
+ dateLib: DateLib,
21
+ ): Selection<T> | undefined {
22
+ const single = useSingle(props, dateLib);
23
+ const multi = useMulti(props, dateLib);
24
+ const range = useRange(props, dateLib);
25
+
26
+ switch (props.mode) {
27
+ case 'single':
28
+ return single;
29
+ case 'multiple':
30
+ return multi;
31
+ case 'range':
32
+ return range;
33
+ default:
34
+ return undefined;
35
+ }
36
+ }
@@ -0,0 +1,91 @@
1
+ import { type DateLib, defaultDateLib } from '../classes/DateLib.js';
2
+ import type { DateRange } from '../types/index.js';
3
+
4
+ /**
5
+ * Adds a date to an existing range, considering constraints like minimum and
6
+ * maximum range size.
7
+ *
8
+ * @param date - The date to add to the range.
9
+ * @param initialRange - The initial range to which the date will be added.
10
+ * @param min - The minimum number of days in the range.
11
+ * @param max - The maximum number of days in the range.
12
+ * @param required - Whether the range must always include at least one date.
13
+ * @param dateLib - The date utility library instance.
14
+ * @returns The updated date range, or `undefined` if the range is cleared.
15
+ * @group Utilities
16
+ */
17
+ export function addToRange(
18
+ date: Date,
19
+ initialRange: DateRange | undefined,
20
+ min = 0,
21
+ max = 0,
22
+ required = false,
23
+ dateLib: DateLib = defaultDateLib,
24
+ ): DateRange | undefined {
25
+ const { from, to } = initialRange || {};
26
+ const { isSameDay, isAfter, isBefore } = dateLib;
27
+
28
+ let range: DateRange | undefined;
29
+
30
+ if (!from && !to) {
31
+ // the range is empty, add the date
32
+ range = { from: date, to: min > 0 ? undefined : date };
33
+ } else if (from && !to) {
34
+ // adding date to an incomplete range
35
+ if (isSameDay(from, date)) {
36
+ // adding a date equal to the start of the range
37
+ if (min === 0) {
38
+ range = { from, to: date };
39
+ } else if (required) {
40
+ range = { from, to: undefined };
41
+ } else {
42
+ range = undefined;
43
+ }
44
+ } else if (isBefore(date, from)) {
45
+ // adding a date before the start of the range
46
+ range = { from: date, to: from };
47
+ } else {
48
+ // adding a date after the start of the range
49
+ range = { from, to: date };
50
+ }
51
+ } else if (from && to) {
52
+ // adding date to a complete range
53
+ if (isSameDay(from, date) && isSameDay(to, date)) {
54
+ // adding a date that is equal to both start and end of the range
55
+ if (required) {
56
+ range = { from, to };
57
+ } else {
58
+ range = undefined;
59
+ }
60
+ } else if (isSameDay(from, date)) {
61
+ // adding a date equal to the the start of the range
62
+ range = { from, to: min > 0 ? undefined : date };
63
+ } else if (isSameDay(to, date)) {
64
+ // adding a dare equal to the end of the range
65
+ range = { from: date, to: min > 0 ? undefined : date };
66
+ } else if (isBefore(date, from)) {
67
+ // adding a date before the start of the range
68
+ range = { from: date, to: to };
69
+ } else if (isAfter(date, from)) {
70
+ // adding a date after the start of the range
71
+ range = { from, to: date };
72
+ } else if (isAfter(date, to)) {
73
+ // adding a date after the end of the range
74
+ range = { from, to: date };
75
+ } else {
76
+ throw new Error('Invalid range');
77
+ }
78
+ }
79
+
80
+ // check for min / max
81
+ if (range?.from && range?.to) {
82
+ const diff = dateLib.differenceInCalendarDays(range.to, range.from);
83
+ if (max > 0 && diff > max) {
84
+ range = { from: date, to: undefined };
85
+ } else if (min > 1 && diff < min) {
86
+ range = { from: date, to: undefined };
87
+ }
88
+ }
89
+
90
+ return range;
91
+ }
@@ -0,0 +1,88 @@
1
+ import { TZDate } from '@date-fns/tz';
2
+ import type { Matcher } from '../types/index.js';
3
+ import { toTimeZone } from './toTimeZone.js';
4
+ import { isDateAfterType, isDateBeforeType, isDateInterval, isDateRange } from './typeguards.js';
5
+
6
+ function toZoneNoon(date: Date, timeZone: string, noonSafe?: boolean) {
7
+ if (!noonSafe) return toTimeZone(date, timeZone);
8
+ const zoned = toTimeZone(date, timeZone);
9
+ const noonZoned = new TZDate(
10
+ zoned.getFullYear(),
11
+ zoned.getMonth(),
12
+ zoned.getDate(),
13
+ 12,
14
+ 0,
15
+ 0,
16
+ timeZone,
17
+ );
18
+ return new Date(noonZoned.getTime());
19
+ }
20
+
21
+ function convertMatcher(matcher: Matcher, timeZone: string, noonSafe?: boolean): Matcher {
22
+ if (typeof matcher === 'boolean' || typeof matcher === 'function') {
23
+ return matcher;
24
+ }
25
+
26
+ if (matcher instanceof Date) {
27
+ return toZoneNoon(matcher, timeZone, noonSafe);
28
+ }
29
+
30
+ if (Array.isArray(matcher)) {
31
+ return matcher.map((value) =>
32
+ value instanceof Date ? toZoneNoon(value, timeZone, noonSafe) : value,
33
+ );
34
+ }
35
+
36
+ if (isDateRange(matcher)) {
37
+ return {
38
+ ...matcher,
39
+ from: matcher.from ? toTimeZone(matcher.from, timeZone) : matcher.from,
40
+ to: matcher.to ? toTimeZone(matcher.to, timeZone) : matcher.to,
41
+ };
42
+ }
43
+
44
+ if (isDateInterval(matcher)) {
45
+ return {
46
+ before: toZoneNoon(matcher.before, timeZone, noonSafe),
47
+ after: toZoneNoon(matcher.after, timeZone, noonSafe),
48
+ };
49
+ }
50
+
51
+ if (isDateAfterType(matcher)) {
52
+ return {
53
+ after: toZoneNoon(matcher.after, timeZone, noonSafe),
54
+ };
55
+ }
56
+
57
+ if (isDateBeforeType(matcher)) {
58
+ return {
59
+ before: toZoneNoon(matcher.before, timeZone, noonSafe),
60
+ };
61
+ }
62
+
63
+ return matcher;
64
+ }
65
+
66
+ /**
67
+ * Convert any {@link Matcher} or array of matchers to the specified time zone.
68
+ *
69
+ * @param matchers - The matcher or matchers to convert.
70
+ * @param timeZone - The target IANA time zone.
71
+ * @returns The converted matcher(s).
72
+ * @group Utilities
73
+ */
74
+ export function convertMatchersToTimeZone(
75
+ matchers: Matcher | Matcher[] | undefined,
76
+ timeZone: string,
77
+ noonSafe?: boolean,
78
+ ): Matcher | Matcher[] | undefined {
79
+ if (!matchers) {
80
+ return matchers;
81
+ }
82
+
83
+ if (Array.isArray(matchers)) {
84
+ return matchers.map((matcher) => convertMatcher(matcher, timeZone, noonSafe));
85
+ }
86
+
87
+ return convertMatcher(matchers, timeZone, noonSafe);
88
+ }