bits-ui 1.5.2 → 1.6.0
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/bits/accordion/accordion.svelte.js +0 -1
- package/dist/bits/date-field/date-field.svelte.d.ts +48 -163
- package/dist/bits/date-field/date-field.svelte.js +371 -1202
- package/dist/bits/menu/menu.svelte.js +0 -1
- package/dist/bits/navigation-menu/components/navigation-menu-content.svelte +11 -13
- package/dist/bits/navigation-menu/navigation-menu.svelte.js +5 -4
- package/dist/bits/scroll-area/scroll-area.svelte.js +0 -1
- package/dist/bits/slider/components/slider-thumb.svelte +7 -2
- package/dist/bits/slider/slider.svelte.d.ts +4 -0
- package/dist/bits/slider/slider.svelte.js +7 -0
- package/dist/bits/slider/types.d.ts +4 -1
- package/package.json +2 -2
|
@@ -13,6 +13,60 @@ import { getDaysInMonth, isBefore, toDate } from "../../internal/date-time/utils
|
|
|
13
13
|
import { getFirstSegment, handleSegmentNavigation, isSegmentNavigationKey, moveToNextSegment, moveToPrevSegment, } from "../../internal/date-time/field/segments.js";
|
|
14
14
|
export const DATE_FIELD_INPUT_ATTR = "data-date-field-input";
|
|
15
15
|
const DATE_FIELD_LABEL_ATTR = "data-date-field-label";
|
|
16
|
+
const SEGMENT_CONFIGS = {
|
|
17
|
+
day: {
|
|
18
|
+
min: 1,
|
|
19
|
+
max: (root) => {
|
|
20
|
+
const segmentMonthValue = root.segmentValues.month;
|
|
21
|
+
const placeholder = root.value.current ?? root.placeholder.current;
|
|
22
|
+
return segmentMonthValue
|
|
23
|
+
? getDaysInMonth(placeholder.set({ month: Number.parseInt(segmentMonthValue) }))
|
|
24
|
+
: getDaysInMonth(placeholder);
|
|
25
|
+
},
|
|
26
|
+
cycle: 1,
|
|
27
|
+
padZero: true,
|
|
28
|
+
},
|
|
29
|
+
month: {
|
|
30
|
+
min: 1,
|
|
31
|
+
max: 12,
|
|
32
|
+
cycle: 1,
|
|
33
|
+
padZero: true,
|
|
34
|
+
getAnnouncement: (month, root) => `${month} - ${root.formatter.fullMonth(toDate(root.placeholder.current.set({ month })))}`,
|
|
35
|
+
},
|
|
36
|
+
year: {
|
|
37
|
+
min: 1,
|
|
38
|
+
max: 9999,
|
|
39
|
+
cycle: 1,
|
|
40
|
+
padZero: false,
|
|
41
|
+
},
|
|
42
|
+
hour: {
|
|
43
|
+
min: (root) => (root.hourCycle.current === 12 ? 1 : 0),
|
|
44
|
+
max: (root) => {
|
|
45
|
+
if (root.hourCycle.current === 24)
|
|
46
|
+
return 23;
|
|
47
|
+
if ("dayPeriod" in root.segmentValues && root.segmentValues.dayPeriod !== null)
|
|
48
|
+
return 12;
|
|
49
|
+
return 23;
|
|
50
|
+
},
|
|
51
|
+
cycle: 1,
|
|
52
|
+
canBeZero: true,
|
|
53
|
+
padZero: true,
|
|
54
|
+
},
|
|
55
|
+
minute: {
|
|
56
|
+
min: 0,
|
|
57
|
+
max: 59,
|
|
58
|
+
cycle: 1,
|
|
59
|
+
canBeZero: true,
|
|
60
|
+
padZero: true,
|
|
61
|
+
},
|
|
62
|
+
second: {
|
|
63
|
+
min: 0,
|
|
64
|
+
max: 59,
|
|
65
|
+
cycle: 1,
|
|
66
|
+
canBeZero: true,
|
|
67
|
+
padZero: true,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
16
70
|
export class DateFieldRootState {
|
|
17
71
|
value;
|
|
18
72
|
placeholder;
|
|
@@ -579,498 +633,277 @@ class DateFieldLabelState {
|
|
|
579
633
|
onclick: this.onclick,
|
|
580
634
|
}));
|
|
581
635
|
}
|
|
582
|
-
class
|
|
636
|
+
// Base class for numeric segments
|
|
637
|
+
class BaseNumericSegmentState {
|
|
583
638
|
opts;
|
|
584
639
|
root;
|
|
585
|
-
|
|
586
|
-
|
|
640
|
+
announcer;
|
|
641
|
+
part;
|
|
642
|
+
config;
|
|
643
|
+
constructor(opts, root, part, config) {
|
|
587
644
|
this.opts = opts;
|
|
588
645
|
this.root = root;
|
|
589
|
-
this
|
|
646
|
+
this.part = part;
|
|
647
|
+
this.config = config;
|
|
648
|
+
this.announcer = root.announcer;
|
|
590
649
|
this.onkeydown = this.onkeydown.bind(this);
|
|
591
650
|
this.onfocusout = this.onfocusout.bind(this);
|
|
592
651
|
useRefById(opts);
|
|
593
652
|
}
|
|
653
|
+
#getMax() {
|
|
654
|
+
return typeof this.config.max === "function" ? this.config.max(this.root) : this.config.max;
|
|
655
|
+
}
|
|
656
|
+
#getMin() {
|
|
657
|
+
return typeof this.config.min === "function" ? this.config.min(this.root) : this.config.min;
|
|
658
|
+
}
|
|
659
|
+
#getAnnouncement(value) {
|
|
660
|
+
if (this.config.getAnnouncement) {
|
|
661
|
+
return this.config.getAnnouncement(value, this.root);
|
|
662
|
+
}
|
|
663
|
+
return value;
|
|
664
|
+
}
|
|
665
|
+
#formatValue(value, forDisplay = true) {
|
|
666
|
+
const str = String(value);
|
|
667
|
+
if (forDisplay && this.config.padZero && str.length === 1) {
|
|
668
|
+
return `0${value}`;
|
|
669
|
+
}
|
|
670
|
+
return str;
|
|
671
|
+
}
|
|
594
672
|
onkeydown(e) {
|
|
595
|
-
const placeholder = this.root.placeholder.current;
|
|
673
|
+
const placeholder = this.root.value.current ?? this.root.placeholder.current;
|
|
596
674
|
if (e.ctrlKey || e.metaKey || this.root.disabled.current)
|
|
597
675
|
return;
|
|
676
|
+
// Special check for time segments
|
|
677
|
+
if ((this.part === "hour" || this.part === "minute" || this.part === "second") &&
|
|
678
|
+
!(this.part in placeholder))
|
|
679
|
+
return;
|
|
598
680
|
if (e.key !== kbd.TAB)
|
|
599
681
|
e.preventDefault();
|
|
600
682
|
if (!isAcceptableSegmentKey(e.key))
|
|
601
683
|
return;
|
|
602
|
-
const segmentMonthValue = this.root.segmentValues.month;
|
|
603
|
-
const daysInMonth = segmentMonthValue
|
|
604
|
-
? getDaysInMonth(placeholder.set({ month: Number.parseInt(segmentMonthValue) }))
|
|
605
|
-
: getDaysInMonth(placeholder);
|
|
606
684
|
if (isArrowUp(e.key)) {
|
|
607
|
-
this
|
|
608
|
-
if (prev === null) {
|
|
609
|
-
const next = placeholder.day;
|
|
610
|
-
this.#announcer.announce(next);
|
|
611
|
-
if (next < 10)
|
|
612
|
-
return `0${next}`;
|
|
613
|
-
return `${next}`;
|
|
614
|
-
}
|
|
615
|
-
const next = placeholder.set({ day: Number.parseInt(prev) }).cycle("day", 1).day;
|
|
616
|
-
this.#announcer.announce(next);
|
|
617
|
-
if (next < 10)
|
|
618
|
-
return `0${next}`;
|
|
619
|
-
return `${next}`;
|
|
620
|
-
});
|
|
685
|
+
this.#handleArrowUp(placeholder);
|
|
621
686
|
return;
|
|
622
687
|
}
|
|
623
688
|
if (isArrowDown(e.key)) {
|
|
624
|
-
this
|
|
625
|
-
if (prev === null) {
|
|
626
|
-
const next = placeholder.day;
|
|
627
|
-
this.#announcer.announce(next);
|
|
628
|
-
if (next < 10)
|
|
629
|
-
return `0${next}`;
|
|
630
|
-
return `${next}`;
|
|
631
|
-
}
|
|
632
|
-
const next = placeholder.set({ day: Number.parseInt(prev) }).cycle("day", -1).day;
|
|
633
|
-
this.#announcer.announce(next);
|
|
634
|
-
if (next < 10)
|
|
635
|
-
return `0${next}`;
|
|
636
|
-
return `${next}`;
|
|
637
|
-
});
|
|
689
|
+
this.#handleArrowDown(placeholder);
|
|
638
690
|
return;
|
|
639
691
|
}
|
|
640
|
-
const fieldNode = this.root.getFieldNode();
|
|
641
692
|
if (isNumberString(e.key)) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
this.root.updateSegment("day", (prev) => {
|
|
645
|
-
const max = daysInMonth;
|
|
646
|
-
const maxStart = Math.floor(max / 10);
|
|
647
|
-
const numIsZero = num === 0;
|
|
648
|
-
/**
|
|
649
|
-
* If the user has left the segment, we want to reset the
|
|
650
|
-
* `prev` value so that we can start the segment over again
|
|
651
|
-
* when the user types a number.
|
|
652
|
-
*/
|
|
653
|
-
if (this.root.states.day.hasLeftFocus) {
|
|
654
|
-
prev = null;
|
|
655
|
-
this.root.states.day.hasLeftFocus = false;
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* We are starting over in the segment if prev is null, which could
|
|
659
|
-
* happen in one of two scenarios:
|
|
660
|
-
* - the user has left the segment and then comes back to it
|
|
661
|
-
* - the segment was empty and the user begins typing a number
|
|
662
|
-
*/
|
|
663
|
-
if (prev === null) {
|
|
664
|
-
/**
|
|
665
|
-
* If the user types a 0 as the first number, we want
|
|
666
|
-
* to keep track of that so that when they type the next
|
|
667
|
-
* number, we can move to the next segment.
|
|
668
|
-
*/
|
|
669
|
-
if (numIsZero) {
|
|
670
|
-
this.root.states.day.lastKeyZero = true;
|
|
671
|
-
this.#announcer.announce("0");
|
|
672
|
-
return "0";
|
|
673
|
-
}
|
|
674
|
-
///////////////////////////
|
|
675
|
-
/**
|
|
676
|
-
* If the last key was a 0, or if the first number is
|
|
677
|
-
* greater than the max start digit (0-3 in most cases), then
|
|
678
|
-
* we want to move to the next segment, since it's not possible
|
|
679
|
-
* to continue typing a valid number in this segment.
|
|
680
|
-
*/
|
|
681
|
-
if (this.root.states.day.lastKeyZero || num > maxStart) {
|
|
682
|
-
moveToNext = true;
|
|
683
|
-
}
|
|
684
|
-
this.root.states.day.lastKeyZero = false;
|
|
685
|
-
/**
|
|
686
|
-
* If we're moving to the next segment and the number is less than
|
|
687
|
-
* two digits, we want to announce the number and return it with a
|
|
688
|
-
* leading zero to follow the placeholder format of `MM/DD/YYYY`.
|
|
689
|
-
*/
|
|
690
|
-
if (moveToNext && String(num).length === 1) {
|
|
691
|
-
this.#announcer.announce(num);
|
|
692
|
-
return `0${num}`;
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* If none of the above conditions are met, then we can just
|
|
696
|
-
* return the number as the segment value and continue typing
|
|
697
|
-
* in this segment.
|
|
698
|
-
*/
|
|
699
|
-
return `${num}`;
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* If the number of digits is 2, or if the total with the existing digit
|
|
703
|
-
* and the pressed digit is greater than the maximum value for this
|
|
704
|
-
* month, then we will reset the segment as if the user had pressed the
|
|
705
|
-
* backspace key and then typed the number.
|
|
706
|
-
*/
|
|
707
|
-
const total = Number.parseInt(prev + num.toString());
|
|
708
|
-
if (this.root.states.day.lastKeyZero) {
|
|
709
|
-
/**
|
|
710
|
-
* If the new number is not 0, then we reset the lastKeyZero state and
|
|
711
|
-
* move to the next segment, returning the new number with a leading 0.
|
|
712
|
-
*/
|
|
713
|
-
if (num !== 0) {
|
|
714
|
-
moveToNext = true;
|
|
715
|
-
this.root.states.day.lastKeyZero = false;
|
|
716
|
-
return `0${num}`;
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* If the new number is 0, then we simply return the previous value, since
|
|
720
|
-
* they didn't actually type a new number.
|
|
721
|
-
*/
|
|
722
|
-
return prev;
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* If the total is greater than the max day value possible for this month, then
|
|
726
|
-
* we want to move to the next segment, trimming the first digit from the total,
|
|
727
|
-
* replacing it with a 0.
|
|
728
|
-
*/
|
|
729
|
-
if (total > max) {
|
|
730
|
-
moveToNext = true;
|
|
731
|
-
return `0${num}`;
|
|
732
|
-
}
|
|
733
|
-
/**
|
|
734
|
-
* If the total has two digits and is less than or equal to the max day value,
|
|
735
|
-
* we will move to the next segment and return the total as the segment value.
|
|
736
|
-
*/
|
|
737
|
-
moveToNext = true;
|
|
738
|
-
return `${total}`;
|
|
739
|
-
});
|
|
740
|
-
if (moveToNext) {
|
|
741
|
-
moveToNextSegment(e, fieldNode);
|
|
742
|
-
}
|
|
693
|
+
this.#handleNumberKey(e);
|
|
694
|
+
return;
|
|
743
695
|
}
|
|
744
696
|
if (isBackspace(e.key)) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
this.root.states.day.hasLeftFocus = false;
|
|
748
|
-
if (prev === null) {
|
|
749
|
-
moveToPrev = true;
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
if (prev.length === 2 && prev.startsWith("0")) {
|
|
753
|
-
return null;
|
|
754
|
-
}
|
|
755
|
-
const str = prev.toString();
|
|
756
|
-
if (str.length === 1)
|
|
757
|
-
return null;
|
|
758
|
-
return str.slice(0, -1);
|
|
759
|
-
});
|
|
760
|
-
if (moveToPrev) {
|
|
761
|
-
moveToPrevSegment(e, fieldNode);
|
|
762
|
-
}
|
|
697
|
+
this.#handleBackspace(e);
|
|
698
|
+
return;
|
|
763
699
|
}
|
|
764
700
|
if (isSegmentNavigationKey(e.key)) {
|
|
765
|
-
handleSegmentNavigation(e,
|
|
701
|
+
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
766
702
|
}
|
|
767
703
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
this.root.
|
|
771
|
-
|
|
772
|
-
|
|
704
|
+
#handleArrowUp(placeholder) {
|
|
705
|
+
const stateKey = this.part;
|
|
706
|
+
if (stateKey in this.root.states) {
|
|
707
|
+
this.root.states[stateKey].hasLeftFocus = false;
|
|
708
|
+
}
|
|
709
|
+
// @ts-expect-error this is a part
|
|
710
|
+
this.root.updateSegment(this.part, (prev) => {
|
|
711
|
+
if (prev === null) {
|
|
712
|
+
const next = placeholder[this.part];
|
|
713
|
+
this.announcer.announce(this.#getAnnouncement(next));
|
|
714
|
+
return this.#formatValue(next);
|
|
773
715
|
}
|
|
774
|
-
|
|
716
|
+
const current = placeholder.set({
|
|
717
|
+
[this.part]: Number.parseInt(prev),
|
|
718
|
+
});
|
|
719
|
+
// @ts-expect-error this is a part
|
|
720
|
+
const next = current.cycle(this.part, this.config.cycle)[this.part];
|
|
721
|
+
this.announcer.announce(this.#getAnnouncement(next));
|
|
722
|
+
return this.#formatValue(next);
|
|
775
723
|
});
|
|
776
724
|
}
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
class DateFieldMonthSegmentState {
|
|
799
|
-
opts;
|
|
800
|
-
root;
|
|
801
|
-
#announcer;
|
|
802
|
-
constructor(opts, root) {
|
|
803
|
-
this.opts = opts;
|
|
804
|
-
this.root = root;
|
|
805
|
-
this.#announcer = this.root.announcer;
|
|
806
|
-
this.onkeydown = this.onkeydown.bind(this);
|
|
807
|
-
this.onfocusout = this.onfocusout.bind(this);
|
|
808
|
-
useRefById(opts);
|
|
809
|
-
}
|
|
810
|
-
#getAnnouncement(month) {
|
|
811
|
-
return `${month} - ${this.root.formatter.fullMonth(toDate(this.root.placeholder.current.set({ month })))}`;
|
|
725
|
+
#handleArrowDown(placeholder) {
|
|
726
|
+
const stateKey = this.part;
|
|
727
|
+
if (stateKey in this.root.states) {
|
|
728
|
+
this.root.states[stateKey].hasLeftFocus = false;
|
|
729
|
+
}
|
|
730
|
+
// @ts-expect-error this is a part
|
|
731
|
+
this.root.updateSegment(this.part, (prev) => {
|
|
732
|
+
if (prev === null) {
|
|
733
|
+
const next = placeholder[this.part];
|
|
734
|
+
this.announcer.announce(this.#getAnnouncement(next));
|
|
735
|
+
return this.#formatValue(next);
|
|
736
|
+
}
|
|
737
|
+
const current = placeholder.set({
|
|
738
|
+
[this.part]: Number.parseInt(prev),
|
|
739
|
+
});
|
|
740
|
+
// @ts-expect-error this is a part
|
|
741
|
+
const next = current.cycle(this.part, -this.config.cycle)[this.part];
|
|
742
|
+
this.announcer.announce(this.#getAnnouncement(next));
|
|
743
|
+
return this.#formatValue(next);
|
|
744
|
+
});
|
|
812
745
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
746
|
+
#handleNumberKey(e) {
|
|
747
|
+
const num = Number.parseInt(e.key);
|
|
748
|
+
let moveToNext = false;
|
|
749
|
+
const max = this.#getMax();
|
|
750
|
+
const maxStart = Math.floor(max / 10);
|
|
751
|
+
const numIsZero = num === 0;
|
|
752
|
+
const stateKey = this.part;
|
|
753
|
+
// @ts-expect-error this is a part
|
|
754
|
+
this.root.updateSegment(this.part, (prev) => {
|
|
755
|
+
// Check if user has left focus
|
|
756
|
+
if (stateKey in this.root.states && this.root.states[stateKey].hasLeftFocus) {
|
|
757
|
+
prev = null;
|
|
758
|
+
this.root.states[stateKey].hasLeftFocus = false;
|
|
759
|
+
}
|
|
760
|
+
// Starting fresh
|
|
761
|
+
if (prev === null) {
|
|
762
|
+
if (numIsZero) {
|
|
763
|
+
if (stateKey in this.root.states) {
|
|
764
|
+
this.root.states[stateKey].lastKeyZero = true;
|
|
828
765
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const next = this.root.placeholder.current
|
|
832
|
-
.set({ month: Number.parseInt(prev) })
|
|
833
|
-
.cycle("month", 1).month;
|
|
834
|
-
this.#announcer.announce(this.#getAnnouncement(next));
|
|
835
|
-
if (String(next).length === 1) {
|
|
836
|
-
return `0${next}`;
|
|
766
|
+
this.announcer.announce("0");
|
|
767
|
+
return "0";
|
|
837
768
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
if (isArrowDown(e.key)) {
|
|
843
|
-
this.root.updateSegment("month", (prev) => {
|
|
844
|
-
if (prev === null) {
|
|
845
|
-
const next = this.root.placeholder.current.month;
|
|
846
|
-
this.#announcer.announce(this.#getAnnouncement(next));
|
|
847
|
-
if (String(next).length === 1) {
|
|
848
|
-
return `0${next}`;
|
|
849
|
-
}
|
|
850
|
-
return `${next}`;
|
|
769
|
+
if (stateKey in this.root.states &&
|
|
770
|
+
(this.root.states[stateKey].lastKeyZero || num > maxStart)) {
|
|
771
|
+
moveToNext = true;
|
|
851
772
|
}
|
|
852
|
-
|
|
853
|
-
.
|
|
854
|
-
.cycle("month", -1).month;
|
|
855
|
-
this.#announcer.announce(this.#getAnnouncement(next));
|
|
856
|
-
if (String(next).length === 1) {
|
|
857
|
-
return `0${next}`;
|
|
773
|
+
if (stateKey in this.root.states) {
|
|
774
|
+
this.root.states[stateKey].lastKeyZero = false;
|
|
858
775
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
if (isNumberString(e.key)) {
|
|
864
|
-
const num = Number.parseInt(e.key);
|
|
865
|
-
let moveToNext = false;
|
|
866
|
-
this.root.updateSegment("month", (prev) => {
|
|
867
|
-
const maxStart = Math.floor(max / 10);
|
|
868
|
-
const numIsZero = num === 0;
|
|
869
|
-
/**
|
|
870
|
-
* If the user has left the segment, we want to reset the
|
|
871
|
-
* `prev` value so that we can start the segment over again
|
|
872
|
-
* when the user types a number.
|
|
873
|
-
*/
|
|
874
|
-
if (this.root.states.month.hasLeftFocus) {
|
|
875
|
-
prev = null;
|
|
876
|
-
this.root.states.month.hasLeftFocus = false;
|
|
776
|
+
if (moveToNext && String(num).length === 1) {
|
|
777
|
+
this.announcer.announce(num);
|
|
778
|
+
return `0${num}`;
|
|
877
779
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
* If the user types a 0 as the first number, we want
|
|
887
|
-
* to keep track of that so that when they type the next
|
|
888
|
-
* number, we can move to the next segment.
|
|
889
|
-
*/
|
|
890
|
-
if (numIsZero) {
|
|
891
|
-
this.root.states.month.lastKeyZero = true;
|
|
892
|
-
this.#announcer.announce("0");
|
|
893
|
-
return "0";
|
|
894
|
-
}
|
|
895
|
-
///////////////////////////
|
|
896
|
-
/**
|
|
897
|
-
* If the last key was a 0, or if the first number is
|
|
898
|
-
* greater than the max start digit (0-3 in most cases), then
|
|
899
|
-
* we want to move to the next segment, since it's not possible
|
|
900
|
-
* to continue typing a valid number in this segment.
|
|
901
|
-
*/
|
|
902
|
-
if (this.root.states.month.lastKeyZero || num > maxStart) {
|
|
903
|
-
moveToNext = true;
|
|
904
|
-
}
|
|
905
|
-
this.root.states.month.lastKeyZero = false;
|
|
906
|
-
/**
|
|
907
|
-
* If we're moving to the next segment and the number is less than
|
|
908
|
-
* two digits, we want to announce the number and return it with a
|
|
909
|
-
* leading zero to follow the placeholder format of `MM/DD/YYYY`.
|
|
910
|
-
*/
|
|
911
|
-
if (moveToNext && String(num).length === 1) {
|
|
912
|
-
this.#announcer.announce(num);
|
|
913
|
-
return `0${num}`;
|
|
914
|
-
}
|
|
915
|
-
/**
|
|
916
|
-
* If none of the above conditions are met, then we can just
|
|
917
|
-
* return the number as the segment value and continue typing
|
|
918
|
-
* in this segment.
|
|
919
|
-
*/
|
|
920
|
-
return `${num}`;
|
|
780
|
+
return `${num}`;
|
|
781
|
+
}
|
|
782
|
+
// Handle special cases for segments with lastKeyZero tracking
|
|
783
|
+
if (stateKey in this.root.states && this.root.states[stateKey].lastKeyZero) {
|
|
784
|
+
if (num !== 0) {
|
|
785
|
+
moveToNext = true;
|
|
786
|
+
this.root.states[stateKey].lastKeyZero = false;
|
|
787
|
+
return `0${num}`;
|
|
921
788
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
*/
|
|
928
|
-
const total = Number.parseInt(prev + num.toString());
|
|
929
|
-
if (this.root.states.month.lastKeyZero) {
|
|
930
|
-
/**
|
|
931
|
-
* If the new number is not 0, then we reset the lastKeyZero state and
|
|
932
|
-
* move to the next segment, returning the new number with a leading 0.
|
|
933
|
-
*/
|
|
934
|
-
if (num !== 0) {
|
|
935
|
-
moveToNext = true;
|
|
936
|
-
this.root.states.month.lastKeyZero = false;
|
|
937
|
-
return `0${num}`;
|
|
938
|
-
}
|
|
939
|
-
/**
|
|
940
|
-
* If the new number is 0, then we simply return the previous value, since
|
|
941
|
-
* they didn't actually type a new number.
|
|
942
|
-
*/
|
|
943
|
-
return prev;
|
|
789
|
+
// Special handling for hour segment with 24-hour cycle
|
|
790
|
+
if (this.part === "hour" && num === 0 && this.root.hourCycle.current === 24) {
|
|
791
|
+
moveToNext = true;
|
|
792
|
+
this.root.states[stateKey].lastKeyZero = false;
|
|
793
|
+
return `00`;
|
|
944
794
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
* we want to move to the next segment, trimming the first digit from the total,
|
|
948
|
-
* replacing it with a 0.
|
|
949
|
-
*/
|
|
950
|
-
if (total > max) {
|
|
795
|
+
// Special handling for minute/second segments
|
|
796
|
+
if ((this.part === "minute" || this.part === "second") && num === 0) {
|
|
951
797
|
moveToNext = true;
|
|
952
|
-
|
|
798
|
+
this.root.states[stateKey].lastKeyZero = false;
|
|
799
|
+
return "00";
|
|
953
800
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
801
|
+
return prev;
|
|
802
|
+
}
|
|
803
|
+
const total = Number.parseInt(prev + num.toString());
|
|
804
|
+
if (total > max) {
|
|
958
805
|
moveToNext = true;
|
|
959
|
-
return
|
|
960
|
-
});
|
|
961
|
-
if (moveToNext) {
|
|
962
|
-
moveToNextSegment(e, this.root.getFieldNode());
|
|
806
|
+
return `0${num}`;
|
|
963
807
|
}
|
|
808
|
+
moveToNext = true;
|
|
809
|
+
return `${total}`;
|
|
810
|
+
});
|
|
811
|
+
if (moveToNext) {
|
|
812
|
+
moveToNextSegment(e, this.root.getFieldNode());
|
|
964
813
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
this.#announcer.announce(null);
|
|
971
|
-
moveToPrev = true;
|
|
972
|
-
return null;
|
|
973
|
-
}
|
|
974
|
-
if (prev.length === 2 && prev.startsWith("0")) {
|
|
975
|
-
this.#announcer.announce(null);
|
|
976
|
-
return null;
|
|
977
|
-
}
|
|
978
|
-
const str = prev.toString();
|
|
979
|
-
if (str.length === 1) {
|
|
980
|
-
this.#announcer.announce(null);
|
|
981
|
-
return null;
|
|
982
|
-
}
|
|
983
|
-
const next = Number.parseInt(str.slice(0, -1));
|
|
984
|
-
this.#announcer.announce(this.#getAnnouncement(next));
|
|
985
|
-
return `${next}`;
|
|
986
|
-
});
|
|
987
|
-
if (moveToPrev) {
|
|
988
|
-
moveToPrevSegment(e, this.root.getFieldNode());
|
|
989
|
-
}
|
|
814
|
+
}
|
|
815
|
+
#handleBackspace(e) {
|
|
816
|
+
const stateKey = this.part;
|
|
817
|
+
if (stateKey in this.root.states) {
|
|
818
|
+
this.root.states[stateKey].hasLeftFocus = false;
|
|
990
819
|
}
|
|
991
|
-
|
|
992
|
-
|
|
820
|
+
let moveToPrev = false;
|
|
821
|
+
// @ts-expect-error this is a part
|
|
822
|
+
this.root.updateSegment(this.part, (prev) => {
|
|
823
|
+
if (prev === null) {
|
|
824
|
+
moveToPrev = true;
|
|
825
|
+
this.announcer.announce(null);
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
if (prev.length === 2 && prev.startsWith("0")) {
|
|
829
|
+
this.announcer.announce(null);
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
const str = prev.toString();
|
|
833
|
+
if (str.length === 1) {
|
|
834
|
+
this.announcer.announce(null);
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
const next = Number.parseInt(str.slice(0, -1));
|
|
838
|
+
this.announcer.announce(this.#getAnnouncement(next));
|
|
839
|
+
return `${next}`;
|
|
840
|
+
});
|
|
841
|
+
if (moveToPrev) {
|
|
842
|
+
moveToPrevSegment(e, this.root.getFieldNode());
|
|
993
843
|
}
|
|
994
844
|
}
|
|
995
845
|
onfocusout(_) {
|
|
996
|
-
|
|
997
|
-
this.root.
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
846
|
+
const stateKey = this.part;
|
|
847
|
+
if (stateKey in this.root.states) {
|
|
848
|
+
this.root.states[stateKey].hasLeftFocus = true;
|
|
849
|
+
}
|
|
850
|
+
// Pad with zero if needed
|
|
851
|
+
if (this.config.padZero) {
|
|
852
|
+
// @ts-expect-error this is a part
|
|
853
|
+
this.root.updateSegment(this.part, (prev) => {
|
|
854
|
+
if (prev && prev.length === 1) {
|
|
855
|
+
return `0${prev}`;
|
|
856
|
+
}
|
|
857
|
+
return prev;
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
getSegmentProps() {
|
|
862
|
+
const segmentValues = this.root.segmentValues;
|
|
863
|
+
const placeholder = this.root.placeholder.current;
|
|
864
|
+
const isEmpty = segmentValues[this.part] === null;
|
|
865
|
+
let date = placeholder;
|
|
866
|
+
if (segmentValues[this.part]) {
|
|
867
|
+
date = placeholder.set({
|
|
868
|
+
[this.part]: Number.parseInt(segmentValues[this.part]),
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
const valueNow = date[this.part];
|
|
872
|
+
const valueMin = this.#getMin();
|
|
873
|
+
const valueMax = this.#getMax();
|
|
874
|
+
let valueText = isEmpty ? "Empty" : `${valueNow}`;
|
|
875
|
+
// Special handling for hour segment with dayPeriod
|
|
876
|
+
if (this.part === "hour" && "dayPeriod" in segmentValues && segmentValues.dayPeriod) {
|
|
877
|
+
valueText = isEmpty ? "Empty" : `${valueNow} ${segmentValues.dayPeriod}`;
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
"aria-label": `${this.part}, `,
|
|
881
|
+
"aria-valuemin": valueMin,
|
|
882
|
+
"aria-valuemax": valueMax,
|
|
883
|
+
"aria-valuenow": valueNow,
|
|
884
|
+
"aria-valuetext": valueText,
|
|
885
|
+
};
|
|
1003
886
|
}
|
|
1004
887
|
props = $derived.by(() => {
|
|
1005
|
-
const date = this.root.segmentValues.month
|
|
1006
|
-
? this.root.placeholder.current.set({
|
|
1007
|
-
month: Number.parseInt(this.root.segmentValues.month),
|
|
1008
|
-
})
|
|
1009
|
-
: this.root.placeholder.current;
|
|
1010
888
|
return {
|
|
1011
889
|
...this.root.sharedSegmentAttrs,
|
|
1012
890
|
id: this.opts.id.current,
|
|
1013
|
-
|
|
1014
|
-
contenteditable: "true",
|
|
1015
|
-
"aria-valuemin": 1,
|
|
1016
|
-
"aria-valuemax": 12,
|
|
1017
|
-
"aria-valuenow": date.month,
|
|
1018
|
-
"aria-valuetext": this.root.segmentValues.month === null
|
|
1019
|
-
? "Empty"
|
|
1020
|
-
: `${date.month} - ${this.root.formatter.fullMonth(toDate(date))}`,
|
|
891
|
+
...this.getSegmentProps(),
|
|
1021
892
|
onkeydown: this.onkeydown,
|
|
1022
893
|
onfocusout: this.onfocusout,
|
|
1023
894
|
onclick: this.root.handleSegmentClick,
|
|
1024
|
-
...this.root.getBaseSegmentAttrs(
|
|
895
|
+
...this.root.getBaseSegmentAttrs(this.part, this.opts.id.current),
|
|
1025
896
|
};
|
|
1026
897
|
});
|
|
1027
898
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
root;
|
|
1031
|
-
#announcer;
|
|
1032
|
-
/**
|
|
1033
|
-
* When typing a year, a user may want to type `0090` to represent `90`.
|
|
1034
|
-
* So we track the keys they've pressed in this specific interaction to
|
|
1035
|
-
* determine once they've pressed four to move to the next segment.
|
|
1036
|
-
*
|
|
1037
|
-
* On `focusout` this is reset to an empty array.
|
|
1038
|
-
*/
|
|
899
|
+
// Year segment needs special handling
|
|
900
|
+
class DateFieldYearSegmentState extends BaseNumericSegmentState {
|
|
1039
901
|
#pressedKeys = [];
|
|
1040
|
-
/**
|
|
1041
|
-
* When a user re-enters a completed segment and backspaces, if they have
|
|
1042
|
-
* leading zeroes on the year, they won't automatically be sent to the next
|
|
1043
|
-
* segment even if they complete all 4 digits. This is because the leading zeroes
|
|
1044
|
-
* get stripped out for the digit count.
|
|
1045
|
-
*
|
|
1046
|
-
* This lets us keep track of how many times the user has backspaced in a row
|
|
1047
|
-
* to determine how many additional key presses should move them to the next segment.
|
|
1048
|
-
*
|
|
1049
|
-
* For example, if the user has `0098` in the year segment and backspaces once,
|
|
1050
|
-
* the segment will contain `009` and if the user types `7`, the segment should
|
|
1051
|
-
* contain `0097` and move to the next segment.
|
|
1052
|
-
*
|
|
1053
|
-
* If the segment contains `0100` and the user backspaces twice, the segment will
|
|
1054
|
-
* contain `01` and if the user types `2`, the segment should contain `012` and
|
|
1055
|
-
* it should _not_ move to the next segment until the user types another digit.
|
|
1056
|
-
*/
|
|
1057
902
|
#backspaceCount = 0;
|
|
1058
903
|
constructor(opts, root) {
|
|
1059
|
-
|
|
1060
|
-
this.root = root;
|
|
1061
|
-
this.#announcer = this.root.announcer;
|
|
1062
|
-
this.onkeydown = this.onkeydown.bind(this);
|
|
1063
|
-
this.onfocusout = this.onfocusout.bind(this);
|
|
1064
|
-
useRefById(opts);
|
|
1065
|
-
}
|
|
1066
|
-
#resetBackspaceCount() {
|
|
1067
|
-
this.#backspaceCount = 0;
|
|
1068
|
-
}
|
|
1069
|
-
#incrementBackspaceCount() {
|
|
1070
|
-
this.#backspaceCount++;
|
|
904
|
+
super(opts, root, "year", SEGMENT_CONFIGS.year);
|
|
1071
905
|
}
|
|
1072
906
|
onkeydown(e) {
|
|
1073
|
-
const placeholder = this.root.placeholder.current;
|
|
1074
907
|
if (e.ctrlKey || e.metaKey || this.root.disabled.current)
|
|
1075
908
|
return;
|
|
1076
909
|
if (e.key !== kbd.TAB)
|
|
@@ -1079,114 +912,97 @@ class DateFieldYearSegmentState {
|
|
|
1079
912
|
return;
|
|
1080
913
|
if (isArrowUp(e.key)) {
|
|
1081
914
|
this.#resetBackspaceCount();
|
|
1082
|
-
|
|
1083
|
-
if (prev === null) {
|
|
1084
|
-
const next = placeholder.year;
|
|
1085
|
-
this.#announcer.announce(next);
|
|
1086
|
-
return `${next}`;
|
|
1087
|
-
}
|
|
1088
|
-
const next = placeholder.set({ year: Number.parseInt(prev) }).cycle("year", 1).year;
|
|
1089
|
-
this.#announcer.announce(next);
|
|
1090
|
-
return `${next}`;
|
|
1091
|
-
});
|
|
915
|
+
super.onkeydown(e);
|
|
1092
916
|
return;
|
|
1093
917
|
}
|
|
1094
918
|
if (isArrowDown(e.key)) {
|
|
1095
919
|
this.#resetBackspaceCount();
|
|
1096
|
-
|
|
1097
|
-
if (prev === null) {
|
|
1098
|
-
const next = placeholder.year;
|
|
1099
|
-
this.#announcer.announce(next);
|
|
1100
|
-
return `${next}`;
|
|
1101
|
-
}
|
|
1102
|
-
const next = placeholder
|
|
1103
|
-
.set({ year: Number.parseInt(prev) })
|
|
1104
|
-
.cycle("year", -1).year;
|
|
1105
|
-
this.#announcer.announce(next);
|
|
1106
|
-
return `${next}`;
|
|
1107
|
-
});
|
|
920
|
+
super.onkeydown(e);
|
|
1108
921
|
return;
|
|
1109
922
|
}
|
|
1110
923
|
if (isNumberString(e.key)) {
|
|
1111
|
-
this.#
|
|
1112
|
-
|
|
1113
|
-
const num = Number.parseInt(e.key);
|
|
1114
|
-
this.root.updateSegment("year", (prev) => {
|
|
1115
|
-
if (this.root.states.year.hasLeftFocus) {
|
|
1116
|
-
prev = null;
|
|
1117
|
-
this.root.states.year.hasLeftFocus = false;
|
|
1118
|
-
}
|
|
1119
|
-
if (prev === null) {
|
|
1120
|
-
this.#announcer.announce(num);
|
|
1121
|
-
return `000${num}`;
|
|
1122
|
-
}
|
|
1123
|
-
const str = prev.toString() + num.toString();
|
|
1124
|
-
const mergedInt = Number.parseInt(str);
|
|
1125
|
-
const mergedIntDigits = String(mergedInt).length;
|
|
1126
|
-
if (mergedIntDigits < 4) {
|
|
1127
|
-
/**
|
|
1128
|
-
* If the user has backspaced and hasn't typed enough digits to make up
|
|
1129
|
-
* for the amount of backspaces, then we want to keep them in the segment
|
|
1130
|
-
* and not prepend any zeroes to the number.
|
|
1131
|
-
*/
|
|
1132
|
-
if (this.#backspaceCount > 0 &&
|
|
1133
|
-
this.#pressedKeys.length <= this.#backspaceCount &&
|
|
1134
|
-
str.length <= 4) {
|
|
1135
|
-
this.#announcer.announce(mergedInt);
|
|
1136
|
-
return str;
|
|
1137
|
-
}
|
|
1138
|
-
/**
|
|
1139
|
-
* If the mergedInt is less than 4 digits and we haven't backspaced,
|
|
1140
|
-
* then we want to prepend zeroes to the number to keep the format
|
|
1141
|
-
* of `YYYY`
|
|
1142
|
-
*/
|
|
1143
|
-
this.#announcer.announce(mergedInt);
|
|
1144
|
-
return prependYearZeros(mergedInt);
|
|
1145
|
-
}
|
|
1146
|
-
this.#announcer.announce(mergedInt);
|
|
1147
|
-
moveToNext = true;
|
|
1148
|
-
const mergedIntStr = `${mergedInt}`;
|
|
1149
|
-
if (mergedIntStr.length > 4) {
|
|
1150
|
-
return mergedIntStr.slice(0, 4);
|
|
1151
|
-
}
|
|
1152
|
-
return mergedIntStr;
|
|
1153
|
-
});
|
|
1154
|
-
if (this.#pressedKeys.length === 4 ||
|
|
1155
|
-
this.#pressedKeys.length === this.#backspaceCount) {
|
|
1156
|
-
moveToNext = true;
|
|
1157
|
-
}
|
|
1158
|
-
if (moveToNext) {
|
|
1159
|
-
moveToNextSegment(e, this.root.getFieldNode());
|
|
1160
|
-
}
|
|
924
|
+
this.#handleYearNumberKey(e);
|
|
925
|
+
return;
|
|
1161
926
|
}
|
|
1162
927
|
if (isBackspace(e.key)) {
|
|
1163
|
-
this.#
|
|
1164
|
-
|
|
1165
|
-
let moveToPrev = false;
|
|
1166
|
-
this.root.updateSegment("year", (prev) => {
|
|
1167
|
-
this.root.states.year.hasLeftFocus = false;
|
|
1168
|
-
if (prev === null) {
|
|
1169
|
-
moveToPrev = true;
|
|
1170
|
-
this.#announcer.announce(null);
|
|
1171
|
-
return null;
|
|
1172
|
-
}
|
|
1173
|
-
const str = prev.toString();
|
|
1174
|
-
if (str.length === 1) {
|
|
1175
|
-
this.#announcer.announce(null);
|
|
1176
|
-
return null;
|
|
1177
|
-
}
|
|
1178
|
-
const next = str.slice(0, -1);
|
|
1179
|
-
this.#announcer.announce(next);
|
|
1180
|
-
return `${next}`;
|
|
1181
|
-
});
|
|
1182
|
-
if (moveToPrev) {
|
|
1183
|
-
moveToPrevSegment(e, this.root.getFieldNode());
|
|
1184
|
-
}
|
|
928
|
+
this.#handleYearBackspace(e);
|
|
929
|
+
return;
|
|
1185
930
|
}
|
|
1186
931
|
if (isSegmentNavigationKey(e.key)) {
|
|
1187
932
|
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
1188
933
|
}
|
|
1189
934
|
}
|
|
935
|
+
#resetBackspaceCount() {
|
|
936
|
+
this.#backspaceCount = 0;
|
|
937
|
+
}
|
|
938
|
+
#incrementBackspaceCount() {
|
|
939
|
+
this.#backspaceCount++;
|
|
940
|
+
}
|
|
941
|
+
#handleYearNumberKey(e) {
|
|
942
|
+
this.#pressedKeys.push(e.key);
|
|
943
|
+
let moveToNext = false;
|
|
944
|
+
const num = Number.parseInt(e.key);
|
|
945
|
+
this.root.updateSegment("year", (prev) => {
|
|
946
|
+
if (this.root.states.year.hasLeftFocus) {
|
|
947
|
+
prev = null;
|
|
948
|
+
this.root.states.year.hasLeftFocus = false;
|
|
949
|
+
}
|
|
950
|
+
if (prev === null) {
|
|
951
|
+
this.announcer.announce(num);
|
|
952
|
+
return `000${num}`;
|
|
953
|
+
}
|
|
954
|
+
const str = prev.toString() + num.toString();
|
|
955
|
+
const mergedInt = Number.parseInt(str);
|
|
956
|
+
const mergedIntDigits = String(mergedInt).length;
|
|
957
|
+
if (mergedIntDigits < 4) {
|
|
958
|
+
if (this.#backspaceCount > 0 &&
|
|
959
|
+
this.#pressedKeys.length <= this.#backspaceCount &&
|
|
960
|
+
str.length <= 4) {
|
|
961
|
+
this.announcer.announce(mergedInt);
|
|
962
|
+
return str;
|
|
963
|
+
}
|
|
964
|
+
this.announcer.announce(mergedInt);
|
|
965
|
+
return prependYearZeros(mergedInt);
|
|
966
|
+
}
|
|
967
|
+
this.announcer.announce(mergedInt);
|
|
968
|
+
moveToNext = true;
|
|
969
|
+
const mergedIntStr = `${mergedInt}`;
|
|
970
|
+
if (mergedIntStr.length > 4) {
|
|
971
|
+
return mergedIntStr.slice(0, 4);
|
|
972
|
+
}
|
|
973
|
+
return mergedIntStr;
|
|
974
|
+
});
|
|
975
|
+
if (this.#pressedKeys.length === 4 || this.#pressedKeys.length === this.#backspaceCount) {
|
|
976
|
+
moveToNext = true;
|
|
977
|
+
}
|
|
978
|
+
if (moveToNext) {
|
|
979
|
+
moveToNextSegment(e, this.root.getFieldNode());
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
#handleYearBackspace(e) {
|
|
983
|
+
this.#pressedKeys = [];
|
|
984
|
+
this.#incrementBackspaceCount();
|
|
985
|
+
let moveToPrev = false;
|
|
986
|
+
this.root.updateSegment("year", (prev) => {
|
|
987
|
+
this.root.states.year.hasLeftFocus = false;
|
|
988
|
+
if (prev === null) {
|
|
989
|
+
moveToPrev = true;
|
|
990
|
+
this.announcer.announce(null);
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
const str = prev.toString();
|
|
994
|
+
if (str.length === 1) {
|
|
995
|
+
this.announcer.announce(null);
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
const next = str.slice(0, -1);
|
|
999
|
+
this.announcer.announce(next);
|
|
1000
|
+
return `${next}`;
|
|
1001
|
+
});
|
|
1002
|
+
if (moveToPrev) {
|
|
1003
|
+
moveToPrevSegment(e, this.root.getFieldNode());
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1190
1006
|
onfocusout(_) {
|
|
1191
1007
|
this.root.states.year.hasLeftFocus = true;
|
|
1192
1008
|
this.#pressedKeys = [];
|
|
@@ -1198,703 +1014,56 @@ class DateFieldYearSegmentState {
|
|
|
1198
1014
|
return prev;
|
|
1199
1015
|
});
|
|
1200
1016
|
}
|
|
1201
|
-
props = $derived.by(() => {
|
|
1202
|
-
const segmentValues = this.root.segmentValues;
|
|
1203
|
-
const placeholder = this.root.placeholder.current;
|
|
1204
|
-
const isEmpty = segmentValues.year === null;
|
|
1205
|
-
const date = segmentValues.year
|
|
1206
|
-
? placeholder.set({ year: Number.parseInt(segmentValues.year) })
|
|
1207
|
-
: placeholder;
|
|
1208
|
-
const valueMin = 1;
|
|
1209
|
-
const valueMax = 9999;
|
|
1210
|
-
const valueNow = date.year;
|
|
1211
|
-
const valueText = isEmpty ? "Empty" : `${valueNow}`;
|
|
1212
|
-
return {
|
|
1213
|
-
...this.root.sharedSegmentAttrs,
|
|
1214
|
-
id: this.opts.id.current,
|
|
1215
|
-
"aria-label": "year, ",
|
|
1216
|
-
"aria-valuemin": valueMin,
|
|
1217
|
-
"aria-valuemax": valueMax,
|
|
1218
|
-
"aria-valuenow": valueNow,
|
|
1219
|
-
"aria-valuetext": valueText,
|
|
1220
|
-
onkeydown: this.onkeydown,
|
|
1221
|
-
onclick: this.root.handleSegmentClick,
|
|
1222
|
-
onfocusout: this.onfocusout,
|
|
1223
|
-
...this.root.getBaseSegmentAttrs("year", this.opts.id.current),
|
|
1224
|
-
};
|
|
1225
|
-
});
|
|
1226
1017
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
root;
|
|
1230
|
-
#announcer;
|
|
1018
|
+
// Create segment states using the base class
|
|
1019
|
+
class DateFieldDaySegmentState extends BaseNumericSegmentState {
|
|
1231
1020
|
constructor(opts, root) {
|
|
1232
|
-
|
|
1233
|
-
this.root = root;
|
|
1234
|
-
this.#announcer = this.root.announcer;
|
|
1235
|
-
this.onkeydown = this.onkeydown.bind(this);
|
|
1236
|
-
this.onfocusout = this.onfocusout.bind(this);
|
|
1237
|
-
useRefById(opts);
|
|
1238
|
-
}
|
|
1239
|
-
onkeydown(e) {
|
|
1240
|
-
const placeholder = this.root.placeholder.current;
|
|
1241
|
-
if (e.ctrlKey || e.metaKey || this.root.disabled.current || !("hour" in placeholder))
|
|
1242
|
-
return;
|
|
1243
|
-
if (e.key !== kbd.TAB)
|
|
1244
|
-
e.preventDefault();
|
|
1245
|
-
if (!isAcceptableSegmentKey(e.key))
|
|
1246
|
-
return;
|
|
1247
|
-
const hourCycle = this.root.hourCycle.current;
|
|
1248
|
-
if (isArrowUp(e.key)) {
|
|
1249
|
-
this.root.updateSegment("hour", (prev) => {
|
|
1250
|
-
if (prev === null) {
|
|
1251
|
-
const next = placeholder.cycle("hour", 1, { hourCycle }).hour;
|
|
1252
|
-
this.#announcer.announce(next);
|
|
1253
|
-
return `${next}`;
|
|
1254
|
-
}
|
|
1255
|
-
const next = placeholder
|
|
1256
|
-
.set({ hour: Number.parseInt(prev) })
|
|
1257
|
-
.cycle("hour", 1, { hourCycle }).hour;
|
|
1258
|
-
if (next === 0 &&
|
|
1259
|
-
"dayPeriod" in this.root.segmentValues &&
|
|
1260
|
-
this.root.segmentValues.dayPeriod !== null &&
|
|
1261
|
-
this.root.hourCycle.current !== 24) {
|
|
1262
|
-
this.#announcer.announce("12");
|
|
1263
|
-
return "12";
|
|
1264
|
-
}
|
|
1265
|
-
if (next === 0 && this.root.hourCycle.current === 24) {
|
|
1266
|
-
this.#announcer.announce("00");
|
|
1267
|
-
return "00";
|
|
1268
|
-
}
|
|
1269
|
-
this.#announcer.announce(next);
|
|
1270
|
-
return `${next}`;
|
|
1271
|
-
});
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
if (isArrowDown(e.key)) {
|
|
1275
|
-
this.root.updateSegment("hour", (prev) => {
|
|
1276
|
-
if (prev === null) {
|
|
1277
|
-
const next = placeholder.cycle("hour", -1, { hourCycle }).hour;
|
|
1278
|
-
this.#announcer.announce(next);
|
|
1279
|
-
return `${next}`;
|
|
1280
|
-
}
|
|
1281
|
-
const next = placeholder
|
|
1282
|
-
.set({ hour: Number.parseInt(prev) })
|
|
1283
|
-
.cycle("hour", -1, { hourCycle }).hour;
|
|
1284
|
-
if (next === 0 &&
|
|
1285
|
-
"dayPeriod" in this.root.segmentValues &&
|
|
1286
|
-
this.root.segmentValues.dayPeriod !== null &&
|
|
1287
|
-
this.root.hourCycle.current !== 24) {
|
|
1288
|
-
this.#announcer.announce("12");
|
|
1289
|
-
return "12";
|
|
1290
|
-
}
|
|
1291
|
-
if (next === 0 && this.root.hourCycle.current === 24) {
|
|
1292
|
-
this.#announcer.announce("00");
|
|
1293
|
-
return "00";
|
|
1294
|
-
}
|
|
1295
|
-
this.#announcer.announce(next);
|
|
1296
|
-
return `${next}`;
|
|
1297
|
-
});
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
if (isNumberString(e.key)) {
|
|
1301
|
-
const num = Number.parseInt(e.key);
|
|
1302
|
-
const max = this.root.hourCycle.current === 24
|
|
1303
|
-
? 23
|
|
1304
|
-
: "dayPeriod" in this.root.segmentValues &&
|
|
1305
|
-
this.root.segmentValues.dayPeriod !== null
|
|
1306
|
-
? 12
|
|
1307
|
-
: 23;
|
|
1308
|
-
const maxStart = Math.floor(max / 10);
|
|
1309
|
-
let moveToNext = false;
|
|
1310
|
-
const numIsZero = num === 0;
|
|
1311
|
-
this.root.updateSegment("hour", (prev) => {
|
|
1312
|
-
/**
|
|
1313
|
-
* If the user has left the segment, we want to reset the
|
|
1314
|
-
* `prev` value so that we can start the segment over again
|
|
1315
|
-
* when the user types a number.
|
|
1316
|
-
*/
|
|
1317
|
-
if (this.root.states.hour.hasLeftFocus) {
|
|
1318
|
-
prev = null;
|
|
1319
|
-
this.root.states.hour.hasLeftFocus = false;
|
|
1320
|
-
}
|
|
1321
|
-
/**
|
|
1322
|
-
* We are starting over in the segment if prev is null, which could
|
|
1323
|
-
* happen in one of two scenarios:
|
|
1324
|
-
* - the user has left the segment and then comes back to it
|
|
1325
|
-
* - the segment was empty and the user begins typing a number
|
|
1326
|
-
*/
|
|
1327
|
-
if (prev === null) {
|
|
1328
|
-
/**
|
|
1329
|
-
* If the user types a 0 as the first number, we want
|
|
1330
|
-
* to keep track of that so that when they type the next
|
|
1331
|
-
* number, we can move to the next segment.
|
|
1332
|
-
*/
|
|
1333
|
-
if (numIsZero) {
|
|
1334
|
-
this.root.states.hour.lastKeyZero = true;
|
|
1335
|
-
this.#announcer.announce("0");
|
|
1336
|
-
return "0";
|
|
1337
|
-
}
|
|
1338
|
-
///////////////////////////
|
|
1339
|
-
/**
|
|
1340
|
-
* If the last key was a 0, or if the first number is
|
|
1341
|
-
* greater than the max start digit (0-3 in most cases), then
|
|
1342
|
-
* we want to move to the next segment, since it's not possible
|
|
1343
|
-
* to continue typing a valid number in this segment.
|
|
1344
|
-
*/
|
|
1345
|
-
if (this.root.states.hour.lastKeyZero || num > maxStart) {
|
|
1346
|
-
moveToNext = true;
|
|
1347
|
-
}
|
|
1348
|
-
this.root.states.hour.lastKeyZero = false;
|
|
1349
|
-
/**
|
|
1350
|
-
* If we're moving to the next segment and the number is less than
|
|
1351
|
-
* two digits, we want to announce the number and return it with a
|
|
1352
|
-
* leading zero to follow the placeholder format of `MM/DD/YYYY`.
|
|
1353
|
-
*/
|
|
1354
|
-
if (moveToNext && String(num).length === 1) {
|
|
1355
|
-
this.#announcer.announce(num);
|
|
1356
|
-
return `0${num}`;
|
|
1357
|
-
}
|
|
1358
|
-
/**
|
|
1359
|
-
* If none of the above conditions are met, then we can just
|
|
1360
|
-
* return the number as the segment value and continue typing
|
|
1361
|
-
* in this segment.
|
|
1362
|
-
*/
|
|
1363
|
-
return `${num}`;
|
|
1364
|
-
}
|
|
1365
|
-
/**
|
|
1366
|
-
* If the number of digits is 2, or if the total with the existing digit
|
|
1367
|
-
* and the pressed digit is greater than the maximum value for this
|
|
1368
|
-
* hour, then we will reset the segment as if the user had pressed the
|
|
1369
|
-
* backspace key and then typed the number.
|
|
1370
|
-
*/
|
|
1371
|
-
const total = Number.parseInt(prev + num.toString());
|
|
1372
|
-
if (this.root.states.hour.lastKeyZero) {
|
|
1373
|
-
/**
|
|
1374
|
-
* If the new number is not 0, then we reset the lastKeyZero state and
|
|
1375
|
-
* move to the next segment, returning the new number with a leading 0.
|
|
1376
|
-
*/
|
|
1377
|
-
if (num !== 0) {
|
|
1378
|
-
moveToNext = true;
|
|
1379
|
-
this.root.states.hour.lastKeyZero = false;
|
|
1380
|
-
return `0${num}`;
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* If the new number is 0 and the hour cycle is set to 24, then we move
|
|
1384
|
-
* to the next segment, returning the new number with a leading 0.
|
|
1385
|
-
*/
|
|
1386
|
-
if (num === 0 && this.root.hourCycle.current === 24) {
|
|
1387
|
-
moveToNext = true;
|
|
1388
|
-
this.root.states.hour.lastKeyZero = false;
|
|
1389
|
-
return `0${num}`;
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* If the new number is 0, then we simply return the previous value, since
|
|
1393
|
-
* they didn't actually type a new number.
|
|
1394
|
-
*/
|
|
1395
|
-
return prev;
|
|
1396
|
-
}
|
|
1397
|
-
/**
|
|
1398
|
-
* If the total is greater than the max day value possible for this month, then
|
|
1399
|
-
* we want to move to the next segment, trimming the first digit from the total,
|
|
1400
|
-
* replacing it with a 0.
|
|
1401
|
-
*/
|
|
1402
|
-
if (total > max) {
|
|
1403
|
-
moveToNext = true;
|
|
1404
|
-
return `0${num}`;
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* If the total has two digits and is less than or equal to the max day value,
|
|
1408
|
-
* we will move to the next segment and return the total as the segment value.
|
|
1409
|
-
*/
|
|
1410
|
-
moveToNext = true;
|
|
1411
|
-
return `${total}`;
|
|
1412
|
-
});
|
|
1413
|
-
if (moveToNext) {
|
|
1414
|
-
moveToNextSegment(e, this.root.getFieldNode());
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
if (isBackspace(e.key)) {
|
|
1418
|
-
this.root.states.hour.hasLeftFocus = false;
|
|
1419
|
-
let moveToPrev = false;
|
|
1420
|
-
this.root.updateSegment("hour", (prev) => {
|
|
1421
|
-
if (prev === null) {
|
|
1422
|
-
this.#announcer.announce(null);
|
|
1423
|
-
moveToPrev = true;
|
|
1424
|
-
return null;
|
|
1425
|
-
}
|
|
1426
|
-
const str = prev.toString();
|
|
1427
|
-
if (str.length === 1) {
|
|
1428
|
-
this.#announcer.announce(null);
|
|
1429
|
-
return null;
|
|
1430
|
-
}
|
|
1431
|
-
const next = Number.parseInt(str.slice(0, -1));
|
|
1432
|
-
this.#announcer.announce(next);
|
|
1433
|
-
return `${next}`;
|
|
1434
|
-
});
|
|
1435
|
-
if (moveToPrev) {
|
|
1436
|
-
moveToPrevSegment(e, this.root.getFieldNode());
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
if (isSegmentNavigationKey(e.key)) {
|
|
1440
|
-
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
1441
|
-
}
|
|
1021
|
+
super(opts, root, "day", SEGMENT_CONFIGS.day);
|
|
1442
1022
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1023
|
+
}
|
|
1024
|
+
class DateFieldMonthSegmentState extends BaseNumericSegmentState {
|
|
1025
|
+
constructor(opts, root) {
|
|
1026
|
+
super(opts, root, "month", SEGMENT_CONFIGS.month);
|
|
1445
1027
|
}
|
|
1446
|
-
props = $derived.by(() => {
|
|
1447
|
-
const segmentValues = this.root.segmentValues;
|
|
1448
|
-
const hourCycle = this.root.hourCycle.current;
|
|
1449
|
-
const placeholder = this.root.placeholder.current;
|
|
1450
|
-
if (!("hour" in segmentValues) || !("hour" in placeholder))
|
|
1451
|
-
return {};
|
|
1452
|
-
const isEmpty = segmentValues.hour === null;
|
|
1453
|
-
const date = segmentValues.hour
|
|
1454
|
-
? placeholder.set({ hour: Number.parseInt(segmentValues.hour) })
|
|
1455
|
-
: placeholder;
|
|
1456
|
-
const valueMin = hourCycle === 12 ? 1 : 0;
|
|
1457
|
-
const valueMax = hourCycle === 12 ? 12 : 23;
|
|
1458
|
-
const valueNow = date.hour;
|
|
1459
|
-
const valueText = isEmpty ? "Empty" : `${valueNow} ${segmentValues.dayPeriod ?? ""}`;
|
|
1460
|
-
return {
|
|
1461
|
-
...this.root.sharedSegmentAttrs,
|
|
1462
|
-
id: this.opts.id.current,
|
|
1463
|
-
"aria-label": "hour, ",
|
|
1464
|
-
"aria-valuemin": valueMin,
|
|
1465
|
-
"aria-valuemax": valueMax,
|
|
1466
|
-
"aria-valuenow": valueNow,
|
|
1467
|
-
"aria-valuetext": valueText,
|
|
1468
|
-
onkeydown: this.onkeydown,
|
|
1469
|
-
onfocusout: this.onfocusout,
|
|
1470
|
-
onclick: this.root.handleSegmentClick,
|
|
1471
|
-
...this.root.getBaseSegmentAttrs("hour", this.opts.id.current),
|
|
1472
|
-
};
|
|
1473
|
-
});
|
|
1474
1028
|
}
|
|
1475
|
-
class
|
|
1476
|
-
opts;
|
|
1477
|
-
root;
|
|
1478
|
-
#announcer;
|
|
1029
|
+
class DateFieldHourSegmentState extends BaseNumericSegmentState {
|
|
1479
1030
|
constructor(opts, root) {
|
|
1480
|
-
|
|
1481
|
-
this.root = root;
|
|
1482
|
-
this.#announcer = this.root.announcer;
|
|
1483
|
-
this.onkeydown = this.onkeydown.bind(this);
|
|
1484
|
-
this.onfocusout = this.onfocusout.bind(this);
|
|
1485
|
-
useRefById(opts);
|
|
1031
|
+
super(opts, root, "hour", SEGMENT_CONFIGS.hour);
|
|
1486
1032
|
}
|
|
1033
|
+
// Override to handle special hour logic
|
|
1487
1034
|
onkeydown(e) {
|
|
1488
|
-
|
|
1489
|
-
if (e.ctrlKey || e.metaKey || this.root.disabled.current || !("minute" in placeholder))
|
|
1490
|
-
return;
|
|
1491
|
-
if (e.key !== kbd.TAB)
|
|
1492
|
-
e.preventDefault();
|
|
1493
|
-
if (!isAcceptableSegmentKey(e.key))
|
|
1494
|
-
return;
|
|
1495
|
-
const min = 0;
|
|
1496
|
-
const max = 59;
|
|
1497
|
-
if (isArrowUp(e.key)) {
|
|
1498
|
-
this.root.updateSegment("minute", (prev) => {
|
|
1499
|
-
if (prev === null) {
|
|
1500
|
-
this.#announcer.announce(min);
|
|
1501
|
-
return `${min}`;
|
|
1502
|
-
}
|
|
1503
|
-
const next = placeholder
|
|
1504
|
-
.set({ minute: Number.parseInt(prev) })
|
|
1505
|
-
.cycle("minute", 1).minute;
|
|
1506
|
-
this.#announcer.announce(next);
|
|
1507
|
-
return `${next}`;
|
|
1508
|
-
});
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
if (isArrowDown(e.key)) {
|
|
1512
|
-
this.root.updateSegment("minute", (prev) => {
|
|
1513
|
-
if (prev === null) {
|
|
1514
|
-
this.#announcer.announce(max);
|
|
1515
|
-
return `${max}`;
|
|
1516
|
-
}
|
|
1517
|
-
const next = placeholder
|
|
1518
|
-
.set({ minute: Number.parseInt(prev) })
|
|
1519
|
-
.cycle("minute", -1).minute;
|
|
1520
|
-
this.#announcer.announce(next);
|
|
1521
|
-
return `${next}`;
|
|
1522
|
-
});
|
|
1523
|
-
return;
|
|
1524
|
-
}
|
|
1035
|
+
// Add special handling for hour display with dayPeriod
|
|
1525
1036
|
if (isNumberString(e.key)) {
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
prev = null;
|
|
1538
|
-
this.root.states.minute.hasLeftFocus = false;
|
|
1539
|
-
}
|
|
1540
|
-
/**
|
|
1541
|
-
* We are starting over in the segment if prev is null, which could
|
|
1542
|
-
* happen in one of two scenarios:
|
|
1543
|
-
* - the user has left the segment and then comes back to it
|
|
1544
|
-
* - the segment was empty and the user begins typing a number
|
|
1545
|
-
*/
|
|
1546
|
-
if (prev === null) {
|
|
1547
|
-
/**
|
|
1548
|
-
* If the user types a 0 as the first number, we want
|
|
1549
|
-
* to keep track of that so that when they type the next
|
|
1550
|
-
* number, we can move to the next segment.
|
|
1551
|
-
*/
|
|
1552
|
-
if (numIsZero) {
|
|
1553
|
-
this.root.states.minute.lastKeyZero = true;
|
|
1554
|
-
this.#announcer.announce("0");
|
|
1555
|
-
return "0";
|
|
1037
|
+
const oldUpdateSegment = this.root.updateSegment.bind(this.root);
|
|
1038
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1039
|
+
this.root.updateSegment = (part, cb) => {
|
|
1040
|
+
const result = oldUpdateSegment(part, cb);
|
|
1041
|
+
// After updating hour, check if we need to display "12" instead of "0"
|
|
1042
|
+
if (part === "hour" && "hour" in this.root.segmentValues) {
|
|
1043
|
+
const hourValue = this.root.segmentValues.hour;
|
|
1044
|
+
if (hourValue === "0" &&
|
|
1045
|
+
this.root.dayPeriodNode &&
|
|
1046
|
+
this.root.hourCycle.current !== 24) {
|
|
1047
|
+
this.root.segmentValues.hour = "12";
|
|
1556
1048
|
}
|
|
1557
|
-
///////////////////////////
|
|
1558
|
-
/**
|
|
1559
|
-
* If the last key was a 0, or if the first number is
|
|
1560
|
-
* greater than the max start digit (0-3 in most cases), then
|
|
1561
|
-
* we want to move to the next segment, since it's not possible
|
|
1562
|
-
* to continue typing a valid number in this segment.
|
|
1563
|
-
*/
|
|
1564
|
-
if (this.root.states.minute.lastKeyZero || num > maxStart) {
|
|
1565
|
-
moveToNext = true;
|
|
1566
|
-
}
|
|
1567
|
-
this.root.states.minute.lastKeyZero = false;
|
|
1568
|
-
/**
|
|
1569
|
-
* If we're moving to the next segment and the number is less than
|
|
1570
|
-
* two digits, we want to announce the number and return it with a
|
|
1571
|
-
* leading zero to follow the placeholder format of `MM/DD/YYYY`.
|
|
1572
|
-
*/
|
|
1573
|
-
if (moveToNext && String(num).length === 1) {
|
|
1574
|
-
this.#announcer.announce(num);
|
|
1575
|
-
return `0${num}`;
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* If none of the above conditions are met, then we can just
|
|
1579
|
-
* return the number as the segment value and continue typing
|
|
1580
|
-
* in this segment.
|
|
1581
|
-
*/
|
|
1582
|
-
return `${num}`;
|
|
1583
1049
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
* and the pressed digit is greater than the maximum value for this
|
|
1587
|
-
* minute, then we will reset the segment as if the user had pressed the
|
|
1588
|
-
* backspace key and then typed the number.
|
|
1589
|
-
*/
|
|
1590
|
-
const total = Number.parseInt(prev + num.toString());
|
|
1591
|
-
if (this.root.states.minute.lastKeyZero) {
|
|
1592
|
-
/**
|
|
1593
|
-
* If the new number is not 0, then we reset the lastKeyZero state and
|
|
1594
|
-
* move to the next segment, returning the new number with a leading 0.
|
|
1595
|
-
*/
|
|
1596
|
-
if (num !== 0) {
|
|
1597
|
-
moveToNext = true;
|
|
1598
|
-
this.root.states.minute.lastKeyZero = false;
|
|
1599
|
-
return `0${num}`;
|
|
1600
|
-
}
|
|
1601
|
-
/**
|
|
1602
|
-
* If the new number is 0, then we simply return `00` since that is
|
|
1603
|
-
* an acceptable minute value.
|
|
1604
|
-
*/
|
|
1605
|
-
moveToNext = true;
|
|
1606
|
-
this.root.states.minute.lastKeyZero = false;
|
|
1607
|
-
return "00";
|
|
1608
|
-
}
|
|
1609
|
-
/**
|
|
1610
|
-
* If the total is greater than the max day value possible for this month, then
|
|
1611
|
-
* we want to move to the next segment, trimming the first digit from the total,
|
|
1612
|
-
* replacing it with a 0.
|
|
1613
|
-
*/
|
|
1614
|
-
if (total > max) {
|
|
1615
|
-
moveToNext = true;
|
|
1616
|
-
return `0${num}`;
|
|
1617
|
-
}
|
|
1618
|
-
/**
|
|
1619
|
-
* If the total has two digits and is less than or equal to the max day value,
|
|
1620
|
-
* we will move to the next segment and return the total as the segment value.
|
|
1621
|
-
*/
|
|
1622
|
-
moveToNext = true;
|
|
1623
|
-
return `${total}`;
|
|
1624
|
-
});
|
|
1625
|
-
if (moveToNext) {
|
|
1626
|
-
moveToNextSegment(e, this.root.getFieldNode());
|
|
1627
|
-
}
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
if (isBackspace(e.key)) {
|
|
1631
|
-
this.root.states.minute.hasLeftFocus = false;
|
|
1632
|
-
let moveToPrev = false;
|
|
1633
|
-
this.root.updateSegment("minute", (prev) => {
|
|
1634
|
-
if (prev === null) {
|
|
1635
|
-
moveToPrev = true;
|
|
1636
|
-
this.#announcer.announce("Empty");
|
|
1637
|
-
return null;
|
|
1638
|
-
}
|
|
1639
|
-
const str = prev.toString();
|
|
1640
|
-
if (str.length === 1) {
|
|
1641
|
-
this.#announcer.announce("Empty");
|
|
1642
|
-
return null;
|
|
1643
|
-
}
|
|
1644
|
-
const next = Number.parseInt(str.slice(0, -1));
|
|
1645
|
-
this.#announcer.announce(next);
|
|
1646
|
-
return `${next}`;
|
|
1647
|
-
});
|
|
1648
|
-
if (moveToPrev) {
|
|
1649
|
-
moveToPrevSegment(e, this.root.getFieldNode());
|
|
1650
|
-
}
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
if (isSegmentNavigationKey(e.key)) {
|
|
1654
|
-
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
1050
|
+
return result;
|
|
1051
|
+
};
|
|
1655
1052
|
}
|
|
1053
|
+
super.onkeydown(e);
|
|
1054
|
+
// Restore original updateSegment
|
|
1055
|
+
this.root.updateSegment = this.root.updateSegment.bind(this.root);
|
|
1656
1056
|
}
|
|
1657
|
-
onfocusout(_) {
|
|
1658
|
-
this.root.states.minute.hasLeftFocus = true;
|
|
1659
|
-
}
|
|
1660
|
-
props = $derived.by(() => {
|
|
1661
|
-
const segmentValues = this.root.segmentValues;
|
|
1662
|
-
const placeholder = this.root.placeholder.current;
|
|
1663
|
-
if (!("minute" in segmentValues) || !("minute" in placeholder))
|
|
1664
|
-
return {};
|
|
1665
|
-
const isEmpty = segmentValues.minute === null;
|
|
1666
|
-
const date = segmentValues.minute
|
|
1667
|
-
? placeholder.set({ minute: Number.parseInt(segmentValues.minute) })
|
|
1668
|
-
: placeholder;
|
|
1669
|
-
const valueNow = date.minute;
|
|
1670
|
-
const valueMin = 0;
|
|
1671
|
-
const valueMax = 59;
|
|
1672
|
-
const valueText = isEmpty ? "Empty" : `${valueNow}`;
|
|
1673
|
-
return {
|
|
1674
|
-
...this.root.sharedSegmentAttrs,
|
|
1675
|
-
id: this.opts.id.current,
|
|
1676
|
-
"aria-label": "minute, ",
|
|
1677
|
-
"aria-valuemin": valueMin,
|
|
1678
|
-
"aria-valuemax": valueMax,
|
|
1679
|
-
"aria-valuenow": valueNow,
|
|
1680
|
-
"aria-valuetext": valueText,
|
|
1681
|
-
onkeydown: this.onkeydown,
|
|
1682
|
-
onfocusout: this.onfocusout,
|
|
1683
|
-
onclick: this.root.handleSegmentClick,
|
|
1684
|
-
...this.root.getBaseSegmentAttrs("minute", this.opts.id.current),
|
|
1685
|
-
};
|
|
1686
|
-
});
|
|
1687
1057
|
}
|
|
1688
|
-
class
|
|
1689
|
-
opts;
|
|
1690
|
-
root;
|
|
1691
|
-
#announcer;
|
|
1058
|
+
class DateFieldMinuteSegmentState extends BaseNumericSegmentState {
|
|
1692
1059
|
constructor(opts, root) {
|
|
1693
|
-
|
|
1694
|
-
this.root = root;
|
|
1695
|
-
this.#announcer = this.root.announcer;
|
|
1696
|
-
this.onkeydown = this.onkeydown.bind(this);
|
|
1697
|
-
this.onfocusout = this.onfocusout.bind(this);
|
|
1698
|
-
useRefById(opts);
|
|
1060
|
+
super(opts, root, "minute", SEGMENT_CONFIGS.minute);
|
|
1699
1061
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
if (e.key !== kbd.TAB)
|
|
1705
|
-
e.preventDefault();
|
|
1706
|
-
if (!isAcceptableSegmentKey(e.key))
|
|
1707
|
-
return;
|
|
1708
|
-
const min = 0;
|
|
1709
|
-
const max = 59;
|
|
1710
|
-
if (isArrowUp(e.key)) {
|
|
1711
|
-
this.root.updateSegment("second", (prev) => {
|
|
1712
|
-
if (prev === null) {
|
|
1713
|
-
this.#announcer.announce(min);
|
|
1714
|
-
return `${min}`;
|
|
1715
|
-
}
|
|
1716
|
-
const next = placeholder
|
|
1717
|
-
.set({ second: Number.parseInt(prev) })
|
|
1718
|
-
.cycle("second", 1).second;
|
|
1719
|
-
this.#announcer.announce(next);
|
|
1720
|
-
return `${next}`;
|
|
1721
|
-
});
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
if (isArrowDown(e.key)) {
|
|
1725
|
-
this.root.updateSegment("second", (prev) => {
|
|
1726
|
-
if (prev === null) {
|
|
1727
|
-
this.#announcer.announce(max);
|
|
1728
|
-
return `${max}`;
|
|
1729
|
-
}
|
|
1730
|
-
const next = placeholder
|
|
1731
|
-
.set({ second: Number.parseInt(prev) })
|
|
1732
|
-
.cycle("second", -1).second;
|
|
1733
|
-
this.#announcer.announce(next);
|
|
1734
|
-
return `${next}`;
|
|
1735
|
-
});
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
if (isNumberString(e.key)) {
|
|
1739
|
-
const num = Number.parseInt(e.key);
|
|
1740
|
-
const numIsZero = num === 0;
|
|
1741
|
-
let moveToNext = false;
|
|
1742
|
-
this.root.updateSegment("second", (prev) => {
|
|
1743
|
-
const maxStart = Math.floor(max / 10);
|
|
1744
|
-
/**
|
|
1745
|
-
* If the user has left the segment, we want to reset the
|
|
1746
|
-
* `prev` value so that we can start the segment over again
|
|
1747
|
-
* when the user types a number.
|
|
1748
|
-
*/
|
|
1749
|
-
if (this.root.states.second.hasLeftFocus) {
|
|
1750
|
-
prev = null;
|
|
1751
|
-
this.root.states.second.hasLeftFocus = false;
|
|
1752
|
-
}
|
|
1753
|
-
/**
|
|
1754
|
-
* We are starting over in the segment if prev is null, which could
|
|
1755
|
-
* happen in one of two scenarios:
|
|
1756
|
-
* - the user has left the segment and then comes back to it
|
|
1757
|
-
* - the segment was empty and the user begins typing a number
|
|
1758
|
-
*/
|
|
1759
|
-
if (prev === null) {
|
|
1760
|
-
/**
|
|
1761
|
-
* If the user types a 0 as the first number, we want
|
|
1762
|
-
* to keep track of that so that when they type the next
|
|
1763
|
-
* number, we can move to the next segment.
|
|
1764
|
-
*/
|
|
1765
|
-
if (numIsZero) {
|
|
1766
|
-
this.root.states.second.lastKeyZero = true;
|
|
1767
|
-
this.#announcer.announce("0");
|
|
1768
|
-
return "0";
|
|
1769
|
-
}
|
|
1770
|
-
///////////////////////////
|
|
1771
|
-
/**
|
|
1772
|
-
* If the last key was a 0, or if the first number is
|
|
1773
|
-
* greater than the max start digit (0-3 in most cases), then
|
|
1774
|
-
* we want to move to the next segment, since it's not possible
|
|
1775
|
-
* to continue typing a valid number in this segment.
|
|
1776
|
-
*/
|
|
1777
|
-
if (this.root.states.second.lastKeyZero || num > maxStart) {
|
|
1778
|
-
moveToNext = true;
|
|
1779
|
-
}
|
|
1780
|
-
this.root.states.second.lastKeyZero = false;
|
|
1781
|
-
/**
|
|
1782
|
-
* If we're moving to the next segment and the number is less than
|
|
1783
|
-
* two digits, we want to announce the number and return it with a
|
|
1784
|
-
* leading zero to follow the placeholder format of `MM/DD/YYYY`.
|
|
1785
|
-
*/
|
|
1786
|
-
if (moveToNext && String(num).length === 1) {
|
|
1787
|
-
this.#announcer.announce(num);
|
|
1788
|
-
return `0${num}`;
|
|
1789
|
-
}
|
|
1790
|
-
/**
|
|
1791
|
-
* If none of the above conditions are met, then we can just
|
|
1792
|
-
* return the number as the segment value and continue typing
|
|
1793
|
-
* in this segment.
|
|
1794
|
-
*/
|
|
1795
|
-
return `${num}`;
|
|
1796
|
-
}
|
|
1797
|
-
/**
|
|
1798
|
-
* If the number of digits is 2, or if the total with the existing digit
|
|
1799
|
-
* and the pressed digit is greater than the maximum value for this
|
|
1800
|
-
* second, then we will reset the segment as if the user had pressed the
|
|
1801
|
-
* backspace key and then typed the number.
|
|
1802
|
-
*/
|
|
1803
|
-
const total = Number.parseInt(prev + num.toString());
|
|
1804
|
-
if (this.root.states.second.lastKeyZero) {
|
|
1805
|
-
/**
|
|
1806
|
-
* If the new number is not 0, then we reset the lastKeyZero state and
|
|
1807
|
-
* move to the next segment, returning the new number with a leading 0.
|
|
1808
|
-
*/
|
|
1809
|
-
if (num !== 0) {
|
|
1810
|
-
moveToNext = true;
|
|
1811
|
-
this.root.states.second.lastKeyZero = false;
|
|
1812
|
-
return `0${num}`;
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* If the new number is 0, then we simply return `00` since that is
|
|
1816
|
-
* an acceptable second value.
|
|
1817
|
-
*/
|
|
1818
|
-
moveToNext = true;
|
|
1819
|
-
this.root.states.second.lastKeyZero = false;
|
|
1820
|
-
return "00";
|
|
1821
|
-
}
|
|
1822
|
-
/**
|
|
1823
|
-
* If the total is greater than the max day value possible for this month, then
|
|
1824
|
-
* we want to move to the next segment, trimming the first digit from the total,
|
|
1825
|
-
* replacing it with a 0.
|
|
1826
|
-
*/
|
|
1827
|
-
if (total > max) {
|
|
1828
|
-
moveToNext = true;
|
|
1829
|
-
return `0${num}`;
|
|
1830
|
-
}
|
|
1831
|
-
/**
|
|
1832
|
-
* If the total has two digits and is less than or equal to the max day value,
|
|
1833
|
-
* we will move to the next segment and return the total as the segment value.
|
|
1834
|
-
*/
|
|
1835
|
-
moveToNext = true;
|
|
1836
|
-
return `${total}`;
|
|
1837
|
-
});
|
|
1838
|
-
if (moveToNext) {
|
|
1839
|
-
moveToNextSegment(e, this.root.getFieldNode());
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
if (isBackspace(e.key)) {
|
|
1843
|
-
this.root.states.second.hasLeftFocus = false;
|
|
1844
|
-
let moveToPrev = false;
|
|
1845
|
-
this.root.updateSegment("second", (prev) => {
|
|
1846
|
-
if (prev === null) {
|
|
1847
|
-
moveToPrev = true;
|
|
1848
|
-
this.#announcer.announce(null);
|
|
1849
|
-
return null;
|
|
1850
|
-
}
|
|
1851
|
-
const str = prev.toString();
|
|
1852
|
-
if (str.length === 1) {
|
|
1853
|
-
this.#announcer.announce(null);
|
|
1854
|
-
return null;
|
|
1855
|
-
}
|
|
1856
|
-
const next = Number.parseInt(str.slice(0, -1));
|
|
1857
|
-
this.#announcer.announce(next);
|
|
1858
|
-
return `${next}`;
|
|
1859
|
-
});
|
|
1860
|
-
if (moveToPrev) {
|
|
1861
|
-
moveToPrevSegment(e, this.root.getFieldNode());
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
if (isSegmentNavigationKey(e.key)) {
|
|
1865
|
-
handleSegmentNavigation(e, this.root.getFieldNode());
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
onfocusout(_) {
|
|
1869
|
-
this.root.states.second.hasLeftFocus = true;
|
|
1062
|
+
}
|
|
1063
|
+
class DateFieldSecondSegmentState extends BaseNumericSegmentState {
|
|
1064
|
+
constructor(opts, root) {
|
|
1065
|
+
super(opts, root, "second", SEGMENT_CONFIGS.second);
|
|
1870
1066
|
}
|
|
1871
|
-
props = $derived.by(() => {
|
|
1872
|
-
const segmentValues = this.root.segmentValues;
|
|
1873
|
-
const placeholder = this.root.placeholder.current;
|
|
1874
|
-
if (!("second" in segmentValues) || !("second" in placeholder))
|
|
1875
|
-
return {};
|
|
1876
|
-
const isEmpty = segmentValues.second === null;
|
|
1877
|
-
const date = segmentValues.second
|
|
1878
|
-
? placeholder.set({ second: Number.parseInt(segmentValues.second) })
|
|
1879
|
-
: placeholder;
|
|
1880
|
-
const valueNow = date.second;
|
|
1881
|
-
const valueMin = 0;
|
|
1882
|
-
const valueMax = 59;
|
|
1883
|
-
const valueText = isEmpty ? "Empty" : `${valueNow}`;
|
|
1884
|
-
return {
|
|
1885
|
-
...this.root.sharedSegmentAttrs,
|
|
1886
|
-
id: this.opts.id.current,
|
|
1887
|
-
"aria-label": "second, ",
|
|
1888
|
-
"aria-valuemin": valueMin,
|
|
1889
|
-
"aria-valuemax": valueMax,
|
|
1890
|
-
"aria-valuenow": valueNow,
|
|
1891
|
-
"aria-valuetext": valueText,
|
|
1892
|
-
onkeydown: this.onkeydown,
|
|
1893
|
-
onfocusout: this.onfocusout,
|
|
1894
|
-
onclick: this.root.handleSegmentClick,
|
|
1895
|
-
...this.root.getBaseSegmentAttrs("second", this.opts.id.current),
|
|
1896
|
-
};
|
|
1897
|
-
});
|
|
1898
1067
|
}
|
|
1899
1068
|
class DateFieldDayPeriodSegmentState {
|
|
1900
1069
|
opts;
|