@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,202 @@
1
+ import { TZDate } from '@date-fns/tz';
2
+ import type { EndOfWeekOptions, Locale, StartOfWeekOptions } from 'date-fns';
3
+ import {
4
+ differenceInCalendarDays as differenceInCalendarDaysFn,
5
+ differenceInCalendarMonths as differenceInCalendarMonthsFn,
6
+ getISOWeek as getISOWeekFn,
7
+ getWeek as getWeekFn,
8
+ } from 'date-fns';
9
+ import type { DateLib } from './classes/DateLib.js';
10
+
11
+ export interface CreateNoonOverridesOptions {
12
+ weekStartsOn?: number;
13
+ locale?: Locale;
14
+ }
15
+
16
+ type SupportedDate = Date | number | string | TZDate;
17
+ type WeekStartsOn = NonNullable<StartOfWeekOptions['weekStartsOn']>;
18
+
19
+ /**
20
+ * Creates `dateLib` overrides that keep all calendar math at noon in the target
21
+ * time zone. This avoids second-level offset changes (e.g., historical zones
22
+ * with +03:41:12) from pushing dates backward across midnight.
23
+ */
24
+ export function createNoonOverrides(
25
+ timeZone: string,
26
+ options: CreateNoonOverridesOptions = {},
27
+ ): Partial<typeof DateLib.prototype> {
28
+ const { weekStartsOn, locale } = options;
29
+ const fallbackWeekStartsOn: WeekStartsOn = (weekStartsOn ??
30
+ locale?.options?.weekStartsOn ??
31
+ 0) as WeekStartsOn;
32
+
33
+ // Keep all internal math anchored at noon in the target zone to avoid
34
+ // historical second-level offsets from crossing midnight.
35
+ const toNoonTZDate = (date: SupportedDate): TZDate => {
36
+ const normalizedDate =
37
+ typeof date === 'number' || typeof date === 'string' ? new Date(date) : date;
38
+ return new TZDate(
39
+ normalizedDate.getFullYear(),
40
+ normalizedDate.getMonth(),
41
+ normalizedDate.getDate(),
42
+ 12,
43
+ 0,
44
+ 0,
45
+ timeZone,
46
+ );
47
+ };
48
+
49
+ // Convert a value into a host `Date` that represents the same calendar day
50
+ // as the target-zone noon. This is useful for helpers (e.g., date-fns week
51
+ // utilities) that expect local `Date` instances rather than `TZDate`s.
52
+ const toCalendarDate = (date: SupportedDate): Date => {
53
+ const zoned = toNoonTZDate(date);
54
+ return new Date(zoned.getFullYear(), zoned.getMonth(), zoned.getDate(), 0, 0, 0, 0);
55
+ };
56
+
57
+ return {
58
+ today: () => {
59
+ return toNoonTZDate(TZDate.tz(timeZone));
60
+ },
61
+
62
+ newDate: (year: number, monthIndex: number, date: number) => {
63
+ return new TZDate(year, monthIndex, date, 12, 0, 0, timeZone);
64
+ },
65
+
66
+ startOfDay: (date) => {
67
+ return toNoonTZDate(date);
68
+ },
69
+
70
+ startOfWeek: (date, options?: StartOfWeekOptions) => {
71
+ const base = toNoonTZDate(date);
72
+ const weekStartsOnValue = (options?.weekStartsOn ?? fallbackWeekStartsOn) as WeekStartsOn;
73
+ const diff = (base.getDay() - weekStartsOnValue + 7) % 7;
74
+ base.setDate(base.getDate() - diff);
75
+ return base;
76
+ },
77
+
78
+ startOfISOWeek: (date) => {
79
+ const base = toNoonTZDate(date);
80
+ const diff = (base.getDay() - 1 + 7) % 7;
81
+ base.setDate(base.getDate() - diff);
82
+ return base;
83
+ },
84
+
85
+ startOfMonth: (date) => {
86
+ const base = toNoonTZDate(date);
87
+ base.setDate(1);
88
+ return base;
89
+ },
90
+
91
+ startOfYear: (date) => {
92
+ const base = toNoonTZDate(date);
93
+ base.setMonth(0, 1);
94
+ return base;
95
+ },
96
+
97
+ endOfWeek: (date, options?: EndOfWeekOptions) => {
98
+ const base = toNoonTZDate(date);
99
+ const weekStartsOnValue = (options?.weekStartsOn ?? fallbackWeekStartsOn) as WeekStartsOn;
100
+ const endDow = (weekStartsOnValue + 6) % 7;
101
+ const diff = (endDow - base.getDay() + 7) % 7;
102
+ base.setDate(base.getDate() + diff);
103
+ return base;
104
+ },
105
+
106
+ endOfISOWeek: (date) => {
107
+ const base = toNoonTZDate(date);
108
+ const diff = (7 - base.getDay()) % 7;
109
+ base.setDate(base.getDate() + diff);
110
+ return base;
111
+ },
112
+
113
+ endOfMonth: (date) => {
114
+ const base = toNoonTZDate(date);
115
+ base.setMonth(base.getMonth() + 1, 0);
116
+ return base;
117
+ },
118
+
119
+ endOfYear: (date) => {
120
+ const base = toNoonTZDate(date);
121
+ base.setMonth(11, 31);
122
+ return base;
123
+ },
124
+
125
+ eachMonthOfInterval: (interval) => {
126
+ const start = toNoonTZDate(interval.start);
127
+ const end = toNoonTZDate(interval.end);
128
+ const result: Date[] = [];
129
+ const cursor = new TZDate(start.getFullYear(), start.getMonth(), 1, 12, 0, 0, timeZone);
130
+ const endKey = end.getFullYear() * 12 + end.getMonth();
131
+ while (cursor.getFullYear() * 12 + cursor.getMonth() <= endKey) {
132
+ result.push(new TZDate(cursor, timeZone));
133
+ cursor.setMonth(cursor.getMonth() + 1, 1);
134
+ }
135
+ return result;
136
+ },
137
+
138
+ // Normalize to noon once before arithmetic (avoid DST/midnight edge cases),
139
+ // mutate the same TZDate, and return it.
140
+ addDays: (date, amount) => {
141
+ const base = toNoonTZDate(date);
142
+ base.setDate(base.getDate() + amount);
143
+ return base;
144
+ },
145
+
146
+ addWeeks: (date, amount) => {
147
+ const base = toNoonTZDate(date);
148
+ base.setDate(base.getDate() + amount * 7);
149
+ return base;
150
+ },
151
+
152
+ addMonths: (date, amount) => {
153
+ const base = toNoonTZDate(date);
154
+ base.setMonth(base.getMonth() + amount);
155
+ return base;
156
+ },
157
+
158
+ addYears: (date, amount) => {
159
+ const base = toNoonTZDate(date);
160
+ base.setFullYear(base.getFullYear() + amount);
161
+ return base;
162
+ },
163
+
164
+ eachYearOfInterval: (interval) => {
165
+ const start = toNoonTZDate(interval.start);
166
+ const end = toNoonTZDate(interval.end);
167
+ const years: Date[] = [];
168
+ const cursor = new TZDate(start.getFullYear(), 0, 1, 12, 0, 0, timeZone);
169
+ while (cursor.getFullYear() <= end.getFullYear()) {
170
+ years.push(new TZDate(cursor, timeZone));
171
+ cursor.setFullYear(cursor.getFullYear() + 1, 0, 1);
172
+ }
173
+ return years;
174
+ },
175
+
176
+ getWeek: (date, options) => {
177
+ const base = toCalendarDate(date);
178
+ return getWeekFn(base, {
179
+ weekStartsOn: options?.weekStartsOn ?? fallbackWeekStartsOn,
180
+ firstWeekContainsDate:
181
+ options?.firstWeekContainsDate ?? locale?.options?.firstWeekContainsDate ?? 1,
182
+ });
183
+ },
184
+
185
+ getISOWeek: (date) => {
186
+ const base = toCalendarDate(date);
187
+ return getISOWeekFn(base);
188
+ },
189
+
190
+ differenceInCalendarDays: (dateLeft, dateRight) => {
191
+ const left = toCalendarDate(dateLeft);
192
+ const right = toCalendarDate(dateRight);
193
+ return differenceInCalendarDaysFn(left, right);
194
+ },
195
+
196
+ differenceInCalendarMonths: (dateLeft, dateRight) => {
197
+ const left = toCalendarDate(dateLeft);
198
+ const right = toCalendarDate(dateRight);
199
+ return differenceInCalendarMonthsFn(left, right);
200
+ },
201
+ };
202
+ }
@@ -0,0 +1,74 @@
1
+ import type React from 'react';
2
+
3
+ import type { DateLib } from '../classes/DateLib.js';
4
+ import { useControlledValue } from '../helpers/useControlledValue.js';
5
+ import type { DayPickerProps, Modifiers, PropsMulti, Selection } from '../types/index.js';
6
+
7
+ const multiSelectionSlot = Symbol.for('@octanejs/day-picker/useMulti/selection');
8
+
9
+ /**
10
+ * Hook to manage multiple-date selection in the DayPicker component.
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 An object containing the selected dates, a function to select dates,
16
+ * and a function to check if a date is selected.
17
+ */
18
+ export function useMulti<T extends DayPickerProps>(props: T, dateLib: DateLib): Selection<T> {
19
+ const { selected: initiallySelected, required, onSelect } = props as PropsMulti;
20
+
21
+ const [internallySelected, setSelected] = useControlledValue(
22
+ initiallySelected,
23
+ onSelect ? initiallySelected : undefined,
24
+ multiSelectionSlot,
25
+ );
26
+
27
+ const selected = !onSelect ? internallySelected : initiallySelected;
28
+
29
+ const { isSameDay } = dateLib;
30
+
31
+ const isSelected = (date: Date) => {
32
+ return selected?.some((d) => isSameDay(d, date)) ?? false;
33
+ };
34
+
35
+ const { min, max } = props as PropsMulti;
36
+
37
+ const select = (
38
+ triggerDate: Date,
39
+ modifiers: Modifiers,
40
+ e: React.MouseEvent | React.KeyboardEvent,
41
+ ) => {
42
+ let newDates: Date[] | undefined = [...(selected ?? [])];
43
+ if (isSelected(triggerDate)) {
44
+ if (selected?.length === min) {
45
+ // Min value reached, do nothing
46
+ return;
47
+ }
48
+ if (required && selected?.length === 1) {
49
+ // Required value already selected do nothing
50
+ return;
51
+ }
52
+ newDates = selected?.filter((d) => !isSameDay(d, triggerDate));
53
+ } else {
54
+ if (selected?.length === max) {
55
+ // Max value reached, reset the selection to date
56
+ newDates = [triggerDate];
57
+ } else {
58
+ // Add the date to the selection
59
+ newDates = [...newDates, triggerDate];
60
+ }
61
+ }
62
+ if (!onSelect) {
63
+ setSelected(newDates);
64
+ }
65
+ onSelect?.(newDates, triggerDate, modifiers, e);
66
+ return newDates;
67
+ };
68
+
69
+ return {
70
+ selected,
71
+ select,
72
+ isSelected,
73
+ } as Selection<T>;
74
+ }
@@ -0,0 +1,89 @@
1
+ import type React from 'react';
2
+
3
+ import type { DateLib } from '../classes/DateLib.js';
4
+ import { useControlledValue } from '../helpers/useControlledValue.js';
5
+ import type { DayPickerProps, Modifiers, PropsRange, Selection } from '../types/index.js';
6
+ import { addToRange, rangeContainsModifiers } from '../utils/index.js';
7
+ import { rangeIncludesDate } from '../utils/rangeIncludesDate.js';
8
+
9
+ const rangeSelectionSlot = Symbol.for('@octanejs/day-picker/useRange/selection');
10
+
11
+ /**
12
+ * Hook to manage range selection in the DayPicker component.
13
+ *
14
+ * @template T - The type of DayPicker props.
15
+ * @param props - The DayPicker props.
16
+ * @param dateLib - The date utility library instance.
17
+ * @returns An object containing the selected range, a function to select a
18
+ * range, and a function to check if a date is within the range.
19
+ */
20
+ export function useRange<T extends DayPickerProps>(props: T, dateLib: DateLib): Selection<T> {
21
+ const {
22
+ disabled,
23
+ excludeDisabled,
24
+ resetOnSelect,
25
+ selected: initiallySelected,
26
+ required,
27
+ onSelect,
28
+ } = props as PropsRange;
29
+
30
+ const [internallySelected, setSelected] = useControlledValue(
31
+ initiallySelected,
32
+ onSelect ? initiallySelected : undefined,
33
+ rangeSelectionSlot,
34
+ );
35
+
36
+ const selected = !onSelect ? internallySelected : initiallySelected;
37
+
38
+ const isSelected = (date: Date) => selected && rangeIncludesDate(selected, date, false, dateLib);
39
+
40
+ const select = (
41
+ triggerDate: Date,
42
+ modifiers: Modifiers,
43
+ e: React.MouseEvent | React.KeyboardEvent,
44
+ ) => {
45
+ const { min, max } = props as PropsRange;
46
+ let newRange: ReturnType<typeof addToRange>;
47
+ if (triggerDate) {
48
+ const selectedFrom = selected?.from;
49
+ const selectedTo = selected?.to;
50
+ const hasFullRange = !!selectedFrom && !!selectedTo;
51
+ const isClickingSingleDayRange =
52
+ !!selectedFrom &&
53
+ !!selectedTo &&
54
+ dateLib.isSameDay(selectedFrom, selectedTo) &&
55
+ dateLib.isSameDay(triggerDate, selectedFrom);
56
+
57
+ if (resetOnSelect && (hasFullRange || !selected?.from)) {
58
+ if (!required && isClickingSingleDayRange) {
59
+ newRange = undefined;
60
+ } else {
61
+ newRange = { from: triggerDate, to: undefined };
62
+ }
63
+ } else {
64
+ newRange = addToRange(triggerDate, selected, min, max, required, dateLib);
65
+ }
66
+ }
67
+
68
+ if (excludeDisabled && disabled && newRange?.from && newRange.to) {
69
+ if (rangeContainsModifiers({ from: newRange.from, to: newRange.to }, disabled, dateLib)) {
70
+ // if a disabled days is found, the range is reset
71
+ newRange.from = triggerDate;
72
+ newRange.to = undefined;
73
+ }
74
+ }
75
+
76
+ if (!onSelect) {
77
+ setSelected(newRange);
78
+ }
79
+ onSelect?.(newRange, triggerDate, modifiers, e);
80
+
81
+ return newRange;
82
+ };
83
+
84
+ return {
85
+ selected,
86
+ select,
87
+ isSelected,
88
+ } as Selection<T>;
89
+ }
@@ -0,0 +1,77 @@
1
+ import type React from 'react';
2
+
3
+ import type { DateLib } from '../classes/DateLib.js';
4
+ import { useControlledValue } from '../helpers/useControlledValue.js';
5
+ import type {
6
+ DayPickerProps,
7
+ Modifiers,
8
+ PropsSingle,
9
+ SelectedValue,
10
+ SelectHandler,
11
+ Selection,
12
+ } from '../types/index.js';
13
+
14
+ const singleSelectionSlot = Symbol.for('@octanejs/day-picker/useSingle/selection');
15
+
16
+ export type UseSingle<T extends DayPickerProps> = {
17
+ select: SelectHandler<T>;
18
+ isSelected: (date: Date) => boolean;
19
+ selected: SelectedValue<T>;
20
+ };
21
+
22
+ /**
23
+ * Hook to manage single-date selection in the DayPicker component.
24
+ *
25
+ * @template T - The type of DayPicker props.
26
+ * @param props - The DayPicker props.
27
+ * @param dateLib - The date utility library instance.
28
+ * @returns An object containing the selected date, a function to select a date,
29
+ * and a function to check if a date is selected.
30
+ */
31
+ export function useSingle<T extends DayPickerProps>(
32
+ props: DayPickerProps,
33
+ dateLib: DateLib,
34
+ ): Selection<T> {
35
+ const { selected: initiallySelected, required, onSelect } = props as PropsSingle;
36
+
37
+ const [internallySelected, setSelected] = useControlledValue(
38
+ initiallySelected,
39
+ onSelect ? initiallySelected : undefined,
40
+ singleSelectionSlot,
41
+ );
42
+
43
+ const selected = !onSelect ? internallySelected : initiallySelected;
44
+
45
+ const { isSameDay } = dateLib;
46
+
47
+ const isSelected = (compareDate: Date) => {
48
+ return selected ? isSameDay(selected, compareDate) : false;
49
+ };
50
+
51
+ const select = (
52
+ triggerDate: Date,
53
+ modifiers: Modifiers,
54
+ e: React.MouseEvent | React.KeyboardEvent,
55
+ ) => {
56
+ let newDate: Date | undefined = triggerDate;
57
+ if (!required && selected && selected && isSameDay(triggerDate, selected)) {
58
+ // If the date is the same, clear the selection.
59
+ newDate = undefined;
60
+ }
61
+ if (!onSelect) {
62
+ setSelected(newDate);
63
+ }
64
+ if (required) {
65
+ onSelect?.(newDate as Date, triggerDate, modifiers, e);
66
+ } else {
67
+ onSelect?.(newDate, triggerDate, modifiers, e);
68
+ }
69
+ return newDate;
70
+ };
71
+
72
+ return {
73
+ selected,
74
+ select,
75
+ isSelected,
76
+ } as Selection<T>;
77
+ }