@arbor-education/design-system.components 0.8.1 → 0.10.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/.github/workflows/release.yml +1 -1
- package/CHANGELOG.md +22 -0
- package/dist/components/button/Button.d.ts.map +1 -1
- package/dist/components/button/Button.js +2 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/combobox/Combobox.d.ts.map +1 -1
- package/dist/components/combobox/Combobox.js +10 -8
- package/dist/components/combobox/Combobox.js.map +1 -1
- package/dist/components/combobox/Combobox.stories.d.ts +1 -0
- package/dist/components/combobox/Combobox.stories.d.ts.map +1 -1
- package/dist/components/combobox/Combobox.stories.js +16 -0
- package/dist/components/combobox/Combobox.stories.js.map +1 -1
- package/dist/components/combobox/Combobox.test.js +107 -61
- package/dist/components/combobox/Combobox.test.js.map +1 -1
- package/dist/components/combobox/ComboboxButtonTrigger.d.ts +4 -2
- package/dist/components/combobox/ComboboxButtonTrigger.d.ts.map +1 -1
- package/dist/components/combobox/ComboboxButtonTrigger.js +11 -4
- package/dist/components/combobox/ComboboxButtonTrigger.js.map +1 -1
- package/dist/components/combobox/ComboboxTrigger.d.ts +3 -1
- package/dist/components/combobox/ComboboxTrigger.d.ts.map +1 -1
- package/dist/components/combobox/ComboboxTrigger.js +10 -2
- package/dist/components/combobox/ComboboxTrigger.js.map +1 -1
- package/dist/components/combobox/types.d.ts +3 -0
- package/dist/components/combobox/types.d.ts.map +1 -1
- package/dist/components/combobox/useComboboxPopoverBehavior.d.ts +3 -1
- package/dist/components/combobox/useComboboxPopoverBehavior.d.ts.map +1 -1
- package/dist/components/combobox/useComboboxPopoverBehavior.js +7 -6
- package/dist/components/combobox/useComboboxPopoverBehavior.js.map +1 -1
- package/dist/components/combobox/useComboboxState.d.ts.map +1 -1
- package/dist/components/combobox/useComboboxState.js +4 -1
- package/dist/components/combobox/useComboboxState.js.map +1 -1
- package/dist/components/datePicker/DatePicker.d.ts +4 -1
- package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
- package/dist/components/datePicker/DatePicker.js +77 -37
- package/dist/components/datePicker/DatePicker.js.map +1 -1
- package/dist/components/datePicker/DatePicker.stories.d.ts +28 -3
- package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
- package/dist/components/datePicker/DatePicker.stories.js +62 -9
- package/dist/components/datePicker/DatePicker.stories.js.map +1 -1
- package/dist/components/datePicker/DatePicker.test.js +133 -66
- package/dist/components/datePicker/DatePicker.test.js.map +1 -1
- package/dist/components/datePicker/DatePickerCalendarHeader.d.ts +8 -0
- package/dist/components/datePicker/DatePickerCalendarHeader.d.ts.map +1 -0
- package/dist/components/datePicker/DatePickerCalendarHeader.js +36 -0
- package/dist/components/datePicker/DatePickerCalendarHeader.js.map +1 -0
- package/dist/components/datePicker/dateInputUtils.d.ts +25 -0
- package/dist/components/datePicker/dateInputUtils.d.ts.map +1 -0
- package/dist/components/datePicker/dateInputUtils.js +60 -0
- package/dist/components/datePicker/dateInputUtils.js.map +1 -0
- package/dist/components/datePicker/datePickerTestUtils.test-helpers.d.ts +2 -0
- package/dist/components/datePicker/datePickerTestUtils.test-helpers.d.ts.map +1 -0
- package/dist/components/datePicker/datePickerTestUtils.test-helpers.js +4 -0
- package/dist/components/datePicker/datePickerTestUtils.test-helpers.js.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.d.ts +22 -0
- package/dist/components/dateTimePicker/DateTimePicker.d.ts.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.js +132 -0
- package/dist/components/dateTimePicker/DateTimePicker.js.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts +77 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.js +163 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.js.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.d.ts +2 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.d.ts.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.js +235 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.js.map +1 -0
- package/dist/components/formField/FormField.d.ts +4 -0
- package/dist/components/formField/FormField.d.ts.map +1 -1
- package/dist/components/formField/FormField.js +2 -1
- package/dist/components/formField/FormField.js.map +1 -1
- package/dist/components/formField/FormField.stories.d.ts.map +1 -1
- package/dist/components/formField/FormField.stories.js +4 -1
- package/dist/components/formField/FormField.stories.js.map +1 -1
- package/dist/components/formField/FormField.test.d.ts.map +1 -1
- package/dist/components/formField/FormField.test.js +10 -5
- package/dist/components/formField/FormField.test.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts +1 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +7 -3
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js +12 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.d.ts +4 -1
- package/dist/components/formField/inputs/text/TextInput.d.ts.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.js +5 -4
- package/dist/components/formField/inputs/text/TextInput.js.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts +4 -1
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.d.ts +29 -0
- package/dist/components/formField/inputs/time/TimeInput.d.ts.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.js +67 -0
- package/dist/components/formField/inputs/time/TimeInput.js.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +60 -0
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.stories.js +132 -0
- package/dist/components/formField/inputs/time/TimeInput.stories.js.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.test.d.ts +2 -0
- package/dist/components/formField/inputs/time/TimeInput.test.d.ts.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.test.js +58 -0
- package/dist/components/formField/inputs/time/TimeInput.test.js.map +1 -0
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/Table.js +2 -0
- package/dist/components/table/Table.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts +1 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +37 -0
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/cellRenderers/BooleanCellRenderer.d.ts +3 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.js +15 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.js.map +1 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.test.d.ts +2 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.test.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.test.js +31 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.test.js.map +1 -0
- package/dist/index.css +309 -4
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/button/Button.tsx +2 -1
- package/src/components/combobox/Combobox.stories.tsx +18 -0
- package/src/components/combobox/Combobox.test.tsx +131 -61
- package/src/components/combobox/Combobox.tsx +15 -6
- package/src/components/combobox/ComboboxButtonTrigger.tsx +54 -25
- package/src/components/combobox/ComboboxTrigger.tsx +39 -15
- package/src/components/combobox/combobox.scss +18 -0
- package/src/components/combobox/types.ts +3 -0
- package/src/components/combobox/useComboboxPopoverBehavior.ts +10 -5
- package/src/components/combobox/useComboboxState.ts +4 -1
- package/src/components/datePicker/DatePicker.stories.tsx +67 -9
- package/src/components/datePicker/DatePicker.test.tsx +157 -72
- package/src/components/datePicker/DatePicker.tsx +163 -69
- package/src/components/datePicker/DatePickerCalendarHeader.tsx +82 -0
- package/src/components/datePicker/date-field-hint.scss +152 -0
- package/src/components/datePicker/dateInputUtils.ts +117 -0
- package/src/components/datePicker/datePicker.scss +53 -29
- package/src/components/datePicker/datePickerTestUtils.test-helpers.ts +6 -0
- package/src/components/dateTimePicker/DateTimePicker.stories.tsx +202 -0
- package/src/components/dateTimePicker/DateTimePicker.test.tsx +295 -0
- package/src/components/dateTimePicker/DateTimePicker.tsx +293 -0
- package/src/components/dateTimePicker/dateTimePicker.scss +17 -0
- package/src/components/formField/FormField.stories.tsx +10 -1
- package/src/components/formField/FormField.test.tsx +11 -5
- package/src/components/formField/FormField.tsx +5 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.test.tsx +28 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +8 -2
- package/src/components/formField/inputs/text/TextInput.tsx +6 -3
- package/src/components/formField/inputs/time/TimeInput.stories.tsx +170 -0
- package/src/components/formField/inputs/time/TimeInput.test.tsx +86 -0
- package/src/components/formField/inputs/time/TimeInput.tsx +168 -0
- package/src/components/formField/inputs/time/timeInput.scss +33 -0
- package/src/components/row/row.scss +2 -2
- package/src/components/table/Table.stories.tsx +48 -0
- package/src/components/table/Table.tsx +2 -0
- package/src/components/table/cellRenderers/BooleanCellRenderer.test.tsx +37 -0
- package/src/components/table/cellRenderers/BooleanCellRenderer.tsx +34 -0
- package/src/components/table/cellRenderers/booleanCellRenderer.scss +7 -0
- package/src/index.scss +3 -0
- package/src/index.ts +5 -0
|
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|
|
2
2
|
import { Icon } from 'Components/icon/Icon';
|
|
3
3
|
import { Tag } from 'Components/tag/Tag';
|
|
4
4
|
import { Popover } from 'radix-ui';
|
|
5
|
-
import type { ComboboxAriaInvalid, ComboboxOption } from './types';
|
|
5
|
+
import type { ComboboxAriaInvalid, ComboboxOption, ComboboxSelectedValueDisplay } from './types';
|
|
6
6
|
|
|
7
7
|
export type ComboboxTriggerProps = {
|
|
8
8
|
'inputRef': React.RefObject<HTMLInputElement | null>;
|
|
@@ -21,6 +21,8 @@ export type ComboboxTriggerProps = {
|
|
|
21
21
|
'aria-invalid'?: ComboboxAriaInvalid;
|
|
22
22
|
'aria-label'?: string;
|
|
23
23
|
'showDropdownTrigger': boolean;
|
|
24
|
+
'selectedValueDisplay': ComboboxSelectedValueDisplay;
|
|
25
|
+
'triggerEndContent'?: React.ReactNode;
|
|
24
26
|
'selectedChips': ComboboxOption[];
|
|
25
27
|
'selectedChipValuesSet': Set<string>;
|
|
26
28
|
'focusedChipIndex': number | null;
|
|
@@ -52,6 +54,8 @@ export const ComboboxTrigger = (props: ComboboxTriggerProps): React.JSX.Element
|
|
|
52
54
|
'aria-invalid': ariaInvalid,
|
|
53
55
|
'aria-label': ariaLabel,
|
|
54
56
|
showDropdownTrigger,
|
|
57
|
+
selectedValueDisplay,
|
|
58
|
+
triggerEndContent,
|
|
55
59
|
selectedChips,
|
|
56
60
|
selectedChipValuesSet,
|
|
57
61
|
focusedChipIndex,
|
|
@@ -65,6 +69,12 @@ export const ComboboxTrigger = (props: ComboboxTriggerProps): React.JSX.Element
|
|
|
65
69
|
handleChevronClick,
|
|
66
70
|
} = props;
|
|
67
71
|
|
|
72
|
+
const selectedValueText = selectedChips.map(resolveTagLabel).join(', ');
|
|
73
|
+
const showSelectedValueText
|
|
74
|
+
= selectedValueDisplay === 'text'
|
|
75
|
+
&& query.length === 0
|
|
76
|
+
&& selectedValueText.length > 0;
|
|
77
|
+
|
|
68
78
|
return (
|
|
69
79
|
<Popover.Anchor asChild>
|
|
70
80
|
<div
|
|
@@ -77,23 +87,32 @@ export const ComboboxTrigger = (props: ComboboxTriggerProps): React.JSX.Element
|
|
|
77
87
|
onClick={handleTriggerClick}
|
|
78
88
|
>
|
|
79
89
|
<div className="ds-combobox__chips-and-input">
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
{selectedValueDisplay === 'tags'
|
|
91
|
+
? selectedChips.map((opt, chipIdx) => (
|
|
92
|
+
<Tag
|
|
93
|
+
key={opt.value}
|
|
94
|
+
color="neutral"
|
|
95
|
+
selected={selectedChipValuesSet.has(opt.value) || focusedChipIndex === chipIdx}
|
|
96
|
+
slotStart={opt.iconName ? <Icon name={opt.iconName} size={12} /> : undefined}
|
|
97
|
+
onRemove={disabled ? undefined : () => removeValue(opt.value)}
|
|
98
|
+
removeLabel={`Remove ${resolveTagLabel(opt)}`}
|
|
99
|
+
removeButtonTabIndex={-1}
|
|
100
|
+
>
|
|
101
|
+
{resolveTagLabel(opt)}
|
|
102
|
+
</Tag>
|
|
103
|
+
))
|
|
104
|
+
: null}
|
|
105
|
+
{showSelectedValueText && (
|
|
106
|
+
<span className="ds-combobox__selected-value">
|
|
107
|
+
{selectedValueText}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
93
110
|
<input
|
|
94
111
|
ref={inputRef}
|
|
95
112
|
id={comboboxId}
|
|
96
|
-
className=
|
|
113
|
+
className={classNames('ds-combobox__input', {
|
|
114
|
+
'ds-combobox__input--with-selected-value': showSelectedValueText,
|
|
115
|
+
})}
|
|
97
116
|
type="text"
|
|
98
117
|
role="combobox"
|
|
99
118
|
aria-autocomplete="list"
|
|
@@ -114,6 +133,11 @@ export const ComboboxTrigger = (props: ComboboxTriggerProps): React.JSX.Element
|
|
|
114
133
|
autoComplete="off"
|
|
115
134
|
/>
|
|
116
135
|
</div>
|
|
136
|
+
{triggerEndContent && (
|
|
137
|
+
<span className="ds-combobox__end-content" aria-hidden="true">
|
|
138
|
+
{triggerEndContent}
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
117
141
|
{showDropdownTrigger && (
|
|
118
142
|
<button
|
|
119
143
|
type="button"
|
|
@@ -88,6 +88,13 @@
|
|
|
88
88
|
white-space: nowrap;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
.ds-combobox__button-value {
|
|
92
|
+
min-width: 0;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
text-overflow: ellipsis;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
.ds-combobox__button-ellipsis {
|
|
92
99
|
flex-shrink: 0;
|
|
93
100
|
font-size: var(--font-size-medium);
|
|
@@ -127,6 +134,10 @@
|
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
.ds-combobox__input--with-selected-value {
|
|
138
|
+
min-width: 1ch;
|
|
139
|
+
}
|
|
140
|
+
|
|
130
141
|
// Chevron
|
|
131
142
|
.ds-combobox__chevron {
|
|
132
143
|
flex-shrink: 0;
|
|
@@ -144,6 +155,13 @@
|
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
|
|
158
|
+
.ds-combobox__end-content {
|
|
159
|
+
flex-shrink: 0;
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
}
|
|
164
|
+
|
|
147
165
|
// Popover
|
|
148
166
|
.ds-combobox__popover {
|
|
149
167
|
min-width: var(--radix-popper-anchor-width);
|
|
@@ -16,6 +16,7 @@ export type ComboboxOption = {
|
|
|
16
16
|
export type ComboboxCreateResult = string | ComboboxOption | void;
|
|
17
17
|
export type ComboboxSearchFn = (option: ComboboxOption, query: string) => boolean;
|
|
18
18
|
export type ComboboxSearchType = 'prefix' | 'substring' | ComboboxSearchFn;
|
|
19
|
+
export type ComboboxSelectedValueDisplay = 'tags' | 'text';
|
|
19
20
|
|
|
20
21
|
export type ComboboxProps = {
|
|
21
22
|
'options': ComboboxOption[];
|
|
@@ -36,7 +37,9 @@ export type ComboboxProps = {
|
|
|
36
37
|
'disabled'?: boolean;
|
|
37
38
|
'dropdownOnFocus'?: boolean;
|
|
38
39
|
'triggerVariant'?: 'input' | 'button';
|
|
40
|
+
'selectedValueDisplay'?: ComboboxSelectedValueDisplay;
|
|
39
41
|
'showDropdownTrigger'?: boolean;
|
|
42
|
+
'triggerEndContent'?: React.ReactNode;
|
|
40
43
|
'showSelectionCountBadge'?: boolean;
|
|
41
44
|
'selectionCountA11yLabel'?: string | ((count: number) => string);
|
|
42
45
|
'loading'?: boolean;
|
|
@@ -26,6 +26,8 @@ type UseComboboxPopoverBehaviorParams = {
|
|
|
26
26
|
clearChipSelection: () => void;
|
|
27
27
|
exitChipNav: () => void;
|
|
28
28
|
triggerContainsInput?: boolean;
|
|
29
|
+
/** Search field is inside the popover (button trigger); keep shell mounted while open even with zero matches. */
|
|
30
|
+
renderSearchInputInListbox: boolean;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export type UseComboboxPopoverBehaviorReturn = {
|
|
@@ -61,10 +63,13 @@ export const useComboboxPopoverBehavior = ({
|
|
|
61
63
|
clearChipSelection,
|
|
62
64
|
exitChipNav,
|
|
63
65
|
triggerContainsInput = true,
|
|
66
|
+
renderSearchInputInListbox,
|
|
64
67
|
}: UseComboboxPopoverBehaviorParams): UseComboboxPopoverBehaviorReturn => {
|
|
65
|
-
const shouldRenderPopoverContent
|
|
68
|
+
const shouldRenderPopoverContent
|
|
69
|
+
= loading || totalItems > 0 || (renderSearchInputInListbox && isOpen);
|
|
66
70
|
const shouldShowPopover = isOpen && shouldRenderPopoverContent;
|
|
67
71
|
const showListboxLoading = Boolean(loading && shouldShowPopover);
|
|
72
|
+
const canOpenPopover = loading || totalItems > 0 || renderSearchInputInListbox;
|
|
68
73
|
|
|
69
74
|
const handleInputChange = useCallback(
|
|
70
75
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -110,10 +115,10 @@ export const useComboboxPopoverBehavior = ({
|
|
|
110
115
|
|
|
111
116
|
const requestOpen = useCallback(() => {
|
|
112
117
|
if (disabled) return;
|
|
113
|
-
if (
|
|
118
|
+
if (canOpenPopover) {
|
|
114
119
|
openPopover();
|
|
115
120
|
}
|
|
116
|
-
}, [disabled, openPopover,
|
|
121
|
+
}, [disabled, openPopover, canOpenPopover]);
|
|
117
122
|
|
|
118
123
|
const handleTriggerClick = useCallback(() => {
|
|
119
124
|
if (disabled) return;
|
|
@@ -148,11 +153,11 @@ export const useComboboxPopoverBehavior = ({
|
|
|
148
153
|
if (triggerContainsInput) {
|
|
149
154
|
inputRef.current?.focus();
|
|
150
155
|
}
|
|
151
|
-
if (
|
|
156
|
+
if (canOpenPopover) {
|
|
152
157
|
openPopover();
|
|
153
158
|
}
|
|
154
159
|
},
|
|
155
|
-
[disabled, inputRef, openPopover,
|
|
160
|
+
[disabled, inputRef, openPopover, canOpenPopover, triggerContainsInput],
|
|
156
161
|
);
|
|
157
162
|
|
|
158
163
|
const handlePopoverInteractOutside = useCallback(
|
|
@@ -129,8 +129,11 @@ export const useComboboxState = (params: UseComboboxStateParams): UseComboboxSta
|
|
|
129
129
|
: [optionValue];
|
|
130
130
|
updateValue(next);
|
|
131
131
|
setQuery('');
|
|
132
|
+
if (!multiple) {
|
|
133
|
+
setIsOpen(false);
|
|
134
|
+
}
|
|
132
135
|
},
|
|
133
|
-
[multiple, selectedValues, selectedValuesSet, updateValue],
|
|
136
|
+
[multiple, selectedValues, selectedValuesSet, setIsOpen, updateValue],
|
|
134
137
|
);
|
|
135
138
|
|
|
136
139
|
const removeValue = useCallback(
|
|
@@ -5,17 +5,35 @@ import { DatePicker } from './DatePicker';
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: 'Components/DatePicker',
|
|
7
7
|
component: DatePicker,
|
|
8
|
+
decorators: [
|
|
9
|
+
Story => (
|
|
10
|
+
<div style={{ maxWidth: '220px', width: '100%' }}>
|
|
11
|
+
<Story />
|
|
12
|
+
</div>
|
|
13
|
+
),
|
|
14
|
+
],
|
|
8
15
|
parameters: {
|
|
9
16
|
layout: 'centered',
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'`DatePicker` uses a native `type="date"` input (`yyyy-MM-dd`) with a synced calendar popover. When empty, hint copy uses a decorative span (browsers do not reliably show `placeholder` on native date fields). `displayFormat` controls that hint (`DD/MM/YYYY` vs `Pick a date`). The custom month/year header uses the design-system select control.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
10
23
|
},
|
|
11
24
|
tags: ['autodocs'],
|
|
12
25
|
args: {
|
|
13
26
|
onChange: fn(),
|
|
14
27
|
},
|
|
15
28
|
argTypes: {
|
|
16
|
-
|
|
29
|
+
displayFormat: {
|
|
30
|
+
control: 'inline-radio',
|
|
31
|
+
options: ['default', 'friendly'],
|
|
32
|
+
description: 'Controls the empty-state hint text (`DD/MM/YYYY` vs `Pick a date`). The field value is always native ISO `yyyy-MM-dd`.',
|
|
33
|
+
},
|
|
34
|
+
placeholder: {
|
|
17
35
|
control: 'text',
|
|
18
|
-
description: '
|
|
36
|
+
description: 'Optional override for the empty-state hint (defaults from `displayFormat`).',
|
|
19
37
|
},
|
|
20
38
|
className: {
|
|
21
39
|
control: 'text',
|
|
@@ -30,18 +48,58 @@ const meta = {
|
|
|
30
48
|
export default meta;
|
|
31
49
|
type Story = StoryObj<typeof meta>;
|
|
32
50
|
|
|
33
|
-
export const Default: Story = {
|
|
51
|
+
export const Default: Story = {
|
|
52
|
+
parameters: {
|
|
53
|
+
docs: {
|
|
54
|
+
description: {
|
|
55
|
+
story: 'Empty field shows hint **DD/MM/YYYY** (`displayFormat="default"`).',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
34
60
|
|
|
35
|
-
export const
|
|
36
|
-
name: '
|
|
61
|
+
export const FriendlyFormat: Story = {
|
|
62
|
+
name: 'Friendly Format',
|
|
37
63
|
args: {
|
|
38
|
-
|
|
64
|
+
displayFormat: 'friendly',
|
|
65
|
+
},
|
|
66
|
+
parameters: {
|
|
67
|
+
docs: {
|
|
68
|
+
description: {
|
|
69
|
+
story: 'Empty field shows hint **Pick a date**.',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
39
72
|
},
|
|
40
73
|
};
|
|
41
74
|
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
/** `placeholder` overrides the default empty-state hint. */
|
|
76
|
+
export const PlaceholderCustomOverride: Story = {
|
|
77
|
+
name: 'Placeholder · custom copy',
|
|
44
78
|
args: {
|
|
45
|
-
|
|
79
|
+
displayFormat: 'default',
|
|
80
|
+
placeholder: 'e.g. 25/12/2024',
|
|
81
|
+
},
|
|
82
|
+
parameters: {
|
|
83
|
+
docs: {
|
|
84
|
+
description: {
|
|
85
|
+
story: 'The optional `placeholder` prop replaces the default hint.',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Friendly format with custom hint copy. */
|
|
92
|
+
export const PlaceholderFriendlyWithCustomCopy: Story = {
|
|
93
|
+
name: 'Placeholder · friendly format, custom hint',
|
|
94
|
+
args: {
|
|
95
|
+
displayFormat: 'friendly',
|
|
96
|
+
placeholder: 'Choose a date…',
|
|
97
|
+
},
|
|
98
|
+
parameters: {
|
|
99
|
+
docs: {
|
|
100
|
+
description: {
|
|
101
|
+
story: '`displayFormat="friendly"` with a custom hint instead of **Pick a date**.',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
46
104
|
},
|
|
47
105
|
};
|
|
@@ -2,18 +2,22 @@ import { describe, expect, test, vi } from 'vitest';
|
|
|
2
2
|
import { render, screen, within, fireEvent } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import '@testing-library/jest-dom/vitest';
|
|
5
|
-
import { format } from 'date-fns';
|
|
6
5
|
import { DatePicker } from './DatePicker';
|
|
6
|
+
import { formatNativeDateInputValue } from './dateInputUtils';
|
|
7
|
+
import { getDropdownTrigger } from './datePickerTestUtils.test-helpers';
|
|
8
|
+
|
|
9
|
+
const getDateField = (container: HTMLElement) => (
|
|
10
|
+
container.querySelector('input[type="date"]') as HTMLInputElement
|
|
11
|
+
);
|
|
7
12
|
|
|
8
13
|
describe('DatePicker', () => {
|
|
9
|
-
test('renders a
|
|
10
|
-
render(<DatePicker />);
|
|
11
|
-
expect(
|
|
14
|
+
test('renders a date input', () => {
|
|
15
|
+
const { container } = render(<DatePicker />);
|
|
16
|
+
expect(getDateField(container)).toBeInTheDocument();
|
|
12
17
|
});
|
|
13
18
|
|
|
14
19
|
test('renders a calendar toggle button', () => {
|
|
15
20
|
render(<DatePicker />);
|
|
16
|
-
// Button has an icon with screenReaderText="date" (from iconLeftName fallback in Button/Icon)
|
|
17
21
|
expect(screen.getByRole('button', { name: 'Open date picker' })).toBeInTheDocument();
|
|
18
22
|
});
|
|
19
23
|
|
|
@@ -22,6 +26,49 @@ describe('DatePicker', () => {
|
|
|
22
26
|
expect(screen.queryByRole('application')).not.toBeInTheDocument();
|
|
23
27
|
});
|
|
24
28
|
|
|
29
|
+
test('shows empty-state hint copy when empty (native placeholder is unreliable)', () => {
|
|
30
|
+
const { container } = render(<DatePicker />);
|
|
31
|
+
expect(getDateField(container)).toHaveAttribute('placeholder', '');
|
|
32
|
+
expect(screen.getByText('DD/MM/YYYY')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('placeholder prop overrides the empty-state hint', () => {
|
|
36
|
+
const { container } = render(<DatePicker placeholder="When?" />);
|
|
37
|
+
expect(getDateField(container)).toHaveAttribute('placeholder', '');
|
|
38
|
+
expect(screen.getByText('When?')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('friendly displayFormat changes the empty-state hint', () => {
|
|
42
|
+
const { container } = render(<DatePicker displayFormat="friendly" />);
|
|
43
|
+
expect(getDateField(container)).toHaveAttribute('placeholder', '');
|
|
44
|
+
expect(screen.getByText('Pick a date')).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('custom placeholder empty state', () => {
|
|
48
|
+
test('uses read-only date input and opens the calendar on pointer down', () => {
|
|
49
|
+
const { container } = render(<DatePicker placeholder="Pick one" />);
|
|
50
|
+
const input = getDateField(container);
|
|
51
|
+
expect(input.readOnly).toBe(true);
|
|
52
|
+
fireEvent.pointerDown(input);
|
|
53
|
+
expect(screen.getByRole('application')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('does not open the calendar on keyboard focus alone', () => {
|
|
57
|
+
const { container } = render(<DatePicker placeholder="Pick one" />);
|
|
58
|
+
const input = getDateField(container);
|
|
59
|
+
fireEvent.focus(input);
|
|
60
|
+
expect(screen.queryByRole('application')).not.toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('opens the calendar when the field is activated with a click', async () => {
|
|
64
|
+
const user = userEvent.setup();
|
|
65
|
+
const { container } = render(<DatePicker placeholder="Pick one" />);
|
|
66
|
+
const input = getDateField(container);
|
|
67
|
+
await user.click(input);
|
|
68
|
+
expect(screen.getByRole('application')).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
25
72
|
describe('calendar toggle', () => {
|
|
26
73
|
test('clicking the button opens the day picker', async () => {
|
|
27
74
|
render(<DatePicker />);
|
|
@@ -37,61 +84,57 @@ describe('DatePicker', () => {
|
|
|
37
84
|
});
|
|
38
85
|
});
|
|
39
86
|
|
|
40
|
-
describe('
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fireEvent.change(screen.getByRole('textbox'), { target: { value: '15/06/2024' } });
|
|
47
|
-
expect(screen.getByRole('textbox')).toHaveValue('15/06/2024');
|
|
87
|
+
describe('date input', () => {
|
|
88
|
+
test('entering a valid ISO date updates the input value', () => {
|
|
89
|
+
const { container } = render(<DatePicker />);
|
|
90
|
+
const input = getDateField(container);
|
|
91
|
+
fireEvent.change(input, { target: { value: '2024-06-15' } });
|
|
92
|
+
expect(input).toHaveValue('2024-06-15');
|
|
48
93
|
});
|
|
49
94
|
|
|
50
|
-
test('entering a valid date calls onChange with the parsed Date', () => {
|
|
95
|
+
test('entering a valid ISO date calls onChange with the parsed Date', () => {
|
|
51
96
|
const onChange = vi.fn();
|
|
52
|
-
render(<DatePicker onChange={onChange} />);
|
|
53
|
-
|
|
97
|
+
const { container } = render(<DatePicker onChange={onChange} />);
|
|
98
|
+
const input = getDateField(container);
|
|
54
99
|
|
|
55
|
-
fireEvent.change(
|
|
100
|
+
fireEvent.change(input, { target: { value: '2024-06-15' } });
|
|
56
101
|
|
|
102
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
57
103
|
expect(onChange).toHaveBeenLastCalledWith(expect.any(Date));
|
|
58
104
|
const lastDate: Date = onChange.mock.lastCall![0];
|
|
59
105
|
expect(lastDate.getFullYear()).toBe(2024);
|
|
60
|
-
expect(lastDate.getMonth()).toBe(5);
|
|
106
|
+
expect(lastDate.getMonth()).toBe(5);
|
|
61
107
|
expect(lastDate.getDate()).toBe(15);
|
|
108
|
+
expect(lastDate.getHours()).toBe(0);
|
|
109
|
+
expect(lastDate.getMinutes()).toBe(0);
|
|
62
110
|
});
|
|
63
111
|
|
|
64
|
-
test('entering an invalid string
|
|
65
|
-
// onChange is only fired by the useEffect when selectedDate actually changes.
|
|
66
|
-
// So we first need a valid date to put selectedDate into a non-undefined state.
|
|
112
|
+
test('entering an invalid string calls onChange with undefined', () => {
|
|
67
113
|
const onChange = vi.fn();
|
|
68
|
-
render(<DatePicker onChange={onChange} />);
|
|
69
|
-
|
|
70
|
-
onChange.mockClear();
|
|
114
|
+
const { container } = render(<DatePicker onChange={onChange} />);
|
|
115
|
+
const input = getDateField(container);
|
|
71
116
|
|
|
72
|
-
fireEvent.change(
|
|
117
|
+
fireEvent.change(input, { target: { value: '2024-06-15' } });
|
|
118
|
+
onChange.mockClear();
|
|
119
|
+
fireEvent.change(input, { target: { value: 'not-a-date' } });
|
|
73
120
|
|
|
121
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
74
122
|
expect(onChange).toHaveBeenLastCalledWith(undefined);
|
|
75
123
|
});
|
|
76
124
|
});
|
|
77
125
|
|
|
78
126
|
describe('day picker selection', () => {
|
|
79
|
-
// Avoid fake timers (incompatible with userEvent) by navigating the picker to a
|
|
80
|
-
// known month via the text input before opening the calendar.
|
|
81
|
-
|
|
82
127
|
test('selecting a date from the picker updates the input', async () => {
|
|
83
|
-
render(<DatePicker />);
|
|
84
|
-
|
|
85
|
-
fireEvent.change(screen.getByRole('textbox'), { target: { value: '01/06/2024' } });
|
|
128
|
+
const { container } = render(<DatePicker />);
|
|
129
|
+
fireEvent.change(getDateField(container), { target: { value: '2024-06-01' } });
|
|
86
130
|
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
87
|
-
// Day aria-labels use date-fns PPPP format e.g. "Thursday, June 20, 2024"
|
|
88
131
|
await userEvent.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
89
|
-
expect(
|
|
132
|
+
expect(getDateField(container)).toHaveValue('2024-06-20');
|
|
90
133
|
});
|
|
91
134
|
|
|
92
135
|
test('selecting a date from the picker closes the picker', async () => {
|
|
93
|
-
render(<DatePicker />);
|
|
94
|
-
fireEvent.change(
|
|
136
|
+
const { container } = render(<DatePicker />);
|
|
137
|
+
fireEvent.change(getDateField(container), { target: { value: '2024-06-01' } });
|
|
95
138
|
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
96
139
|
await userEvent.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
97
140
|
expect(screen.queryByRole('application')).not.toBeInTheDocument();
|
|
@@ -99,8 +142,8 @@ describe('DatePicker', () => {
|
|
|
99
142
|
|
|
100
143
|
test('selecting a date from the picker calls onChange with the selected date', async () => {
|
|
101
144
|
const onChange = vi.fn();
|
|
102
|
-
render(<DatePicker onChange={onChange} />);
|
|
103
|
-
fireEvent.change(
|
|
145
|
+
const { container } = render(<DatePicker onChange={onChange} />);
|
|
146
|
+
fireEvent.change(getDateField(container), { target: { value: '2024-06-01' } });
|
|
104
147
|
onChange.mockClear();
|
|
105
148
|
|
|
106
149
|
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
@@ -109,35 +152,100 @@ describe('DatePicker', () => {
|
|
|
109
152
|
expect(onChange).toHaveBeenCalledWith(expect.any(Date));
|
|
110
153
|
const lastDate: Date = onChange.mock.lastCall![0];
|
|
111
154
|
expect(lastDate.getFullYear()).toBe(2024);
|
|
112
|
-
expect(lastDate.getMonth()).toBe(5);
|
|
155
|
+
expect(lastDate.getMonth()).toBe(5);
|
|
113
156
|
expect(lastDate.getDate()).toBe(20);
|
|
157
|
+
expect(lastDate.getHours()).toBe(0);
|
|
158
|
+
expect(lastDate.getMinutes()).toBe(0);
|
|
114
159
|
});
|
|
115
160
|
|
|
116
|
-
test('clicking the Today button
|
|
117
|
-
render(<DatePicker />);
|
|
161
|
+
test('clicking the Today button sets today and closes the picker', async () => {
|
|
162
|
+
const { container } = render(<DatePicker />);
|
|
118
163
|
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
119
164
|
await userEvent.click(screen.getByRole('button', { name: 'Today' }));
|
|
120
|
-
expect(
|
|
165
|
+
expect(getDateField(container)).toHaveValue(formatNativeDateInputValue(new Date()));
|
|
166
|
+
expect(screen.queryByRole('application')).not.toBeInTheDocument();
|
|
121
167
|
});
|
|
122
168
|
|
|
123
|
-
test('
|
|
124
|
-
render(<DatePicker />);
|
|
169
|
+
test('opening the picker after typing a valid value syncs the month and year controls', async () => {
|
|
170
|
+
const { container } = render(<DatePicker />);
|
|
171
|
+
fireEvent.change(getDateField(container), { target: { value: '2024-06-01' } });
|
|
172
|
+
|
|
125
173
|
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
126
|
-
|
|
127
|
-
expect(
|
|
174
|
+
|
|
175
|
+
expect(getDropdownTrigger(/June/i)).toBeInTheDocument();
|
|
176
|
+
expect(getDropdownTrigger(/2024/i)).toBeInTheDocument();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('changing the custom month select updates the displayed calendar month', async () => {
|
|
180
|
+
const user = userEvent.setup();
|
|
181
|
+
const { container } = render(<DatePicker value={new Date(2024, 5, 15)} />);
|
|
182
|
+
|
|
183
|
+
await user.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
184
|
+
await user.click(getDropdownTrigger(/June/i)!);
|
|
185
|
+
await user.click(screen.getByText('July'));
|
|
186
|
+
|
|
187
|
+
expect(getDropdownTrigger(/July/i)).toBeInTheDocument();
|
|
188
|
+
expect(within(screen.getByRole('application')).getByRole('button', { name: /July 15/ })).toBeInTheDocument();
|
|
189
|
+
expect(getDateField(container)).toHaveValue(formatNativeDateInputValue(new Date(2024, 5, 15)));
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('defaultValue prop', () => {
|
|
194
|
+
test('renders with the defaultValue when no value is provided', () => {
|
|
195
|
+
const { container } = render(<DatePicker defaultValue={new Date(2024, 5, 15)} />);
|
|
196
|
+
expect(getDateField(container)).toHaveValue(formatNativeDateInputValue(new Date(2024, 5, 15)));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('value prop takes precedence over defaultValue', () => {
|
|
200
|
+
const { container } = render(
|
|
201
|
+
<DatePicker value={new Date(2024, 11, 25)} defaultValue={new Date(2024, 5, 15)} />,
|
|
202
|
+
);
|
|
203
|
+
expect(getDateField(container)).toHaveValue(formatNativeDateInputValue(new Date(2024, 11, 25)));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('does not call onChange on mount with defaultValue', () => {
|
|
207
|
+
const onChange = vi.fn();
|
|
208
|
+
render(<DatePicker defaultValue={new Date(2024, 5, 15)} onChange={onChange} />);
|
|
209
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('onChange call-count correctness', () => {
|
|
214
|
+
test('does not call onChange on mount', () => {
|
|
215
|
+
const onChange = vi.fn();
|
|
216
|
+
render(<DatePicker onChange={onChange} />);
|
|
217
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('does not call onChange on mount with an initial value', () => {
|
|
221
|
+
const onChange = vi.fn();
|
|
222
|
+
render(<DatePicker value={new Date(2024, 5, 15)} onChange={onChange} />);
|
|
223
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('calls onChange exactly once per user interaction', async () => {
|
|
227
|
+
const onChange = vi.fn();
|
|
228
|
+
const { container } = render(<DatePicker onChange={onChange} />);
|
|
229
|
+
|
|
230
|
+
fireEvent.change(getDateField(container), { target: { value: '2024-06-15' } });
|
|
231
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
232
|
+
|
|
233
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
234
|
+
await userEvent.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
235
|
+
expect(onChange).toHaveBeenCalledTimes(2);
|
|
128
236
|
});
|
|
129
237
|
});
|
|
130
238
|
|
|
131
239
|
describe('controlled value prop', () => {
|
|
132
240
|
test('renders with the initial value', () => {
|
|
133
|
-
render(<DatePicker value={new Date(2024, 5, 15)} />);
|
|
134
|
-
expect(
|
|
241
|
+
const { container } = render(<DatePicker value={new Date(2024, 5, 15)} />);
|
|
242
|
+
expect(getDateField(container)).toHaveValue(formatNativeDateInputValue(new Date(2024, 5, 15)));
|
|
135
243
|
});
|
|
136
244
|
|
|
137
245
|
test('updates the input when value prop changes', () => {
|
|
138
|
-
const { rerender } = render(<DatePicker value={new Date(2024, 5, 15)} />);
|
|
246
|
+
const { container, rerender } = render(<DatePicker value={new Date(2024, 5, 15)} />);
|
|
139
247
|
rerender(<DatePicker value={new Date(2024, 11, 25)} />);
|
|
140
|
-
expect(
|
|
248
|
+
expect(getDateField(container)).toHaveValue(formatNativeDateInputValue(new Date(2024, 11, 25)));
|
|
141
249
|
});
|
|
142
250
|
|
|
143
251
|
test('does not call onChange when value prop changes externally', () => {
|
|
@@ -155,7 +263,6 @@ describe('DatePicker', () => {
|
|
|
155
263
|
render(<DatePicker value={new Date(2024, 5, 15)} onChange={onChange} />);
|
|
156
264
|
onChange.mockClear();
|
|
157
265
|
|
|
158
|
-
// Navigate to June 2024 (already set via value) and pick a different day
|
|
159
266
|
await userEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
160
267
|
await userEvent.click(within(screen.getByRole('application')).getByRole('button', { name: /June 20/ }));
|
|
161
268
|
|
|
@@ -164,26 +271,4 @@ describe('DatePicker', () => {
|
|
|
164
271
|
expect(lastDate.getDate()).toBe(20);
|
|
165
272
|
});
|
|
166
273
|
});
|
|
167
|
-
|
|
168
|
-
describe('custom dateFormat prop', () => {
|
|
169
|
-
test('formats the value according to the custom format', () => {
|
|
170
|
-
render(<DatePicker dateFormat="yyyy-MM-dd" />);
|
|
171
|
-
fireEvent.change(screen.getByRole('textbox'), { target: { value: '2024-06-15' } });
|
|
172
|
-
expect(screen.getByRole('textbox')).toHaveValue('2024-06-15');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test('calls onChange with the correctly parsed date for a custom format', () => {
|
|
176
|
-
const onChange = vi.fn();
|
|
177
|
-
render(<DatePicker dateFormat="MM/dd/yyyy" onChange={onChange} />);
|
|
178
|
-
onChange.mockClear();
|
|
179
|
-
|
|
180
|
-
fireEvent.change(screen.getByRole('textbox'), { target: { value: '06/15/2024' } });
|
|
181
|
-
|
|
182
|
-
expect(onChange).toHaveBeenLastCalledWith(expect.any(Date));
|
|
183
|
-
const lastDate: Date = onChange.mock.lastCall![0];
|
|
184
|
-
expect(lastDate.getFullYear()).toBe(2024);
|
|
185
|
-
expect(lastDate.getMonth()).toBe(5); // June
|
|
186
|
-
expect(lastDate.getDate()).toBe(15);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
274
|
});
|