@dmsi/wedgekit-react 0.0.29 → 0.0.31
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.
- package/dist/{chunk-57WRM337.js → chunk-5POWRPIO.js} +3 -2
- package/dist/{chunk-S5KPS4IQ.js → chunk-HXEPUO5W.js} +189 -95
- package/dist/chunk-KHQX42T7.js +127 -0
- package/dist/{chunk-OUSNH76S.js → chunk-PCCJ7L3N.js} +29 -6
- package/dist/{chunk-PDDZ5PMY.js → chunk-S46RZBT4.js} +3 -2
- package/dist/components/CalendarRange.cjs +28 -5
- package/dist/components/CalendarRange.js +1 -1
- package/dist/components/DataGrid.cjs +490 -217
- package/dist/components/DataGrid.js +303 -122
- package/dist/components/DataGridCell.cjs +192 -96
- package/dist/components/DataGridCell.js +4 -2
- package/dist/components/DateInput.cjs +231 -30
- package/dist/components/DateInput.js +101 -27
- package/dist/components/DateRangeInput.cjs +550 -37
- package/dist/components/DateRangeInput.js +413 -34
- package/dist/components/MenuOption.cjs +3 -2
- package/dist/components/MenuOption.js +1 -1
- package/dist/components/MobileDataGrid.cjs +3 -2
- package/dist/components/MobileDataGrid.js +1 -1
- package/dist/components/NestedMenu.cjs +3 -2
- package/dist/components/NestedMenu.js +1 -1
- package/dist/components/Notification.cjs +3 -2
- package/dist/components/Notification.js +1 -1
- package/dist/components/SideMenuGroup.cjs +3 -2
- package/dist/components/SideMenuGroup.js +1 -1
- package/dist/components/SideMenuItem.cjs +3 -2
- package/dist/components/SideMenuItem.js +1 -1
- package/dist/components/Stack.cjs +3 -2
- package/dist/components/Stack.js +1 -1
- package/dist/components/Swatch.cjs +3 -2
- package/dist/components/Swatch.js +1 -1
- package/dist/components/Time.cjs +3 -2
- package/dist/components/Time.js +1 -1
- package/dist/index.css +9 -0
- package/package.json +1 -1
- package/src/components/CalendarRange.tsx +37 -6
- package/src/components/DataGrid.tsx +284 -48
- package/src/components/DataGridCell.tsx +122 -35
- package/src/components/DateInput.tsx +118 -25
- package/src/components/DateRangeInput.tsx +544 -30
- package/src/components/MenuOption.tsx +18 -14
- package/src/components/Stack.tsx +4 -2
- package/src/utils/date.ts +206 -0
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { useRef, useEffect, useState } from "react";
|
|
1
|
+
import { useRef, useEffect, useState, useLayoutEffect } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { InputBaseProps, InputBase } from "./Input";
|
|
4
4
|
import { CalendarRange } from "./CalendarRange";
|
|
5
5
|
import { Icon } from "./Icon";
|
|
6
6
|
import { findDocumentRoot } from "../utils";
|
|
7
|
+
import {
|
|
8
|
+
parseInputDate,
|
|
9
|
+
isValidDate,
|
|
10
|
+
formatInputValue,
|
|
11
|
+
calculateCursorPosition,
|
|
12
|
+
formatDate,
|
|
13
|
+
isValidDateRangeOrder,
|
|
14
|
+
} from "../utils/date";
|
|
7
15
|
|
|
8
16
|
type DateRangeInputProps = Omit<InputBaseProps, "id"> & {
|
|
9
17
|
id?: string;
|
|
@@ -23,7 +31,14 @@ type DateRangeInputProps = Omit<InputBaseProps, "id"> & {
|
|
|
23
31
|
* Optional disabled state
|
|
24
32
|
*/
|
|
25
33
|
disabled?: boolean;
|
|
26
|
-
|
|
34
|
+
/**
|
|
35
|
+
* If true, shows single calendar instead of double calendar
|
|
36
|
+
*/
|
|
37
|
+
single?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* If true, disables range selection (single date only)
|
|
40
|
+
*/
|
|
41
|
+
disableRange?: boolean;
|
|
27
42
|
readOnly?: boolean; // If true, input is read-only and cannot be focused
|
|
28
43
|
label?: string; // Optional label for the input
|
|
29
44
|
};
|
|
@@ -35,11 +50,14 @@ export const DateRangeInput = ({
|
|
|
35
50
|
placeholder = "MM/DD/YYYY - MM/DD/YYYY",
|
|
36
51
|
disabled,
|
|
37
52
|
readOnly = false,
|
|
38
|
-
single = false,
|
|
53
|
+
single = false,
|
|
54
|
+
disableRange = false,
|
|
39
55
|
label,
|
|
40
56
|
...props
|
|
41
57
|
}: DateRangeInputProps) => {
|
|
42
58
|
const [visible, setVisible] = useState(false);
|
|
59
|
+
const [inputValue, setInputValue] = useState("");
|
|
60
|
+
const [isTyping, setIsTyping] = useState(false);
|
|
43
61
|
const popoverRef = useRef<HTMLDivElement | null>(null);
|
|
44
62
|
const triggerRef = useRef<HTMLInputElement | null>(null);
|
|
45
63
|
const [calendarPosition, setCalendarPosition] = useState({
|
|
@@ -50,15 +68,26 @@ export const DateRangeInput = ({
|
|
|
50
68
|
|
|
51
69
|
const [from, to] = value.split("|");
|
|
52
70
|
|
|
71
|
+
// Update input value when external value changes (but not when typing)
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isTyping) {
|
|
74
|
+
setInputValue(formatDisplayValue(from, to));
|
|
75
|
+
}
|
|
76
|
+
}, [from, to, isTyping, disableRange]);
|
|
77
|
+
|
|
78
|
+
useLayoutEffect(() => {
|
|
79
|
+
if (visible) {
|
|
80
|
+
updatePosition();
|
|
81
|
+
}
|
|
82
|
+
}, [visible]);
|
|
83
|
+
|
|
53
84
|
const updatePosition = () => {
|
|
54
85
|
if (triggerRef.current) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
width: rect.width,
|
|
61
|
-
});
|
|
86
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
87
|
+
setCalendarPosition({
|
|
88
|
+
top: rect.bottom + window.scrollY,
|
|
89
|
+
left: rect.left + window.scrollX,
|
|
90
|
+
width: rect.width,
|
|
62
91
|
});
|
|
63
92
|
}
|
|
64
93
|
};
|
|
@@ -90,7 +119,7 @@ export const DateRangeInput = ({
|
|
|
90
119
|
return () => {
|
|
91
120
|
document.removeEventListener("keydown", handleKeyDown);
|
|
92
121
|
};
|
|
93
|
-
});
|
|
122
|
+
}, []);
|
|
94
123
|
|
|
95
124
|
useEffect(() => {
|
|
96
125
|
const handleClickOutside = (event: MouseEvent) => {
|
|
@@ -110,27 +139,414 @@ export const DateRangeInput = ({
|
|
|
110
139
|
}, []);
|
|
111
140
|
|
|
112
141
|
function handleRangeChange(fromValue: string, toValue: string) {
|
|
113
|
-
|
|
142
|
+
if (disableRange) {
|
|
143
|
+
onChange(`${fromValue}|${fromValue}`);
|
|
144
|
+
} else {
|
|
145
|
+
onChange(`${fromValue}|${toValue}`);
|
|
146
|
+
}
|
|
114
147
|
setVisible(false);
|
|
148
|
+
setIsTyping(false);
|
|
115
149
|
}
|
|
116
150
|
|
|
117
151
|
const handleFocus = () => {
|
|
118
|
-
if (readOnly) return;
|
|
152
|
+
if (readOnly) return;
|
|
119
153
|
setVisible(true);
|
|
120
|
-
updatePosition();
|
|
121
154
|
};
|
|
122
155
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (from) return `${formatDate(from)} -`;
|
|
127
|
-
return "";
|
|
128
|
-
}
|
|
156
|
+
const handleClick = () => {
|
|
157
|
+
handleFocus();
|
|
158
|
+
};
|
|
129
159
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
161
|
+
if (readOnly) return;
|
|
162
|
+
|
|
163
|
+
const rawValue = event.target.value;
|
|
164
|
+
const cursorPosition = event.target.selectionStart || 0;
|
|
165
|
+
|
|
166
|
+
if (shouldPreventManualDash(rawValue)) {
|
|
167
|
+
handleManualDashRemoval(rawValue, cursorPosition);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setIsTyping(true);
|
|
172
|
+
|
|
173
|
+
const formattedValue = formatInputValue(rawValue);
|
|
174
|
+
const finalValue = shouldAutoInsertDash(formattedValue, rawValue)
|
|
175
|
+
? `${formattedValue} - `
|
|
176
|
+
: formattedValue;
|
|
177
|
+
|
|
178
|
+
setInputValue(finalValue);
|
|
179
|
+
|
|
180
|
+
const newCursorPosition = calculateNewCursorPosition(
|
|
181
|
+
rawValue,
|
|
182
|
+
formattedValue,
|
|
183
|
+
finalValue,
|
|
184
|
+
cursorPosition
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
requestAnimationFrame(() => {
|
|
188
|
+
setCursorPosition(newCursorPosition);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
updateParentValue(finalValue);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const shouldPreventManualDash = (value: string): boolean => {
|
|
195
|
+
return !disableRange && value.includes('-') && !value.includes(' - ');
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleManualDashRemoval = (rawValue: string, cursorPosition: number) => {
|
|
199
|
+
const cleanValue = rawValue.replace(/-/g, '');
|
|
200
|
+
const formattedCleanValue = formatInputValue(cleanValue);
|
|
201
|
+
setInputValue(formattedCleanValue);
|
|
202
|
+
|
|
203
|
+
requestAnimationFrame(() => {
|
|
204
|
+
const newPosition = Math.min(cursorPosition - 1, formattedCleanValue.length);
|
|
205
|
+
setCursorPosition(newPosition);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const shouldAutoInsertDash = (formattedValue: string, rawValue: string): boolean => {
|
|
210
|
+
if (disableRange || formattedValue.includes(' - ')) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const completeDate = formattedValue.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
215
|
+
if (!completeDate || rawValue.length !== formattedValue.length) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Only add dash if user just completed typing the year
|
|
220
|
+
const prevLength = rawValue.length - 1;
|
|
221
|
+
const prevFormatted = formatInputValue(rawValue.slice(0, prevLength));
|
|
222
|
+
|
|
223
|
+
return !prevFormatted.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const calculateNewCursorPosition = (
|
|
227
|
+
rawValue: string,
|
|
228
|
+
formattedValue: string,
|
|
229
|
+
finalValue: string,
|
|
230
|
+
originalPosition: number
|
|
231
|
+
): number => {
|
|
232
|
+
if (finalValue !== formattedValue) {
|
|
233
|
+
return finalValue.length; // Place cursor after auto-inserted dash
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return calculateCursorPositionHelper(rawValue, finalValue, originalPosition, disableRange);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const setCursorPosition = (position: number) => {
|
|
240
|
+
if (triggerRef.current) {
|
|
241
|
+
triggerRef.current.setSelectionRange(position, position);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const updateParentValue = (value: string) => {
|
|
246
|
+
if (disableRange) {
|
|
247
|
+
updateSingleDateValue(value);
|
|
248
|
+
} else {
|
|
249
|
+
updateRangeValue(value);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const updateSingleDateValue = (value: string) => {
|
|
254
|
+
const parsedDate = parseInputDate(value);
|
|
255
|
+
if (parsedDate && isValidDate(parsedDate)) {
|
|
256
|
+
onChange(`${parsedDate}|${parsedDate}`);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const updateRangeValue = (value: string) => {
|
|
261
|
+
if (value === "") {
|
|
262
|
+
onChange("");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const rangeMatch = value.match(/^(.+?)\s*-\s*(.+)$/);
|
|
267
|
+
if (rangeMatch) {
|
|
268
|
+
updateCompleteRange(rangeMatch);
|
|
269
|
+
} else {
|
|
270
|
+
updatePartialRange(value);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const updateCompleteRange = (rangeMatch: RegExpMatchArray) => {
|
|
275
|
+
const [, fromStr, toStr] = rangeMatch;
|
|
276
|
+
const fromDate = parseInputDate(fromStr.trim());
|
|
277
|
+
const toDate = parseInputDate(toStr.trim());
|
|
278
|
+
|
|
279
|
+
if (fromDate && toDate && isValidDateRange(fromDate, toDate)) {
|
|
280
|
+
onChange(`${fromDate}|${toDate}`);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const updatePartialRange = (value: string) => {
|
|
285
|
+
const singleDate = parseInputDate(value);
|
|
286
|
+
if (singleDate && isValidDate(singleDate)) {
|
|
287
|
+
onChange(`${singleDate}|`);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const handleBlur = () => {
|
|
292
|
+
setIsTyping(false);
|
|
293
|
+
|
|
294
|
+
// If the input is invalid, revert to the last valid value or empty
|
|
295
|
+
if (disableRange) {
|
|
296
|
+
const parsedDate = parseInputDate(inputValue);
|
|
297
|
+
if (!parsedDate || !isValidDate(parsedDate)) {
|
|
298
|
+
const lastValidValue = formatDisplayValue(from, to);
|
|
299
|
+
setInputValue(lastValidValue || "");
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
// For range, validate both parts
|
|
303
|
+
const rangeMatch = inputValue.match(/^(.+?)\s*-\s*(.+)$/);
|
|
304
|
+
if (rangeMatch) {
|
|
305
|
+
const [, fromStr, toStr] = rangeMatch;
|
|
306
|
+
const fromDate = parseInputDate(fromStr.trim());
|
|
307
|
+
const toDate = parseInputDate(toStr.trim());
|
|
308
|
+
|
|
309
|
+
// Only accept if both dates are valid AND the range is valid (to >= from)
|
|
310
|
+
if (!fromDate || !toDate || !isValidDateRange(fromDate, toDate)) {
|
|
311
|
+
// Invalid range - clear the input
|
|
312
|
+
setInputValue("");
|
|
313
|
+
onChange("");
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// Check if input is just a dash or incomplete second date
|
|
317
|
+
if (inputValue.includes(' - ')) {
|
|
318
|
+
// Has dash but incomplete second date - clear the input
|
|
319
|
+
setInputValue("");
|
|
320
|
+
onChange("");
|
|
321
|
+
} else {
|
|
322
|
+
// Single date in range mode - check if we have a previous valid state
|
|
323
|
+
const singleDate = parseInputDate(inputValue);
|
|
324
|
+
if (!singleDate || !isValidDate(singleDate)) {
|
|
325
|
+
// Invalid single date - clear the input
|
|
326
|
+
setInputValue("");
|
|
327
|
+
onChange("");
|
|
328
|
+
} else {
|
|
329
|
+
// Valid single date but incomplete range - clear the input
|
|
330
|
+
setInputValue("");
|
|
331
|
+
onChange("");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
339
|
+
// Prevent manual typing of dash in range mode
|
|
340
|
+
if (!disableRange && event.key === "-") {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (event.key === "Backspace") {
|
|
346
|
+
const input = event.target as HTMLInputElement;
|
|
347
|
+
const cursorPosition = input.selectionStart || 0;
|
|
348
|
+
const value = input.value;
|
|
349
|
+
|
|
350
|
+
// If cursor is right after a slash, move it before the slash
|
|
351
|
+
if (cursorPosition > 0 && value[cursorPosition - 1] === "/") {
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
const newValue = value.slice(0, cursorPosition - 2) + value.slice(cursorPosition);
|
|
354
|
+
const formattedValue = formatInputValue(newValue);
|
|
355
|
+
setInputValue(formattedValue);
|
|
356
|
+
|
|
357
|
+
// Set cursor position after the deletion
|
|
358
|
+
requestAnimationFrame(() => {
|
|
359
|
+
if (triggerRef.current) {
|
|
360
|
+
const newPosition = Math.max(0, cursorPosition - 2);
|
|
361
|
+
triggerRef.current.setSelectionRange(newPosition, newPosition);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
setIsTyping(true);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Handle deletion when cursor is on or near the dash separator
|
|
370
|
+
if (!disableRange && value.includes(' - ')) {
|
|
371
|
+
const dashIndex = value.indexOf(' - ');
|
|
372
|
+
const dashStart = dashIndex;
|
|
373
|
+
const dashEnd = dashIndex + 3; // " - " is 3 characters
|
|
374
|
+
|
|
375
|
+
// If cursor is within the dash area (including spaces)
|
|
376
|
+
if (cursorPosition >= dashStart && cursorPosition <= dashEnd) {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
|
|
379
|
+
// Remove the entire range and the last digit of the year from the first date
|
|
380
|
+
const beforeDash = value.slice(0, dashStart).trim();
|
|
381
|
+
|
|
382
|
+
// Extract the year part and remove the last digit
|
|
383
|
+
const yearMatch = beforeDash.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
384
|
+
if (yearMatch) {
|
|
385
|
+
const [, month, day, year] = yearMatch;
|
|
386
|
+
const truncatedYear = year.slice(0, -1); // Remove last digit
|
|
387
|
+
const newValue = `${month}/${day}/${truncatedYear}`;
|
|
388
|
+
const formattedValue = formatInputValue(newValue);
|
|
389
|
+
setInputValue(formattedValue);
|
|
390
|
+
|
|
391
|
+
// Update the value - no valid date yet since year is incomplete
|
|
392
|
+
onChange("");
|
|
393
|
+
|
|
394
|
+
// Position cursor at the end
|
|
395
|
+
requestAnimationFrame(() => {
|
|
396
|
+
if (triggerRef.current) {
|
|
397
|
+
triggerRef.current.setSelectionRange(formattedValue.length, formattedValue.length);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
} else {
|
|
401
|
+
// Fallback to original behavior
|
|
402
|
+
const formattedValue = formatInputValue(beforeDash);
|
|
403
|
+
setInputValue(formattedValue);
|
|
404
|
+
|
|
405
|
+
const singleDate = parseInputDate(beforeDash);
|
|
406
|
+
if (singleDate && isValidDate(singleDate)) {
|
|
407
|
+
onChange(`${singleDate}|`);
|
|
408
|
+
} else {
|
|
409
|
+
onChange("");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
requestAnimationFrame(() => {
|
|
413
|
+
if (triggerRef.current) {
|
|
414
|
+
const newPosition = formattedValue.length;
|
|
415
|
+
triggerRef.current.setSelectionRange(newPosition, newPosition);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
setIsTyping(true);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If cursor is right after the dash, remove the second date
|
|
425
|
+
if (cursorPosition === dashEnd) {
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
|
|
428
|
+
const beforeDash = value.slice(0, dashStart).trim();
|
|
429
|
+
const newValue = `${beforeDash} - `;
|
|
430
|
+
setInputValue(newValue);
|
|
431
|
+
|
|
432
|
+
// Update the value to only have the first date as partial range
|
|
433
|
+
const singleDate = parseInputDate(beforeDash);
|
|
434
|
+
if (singleDate && isValidDate(singleDate)) {
|
|
435
|
+
onChange(`${singleDate}|`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Position cursor after the dash
|
|
439
|
+
requestAnimationFrame(() => {
|
|
440
|
+
if (triggerRef.current) {
|
|
441
|
+
triggerRef.current.setSelectionRange(newValue.length, newValue.length);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
setIsTyping(true);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (event.key === "Delete") {
|
|
452
|
+
const input = event.target as HTMLInputElement;
|
|
453
|
+
const cursorPosition = input.selectionStart || 0;
|
|
454
|
+
const value = input.value;
|
|
455
|
+
|
|
456
|
+
// Handle deletion when cursor is on or near the dash separator
|
|
457
|
+
if (!disableRange && value.includes(' - ')) {
|
|
458
|
+
const dashIndex = value.indexOf(' - ');
|
|
459
|
+
const dashStart = dashIndex;
|
|
460
|
+
const dashEnd = dashIndex + 3; // " - " is 3 characters
|
|
461
|
+
|
|
462
|
+
// If cursor is within the dash area or right before it
|
|
463
|
+
if (cursorPosition >= dashStart && cursorPosition <= dashEnd) {
|
|
464
|
+
event.preventDefault();
|
|
465
|
+
|
|
466
|
+
// Remove the dash and second date, and the last digit of the year from the first date
|
|
467
|
+
const beforeDash = value.slice(0, dashStart).trim();
|
|
468
|
+
|
|
469
|
+
// Extract the year part and remove the last digit
|
|
470
|
+
const yearMatch = beforeDash.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
471
|
+
if (yearMatch) {
|
|
472
|
+
const [, month, day, year] = yearMatch;
|
|
473
|
+
const truncatedYear = year.slice(0, -1); // Remove last digit
|
|
474
|
+
const newValue = `${month}/${day}/${truncatedYear}`;
|
|
475
|
+
const formattedValue = formatInputValue(newValue);
|
|
476
|
+
setInputValue(formattedValue);
|
|
477
|
+
|
|
478
|
+
// Update the value - no valid date yet since year is incomplete
|
|
479
|
+
onChange("");
|
|
480
|
+
|
|
481
|
+
// Position cursor at the end
|
|
482
|
+
requestAnimationFrame(() => {
|
|
483
|
+
if (triggerRef.current) {
|
|
484
|
+
triggerRef.current.setSelectionRange(formattedValue.length, formattedValue.length);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
// Fallback to original behavior
|
|
489
|
+
const formattedValue = formatInputValue(beforeDash);
|
|
490
|
+
setInputValue(formattedValue);
|
|
491
|
+
|
|
492
|
+
const singleDate = parseInputDate(beforeDash);
|
|
493
|
+
if (singleDate && isValidDate(singleDate)) {
|
|
494
|
+
onChange(`${singleDate}|`);
|
|
495
|
+
} else {
|
|
496
|
+
onChange("");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
requestAnimationFrame(() => {
|
|
500
|
+
if (triggerRef.current) {
|
|
501
|
+
const newPosition = formattedValue.length;
|
|
502
|
+
triggerRef.current.setSelectionRange(newPosition, newPosition);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
setIsTyping(true);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (event.key === "Enter") {
|
|
514
|
+
if (disableRange) {
|
|
515
|
+
const parsedDate = parseInputDate(inputValue);
|
|
516
|
+
if (parsedDate && isValidDate(parsedDate)) {
|
|
517
|
+
onChange(`${parsedDate}|${parsedDate}`);
|
|
518
|
+
setVisible(false);
|
|
519
|
+
setIsTyping(false);
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
// In range mode, only accept complete and valid ranges
|
|
523
|
+
const rangeMatch = inputValue.match(/^(.+?)\s*-\s*(.+)$/);
|
|
524
|
+
if (rangeMatch) {
|
|
525
|
+
const [, fromStr, toStr] = rangeMatch;
|
|
526
|
+
const fromDate = parseInputDate(fromStr.trim());
|
|
527
|
+
const toDate = parseInputDate(toStr.trim());
|
|
528
|
+
|
|
529
|
+
if (fromDate && toDate && isValidDateRange(fromDate, toDate)) {
|
|
530
|
+
onChange(`${fromDate}|${toDate}`);
|
|
531
|
+
setVisible(false);
|
|
532
|
+
setIsTyping(false);
|
|
533
|
+
} else {
|
|
534
|
+
// Invalid range - clear the input
|
|
535
|
+
setInputValue("");
|
|
536
|
+
onChange("");
|
|
537
|
+
setVisible(false);
|
|
538
|
+
setIsTyping(false);
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
// Single date in range mode - clear the input
|
|
542
|
+
setInputValue("");
|
|
543
|
+
onChange("");
|
|
544
|
+
setVisible(false);
|
|
545
|
+
setIsTyping(false);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
};
|
|
134
550
|
|
|
135
551
|
return (
|
|
136
552
|
<div className="relative">
|
|
@@ -140,16 +556,17 @@ export const DateRangeInput = ({
|
|
|
140
556
|
triggerRef.current = el;
|
|
141
557
|
}}
|
|
142
558
|
{...props}
|
|
143
|
-
value={
|
|
144
|
-
placeholder={placeholder}
|
|
559
|
+
value={inputValue}
|
|
560
|
+
placeholder={disableRange ? "MM/DD/YYYY" : placeholder}
|
|
145
561
|
disabled={disabled}
|
|
562
|
+
readOnly={readOnly}
|
|
146
563
|
after={<Icon name="calendar_month" />}
|
|
147
564
|
onFocus={handleFocus}
|
|
148
|
-
|
|
565
|
+
onClick={handleClick}
|
|
566
|
+
onChange={handleInputChange}
|
|
567
|
+
onBlur={handleBlur}
|
|
568
|
+
onKeyDown={handleKeyDown}
|
|
149
569
|
label={label}
|
|
150
|
-
onChange={() => {
|
|
151
|
-
// Input is controlled by calendar picker, ignore direct text changes
|
|
152
|
-
}}
|
|
153
570
|
/>
|
|
154
571
|
{visible &&
|
|
155
572
|
!readOnly &&
|
|
@@ -172,12 +589,109 @@ export const DateRangeInput = ({
|
|
|
172
589
|
onChange={handleRangeChange}
|
|
173
590
|
cardStyle
|
|
174
591
|
mode={single ? "single" : "double"}
|
|
592
|
+
disableRange={disableRange}
|
|
175
593
|
/>
|
|
176
594
|
</div>,
|
|
177
595
|
findDocumentRoot(popoverRef.current),
|
|
178
596
|
)}
|
|
179
597
|
</div>
|
|
180
598
|
);
|
|
599
|
+
|
|
600
|
+
function formatInputValue(value: string): string {
|
|
601
|
+
return formatInputValueHelper(value, disableRange);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function formatDisplayValue(from?: string, to?: string) {
|
|
605
|
+
return formatDisplayValueHelper(from, to, disableRange);
|
|
606
|
+
}
|
|
181
607
|
};
|
|
182
608
|
|
|
183
609
|
DateRangeInput.displayName = "DateRangeInput";
|
|
610
|
+
|
|
611
|
+
function isValidDateRange(fromDate: string, toDate: string): boolean {
|
|
612
|
+
return isValidDateRangeOrder(fromDate, toDate);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function formatInputValueHelper(value: string, disableRange: boolean): string {
|
|
616
|
+
if (disableRange) {
|
|
617
|
+
return formatInputValue(value);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (value.includes(' - ')) {
|
|
621
|
+
const [from, to] = value.split(' - ');
|
|
622
|
+
const fromFormatted = formatInputValue(from);
|
|
623
|
+
const toFormatted = formatInputValue(to || '');
|
|
624
|
+
return `${fromFormatted} - ${toFormatted}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const cleanValue = value.replace(/-/g, '');
|
|
628
|
+
return formatInputValue(cleanValue);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function calculateCursorPositionHelper(originalValue: string, formattedValue: string, originalPosition: number, disableRange: boolean): number {
|
|
632
|
+
// Handle range input cursor positioning
|
|
633
|
+
if (!disableRange && formattedValue.includes(' - ')) {
|
|
634
|
+
const dashPosition = formattedValue.indexOf(' - ');
|
|
635
|
+
const originalDashPosition = originalValue.indexOf('-');
|
|
636
|
+
|
|
637
|
+
// If cursor was after the dash in original, maintain relative position
|
|
638
|
+
if (originalDashPosition !== -1 && originalPosition > originalDashPosition) {
|
|
639
|
+
const afterDashDigits = originalValue.slice(originalDashPosition + 1).replace(/\D/g, "").length;
|
|
640
|
+
const formattedAfterDash = formattedValue.slice(dashPosition + 3);
|
|
641
|
+
|
|
642
|
+
let newPosition = dashPosition + 3;
|
|
643
|
+
let digitCount = 0;
|
|
644
|
+
|
|
645
|
+
for (let i = 0; i < formattedAfterDash.length; i++) {
|
|
646
|
+
if (/\d/.test(formattedAfterDash[i])) {
|
|
647
|
+
digitCount++;
|
|
648
|
+
if (digitCount >= afterDashDigits) {
|
|
649
|
+
// Check if we're at a position where a slash was auto-inserted in the second date
|
|
650
|
+
// If the next character is a slash, place cursor after it
|
|
651
|
+
if (i + 1 < formattedAfterDash.length && formattedAfterDash[i + 1] === '/') {
|
|
652
|
+
newPosition = dashPosition + 3 + i + 2;
|
|
653
|
+
} else {
|
|
654
|
+
newPosition = dashPosition + 3 + i + 1;
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (digitCount < afterDashDigits) {
|
|
660
|
+
newPosition = dashPosition + 3 + i + 1;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return Math.min(newPosition, formattedValue.length);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return calculateCursorPosition(originalValue, formattedValue, originalPosition);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function formatDisplayValueHelper(from?: string, to?: string, disableRange?: boolean) {
|
|
672
|
+
if (!from && !to) {
|
|
673
|
+
return "";
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Validate dates before displaying
|
|
677
|
+
const fromValid = from ? isValidDate(from) : false;
|
|
678
|
+
const toValid = to ? isValidDate(to) : false;
|
|
679
|
+
|
|
680
|
+
if (disableRange) {
|
|
681
|
+
return fromValid && from ? formatDate(from) : "";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Return formatted display if we have a complete, valid range
|
|
685
|
+
if (fromValid && toValid && from && to && isValidDateRange(from, to)) {
|
|
686
|
+
return `${formatDate(from)} - ${formatDate(to)}`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// If we have a valid from date but no to date, show the from date only
|
|
690
|
+
// This preserves the previous valid state during editing
|
|
691
|
+
if (fromValid && !to && from) {
|
|
692
|
+
return `${formatDate(from)} - `;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Return empty string if no valid dates or invalid range
|
|
696
|
+
return "";
|
|
697
|
+
}
|
|
@@ -162,10 +162,22 @@ export const MenuOption = ({
|
|
|
162
162
|
const disabledStyles =
|
|
163
163
|
disabled && clsx("bg-transparent cursor-default pointer-events-none");
|
|
164
164
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
const processChildren = typeof children === 'string' && highlightMatchingText
|
|
166
|
+
? (
|
|
167
|
+
highlightMatch(children, menuValue)
|
|
168
|
+
) : children;
|
|
169
|
+
|
|
170
|
+
const renderChildren = typeof children === 'object'
|
|
171
|
+
? children
|
|
172
|
+
: variant === "action" ? (
|
|
173
|
+
<Label padded className={textLabelStyles}>
|
|
174
|
+
{processChildren}
|
|
175
|
+
</Label>
|
|
176
|
+
) : (
|
|
177
|
+
<Paragraph padded className={textLabelStyles}>
|
|
178
|
+
{processChildren}
|
|
179
|
+
</Paragraph>
|
|
180
|
+
)
|
|
169
181
|
|
|
170
182
|
return (
|
|
171
183
|
<>
|
|
@@ -200,16 +212,8 @@ export const MenuOption = ({
|
|
|
200
212
|
>
|
|
201
213
|
{before && <div className="shrink-0 flex items-center">{before}</div>}
|
|
202
214
|
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
{renderChildren}
|
|
206
|
-
</Label>
|
|
207
|
-
) : (
|
|
208
|
-
<Paragraph padded className={textLabelStyles}>
|
|
209
|
-
{renderChildren}
|
|
210
|
-
</Paragraph>
|
|
211
|
-
)}
|
|
212
|
-
|
|
215
|
+
{renderChildren}
|
|
216
|
+
|
|
213
217
|
{renderAfterProp()}
|
|
214
218
|
</div>
|
|
215
219
|
|