@monolith-forensics/monolith-ui 2.1.2 → 2.1.3

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.
@@ -191,6 +191,7 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
191
191
  const [unControlledValue, setUncontrolledValue] = useState(defaultValue);
192
192
  const _value = isControlled.current ? value : unControlledValue;
193
193
  const [selectedSegment, setSelectedSegment] = useState();
194
+ const [draftSegment, setDraftSegment] = useState(null);
194
195
  const [isOpen, setIsOpen] = useState(false);
195
196
  // Check if format is date only and does not include time
196
197
  const isDateOnly = format.match(/(HH|h|mm|ss|SSS|A|Z)/) === null;
@@ -281,6 +282,7 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
281
282
  const handleClear = (e) => {
282
283
  e.preventDefault();
283
284
  e.stopPropagation();
285
+ clearTypedSegment();
284
286
  commitValue(null);
285
287
  setIsOpen(false);
286
288
  };
@@ -289,6 +291,8 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
289
291
  e.preventDefault();
290
292
  if (segment.type === "separator")
291
293
  return;
294
+ commitSelectedDraftSegment();
295
+ clearTypedSegment();
292
296
  if (mainRef === null || mainRef === void 0 ? void 0 : mainRef.current) {
293
297
  const input = mainRef.current.querySelector("div[data-type='input']");
294
298
  if (input) {
@@ -300,6 +304,7 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
300
304
  const handleContainerClick = (e) => {
301
305
  e.stopPropagation();
302
306
  e.preventDefault();
307
+ commitSelectedDraftSegment();
303
308
  if (mainRef === null || mainRef === void 0 ? void 0 : mainRef.current) {
304
309
  const input = mainRef.current.querySelector("div[data-type='input']");
305
310
  if (input) {
@@ -307,11 +312,13 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
307
312
  }
308
313
  }
309
314
  setIsOpen(true);
315
+ clearTypedSegment();
310
316
  setSelectedSegment(segments[0]);
311
317
  };
312
318
  const handleCalendarTriggerClick = (e) => {
313
319
  e.stopPropagation();
314
320
  e.preventDefault();
321
+ commitSelectedDraftSegment();
315
322
  if (mainRef === null || mainRef === void 0 ? void 0 : mainRef.current) {
316
323
  const input = mainRef.current.querySelector("div[data-type='input']");
317
324
  if (input) {
@@ -319,6 +326,7 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
319
326
  }
320
327
  }
321
328
  setIsOpen((prev) => !prev);
329
+ clearTypedSegment();
322
330
  setSelectedSegment(segments[0]);
323
331
  };
324
332
  const nextSegment = () => {
@@ -329,6 +337,145 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
329
337
  return next || prev;
330
338
  });
331
339
  };
340
+ const clearTypedSegment = useCallback(() => {
341
+ typedKeys.current = "";
342
+ setDraftSegment(null);
343
+ }, []);
344
+ const getSegmentInputLength = useCallback((segment) => {
345
+ switch (segment.pattern) {
346
+ case "YYYY":
347
+ return 4;
348
+ case "SSS":
349
+ return 3;
350
+ case "MM":
351
+ case "DD":
352
+ case "HH":
353
+ case "mm":
354
+ case "ss":
355
+ case "h":
356
+ return 2;
357
+ default:
358
+ return segment.text.length;
359
+ }
360
+ }, []);
361
+ const getMomentWithSegmentValue = useCallback((segment, rawValue, currentMoment) => {
362
+ if (!segment.momentType)
363
+ return null;
364
+ const parsedValue = parseInt(rawValue, 10);
365
+ if (!Number.isFinite(parsedValue))
366
+ return null;
367
+ const nextMoment = currentMoment.clone();
368
+ switch (segment.momentType) {
369
+ case "year": {
370
+ if (parsedValue < 0 || parsedValue > 9999)
371
+ return null;
372
+ const daysInTargetMonth = currentMoment
373
+ .clone()
374
+ .year(parsedValue)
375
+ .month(currentMoment.month())
376
+ .daysInMonth();
377
+ if (currentMoment.date() > daysInTargetMonth)
378
+ return null;
379
+ nextMoment.year(parsedValue);
380
+ break;
381
+ }
382
+ case "month": {
383
+ if (parsedValue < 1 || parsedValue > 12)
384
+ return null;
385
+ const targetMonth = parsedValue - 1;
386
+ const daysInTargetMonth = currentMoment
387
+ .clone()
388
+ .month(targetMonth)
389
+ .daysInMonth();
390
+ if (currentMoment.date() > daysInTargetMonth)
391
+ return null;
392
+ nextMoment.month(targetMonth);
393
+ break;
394
+ }
395
+ case "date": {
396
+ if (parsedValue < 1 || parsedValue > currentMoment.daysInMonth()) {
397
+ return null;
398
+ }
399
+ nextMoment.date(parsedValue);
400
+ break;
401
+ }
402
+ case "hour": {
403
+ const isTwelveHourSegment = segment.pattern === "h";
404
+ if (isTwelveHourSegment) {
405
+ if (parsedValue < 1 || parsedValue > 12)
406
+ return null;
407
+ const isPm = currentMoment.hour() >= 12;
408
+ const nextHour = parsedValue === 12
409
+ ? isPm
410
+ ? 12
411
+ : 0
412
+ : parsedValue + (isPm ? 12 : 0);
413
+ nextMoment.hour(nextHour);
414
+ }
415
+ else {
416
+ if (parsedValue < 0 || parsedValue > 23)
417
+ return null;
418
+ nextMoment.hour(parsedValue);
419
+ }
420
+ break;
421
+ }
422
+ case "minute": {
423
+ if (parsedValue < 0 || parsedValue > 59)
424
+ return null;
425
+ nextMoment.minute(parsedValue);
426
+ break;
427
+ }
428
+ case "second": {
429
+ if (parsedValue < 0 || parsedValue > 59)
430
+ return null;
431
+ nextMoment.second(parsedValue);
432
+ break;
433
+ }
434
+ case "millisecond": {
435
+ if (parsedValue < 0 || parsedValue > 999)
436
+ return null;
437
+ nextMoment.millisecond(parsedValue);
438
+ break;
439
+ }
440
+ default:
441
+ return null;
442
+ }
443
+ if (!nextMoment.isValid())
444
+ return null;
445
+ return nextMoment;
446
+ }, []);
447
+ const commitTypedSegment = useCallback((segment, rawValue) => {
448
+ if (!segment.momentType || !rawValue)
449
+ return false;
450
+ const normalizedValue = ["month", "day"].includes(segment.type) &&
451
+ Array.from(rawValue).every((c) => c === "0")
452
+ ? "01"
453
+ : rawValue;
454
+ const nextMoment = getMomentWithSegmentValue(segment, normalizedValue, getEditableMoment(_value));
455
+ if (!nextMoment)
456
+ return false;
457
+ const nextValue = serializeMoment(nextMoment);
458
+ if (!checkValidRange(nextValue))
459
+ return false;
460
+ commitValue(nextValue);
461
+ return true;
462
+ }, [
463
+ _value,
464
+ getEditableMoment,
465
+ getMomentWithSegmentValue,
466
+ serializeMoment,
467
+ checkValidRange,
468
+ commitValue,
469
+ ]);
470
+ const commitSelectedDraftSegment = useCallback(() => {
471
+ if (!selectedSegment || !typedKeys.current)
472
+ return false;
473
+ if (selectedSegment.pattern === "YYYY" &&
474
+ typedKeys.current.length < getSegmentInputLength(selectedSegment)) {
475
+ return false;
476
+ }
477
+ return commitTypedSegment(selectedSegment, typedKeys.current);
478
+ }, [commitTypedSegment, getSegmentInputLength, selectedSegment]);
332
479
  // prevent click and drag selection
333
480
  const handleMouseDown = (e) => {
334
481
  e.preventDefault();
@@ -338,6 +485,8 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
338
485
  return;
339
486
  // tab
340
487
  if (e.key === "Tab") {
488
+ commitSelectedDraftSegment();
489
+ clearTypedSegment();
341
490
  setSelectedSegment(null);
342
491
  setIsOpen(false);
343
492
  return;
@@ -345,28 +494,35 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
345
494
  // Right Arrow
346
495
  if (e.key === "ArrowRight") {
347
496
  e.preventDefault();
497
+ commitSelectedDraftSegment();
498
+ clearTypedSegment();
348
499
  setSelectedSegment((prev) => {
349
500
  if (!prev)
350
501
  return segments[0];
351
502
  const next = segments[prev.index + 2];
352
503
  return next || prev;
353
504
  });
354
- typedKeys.current = ""; // clear typed keys when moving to next segment
355
505
  }
356
506
  // Left Arrow
357
507
  if (e.key === "ArrowLeft") {
358
508
  e.preventDefault();
509
+ commitSelectedDraftSegment();
510
+ clearTypedSegment();
359
511
  setSelectedSegment((prev) => {
360
512
  if (!prev)
361
513
  return segments[0];
362
514
  const next = segments[prev.index - 2];
363
515
  return next || prev;
364
516
  });
365
- typedKeys.current = ""; // clear typed keys when moving to next segment
366
517
  }
367
518
  // Up Arrow
368
519
  if (e.key === "ArrowUp") {
369
520
  e.preventDefault();
521
+ const hadDraftSegment = !!typedKeys.current;
522
+ commitSelectedDraftSegment();
523
+ clearTypedSegment();
524
+ if (hadDraftSegment)
525
+ return;
370
526
  const segmentType = selectedSegment.type;
371
527
  if (segmentType === "separator")
372
528
  return;
@@ -375,6 +531,11 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
375
531
  // Down Arrow
376
532
  if (e.key === "ArrowDown") {
377
533
  e.preventDefault();
534
+ const hadDraftSegment = !!typedKeys.current;
535
+ commitSelectedDraftSegment();
536
+ clearTypedSegment();
537
+ if (hadDraftSegment)
538
+ return;
378
539
  const segmentType = selectedSegment.type;
379
540
  if (segmentType === "separator")
380
541
  return;
@@ -382,43 +543,29 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
382
543
  }
383
544
  // handle paste
384
545
  if (e.key === "v" && (e.metaKey || e.ctrlKey)) {
546
+ clearTypedSegment();
385
547
  handlePaste();
386
548
  }
387
549
  // only allow numbers
388
- if (e.key.match(/[0-9]/)) {
550
+ if (e.key.match(/^[0-9]$/)) {
389
551
  e.preventDefault();
390
- const segmentLength = selectedSegment.text.length;
391
- typedKeys.current += e.key;
392
- let tempValue = typedKeys.current;
393
- if (["month", "day"].includes(selectedSegment.type) &&
394
- Array.from(tempValue).every((c) => c === "0")) {
395
- tempValue = "01";
396
- }
397
- setUncontrolledValue((prev) => {
398
- if (!(selectedSegment === null || selectedSegment === void 0 ? void 0 : selectedSegment.momentType))
399
- return prev;
400
- const momentValue = getEditableMoment(prev);
401
- let newValue = moment(momentValue)
402
- .set(selectedSegment.momentType, parseInt(tempValue, 10) -
403
- (selectedSegment.type === "month" ? 1 : 0))
404
- .toISOString();
405
- if (!checkValidRange(newValue))
406
- return prev;
407
- if (isDateOnly) {
408
- newValue = utc
409
- ? moment.utc(newValue).format("YYYY-MM-DD")
410
- : moment(newValue).format("YYYY-MM-DD");
411
- }
412
- if (isControlled.current) {
413
- onChange === null || onChange === void 0 ? void 0 : onChange(newValue);
414
- return prev;
415
- }
416
- onChange === null || onChange === void 0 ? void 0 : onChange(newValue);
417
- return newValue;
552
+ if (!selectedSegment.momentType)
553
+ return;
554
+ const segmentLength = getSegmentInputLength(selectedSegment);
555
+ const nextTypedKeys = typedKeys.current.length >= segmentLength
556
+ ? e.key
557
+ : `${typedKeys.current}${e.key}`;
558
+ typedKeys.current = nextTypedKeys;
559
+ setDraftSegment({
560
+ index: selectedSegment.index,
561
+ text: nextTypedKeys,
418
562
  });
419
- if (typedKeys.current.length === segmentLength) {
420
- nextSegment();
421
- typedKeys.current = "";
563
+ if (nextTypedKeys.length === segmentLength) {
564
+ const didCommit = commitTypedSegment(selectedSegment, nextTypedKeys);
565
+ clearTypedSegment();
566
+ if (didCommit) {
567
+ nextSegment();
568
+ }
422
569
  }
423
570
  }
424
571
  };
@@ -492,11 +639,12 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
492
639
  ) {
493
640
  setIsOpen(false);
494
641
  }
642
+ clearTypedSegment();
495
643
  setSelectedSegment(null); // clear selected segment when clicked outside component
496
644
  };
497
645
  document.addEventListener("click", close);
498
646
  return () => document.removeEventListener("click", close);
499
- }, [refs.floating]);
647
+ }, [clearTypedSegment, refs.floating]);
500
648
  return (_jsxs(StyledContainer, Object.assign({ ref: (ref) => {
501
649
  if (typeof _ref === "function") {
502
650
  _ref(ref);
@@ -507,15 +655,22 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
507
655
  e.preventDefault();
508
656
  setSelectedSegment(segments[0]);
509
657
  }, onBlur: () => {
658
+ commitSelectedDraftSegment();
659
+ clearTypedSegment();
510
660
  setSelectedSegment(null);
511
661
  }, "data-empty": !_value, "data-disabled": disabled, role: "textbox", size: size, variant: variant, "data-button-left": enableCalendar, "data-button-right": arrow || clearable, style: style, children: [enableCalendar && (_jsx(CalendarTriggerButton, { size: size, type: "button", "aria-label": "Toggle calendar", onClick: handleCalendarTriggerClick, "data-default-btn": true, children: _jsx(CalendarDaysIcon, {}) })), segments.map((segment, i) => {
662
+ const segmentText = (draftSegment === null || draftSegment === void 0 ? void 0 : draftSegment.index) === segment.index
663
+ ? draftSegment.text
664
+ : segment.text;
512
665
  if (segment.type === "separator") {
513
666
  return (_jsx("div", { className: "separator", tabIndex: -1, onClick: (e) => {
514
667
  e.preventDefault();
515
668
  e.stopPropagation();
516
- }, onFocus: (e) => e.preventDefault(), onPointerDown: (e) => e.preventDefault(), "data-type": "separator", "data-identifier": segment.type, "data-has-space": segment.text.includes(" "), "data-placeholder": segment.placeholder, "data-value": segment.value, children: segment.text }, i));
669
+ }, onFocus: (e) => e.preventDefault(), onPointerDown: (e) => e.preventDefault(), "data-type": "separator", "data-identifier": segment.type, "data-has-space": segmentText.includes(" "), "data-placeholder": segment.placeholder, "data-value": segment.value, children: segmentText }, i));
517
670
  }
518
- return (_jsx(InputSegment, { className: "input", tabIndex: i === 0 ? 0 : -1, onClick: (e) => handleSegmentClick(e, segment), "data-type": "input", size: size, "data-identifier": segment.type, "data-has-space": segment.text.includes(" "), "data-placeholder": segment.placeholder, "data-value": segment.value, "data-selected": (selectedSegment === null || selectedSegment === void 0 ? void 0 : selectedSegment.index) === segment.index, children: _value ? segment.text : segment.placeholder }, i));
671
+ return (_jsx(InputSegment, { className: "input", tabIndex: i === 0 ? 0 : -1, onClick: (e) => handleSegmentClick(e, segment), "data-type": "input", size: size, "data-identifier": segment.type, "data-has-space": segmentText.includes(" "), "data-placeholder": segment.placeholder, "data-value": segment.value, "data-selected": (selectedSegment === null || selectedSegment === void 0 ? void 0 : selectedSegment.index) === segment.index, children: _value || (draftSegment === null || draftSegment === void 0 ? void 0 : draftSegment.index) === segment.index
672
+ ? segmentText
673
+ : segment.placeholder }, i));
519
674
  }), utc && _jsx("div", { style: { marginLeft: 5 }, children: "UTC" }), clearable && _value ? (_jsx(ClearButton, { size: size, onClick: handleClear })) : arrow ? (_jsx(ArrowButton, { size: size })) : null] }), !disabled && enableCalendar && isOpen && (_jsx(FloatingPortal, { preserveTabOrder: true, children: _jsx(StyledFloatContainer, Object.assign({ ref: refs.setFloating, style: floatingStyles }, getFloatingProps(), { children: _jsx(StyledContent, { maxDropdownHeight: "fit-content", children: _jsx("div", { children: _jsx(Calendar, { value: getCalendarValue(), clearable: false, min: min, max: max, onChange: (date, meta) => {
520
675
  if (!date) {
521
676
  commitValue(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monolith-forensics/monolith-ui",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "author": "Matt Danner (Monolith Forensics LLC)",