@os-design/core 1.0.280 → 1.0.281

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 (198) hide show
  1. package/dist/Avatar/utils/nameToInitials.d.ts.map +1 -1
  2. package/dist/Avatar/utils/strToHue.d.ts.map +1 -1
  3. package/dist/Button/index.d.ts.map +1 -1
  4. package/dist/Button/index.js +1 -1
  5. package/dist/Button/utils/useButtonColors.d.ts.map +1 -1
  6. package/dist/ButtonLink/index.d.ts.map +1 -1
  7. package/dist/ButtonLink/index.js +0 -5
  8. package/dist/Checkbox/index.d.ts.map +1 -1
  9. package/dist/Checkbox/index.js +1 -6
  10. package/dist/DateCalendar/Calendar.d.ts +25 -0
  11. package/dist/DateCalendar/Calendar.d.ts.map +1 -0
  12. package/dist/DateCalendar/Calendar.js +271 -0
  13. package/dist/DateCalendar/MonthPicker.d.ts +12 -0
  14. package/dist/DateCalendar/MonthPicker.d.ts.map +1 -0
  15. package/dist/DateCalendar/MonthPicker.js +159 -0
  16. package/dist/DateCalendar/index.d.ts +41 -0
  17. package/dist/DateCalendar/index.d.ts.map +1 -0
  18. package/dist/DateCalendar/index.js +77 -0
  19. package/dist/DateCalendar/locale.d.ts +6 -0
  20. package/dist/DateCalendar/locale.d.ts.map +1 -0
  21. package/dist/DateCalendar/locale.js +4 -0
  22. package/dist/DateCalendar/utils/calendarDays.d.ts +10 -0
  23. package/dist/DateCalendar/utils/calendarDays.d.ts.map +1 -0
  24. package/dist/DateCalendar/utils/calendarDays.js +46 -0
  25. package/dist/DateCalendar/utils/dayOfWeek.d.ts +8 -0
  26. package/dist/DateCalendar/utils/dayOfWeek.d.ts.map +1 -0
  27. package/dist/DateCalendar/utils/dayOfWeek.js +6 -0
  28. package/dist/DateCalendar/utils/daysInMonth.d.ts +7 -0
  29. package/dist/DateCalendar/utils/daysInMonth.d.ts.map +1 -0
  30. package/dist/DateCalendar/utils/daysInMonth.js +14 -0
  31. package/dist/DateCalendar/utils/month.d.ts +14 -0
  32. package/dist/DateCalendar/utils/month.d.ts.map +1 -0
  33. package/dist/DateCalendar/utils/month.js +24 -0
  34. package/dist/DateCalendar/utils/shift.d.ts +3 -0
  35. package/dist/DateCalendar/utils/shift.d.ts.map +1 -0
  36. package/dist/DateCalendar/utils/shift.js +12 -0
  37. package/dist/DatePicker/index.d.ts +68 -62
  38. package/dist/DatePicker/index.d.ts.map +1 -1
  39. package/dist/DatePicker/index.js +359 -265
  40. package/dist/DatePicker/utils/createTimes.d.ts +7 -0
  41. package/dist/DatePicker/utils/createTimes.d.ts.map +1 -0
  42. package/dist/DatePicker/utils/createTimes.js +15 -0
  43. package/dist/GlobalStyles/resetStyles.d.ts.map +1 -1
  44. package/dist/GlobalStyles/typographyStyles.d.ts.map +1 -1
  45. package/dist/Input/index.d.ts +15 -0
  46. package/dist/Input/index.d.ts.map +1 -1
  47. package/dist/Input/index.js +5 -5
  48. package/dist/Input/utils/getFocusableElements.d.ts.map +1 -1
  49. package/dist/InputDateUnstyled/index.d.ts +94 -0
  50. package/dist/InputDateUnstyled/index.d.ts.map +1 -0
  51. package/dist/InputDateUnstyled/index.js +406 -0
  52. package/dist/InputDateUnstyled/utils/convertHours.d.ts +4 -0
  53. package/dist/InputDateUnstyled/utils/convertHours.d.ts.map +1 -0
  54. package/dist/InputDateUnstyled/utils/convertHours.js +12 -0
  55. package/dist/InputDateUnstyled/utils/convertToFullYear.d.ts +3 -0
  56. package/dist/InputDateUnstyled/utils/convertToFullYear.d.ts.map +1 -0
  57. package/dist/InputDateUnstyled/utils/convertToFullYear.js +10 -0
  58. package/dist/InputDateUnstyled/utils/dateToString.d.ts +3 -0
  59. package/dist/InputDateUnstyled/utils/dateToString.d.ts.map +1 -0
  60. package/dist/InputDateUnstyled/utils/dateToString.js +10 -0
  61. package/dist/InputDateUnstyled/utils/daysInMonth.d.ts +7 -0
  62. package/dist/InputDateUnstyled/utils/daysInMonth.d.ts.map +1 -0
  63. package/dist/InputDateUnstyled/utils/daysInMonth.js +14 -0
  64. package/dist/InputDateUnstyled/utils/ensureCaretVisible.d.ts +3 -0
  65. package/dist/InputDateUnstyled/utils/ensureCaretVisible.d.ts.map +1 -0
  66. package/dist/InputDateUnstyled/utils/ensureCaretVisible.js +32 -0
  67. package/dist/InputDateUnstyled/utils/eraseSelectedTokens.d.ts +10 -0
  68. package/dist/InputDateUnstyled/utils/eraseSelectedTokens.d.ts.map +1 -0
  69. package/dist/InputDateUnstyled/utils/eraseSelectedTokens.js +29 -0
  70. package/dist/InputDateUnstyled/utils/replaceSubstring.d.ts +3 -0
  71. package/dist/InputDateUnstyled/utils/replaceSubstring.d.ts.map +1 -0
  72. package/dist/InputDateUnstyled/utils/replaceSubstring.js +9 -0
  73. package/dist/InputDateUnstyled/utils/same.d.ts +6 -0
  74. package/dist/InputDateUnstyled/utils/same.d.ts.map +1 -0
  75. package/dist/InputDateUnstyled/utils/same.js +3 -0
  76. package/dist/InputDateUnstyled/utils/stringToDay.d.ts +12 -0
  77. package/dist/InputDateUnstyled/utils/stringToDay.d.ts.map +1 -0
  78. package/dist/InputDateUnstyled/utils/stringToDay.js +55 -0
  79. package/dist/InputDateUnstyled/utils/stringToTime.d.ts +7 -0
  80. package/dist/InputDateUnstyled/utils/stringToTime.d.ts.map +1 -0
  81. package/dist/InputDateUnstyled/utils/stringToTime.js +40 -0
  82. package/dist/InputDateUnstyled/utils/token.d.ts +9 -0
  83. package/dist/InputDateUnstyled/utils/token.d.ts.map +1 -0
  84. package/dist/InputDateUnstyled/utils/token.js +66 -0
  85. package/dist/Link/index.d.ts.map +1 -1
  86. package/dist/Link/index.js +3 -8
  87. package/dist/LinkButton/index.d.ts.map +1 -1
  88. package/dist/LinkButton/index.js +0 -5
  89. package/dist/List/utils/bodyPointerEvents.d.ts.map +1 -1
  90. package/dist/List/utils/frameTimeout.d.ts.map +1 -1
  91. package/dist/List/utils/useRWLoadNext.d.ts.map +1 -1
  92. package/dist/LogoLink/index.d.ts.map +1 -1
  93. package/dist/LogoLink/index.js +1 -6
  94. package/dist/Menu/utils/useFocusWithArrows.d.ts.map +1 -1
  95. package/dist/Modal/index.d.ts +5 -0
  96. package/dist/Modal/index.d.ts.map +1 -1
  97. package/dist/Modal/index.js +53 -48
  98. package/dist/Navigation/utils/useScrollFlags.d.ts.map +1 -1
  99. package/dist/NavigationItem/index.d.ts.map +1 -1
  100. package/dist/NavigationItem/index.js +1 -6
  101. package/dist/Popover/utils/usePopoverPosition.d.ts.map +1 -1
  102. package/dist/ScrollButton/utils/useContainerPosition.d.ts.map +1 -1
  103. package/dist/ScrollButton/utils/useVisibility.d.ts.map +1 -1
  104. package/dist/Select/index.d.ts.map +1 -1
  105. package/dist/Select/index.js +2 -3
  106. package/dist/Switch/index.d.ts.map +1 -1
  107. package/dist/Switch/index.js +1 -7
  108. package/dist/TagLink/index.d.ts.map +1 -1
  109. package/dist/TagLink/index.js +1 -6
  110. package/dist/TimeGrid/index.d.ts +63 -0
  111. package/dist/TimeGrid/index.d.ts.map +1 -0
  112. package/dist/TimeGrid/index.js +111 -0
  113. package/dist/TimeGrid/utils/convertHours.d.ts +4 -0
  114. package/dist/TimeGrid/utils/convertHours.d.ts.map +1 -0
  115. package/dist/TimeGrid/utils/convertHours.js +12 -0
  116. package/dist/TimeGrid/utils/createTimes.d.ts +7 -0
  117. package/dist/TimeGrid/utils/createTimes.d.ts.map +1 -0
  118. package/dist/TimeGrid/utils/createTimes.js +15 -0
  119. package/dist/TimeGrid/utils/timeToString.d.ts +4 -0
  120. package/dist/TimeGrid/utils/timeToString.d.ts.map +1 -0
  121. package/dist/TimeGrid/utils/timeToString.js +12 -0
  122. package/dist/TimeGridSkeleton/index.d.ts +18 -0
  123. package/dist/TimeGridSkeleton/index.d.ts.map +1 -0
  124. package/dist/TimeGridSkeleton/index.js +33 -0
  125. package/dist/TimeList/index.d.ts +45 -0
  126. package/dist/TimeList/index.d.ts.map +1 -0
  127. package/dist/TimeList/index.js +80 -0
  128. package/dist/TimeList/utils/convertHours.d.ts +4 -0
  129. package/dist/TimeList/utils/convertHours.d.ts.map +1 -0
  130. package/dist/TimeList/utils/convertHours.js +12 -0
  131. package/dist/TimeList/utils/createTimes.d.ts +7 -0
  132. package/dist/TimeList/utils/createTimes.d.ts.map +1 -0
  133. package/dist/TimeList/utils/createTimes.js +15 -0
  134. package/dist/TimeList/utils/timeToString.d.ts +4 -0
  135. package/dist/TimeList/utils/timeToString.d.ts.map +1 -0
  136. package/dist/TimeList/utils/timeToString.js +12 -0
  137. package/dist/TimeListSkeleton/index.d.ts +13 -0
  138. package/dist/TimeListSkeleton/index.d.ts.map +1 -0
  139. package/dist/TimeListSkeleton/index.js +30 -0
  140. package/dist/index.d.ts +12 -0
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +12 -0
  143. package/dist/message/styles.d.ts.map +1 -1
  144. package/package.json +8 -8
  145. package/src/Button/index.tsx +1 -1
  146. package/src/ButtonLink/index.tsx +0 -5
  147. package/src/Checkbox/index.tsx +1 -6
  148. package/src/DateCalendar/Calendar.tsx +400 -0
  149. package/src/DateCalendar/MonthPicker.tsx +212 -0
  150. package/src/DateCalendar/index.tsx +135 -0
  151. package/src/DateCalendar/locale.ts +22 -0
  152. package/src/DateCalendar/utils/calendarDays.ts +61 -0
  153. package/src/DateCalendar/utils/dayOfWeek.ts +14 -0
  154. package/src/DateCalendar/utils/daysInMonth.ts +22 -0
  155. package/src/DateCalendar/utils/month.ts +30 -0
  156. package/src/DateCalendar/utils/shift.ts +14 -0
  157. package/src/DatePicker/index.tsx +506 -417
  158. package/src/DatePicker/utils/createTimes.ts +20 -0
  159. package/src/Input/index.tsx +11 -8
  160. package/src/InputDateUnstyled/index.tsx +533 -0
  161. package/src/InputDateUnstyled/utils/convertHours.ts +15 -0
  162. package/src/InputDateUnstyled/utils/convertToFullYear.ts +11 -0
  163. package/src/InputDateUnstyled/utils/dateToString.ts +21 -0
  164. package/src/InputDateUnstyled/utils/daysInMonth.ts +22 -0
  165. package/src/InputDateUnstyled/utils/ensureCaretVisible.ts +37 -0
  166. package/src/InputDateUnstyled/utils/eraseSelectedTokens.ts +38 -0
  167. package/src/InputDateUnstyled/utils/replaceSubstring.ts +10 -0
  168. package/src/InputDateUnstyled/utils/same.ts +15 -0
  169. package/src/InputDateUnstyled/utils/stringToDay.ts +69 -0
  170. package/src/InputDateUnstyled/utils/stringToTime.ts +48 -0
  171. package/src/InputDateUnstyled/utils/token.ts +102 -0
  172. package/src/Link/index.tsx +5 -25
  173. package/src/LinkButton/index.tsx +2 -15
  174. package/src/LogoLink/index.tsx +2 -6
  175. package/src/Modal/index.tsx +71 -60
  176. package/src/NavigationItem/index.tsx +2 -16
  177. package/src/Select/index.tsx +2 -3
  178. package/src/Switch/index.tsx +1 -11
  179. package/src/TagLink/index.tsx +3 -11
  180. package/src/TimeGrid/index.tsx +189 -0
  181. package/src/TimeGrid/utils/convertHours.ts +15 -0
  182. package/src/TimeGrid/utils/createTimes.ts +20 -0
  183. package/src/TimeGrid/utils/timeToString.ts +17 -0
  184. package/src/TimeGridSkeleton/index.tsx +50 -0
  185. package/src/TimeList/index.tsx +135 -0
  186. package/src/TimeList/utils/convertHours.ts +15 -0
  187. package/src/TimeList/utils/createTimes.ts +20 -0
  188. package/src/TimeList/utils/timeToString.ts +17 -0
  189. package/src/TimeListSkeleton/index.tsx +44 -0
  190. package/src/index.ts +12 -0
  191. package/dist/DatePicker/DatePickerCalendar.d.ts +0 -11
  192. package/dist/DatePicker/DatePickerCalendar.d.ts.map +0 -1
  193. package/dist/DatePicker/DatePickerCalendar.js +0 -178
  194. package/dist/TimePicker/index.d.ts +0 -29
  195. package/dist/TimePicker/index.d.ts.map +0 -1
  196. package/dist/TimePicker/index.js +0 -100
  197. package/src/DatePicker/DatePickerCalendar.tsx +0 -230
  198. package/src/TimePicker/index.tsx +0 -144
@@ -0,0 +1,20 @@
1
+ export interface Time {
2
+ hour: number;
3
+ minute: number;
4
+ }
5
+
6
+ const createTimes = (from: Time, to: Time, stepMinutes: number) => {
7
+ const min = from.hour * 60 + from.minute;
8
+ const max = to.hour * 60 + to.minute;
9
+ const times: Time[] = [];
10
+
11
+ for (let i = min; i <= max - stepMinutes; i += stepMinutes) {
12
+ const hour = Math.floor(i / 60);
13
+ const minute = i % 60;
14
+ times.push({ hour, minute });
15
+ }
16
+
17
+ return times;
18
+ };
19
+
20
+ export default createTimes;
@@ -194,10 +194,13 @@ export const StyledInput = styled(
194
194
  ${notHasRightStyles};
195
195
  `;
196
196
 
197
- interface AddonProps {
198
- hasPadding: boolean;
197
+ interface InputAddonProps {
198
+ hasPadding?: boolean;
199
199
  }
200
- const Addon = styled('span', omitEmotionProps('hasPadding'))<AddonProps>`
200
+ const InputAddon = styled(
201
+ 'span',
202
+ omitEmotionProps('hasPadding')
203
+ )<InputAddonProps>`
201
204
  display: flex;
202
205
  align-items: center;
203
206
  user-select: none;
@@ -208,7 +211,7 @@ const Addon = styled('span', omitEmotionProps('hasPadding'))<AddonProps>`
208
211
  }
209
212
  `;
210
213
 
211
- const LeftAddon = styled(Addon)`
214
+ export const InputLeftAddon = styled(InputAddon)`
212
215
  padding-right: ${(p) => p.theme.inputAddonPaddingHorizontal}em;
213
216
  ${(p) =>
214
217
  p.hasPadding &&
@@ -217,7 +220,7 @@ const LeftAddon = styled(Addon)`
217
220
  `}
218
221
  `;
219
222
 
220
- const RightAddon = styled(Addon)`
223
+ export const InputRightAddon = styled(InputAddon)`
221
224
  padding-left: ${(p) => p.theme.inputAddonPaddingHorizontal}em;
222
225
  ${(p) =>
223
226
  p.hasPadding &&
@@ -309,7 +312,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
309
312
  >
310
313
  {left && (
311
314
  <ThemeOverrider overrides={{ buttonPaddingHorizontal: 0.8 }}>
312
- <LeftAddon hasPadding={leftHasPadding}>{left}</LeftAddon>
315
+ <InputLeftAddon hasPadding={leftHasPadding}>{left}</InputLeftAddon>
313
316
  </ThemeOverrider>
314
317
  )}
315
318
 
@@ -326,9 +329,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
326
329
 
327
330
  {rightValue && (
328
331
  <ThemeOverrider overrides={{ buttonPaddingHorizontal: 0.8 }}>
329
- <RightAddon hasPadding={rightHasPaddingValue}>
332
+ <InputRightAddon hasPadding={rightHasPaddingValue}>
330
333
  {rightValue}
331
- </RightAddon>
334
+ </InputRightAddon>
332
335
  </ThemeOverrider>
333
336
  )}
334
337
  </InputContainer>
@@ -0,0 +1,533 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ type FocusEvent,
8
+ type KeyboardEvent,
9
+ } from 'react';
10
+ import { nextToken, prevToken } from './utils/token.js';
11
+ import replaceSubstring from './utils/replaceSubstring.js';
12
+ import dateToString from './utils/dateToString.js';
13
+ import { useEvent, useForwardedRef, useForwardedState } from '@os-design/utils';
14
+ import eraseSelectedTokens from './utils/eraseSelectedTokens.js';
15
+ import stringToDay, { type Day, type OnDayError } from './utils/stringToDay.js';
16
+ import stringToTime, { type Time } from './utils/stringToTime.js';
17
+ import ensureCaretVisible from './utils/ensureCaretVisible.js';
18
+ import { sameDate, sameDay, sameTime } from './utils/same.js';
19
+
20
+ export const tokens = [
21
+ 'DD',
22
+ 'MM',
23
+ 'YYYY',
24
+ 'YY',
25
+ 'hh',
26
+ 'HH',
27
+ 'mm',
28
+ 'aa',
29
+ ] as const;
30
+ export const numericTokens = ['DD', 'MM', 'YYYY', 'YY', 'hh', 'HH', 'mm'];
31
+
32
+ export type Token = (typeof tokens)[number];
33
+
34
+ export interface Selection {
35
+ start: number;
36
+ end: number;
37
+ }
38
+
39
+ type JsxInputProps = Omit<
40
+ JSX.IntrinsicElements['input'],
41
+ 'value' | 'defaultValue' | 'onChange' | 'ref'
42
+ >;
43
+
44
+ export interface InputDateUnstyledProps extends JsxInputProps {
45
+ /**
46
+ * The string format. Supported tokens:
47
+ * `DD` - The day of the month (01-31).
48
+ * `MM` - The month (01-12).
49
+ * `YYYY` - Four-digit year (2025).
50
+ * `YY` - Two-digit year (25).
51
+ * `hh` - The hour, 12-hour clock (01-12).
52
+ * `HH` - The hour, 24-hour clock (00-23).
53
+ * `mm` - The minute (00-59).
54
+ * `aa` - The meridiem (am, pm).
55
+ * @default `DD.MM.YYYY`
56
+ */
57
+ format?: string;
58
+ /**
59
+ * Selected date.
60
+ * @default undefined
61
+ */
62
+ value?: Date | null;
63
+ /**
64
+ * The value of the input.
65
+ * @default undefined
66
+ */
67
+ inputValue?: string;
68
+ /**
69
+ * The caret position.
70
+ * @default undefined
71
+ */
72
+ selection?: Selection;
73
+ /**
74
+ * The default value.
75
+ * @default undefined
76
+ */
77
+ defaultValue?: Date | null;
78
+ /**
79
+ * The change event handler.
80
+ * @default undefined
81
+ */
82
+ onChange?: (value: Date | null) => void;
83
+ /**
84
+ * The callback called when the input is changed.
85
+ * @default undefined
86
+ */
87
+ onInputChange?: (value: string) => void;
88
+ /**
89
+ * The callback called when the selection has been changed.
90
+ * @default undefined
91
+ */
92
+ onSelectionChange?: (selection: Selection) => void;
93
+ /**
94
+ * The callback called when the day is changed.
95
+ * @default undefined
96
+ */
97
+ onDayChange?: (value: Day) => void;
98
+ /**
99
+ * The callback called when the time is changed.
100
+ * @default undefined
101
+ */
102
+ onTimeChange?: (value: Time) => void;
103
+ /**
104
+ * The error handler called when the day has been entered incorrectly.
105
+ * @default undefined
106
+ */
107
+ onDayError?: OnDayError;
108
+ }
109
+
110
+ /**
111
+ * The input for entering a date/time without any styles.
112
+ */
113
+ const InputDateUnstyled = forwardRef<HTMLInputElement, InputDateUnstyledProps>(
114
+ (
115
+ {
116
+ format = 'DD.MM.YYYY',
117
+ value,
118
+ inputValue,
119
+ selection,
120
+ defaultValue,
121
+ onChange,
122
+ onInputChange,
123
+ onSelectionChange = () => {},
124
+ onDayChange = () => {},
125
+ onTimeChange = () => {},
126
+ onDayError,
127
+ onFocus = () => {},
128
+ onKeyDown = () => {},
129
+ ...rest
130
+ },
131
+ ref
132
+ ) => {
133
+ const [inputRef, mergedInputRef] = useForwardedRef(ref);
134
+ const [forwardedValue, setForwardedValue] = useForwardedState({
135
+ value,
136
+ defaultValue,
137
+ onChange,
138
+ });
139
+ const [forwardedSelection, setForwardedSelection] = useForwardedState({
140
+ value: selection,
141
+ defaultValue: { start: 0, end: 0 },
142
+ onChange: onSelectionChange,
143
+ });
144
+
145
+ const defaultInputValue = useMemo(() => {
146
+ let res = format;
147
+ tokens.forEach((token) => {
148
+ res = res.replaceAll(token, '_'.repeat(token.length));
149
+ });
150
+ return res;
151
+ }, [format]);
152
+
153
+ const [forwardedInputValue, setForwardedInputValue] = useForwardedState({
154
+ value: inputValue,
155
+ defaultValue: defaultInputValue,
156
+ onChange: onInputChange,
157
+ });
158
+
159
+ const dayRef = useRef<Day | null>(null);
160
+ const timeRef = useRef<Time | null>(null);
161
+
162
+ const forwardedValueRef = useRef(forwardedValue);
163
+ useEffect(() => {
164
+ forwardedValueRef.current = forwardedValue;
165
+ }, [forwardedValue]);
166
+
167
+ const setForwardedValueRef = useRef(setForwardedValue);
168
+ useEffect(() => {
169
+ setForwardedValueRef.current = setForwardedValue;
170
+ }, [setForwardedValue]);
171
+
172
+ const forwardedSelectionRef = useRef(forwardedSelection);
173
+ useEffect(() => {
174
+ forwardedSelectionRef.current = forwardedSelection;
175
+ }, [forwardedSelection]);
176
+
177
+ const setForwardedSelectionRef = useRef(setForwardedSelection);
178
+ useEffect(() => {
179
+ setForwardedSelectionRef.current = setForwardedSelection;
180
+ }, [setForwardedSelection]);
181
+
182
+ const defaultInputValueRef = useRef(defaultInputValue);
183
+ useEffect(() => {
184
+ defaultInputValueRef.current = defaultInputValue;
185
+ }, [defaultInputValue]);
186
+
187
+ const forwardedInputValueRef = useRef(forwardedInputValue);
188
+ useEffect(() => {
189
+ forwardedInputValueRef.current = forwardedInputValue;
190
+ }, [forwardedInputValue]);
191
+
192
+ const setForwardedInputValueRef = useRef(setForwardedInputValue);
193
+ useEffect(() => {
194
+ setForwardedInputValueRef.current = setForwardedInputValue;
195
+ }, [setForwardedInputValue]);
196
+
197
+ const formatRef = useRef(format);
198
+ useEffect(() => {
199
+ formatRef.current = format;
200
+ }, [format]);
201
+
202
+ const onDayChangeRef = useRef(onDayChange);
203
+ useEffect(() => {
204
+ onDayChangeRef.current = onDayChange;
205
+ }, [onDayChange]);
206
+
207
+ const onTimeChangeRef = useRef(onTimeChange);
208
+ useEffect(() => {
209
+ onTimeChangeRef.current = onTimeChange;
210
+ }, [onTimeChange]);
211
+
212
+ const onDayErrorRef = useRef(onDayError);
213
+ useEffect(() => {
214
+ onDayErrorRef.current = onDayError;
215
+ }, [onDayError]);
216
+
217
+ const onKeyDownRef = useRef(onKeyDown);
218
+ useEffect(() => {
219
+ onKeyDownRef.current = onKeyDown;
220
+ }, [onKeyDown]);
221
+
222
+ // Change the input selection when the selection state is changed
223
+ useEffect(() => {
224
+ if (!inputRef.current) return;
225
+ const selection = forwardedSelection || { start: 0, end: 0 };
226
+ inputRef.current.setSelectionRange(selection.start, selection.end);
227
+ ensureCaretVisible(inputRef.current);
228
+ }, [forwardedSelection, inputRef]);
229
+
230
+ // Change the input value when the value is changed
231
+ useEffect(() => {
232
+ const nextInputValue = forwardedValue
233
+ ? dateToString(forwardedValue, formatRef.current)
234
+ : defaultInputValueRef.current;
235
+ setForwardedInputValueRef.current(nextInputValue);
236
+ }, [forwardedValue]);
237
+
238
+ // Calls onDayChange, onTimeChange, onChange handlers
239
+ // when the input value is changed.
240
+ useEffect(() => {
241
+ const inputValue = forwardedInputValue || defaultInputValueRef.current;
242
+
243
+ const day = stringToDay(
244
+ inputValue,
245
+ formatRef.current,
246
+ onDayErrorRef.current
247
+ );
248
+ const time = stringToTime(inputValue, formatRef.current);
249
+
250
+ if (day && (!dayRef.current || !sameDay(day, dayRef.current))) {
251
+ onDayChangeRef.current(day);
252
+ dayRef.current = day;
253
+ }
254
+ if (time && (!timeRef.current || !sameTime(time, timeRef.current))) {
255
+ onTimeChangeRef.current(time);
256
+ timeRef.current = time;
257
+ }
258
+ if (!inputValue.includes('_')) {
259
+ const date = forwardedValueRef.current
260
+ ? new Date(forwardedValueRef.current.getTime())
261
+ : new Date();
262
+ if (day) {
263
+ date.setFullYear(day.year);
264
+ date.setMonth(day.month);
265
+ date.setDate(day.day);
266
+ }
267
+ if (time) {
268
+ date.setHours(time.hour);
269
+ date.setMinutes(time.minute);
270
+ }
271
+ if (
272
+ !forwardedValueRef.current ||
273
+ !sameDate(date, forwardedValueRef.current)
274
+ ) {
275
+ setForwardedValueRef.current(date);
276
+ }
277
+ }
278
+ }, [forwardedInputValue]);
279
+
280
+ const keyDownHandler = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
281
+ e.preventDefault(); // Avoid the default input behaviour
282
+
283
+ let start = e.currentTarget.selectionStart || 0;
284
+ const end = e.currentTarget.selectionEnd || 0;
285
+ let nextInputValue =
286
+ forwardedInputValueRef.current || defaultInputValueRef.current;
287
+
288
+ if (/^[0-9]$/.test(e.key)) {
289
+ // If the user has selected multiple characters, erase tokens
290
+ // that contain them.
291
+ nextInputValue = eraseSelectedTokens({
292
+ value: nextInputValue,
293
+ start,
294
+ end,
295
+ format: formatRef.current,
296
+ tokens,
297
+ });
298
+
299
+ // Move the caret to the left, skipping all `_`
300
+ while (start > 0 && nextInputValue.charAt(start - 1) === '_') {
301
+ start--;
302
+ }
303
+
304
+ // Find the next token and check whether it's a numeric one
305
+ const next = nextToken(start, formatRef.current, tokens);
306
+ if (!next || !numericTokens.includes(next.token)) {
307
+ return;
308
+ }
309
+
310
+ // Move the caret the the found token. There are 2 possible cases:
311
+ // 1. If the caret has been inside the token, the caret is not moved.
312
+ // 2. If the token is placed to the right of the caret, the caret
313
+ // moves to the start of the token.
314
+ start = next.pos;
315
+
316
+ let substr = e.key;
317
+ const digit = Number(e.key);
318
+ const prevChar = start > 0 ? nextInputValue.charAt(start - 1) : null;
319
+ const posInToken = next.pos - next.start;
320
+
321
+ switch (next.token) {
322
+ case 'DD':
323
+ if (posInToken === 0 && digit >= 4) {
324
+ substr = `0${digit}`;
325
+ } else if (posInToken === 1 && prevChar === '0' && digit === 0) {
326
+ return;
327
+ } else if (posInToken === 1 && prevChar === '3' && digit >= 2) {
328
+ return;
329
+ }
330
+ break;
331
+ case 'MM':
332
+ if (posInToken === 0 && digit >= 2) {
333
+ substr = `0${digit}`;
334
+ } else if (posInToken === 1 && prevChar === '0' && digit === 0) {
335
+ return;
336
+ } else if (posInToken === 1 && prevChar === '1' && digit >= 3) {
337
+ return;
338
+ }
339
+ break;
340
+ case 'hh':
341
+ if (posInToken === 0 && digit >= 2) {
342
+ substr = `0${digit}`;
343
+ } else if (posInToken === 1 && prevChar === '0' && digit === 0) {
344
+ return;
345
+ } else if (posInToken === 1 && prevChar === '1' && digit >= 3) {
346
+ return;
347
+ }
348
+ break;
349
+ case 'HH':
350
+ if (posInToken === 0 && digit >= 3) {
351
+ substr = `0${digit}`;
352
+ } else if (posInToken === 1 && prevChar === '2' && digit >= 4) {
353
+ return;
354
+ }
355
+ break;
356
+ case 'mm':
357
+ if (posInToken === 0 && digit >= 6) {
358
+ substr = `0${digit}`;
359
+ }
360
+ break;
361
+ }
362
+
363
+ // Replace the next characters of the current token with '_'
364
+ const suffixLength = next.end - start - substr.length;
365
+ const suffix = suffixLength > 0 ? '_'.repeat(suffixLength) : '';
366
+
367
+ nextInputValue = replaceSubstring(
368
+ nextInputValue,
369
+ start,
370
+ `${substr}${suffix}`
371
+ );
372
+
373
+ // Move the caret to the next token
374
+ start += substr.length;
375
+ const next2 = nextToken(start, formatRef.current, tokens);
376
+ if (next2) start = next2.pos;
377
+
378
+ setForwardedInputValueRef.current(nextInputValue);
379
+ setForwardedSelectionRef.current({ start, end: start });
380
+ } else if ('ap'.includes(e.key.toLowerCase())) {
381
+ if (e.metaKey && e.key === 'a') {
382
+ // Select all
383
+ setForwardedSelectionRef.current({
384
+ start: 0,
385
+ end: formatRef.current.length,
386
+ });
387
+ } else {
388
+ // Find the next token and check whether it's the meridiem token
389
+ const next = nextToken(start, formatRef.current, tokens);
390
+ if (!next || next.token !== 'aa') return;
391
+
392
+ // Move the caret to the start of the token even if the caret
393
+ // has been located inside it.
394
+ start = next.start;
395
+
396
+ // Fill in the token
397
+ const substr = e.key.toLowerCase() === 'p' ? 'pm' : 'am';
398
+ nextInputValue = replaceSubstring(nextInputValue, start, substr);
399
+
400
+ // Move the caret to the next token
401
+ start += substr.length;
402
+ const next2 = nextToken(start, formatRef.current, tokens);
403
+ if (next2) start = next2.pos;
404
+
405
+ setForwardedInputValueRef.current(nextInputValue);
406
+ setForwardedSelectionRef.current({ start, end: start });
407
+ }
408
+ } else if (e.key === 'Backspace') {
409
+ if (end > start) {
410
+ // If the user has selected multiple characters, erase tokens
411
+ // that contain them.
412
+ nextInputValue = eraseSelectedTokens({
413
+ value: nextInputValue,
414
+ start,
415
+ end,
416
+ format: formatRef.current,
417
+ tokens,
418
+ });
419
+ } else {
420
+ // If the caret in the first position, just ignore this event.
421
+ if (start === 0) return;
422
+
423
+ // Move the caret to the previous token
424
+ const prev = prevToken(start, formatRef.current, tokens);
425
+ if (!prev) return;
426
+
427
+ let substr = '_';
428
+ if (prev.token === 'aa') {
429
+ // Move the caret to the start of the meridiem token and erase
430
+ // all the characters of this token.
431
+ start = prev.start;
432
+ substr = '__';
433
+ } else {
434
+ // Erase the token to the end.
435
+ // For example, if the token contains 4 characters and the caret
436
+ // is located after the 2nd one, when the user presses backspace,
437
+ // the 2nd, 3rd, and 4th characters are erased.
438
+ start = prev.pos - 1;
439
+ substr = '_'.repeat(prev.end - start);
440
+ }
441
+
442
+ nextInputValue = replaceSubstring(nextInputValue, start, substr);
443
+ }
444
+
445
+ // Move the caret to the previous token
446
+ const prev2 = prevToken(start, formatRef.current, tokens);
447
+ if (prev2) start = prev2.pos;
448
+
449
+ setForwardedInputValueRef.current(nextInputValue);
450
+ setForwardedSelectionRef.current({ start, end: start });
451
+ } else if (e.key === 'ArrowLeft') {
452
+ // If the caret in the first position, just ignore this event.
453
+ if (start === 0) return;
454
+
455
+ // Move the caret to the beginning of the string if the meta key
456
+ // has been pressed (cmd or ctrl).
457
+ if (e.metaKey) start = 0;
458
+ // Move the caret to the left
459
+ else start--;
460
+
461
+ setForwardedSelectionRef.current({ start, end: start });
462
+ } else if (e.key === 'ArrowRight') {
463
+ // If the caret in the last position, just ignore this event.
464
+ if (start === formatRef.current.length) return;
465
+
466
+ // Move the caret to the end of the string if the meta key
467
+ // has been pressed (cmd or ctrl).
468
+ if (e.metaKey) start = formatRef.current.length;
469
+ // Move the caret to the right
470
+ else start++;
471
+
472
+ setForwardedSelectionRef.current({ start, end: start });
473
+ }
474
+
475
+ onKeyDownRef.current(e);
476
+ }, []);
477
+
478
+ // Update the selection when the user clicks inside the input
479
+ const updateSelection = useCallback((e: Event) => {
480
+ const input = e.currentTarget as HTMLInputElement;
481
+ const start = input.selectionStart || 0;
482
+ const end = input.selectionEnd || 0;
483
+ const selection = forwardedSelectionRef.current || { start: 0, end: 0 };
484
+ if (start !== selection.start || end !== selection.end) {
485
+ setForwardedSelectionRef.current({ start, end });
486
+ }
487
+ }, []);
488
+ useEvent(inputRef, 'mouseup', updateSelection);
489
+ useEvent(inputRef, 'touchend', updateSelection);
490
+
491
+ const focusHandler = useCallback(
492
+ (e: FocusEvent<HTMLInputElement>) => {
493
+ setTimeout(() => {
494
+ // Move the caret to the beginning of the first token or if
495
+ // there are no tokens to the beginning of the string.
496
+ const next = nextToken(0, formatRef.current, tokens);
497
+ const pos = next ? next.pos : 0;
498
+ setForwardedSelectionRef.current({ start: pos, end: pos });
499
+ onFocus(e);
500
+ }, 0);
501
+ },
502
+ [onFocus]
503
+ );
504
+
505
+ return (
506
+ <input
507
+ {...rest}
508
+ onFocus={focusHandler}
509
+ onKeyDown={keyDownHandler}
510
+ value={forwardedInputValue}
511
+ ref={mergedInputRef}
512
+ />
513
+ );
514
+ }
515
+ );
516
+
517
+ InputDateUnstyled.displayName = 'InputDateUnstyled';
518
+
519
+ export { default as convertToFullYear } from './utils/convertToFullYear.js';
520
+ export { default as dateToString } from './utils/dateToString.js';
521
+ export { default as daysInMonth } from './utils/daysInMonth.js';
522
+ export { default as ensureCaretVisible } from './utils/ensureCaretVisible.js';
523
+ export { default as eraseSelectedTokens } from './utils/eraseSelectedTokens.js';
524
+ export { default as replaceSubstring } from './utils/replaceSubstring.js';
525
+ export { default as stringToDay } from './utils/stringToDay.js';
526
+ export { default as stringToTime } from './utils/stringToTime.js';
527
+ export * from './utils/convertHours.js';
528
+ export * from './utils/same.js';
529
+ export * from './utils/stringToDay.js';
530
+ export * from './utils/stringToTime.js';
531
+ export * from './utils/token.js';
532
+
533
+ export default InputDateUnstyled;
@@ -0,0 +1,15 @@
1
+ export type Meridiem = 'am' | 'pm';
2
+
3
+ export const convertTo12hours = (hours: number): [number, Meridiem] => {
4
+ if (hours >= 12) {
5
+ const hours12 = hours % 12;
6
+ return [hours12 === 0 ? 12 : hours12, 'pm'];
7
+ } else {
8
+ return [hours === 0 ? 12 : hours, 'am'];
9
+ }
10
+ };
11
+
12
+ export const convertTo24hours = (hours: number, meridiem: Meridiem): number => {
13
+ const h = hours === 12 ? 0 : hours;
14
+ return h + (meridiem === 'am' ? 0 : 12);
15
+ };
@@ -0,0 +1,11 @@
1
+ const convertToFullYear = (twoDigitYear: number, currentYear: number) => {
2
+ const fromYear = currentYear - 50;
3
+ const toYear = currentYear + 50;
4
+ if (twoDigitYear >= fromYear % 100) {
5
+ return Math.floor(fromYear / 100) * 100 + twoDigitYear;
6
+ } else {
7
+ return Math.floor(toYear / 100) * 100 + twoDigitYear;
8
+ }
9
+ };
10
+
11
+ export default convertToFullYear;
@@ -0,0 +1,21 @@
1
+ import { convertTo12hours } from './convertHours.js';
2
+
3
+ const dateToString = (date: Date, format: string) => {
4
+ let str = format
5
+ .replace('DD', date.getDate().toString().padStart(2, '0'))
6
+ .replace('MM', (date.getMonth() + 1).toString().padStart(2, '0'))
7
+ .replace('YYYY', date.getFullYear().toString())
8
+ .replace('YY', date.getFullYear().toString().slice(2, 4))
9
+ .replace('HH', date.getHours().toString().padStart(2, '0'))
10
+ .replace('mm', date.getMinutes().toString().padStart(2, '0'));
11
+
12
+ if (str.includes('hh')) {
13
+ const [hours, meridiem] = convertTo12hours(date.getHours());
14
+ str = str
15
+ .replace('hh', hours.toString().padStart(2, '0'))
16
+ .replace('aa', meridiem);
17
+ }
18
+ return str;
19
+ };
20
+
21
+ export default dateToString;
@@ -0,0 +1,22 @@
1
+ interface Month {
2
+ year: number;
3
+ month: number;
4
+ }
5
+
6
+ const daysInMonth = (month: Month) => {
7
+ switch (month.month) {
8
+ case 1:
9
+ return (month.year % 4 == 0 && month.year % 100) || month.year % 400 == 0
10
+ ? 29
11
+ : 28;
12
+ case 3:
13
+ case 5:
14
+ case 8:
15
+ case 10:
16
+ return 30;
17
+ default:
18
+ return 31;
19
+ }
20
+ };
21
+
22
+ export default daysInMonth;