@transferwise/components 0.0.0-experimental-15273df → 0.0.0-experimental-d6d5b6e
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/build/Calendar/Calendar.js +94 -16
- package/build/Calendar/Calendar.js.map +1 -1
- package/build/Calendar/Calendar.messages.js +12 -0
- package/build/Calendar/Calendar.messages.js.map +1 -1
- package/build/Calendar/Calendar.messages.mjs +12 -0
- package/build/Calendar/Calendar.messages.mjs.map +1 -1
- package/build/Calendar/Calendar.mjs +95 -17
- package/build/Calendar/Calendar.mjs.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.js +5 -1
- package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.mjs +5 -1
- package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.js +14 -0
- package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.mjs +14 -0
- package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +53 -19
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +53 -19
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/MonthCalendar.js +6 -0
- package/build/Calendar/MonthCalendar/MonthCalendar.js.map +1 -1
- package/build/Calendar/MonthCalendar/MonthCalendar.mjs +6 -0
- package/build/Calendar/MonthCalendar/MonthCalendar.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +8 -3
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +8 -3
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
- package/build/Calendar/{tableLink → TableLink}/TableLink.js +26 -11
- package/build/Calendar/TableLink/TableLink.js.map +1 -0
- package/build/Calendar/{tableLink → TableLink}/TableLink.mjs +26 -11
- package/build/Calendar/TableLink/TableLink.mjs.map +1 -0
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +8 -3
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +8 -3
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
- package/build/Calendar/YearCalendar/YearCalendar.js +7 -0
- package/build/Calendar/YearCalendar/YearCalendar.js.map +1 -1
- package/build/Calendar/YearCalendar/YearCalendar.mjs +7 -0
- package/build/Calendar/YearCalendar/YearCalendar.mjs.map +1 -1
- package/build/DateLookup/DateLookup.js +19 -9
- package/build/DateLookup/DateLookup.js.map +1 -1
- package/build/DateLookup/DateLookup.mjs +19 -9
- package/build/DateLookup/DateLookup.mjs.map +1 -1
- package/build/Sticky/Sticky.js +0 -1
- package/build/Sticky/Sticky.js.map +1 -1
- package/build/Sticky/Sticky.mjs +0 -1
- package/build/Sticky/Sticky.mjs.map +1 -1
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +33 -0
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +1 -0
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +31 -0
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +1 -0
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.js +40 -0
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.js.map +1 -0
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs +38 -0
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs.map +1 -0
- package/build/i18n/en.json +4 -0
- package/build/i18n/en.json.js +4 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +4 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +2 -2
- package/build/index.mjs +1 -1
- package/build/main.css +123 -48
- package/build/styles/Calendar/Calendar.css +112 -37
- package/build/styles/Calendar/TableLink/TableLink.css +115 -0
- package/build/styles/main.css +123 -48
- package/build/types/Calendar/Calendar.d.ts +16 -3
- package/build/types/Calendar/Calendar.d.ts.map +1 -1
- package/build/types/Calendar/Calendar.messages.d.ts +20 -0
- package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
- package/build/types/Calendar/Calendar.types.d.ts +32 -0
- package/build/types/Calendar/Calendar.types.d.ts.map +1 -0
- package/build/types/Calendar/DateHeader/DateHeader.d.ts +2 -0
- package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +5 -1
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +4 -0
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +1 -0
- package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
- package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +2 -1
- package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/{tableLink → TableLink}/TableLink.d.ts +3 -1
- package/build/types/Calendar/TableLink/TableLink.d.ts.map +1 -0
- package/build/types/Calendar/TableLink/index.d.ts.map +1 -0
- package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +2 -1
- package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +1 -0
- package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
- package/build/types/Calendar/index.d.ts +1 -0
- package/build/types/Calendar/index.d.ts.map +1 -1
- package/build/types/DateLookup/DateLookup.d.ts +23 -4
- package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/DateLookup/index.d.ts +1 -0
- package/build/types/DateLookup/index.d.ts.map +1 -1
- package/build/types/Sticky/Sticky.d.ts.map +1 -1
- package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +3 -0
- package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +1 -0
- package/build/types/common/DateUtils/index.d.ts +2 -0
- package/build/types/common/DateUtils/index.d.ts.map +1 -1
- package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts +3 -0
- package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts.map +1 -0
- package/build/types/index.d.ts +3 -2
- package/build/types/index.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +8 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Calendar/Calendar.css +112 -37
- package/src/Calendar/Calendar.disabledDates.test.tsx +518 -0
- package/src/Calendar/Calendar.less +5 -35
- package/src/Calendar/Calendar.messages.ts +21 -0
- package/src/Calendar/Calendar.test.tsx +217 -28
- package/src/Calendar/Calendar.tsx +136 -19
- package/src/Calendar/Calendar.types.ts +35 -0
- package/src/Calendar/DateHeader/DateHeader.tsx +6 -0
- package/src/Calendar/DayCalendar/DayCalendar.tsx +33 -1
- package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +62 -28
- package/src/Calendar/MonthCalendar/MonthCalendar.tsx +8 -0
- package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +15 -3
- package/src/Calendar/TableLink/TableLink.css +115 -0
- package/src/Calendar/TableLink/TableLink.less +68 -0
- package/src/Calendar/{tableLink → TableLink}/TableLink.tsx +29 -14
- package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +15 -3
- package/src/Calendar/YearCalendar/YearCalendar.tsx +9 -0
- package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +118 -0
- package/src/Calendar/_stories/Calendar.story.tsx +138 -4
- package/src/Calendar/index.ts +7 -0
- package/src/DateLookup/DateLookup.disabledDates.test.tsx +293 -0
- package/src/DateLookup/DateLookup.test.tsx +34 -37
- package/src/DateLookup/DateLookup.tsx +36 -13
- package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +108 -0
- package/src/DateLookup/_stories/DateLookup.story.tsx +124 -2
- package/src/DateLookup/index.ts +6 -0
- package/src/Sticky/Sticky.test.tsx +0 -1
- package/src/Sticky/Sticky.tsx +0 -1
- package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +98 -0
- package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +45 -0
- package/src/common/DateUtils/index.ts +2 -0
- package/src/common/DateUtils/isDateDisabled/isDateDisabled.test.ts +159 -0
- package/src/common/DateUtils/isDateDisabled/isDateDisabled.ts +59 -0
- package/src/i18n/en.json +4 -0
- package/src/index.ts +3 -2
- package/src/main.css +123 -48
- package/src/main.less +0 -1
- package/build/Calendar/tableLink/TableLink.js.map +0 -1
- package/build/Calendar/tableLink/TableLink.mjs.map +0 -1
- package/build/styles/Sticky/Sticky.css +0 -4
- package/build/types/Calendar/tableLink/TableLink.d.ts.map +0 -1
- package/build/types/Calendar/tableLink/index.d.ts.map +0 -1
- package/src/Sticky/Sticky.css +0 -4
- package/src/Sticky/Sticky.less +0 -6
- package/src/Sticky/_stories/Sticky.test.story.tsx +0 -177
- /package/build/types/Calendar/{tableLink → TableLink}/index.d.ts +0 -0
- /package/src/Calendar/{tableLink → TableLink}/index.ts +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mockMatchMedia, render, screen, userEvent } from '../test-utils';
|
|
2
2
|
import Calendar from './Calendar';
|
|
3
|
+
import type { DisabledRules } from './Calendar.types';
|
|
3
4
|
|
|
4
5
|
mockMatchMedia();
|
|
5
6
|
|
|
@@ -15,35 +16,87 @@ describe('Calendar', () => {
|
|
|
15
16
|
['{ArrowUp}', -7],
|
|
16
17
|
['{ArrowDown}', +7],
|
|
17
18
|
])("handles '%s' to step %d day(s)", async (text: string, step: number) => {
|
|
18
|
-
const
|
|
19
|
-
render(<Calendar value={initialValue}
|
|
19
|
+
const handleDateFocus = jest.fn();
|
|
20
|
+
render(<Calendar value={initialValue} onDateFocus={handleDateFocus} />);
|
|
20
21
|
|
|
21
22
|
await user.click(screen.getByRole('button', { name: /selected day/iu }));
|
|
22
23
|
await user.keyboard(text);
|
|
23
24
|
|
|
24
25
|
const expected = new Date(initialValue);
|
|
25
26
|
expected.setDate(initialValue.getDate() + step);
|
|
26
|
-
expect(
|
|
27
|
+
expect(handleDateFocus).toHaveBeenCalledWith(expected, { isEnabled: true });
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
it('selects a day on click', async () => {
|
|
31
|
+
const handleDateFocus = jest.fn();
|
|
32
|
+
render(<Calendar value={initialValue} onDateFocus={handleDateFocus} />);
|
|
33
|
+
|
|
34
|
+
await user.click(screen.getByText('15'));
|
|
35
|
+
|
|
36
|
+
expect(handleDateFocus).toHaveBeenCalledWith(new Date(2000, 0, 15), { isEnabled: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('fires both onDateFocus and onChange when clicking a day', async () => {
|
|
40
|
+
const handleDateFocus = jest.fn();
|
|
30
41
|
const handleChange = jest.fn();
|
|
31
|
-
render(
|
|
42
|
+
render(
|
|
43
|
+
<Calendar value={initialValue} onDateFocus={handleDateFocus} onChange={handleChange} />,
|
|
44
|
+
);
|
|
32
45
|
|
|
33
46
|
await user.click(screen.getByText('15'));
|
|
34
47
|
|
|
48
|
+
expect(handleDateFocus).toHaveBeenCalledWith(new Date(2000, 0, 15), { isEnabled: true });
|
|
35
49
|
expect(handleChange).toHaveBeenCalledWith(new Date(2000, 0, 15));
|
|
36
50
|
});
|
|
37
51
|
|
|
38
|
-
it('
|
|
52
|
+
it('does not fire onChange during arrow key navigation', async () => {
|
|
39
53
|
const handleChange = jest.fn();
|
|
40
|
-
|
|
41
|
-
render(<Calendar value={initialValue} onChange={handleChange} onSelect={handleSelect} />);
|
|
54
|
+
render(<Calendar value={initialValue} onChange={handleChange} />);
|
|
42
55
|
|
|
43
|
-
await user.click(screen.
|
|
56
|
+
await user.click(screen.getByRole('button', { name: /selected day/iu }));
|
|
57
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
await user.keyboard('{ArrowRight}');
|
|
60
|
+
|
|
61
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('fires onChange when Enter is pressed on focused date', async () => {
|
|
65
|
+
const handleChange = jest.fn();
|
|
66
|
+
render(<Calendar value={initialValue} onChange={handleChange} />);
|
|
67
|
+
|
|
68
|
+
await user.click(screen.getByRole('button', { name: /selected day/iu }));
|
|
69
|
+
await user.keyboard('{ArrowRight}');
|
|
70
|
+
await user.keyboard('{Enter}');
|
|
71
|
+
|
|
72
|
+
const expected = new Date(initialValue);
|
|
73
|
+
expected.setDate(initialValue.getDate() + 1);
|
|
74
|
+
expect(handleChange).toHaveBeenCalledWith(expected);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not fire onChange when Enter is pressed on a disabled focused date', async () => {
|
|
78
|
+
const handleChange = jest.fn();
|
|
79
|
+
const disabledRules: DisabledRules = {
|
|
80
|
+
weekdays: ['sat', 'sun'],
|
|
81
|
+
reason: 'Closed on weekends.',
|
|
82
|
+
};
|
|
83
|
+
render(
|
|
84
|
+
<Calendar
|
|
85
|
+
value={new Date(2000, 0, 7)}
|
|
86
|
+
disabledRules={disabledRules}
|
|
87
|
+
onChange={handleChange}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await user.click(screen.getByRole('button', { name: /selected day/iu }));
|
|
92
|
+
// Click on selected day (Friday) fires onChange once
|
|
93
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
94
|
+
|
|
95
|
+
// ArrowRight from Jan 7 (Friday) → Jan 8 (Saturday = disabled)
|
|
96
|
+
await user.keyboard('{ArrowRight}');
|
|
97
|
+
await user.keyboard('{Enter}');
|
|
98
|
+
|
|
99
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
47
100
|
});
|
|
48
101
|
});
|
|
49
102
|
|
|
@@ -54,15 +107,15 @@ describe('Calendar', () => {
|
|
|
54
107
|
['{ArrowUp}', -4],
|
|
55
108
|
['{ArrowDown}', +4],
|
|
56
109
|
])("handles '%s' to step %d year(s)", async (text: string, step: number) => {
|
|
57
|
-
const
|
|
58
|
-
render(<Calendar value={initialValue}
|
|
110
|
+
const handleDateFocus = jest.fn();
|
|
111
|
+
render(<Calendar value={initialValue} onDateFocus={handleDateFocus} />);
|
|
59
112
|
|
|
60
113
|
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
61
114
|
await user.keyboard(text);
|
|
62
115
|
|
|
63
116
|
const expected = new Date(initialValue);
|
|
64
117
|
expected.setFullYear(initialValue.getFullYear() + step);
|
|
65
|
-
expect(
|
|
118
|
+
expect(handleDateFocus).toHaveBeenCalledWith(expected, { isEnabled: true });
|
|
66
119
|
});
|
|
67
120
|
});
|
|
68
121
|
|
|
@@ -73,8 +126,8 @@ describe('Calendar', () => {
|
|
|
73
126
|
['{ArrowUp}', -4],
|
|
74
127
|
['{ArrowDown}', +4],
|
|
75
128
|
])("handles '%s' to step %d month(s)", async (text: string, step: number) => {
|
|
76
|
-
const
|
|
77
|
-
render(<Calendar value={initialValue}
|
|
129
|
+
const handleDateFocus = jest.fn();
|
|
130
|
+
render(<Calendar value={initialValue} onDateFocus={handleDateFocus} />);
|
|
78
131
|
|
|
79
132
|
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
80
133
|
await user.keyboard(' ');
|
|
@@ -82,48 +135,166 @@ describe('Calendar', () => {
|
|
|
82
135
|
|
|
83
136
|
const expected = new Date(initialValue);
|
|
84
137
|
expected.setMonth(initialValue.getMonth() + step);
|
|
85
|
-
expect(
|
|
138
|
+
expect(handleDateFocus).toHaveBeenCalledWith(expected, { isEnabled: true });
|
|
86
139
|
});
|
|
87
140
|
});
|
|
88
141
|
|
|
89
142
|
describe('min/max clamping', () => {
|
|
90
|
-
it('limits navigation to min value', async () => {
|
|
143
|
+
it('limits keyboard navigation to min value', async () => {
|
|
91
144
|
const min = new Date(initialValue);
|
|
92
145
|
min.setDate(min.getDate() - 1);
|
|
93
|
-
const
|
|
94
|
-
render(<Calendar value={initialValue} min={min}
|
|
146
|
+
const handleDateFocus = jest.fn();
|
|
147
|
+
render(<Calendar value={initialValue} min={min} onDateFocus={handleDateFocus} />);
|
|
95
148
|
|
|
96
149
|
await user.click(screen.getByRole('button', { name: /selected day/iu }));
|
|
97
150
|
await user.keyboard('{ArrowLeft}{ArrowLeft}');
|
|
98
151
|
|
|
99
|
-
expect(
|
|
152
|
+
expect(handleDateFocus).toHaveBeenLastCalledWith(min, { isEnabled: true });
|
|
100
153
|
});
|
|
101
154
|
|
|
102
|
-
it('limits navigation to max value', async () => {
|
|
155
|
+
it('limits keyboard navigation to max value', async () => {
|
|
103
156
|
const max = new Date(initialValue);
|
|
104
157
|
max.setDate(max.getDate() + 1);
|
|
105
|
-
const
|
|
106
|
-
render(<Calendar value={initialValue} max={max}
|
|
158
|
+
const handleDateFocus = jest.fn();
|
|
159
|
+
render(<Calendar value={initialValue} max={max} onDateFocus={handleDateFocus} />);
|
|
107
160
|
|
|
108
161
|
await user.click(screen.getByRole('button', { name: /selected day/iu }));
|
|
109
162
|
await user.keyboard('{ArrowRight}{ArrowRight}');
|
|
110
163
|
|
|
111
|
-
expect(
|
|
164
|
+
expect(handleDateFocus).toHaveBeenLastCalledWith(max, { isEnabled: true });
|
|
112
165
|
});
|
|
113
166
|
|
|
114
167
|
it('clamps value to range when value is outside min/max', () => {
|
|
115
168
|
const min = new Date(2000, 0, 5);
|
|
116
169
|
const max = new Date(2000, 0, 20);
|
|
117
|
-
const
|
|
118
|
-
render(<Calendar value={initialValue} min={min} max={max}
|
|
170
|
+
const handleDateFocus = jest.fn();
|
|
171
|
+
render(<Calendar value={initialValue} min={min} max={max} onDateFocus={handleDateFocus} />);
|
|
119
172
|
|
|
120
|
-
expect(
|
|
173
|
+
expect(handleDateFocus).toHaveBeenCalledWith(min, { isEnabled: true });
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('navigation button boundary disabling', () => {
|
|
178
|
+
describe('day view — previous/next month', () => {
|
|
179
|
+
it('disables previous month button when min is in the current view month', () => {
|
|
180
|
+
render(<Calendar value={new Date(2000, 3, 15)} min={new Date(2000, 3, 1)} />);
|
|
181
|
+
expect(screen.getByRole('button', { name: /previous month/iu })).toBeDisabled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('disables next month button when max is in the current view month', () => {
|
|
185
|
+
render(<Calendar value={new Date(2000, 3, 15)} max={new Date(2000, 3, 30)} />);
|
|
186
|
+
expect(screen.getByRole('button', { name: /next month/iu })).toBeDisabled();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('does not disable month buttons when min/max are in adjacent months', () => {
|
|
190
|
+
render(
|
|
191
|
+
<Calendar
|
|
192
|
+
value={new Date(2000, 3, 15)}
|
|
193
|
+
min={new Date(2000, 2, 1)}
|
|
194
|
+
max={new Date(2000, 4, 30)}
|
|
195
|
+
/>,
|
|
196
|
+
);
|
|
197
|
+
expect(screen.getByRole('button', { name: /previous month/iu })).toBeEnabled();
|
|
198
|
+
expect(screen.getByRole('button', { name: /next month/iu })).toBeEnabled();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('does not disable month buttons when no min/max is set', () => {
|
|
202
|
+
render(<Calendar value={new Date(2000, 3, 15)} />);
|
|
203
|
+
expect(screen.getByRole('button', { name: /previous month/iu })).toBeEnabled();
|
|
204
|
+
expect(screen.getByRole('button', { name: /next month/iu })).toBeEnabled();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('disables previous month button when min is in January and view is January', () => {
|
|
208
|
+
// Edge case: month wraps to previous year
|
|
209
|
+
render(<Calendar value={new Date(2000, 0, 15)} min={new Date(2000, 0, 1)} />);
|
|
210
|
+
expect(screen.getByRole('button', { name: /previous month/iu })).toBeDisabled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('disables next month button when max is in December and view is December', () => {
|
|
214
|
+
// Edge case: month wraps to next year
|
|
215
|
+
render(<Calendar value={new Date(2000, 11, 15)} max={new Date(2000, 11, 31)} />);
|
|
216
|
+
expect(screen.getByRole('button', { name: /next month/iu })).toBeDisabled();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('month view — previous/next year', () => {
|
|
221
|
+
it('disables previous year button when min year equals view year', async () => {
|
|
222
|
+
render(<Calendar value={new Date(2000, 3, 15)} min={new Date(2000, 0, 1)} />);
|
|
223
|
+
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
224
|
+
await user.click(screen.getByRole('button', { name: /2000, selected year/iu }));
|
|
225
|
+
expect(screen.getByRole('button', { name: /previous year/iu })).toBeDisabled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('disables next year button when max year equals view year', async () => {
|
|
229
|
+
render(<Calendar value={new Date(2000, 3, 15)} max={new Date(2000, 11, 31)} />);
|
|
230
|
+
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
231
|
+
await user.click(screen.getByRole('button', { name: /2000, selected year/iu }));
|
|
232
|
+
expect(screen.getByRole('button', { name: /next year/iu })).toBeDisabled();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('does not disable year buttons when min/max are in adjacent years', async () => {
|
|
236
|
+
render(
|
|
237
|
+
<Calendar
|
|
238
|
+
value={new Date(2000, 3, 15)}
|
|
239
|
+
min={new Date(1999, 0, 1)}
|
|
240
|
+
max={new Date(2001, 11, 31)}
|
|
241
|
+
/>,
|
|
242
|
+
);
|
|
243
|
+
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
244
|
+
await user.click(screen.getByRole('button', { name: /2000, selected year/iu }));
|
|
245
|
+
expect(screen.getByRole('button', { name: /previous year/iu })).toBeEnabled();
|
|
246
|
+
expect(screen.getByRole('button', { name: /next year/iu })).toBeEnabled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('does not disable year buttons when no min/max is set', async () => {
|
|
250
|
+
render(<Calendar value={new Date(2000, 3, 15)} />);
|
|
251
|
+
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
252
|
+
await user.click(screen.getByRole('button', { name: /2000, selected year/iu }));
|
|
253
|
+
expect(screen.getByRole('button', { name: /previous year/iu })).toBeEnabled();
|
|
254
|
+
expect(screen.getByRole('button', { name: /next year/iu })).toBeEnabled();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('year view — previous/next 20 years', () => {
|
|
259
|
+
it('disables previous 20-year button when min falls in the current block', async () => {
|
|
260
|
+
// value is 2000, block is 2000–2019, min is 2005 → previous block (1980–1999) is entirely before min
|
|
261
|
+
render(<Calendar value={new Date(2005, 0, 1)} min={new Date(2005, 0, 1)} />);
|
|
262
|
+
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
263
|
+
expect(screen.getByRole('button', { name: /previous 20 years/iu })).toBeDisabled();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('disables next 20-year button when max falls in the current block', async () => {
|
|
267
|
+
// value is 2000, block is 2000–2019, max is 2015 → next block (2020–2039) is entirely after max
|
|
268
|
+
render(<Calendar value={new Date(2000, 0, 1)} max={new Date(2015, 11, 31)} />);
|
|
269
|
+
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
270
|
+
expect(screen.getByRole('button', { name: /next 20 years/iu })).toBeDisabled();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('does not disable 20-year buttons when min/max span multiple blocks', async () => {
|
|
274
|
+
render(
|
|
275
|
+
<Calendar
|
|
276
|
+
value={new Date(2000, 0, 1)}
|
|
277
|
+
min={new Date(1985, 0, 1)}
|
|
278
|
+
max={new Date(2025, 11, 31)}
|
|
279
|
+
/>,
|
|
280
|
+
);
|
|
281
|
+
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
282
|
+
expect(screen.getByRole('button', { name: /previous 20 years/iu })).toBeEnabled();
|
|
283
|
+
expect(screen.getByRole('button', { name: /next 20 years/iu })).toBeEnabled();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('does not disable 20-year buttons when no min/max is set', async () => {
|
|
287
|
+
render(<Calendar value={new Date(2000, 0, 1)} />);
|
|
288
|
+
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
289
|
+
expect(screen.getByRole('button', { name: /previous 20 years/iu })).toBeEnabled();
|
|
290
|
+
expect(screen.getByRole('button', { name: /next 20 years/iu })).toBeEnabled();
|
|
291
|
+
});
|
|
121
292
|
});
|
|
122
293
|
});
|
|
123
294
|
|
|
124
295
|
describe('view switching', () => {
|
|
125
296
|
it('switches to year view and back to day view', async () => {
|
|
126
|
-
render(<Calendar value={initialValue}
|
|
297
|
+
render(<Calendar value={initialValue} />);
|
|
127
298
|
|
|
128
299
|
await user.click(screen.getByRole('button', { name: /year view/iu }));
|
|
129
300
|
expect(screen.getByRole('button', { name: /selected year/iu })).toBeInTheDocument();
|
|
@@ -134,5 +305,23 @@ describe('Calendar', () => {
|
|
|
134
305
|
await user.click(screen.getByRole('button', { name: /selected month/iu }));
|
|
135
306
|
expect(screen.getByRole('button', { name: /next month/iu })).toBeInTheDocument();
|
|
136
307
|
});
|
|
308
|
+
|
|
309
|
+
it('switches to year view when header button is activated with keyboard', async () => {
|
|
310
|
+
const handleChange = jest.fn();
|
|
311
|
+
render(<Calendar value={initialValue} onChange={handleChange} />);
|
|
312
|
+
|
|
313
|
+
// Tab through to the year view header button
|
|
314
|
+
const yearViewButton = screen.getByRole('button', { name: /year view/iu });
|
|
315
|
+
await user.tab();
|
|
316
|
+
while (document.activeElement !== yearViewButton) {
|
|
317
|
+
await user.tab();
|
|
318
|
+
}
|
|
319
|
+
expect(yearViewButton).toHaveFocus();
|
|
320
|
+
|
|
321
|
+
await user.keyboard('{Enter}');
|
|
322
|
+
|
|
323
|
+
expect(screen.getByRole('button', { name: /selected year/iu })).toBeInTheDocument();
|
|
324
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
325
|
+
});
|
|
137
326
|
});
|
|
138
327
|
});
|
|
@@ -1,40 +1,88 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect, KeyboardEvent } from 'react';
|
|
1
|
+
import { useState, useCallback, useEffect, useId, useRef, KeyboardEvent } from 'react';
|
|
2
|
+
import { useIntl } from 'react-intl';
|
|
3
|
+
import { formatDate } from '@transferwise/formatting';
|
|
2
4
|
|
|
3
5
|
import { MonthFormat } from '../common';
|
|
4
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isWithinRange,
|
|
8
|
+
moveToWithinRange,
|
|
9
|
+
returnDateView,
|
|
10
|
+
isDateDisabled,
|
|
11
|
+
hasDisabledDatesInMonth,
|
|
12
|
+
} from '../common/DateUtils';
|
|
13
|
+
import { InlinePrompt } from '../Prompt/InlinePrompt';
|
|
14
|
+
import type { DateChangeMeta, DisabledRules } from './Calendar.types';
|
|
5
15
|
import DayCalendar from './DayCalendar';
|
|
6
16
|
import { getStartOfDay } from './GetStartOfDay';
|
|
17
|
+
import messages from './Calendar.messages';
|
|
7
18
|
import MonthCalendar from './MonthCalendar';
|
|
8
19
|
import YearCalendar from './YearCalendar';
|
|
9
20
|
|
|
10
21
|
export interface CalendarProps {
|
|
22
|
+
/** The currently selected date, or `null` for no selection. */
|
|
11
23
|
value: Date | null;
|
|
24
|
+
/** Earliest selectable date (inclusive). Dates before this are interactive but not selectable, and show a boundary prompt. */
|
|
12
25
|
min?: Date | null;
|
|
26
|
+
/** Latest selectable date (inclusive). Dates after this are interactive but not selectable, and show a boundary prompt. */
|
|
13
27
|
max?: Date | null;
|
|
28
|
+
/** Month name format in the header. Defaults to `'long'` (e.g. "January"). */
|
|
14
29
|
monthFormat?: `${MonthFormat}`;
|
|
30
|
+
/** Placeholder text shown in the month/year cells when no date is selected. */
|
|
15
31
|
placeholder?: string;
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
/** Rules for disabling specific weekdays, individual dates, or date ranges. The required `reason` is shown in an inline prompt and linked to disabled cells via `aria-describedby` for screen reader accessibility. */
|
|
33
|
+
disabledRules?: DisabledRules;
|
|
34
|
+
/** Called whenever the focused date changes — including disabled and out-of-range dates. `meta.isEnabled` is `false` when the date is not selectable. */
|
|
35
|
+
onDateFocus?: (date: Date | null, meta: DateChangeMeta) => void;
|
|
36
|
+
/** Called when a date is confirmed (click or Enter). Only fires for enabled dates. */
|
|
37
|
+
onChange?: (date: Date) => void;
|
|
18
38
|
}
|
|
19
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Inline calendar for selecting a single date. Powers [DateLookup](?path=/docs/forms-datelookup--docs).
|
|
42
|
+
*/
|
|
20
43
|
const Calendar = ({
|
|
21
44
|
value,
|
|
22
45
|
min: minProp = null,
|
|
23
46
|
max: maxProp = null,
|
|
24
47
|
monthFormat = MonthFormat.LONG,
|
|
25
48
|
placeholder,
|
|
49
|
+
disabledRules,
|
|
50
|
+
onDateFocus,
|
|
26
51
|
onChange,
|
|
27
|
-
onSelect,
|
|
28
52
|
}: CalendarProps) => {
|
|
53
|
+
const intl = useIntl();
|
|
29
54
|
const selectedDate = getStartOfDay(value);
|
|
30
55
|
const min = getStartOfDay(minProp);
|
|
31
56
|
const max = getStartOfDay(maxProp);
|
|
57
|
+
const disabledReasonId = useId();
|
|
58
|
+
const disabledRulesRef = useRef(disabledRules);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
disabledRulesRef.current = disabledRules;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const [focusedDate, setFocusedDate] = useState<Date | null>(selectedDate);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
67
|
+
setFocusedDate(selectedDate);
|
|
68
|
+
}, [selectedDate?.getTime()]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
69
|
+
|
|
70
|
+
const checkIsEnabled = (date: Date | null) => {
|
|
71
|
+
if (!date) return true;
|
|
72
|
+
return isWithinRange(date, min, max) && !isDateDisabled(date, disabledRulesRef.current);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const focusedDateDisabled = focusedDate
|
|
76
|
+
? !isWithinRange(focusedDate, min, max) || isDateDisabled(focusedDate, disabledRules)
|
|
77
|
+
: false;
|
|
32
78
|
|
|
33
79
|
useEffect(() => {
|
|
34
80
|
if (selectedDate && !isWithinRange(selectedDate, min, max)) {
|
|
35
|
-
|
|
81
|
+
const clamped = moveToWithinRange(selectedDate, min, max);
|
|
82
|
+
setFocusedDate(clamped); // eslint-disable-line react-hooks/set-state-in-effect
|
|
83
|
+
onDateFocus?.(clamped, { isEnabled: checkIsEnabled(clamped) });
|
|
36
84
|
}
|
|
37
|
-
}, [selectedDate?.getTime(), min?.getTime(), max?.getTime(),
|
|
85
|
+
}, [selectedDate?.getTime(), min?.getTime(), max?.getTime(), onDateFocus]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
38
86
|
|
|
39
87
|
const dateView = returnDateView(selectedDate, min, max);
|
|
40
88
|
const [viewMonth, setViewMonth] = useState(dateView.getMonth());
|
|
@@ -49,6 +97,14 @@ const Calendar = ({
|
|
|
49
97
|
setViewYear(view.getFullYear());
|
|
50
98
|
}, [selectedDate?.getTime(), min?.getTime(), max?.getTime()]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
51
99
|
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (focusedDate) {
|
|
102
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
103
|
+
setViewMonth(focusedDate.getMonth());
|
|
104
|
+
setViewYear(focusedDate.getFullYear());
|
|
105
|
+
}
|
|
106
|
+
}, [focusedDate?.getTime()]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
107
|
+
|
|
52
108
|
const handleViewDateUpdate = useCallback(
|
|
53
109
|
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
|
54
110
|
({ month, year }: { month?: number; year?: number }) => {
|
|
@@ -64,33 +120,36 @@ const Calendar = ({
|
|
|
64
120
|
|
|
65
121
|
const handleSelectedDateUpdate = useCallback(
|
|
66
122
|
(date: Date) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
123
|
+
const isEnabled = checkIsEnabled(date);
|
|
124
|
+
setFocusedDate(date);
|
|
125
|
+
onDateFocus?.(date, { isEnabled });
|
|
126
|
+
if (isEnabled && onChange) {
|
|
70
127
|
onChange(date);
|
|
71
128
|
}
|
|
72
129
|
},
|
|
73
|
-
[
|
|
130
|
+
[onDateFocus, onChange], // eslint-disable-line react-hooks/exhaustive-deps
|
|
74
131
|
);
|
|
75
132
|
|
|
76
133
|
const adjustDate = useCallback(
|
|
77
134
|
(daysToAdd: number, monthsToAdd: number, yearsToAdd: number) => {
|
|
135
|
+
const base = focusedDate ?? selectedDate;
|
|
78
136
|
let date: Date | null;
|
|
79
|
-
if (
|
|
137
|
+
if (base) {
|
|
80
138
|
date = new Date(
|
|
81
|
-
mode === 'year' ?
|
|
82
|
-
mode === 'month' ?
|
|
83
|
-
mode === 'day' ?
|
|
139
|
+
mode === 'year' ? base.getFullYear() + yearsToAdd : base.getFullYear(),
|
|
140
|
+
mode === 'month' ? base.getMonth() + monthsToAdd : base.getMonth(),
|
|
141
|
+
mode === 'day' ? base.getDate() + daysToAdd : base.getDate(),
|
|
84
142
|
);
|
|
85
143
|
} else {
|
|
86
144
|
date = getStartOfDay(new Date());
|
|
87
145
|
}
|
|
88
146
|
date &&= moveToWithinRange(date, min, max);
|
|
89
|
-
if (date?.getTime() !==
|
|
90
|
-
|
|
147
|
+
if (date?.getTime() !== focusedDate?.getTime()) {
|
|
148
|
+
setFocusedDate(date);
|
|
149
|
+
onDateFocus?.(date, { isEnabled: checkIsEnabled(date) });
|
|
91
150
|
}
|
|
92
151
|
},
|
|
93
|
-
[selectedDate, min, max, mode,
|
|
152
|
+
[focusedDate, selectedDate, min, max, mode, onDateFocus], // eslint-disable-line react-hooks/exhaustive-deps
|
|
94
153
|
);
|
|
95
154
|
|
|
96
155
|
const handleKeyDown = useCallback(
|
|
@@ -112,24 +171,70 @@ const Calendar = ({
|
|
|
112
171
|
adjustDate(7, 4, 4);
|
|
113
172
|
event.preventDefault();
|
|
114
173
|
break;
|
|
174
|
+
case 'Enter':
|
|
175
|
+
case ' ':
|
|
176
|
+
if (
|
|
177
|
+
mode === 'day' &&
|
|
178
|
+
focusedDate &&
|
|
179
|
+
(event.target as HTMLElement).hasAttribute('data-calendar-cell')
|
|
180
|
+
) {
|
|
181
|
+
handleSelectedDateUpdate(focusedDate);
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
115
185
|
default:
|
|
116
186
|
break;
|
|
117
187
|
}
|
|
118
188
|
},
|
|
119
|
-
[adjustDate],
|
|
189
|
+
[adjustDate, focusedDate, mode, handleSelectedDateUpdate],
|
|
120
190
|
);
|
|
121
191
|
|
|
192
|
+
const focusedDateOutOfRange = focusedDate ? !isWithinRange(focusedDate, min, max) : false;
|
|
193
|
+
|
|
194
|
+
const formatDateForPrompt = (date: Date) =>
|
|
195
|
+
formatDate(date, intl.locale, { day: 'numeric', month: 'long', year: 'numeric' });
|
|
196
|
+
|
|
197
|
+
const outOfRangePromptText =
|
|
198
|
+
min && max
|
|
199
|
+
? intl.formatMessage(messages.outOfRangeBetween, {
|
|
200
|
+
min: formatDateForPrompt(min),
|
|
201
|
+
max: formatDateForPrompt(max),
|
|
202
|
+
})
|
|
203
|
+
: min
|
|
204
|
+
? intl.formatMessage(messages.outOfRangeAfter, { min: formatDateForPrompt(min) })
|
|
205
|
+
: max
|
|
206
|
+
? intl.formatMessage(messages.outOfRangeBefore, { max: formatDateForPrompt(max) })
|
|
207
|
+
: '';
|
|
208
|
+
|
|
209
|
+
const hasOutOfRangeDates = mode === 'day' && !!(min || max);
|
|
210
|
+
const showRulesPrompt =
|
|
211
|
+
mode === 'day' &&
|
|
212
|
+
!!disabledRules &&
|
|
213
|
+
hasDisabledDatesInMonth(viewMonth, viewYear, disabledRules, min, max);
|
|
214
|
+
const showOutOfRangePrompt = mode === 'day' && focusedDateOutOfRange;
|
|
215
|
+
const showPrompt = hasOutOfRangeDates || showRulesPrompt;
|
|
216
|
+
|
|
217
|
+
const promptText = showOutOfRangePrompt
|
|
218
|
+
? outOfRangePromptText
|
|
219
|
+
: (disabledRules?.reason ?? outOfRangePromptText);
|
|
220
|
+
const promptSentiment = showOutOfRangePrompt || focusedDateDisabled ? 'negative' : 'neutral';
|
|
221
|
+
const promptClassName =
|
|
222
|
+
showOutOfRangePrompt || (showRulesPrompt && !showOutOfRangePrompt) ? 'm-t-1' : 'sr-only';
|
|
223
|
+
|
|
122
224
|
return (
|
|
123
225
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
124
226
|
<div onKeyDown={handleKeyDown}>
|
|
125
227
|
{mode === 'day' && (
|
|
126
228
|
<DayCalendar
|
|
127
229
|
selectedDate={selectedDate}
|
|
230
|
+
focusedDate={focusedDate}
|
|
128
231
|
min={min}
|
|
129
232
|
max={max}
|
|
130
233
|
viewMonth={viewMonth}
|
|
131
234
|
viewYear={viewYear}
|
|
132
235
|
monthFormat={monthFormat}
|
|
236
|
+
disabledRules={disabledRules}
|
|
237
|
+
disabledReasonId={showPrompt ? disabledReasonId : undefined}
|
|
133
238
|
onSelect={handleSelectedDateUpdate}
|
|
134
239
|
onLabelClick={switchToYears}
|
|
135
240
|
onViewDateUpdate={handleViewDateUpdate}
|
|
@@ -138,6 +243,7 @@ const Calendar = ({
|
|
|
138
243
|
{mode === 'month' && (
|
|
139
244
|
<MonthCalendar
|
|
140
245
|
selectedDate={selectedDate}
|
|
246
|
+
focusedDate={focusedDate}
|
|
141
247
|
min={min}
|
|
142
248
|
max={max}
|
|
143
249
|
viewYear={viewYear}
|
|
@@ -150,6 +256,7 @@ const Calendar = ({
|
|
|
150
256
|
{mode === 'year' && (
|
|
151
257
|
<YearCalendar
|
|
152
258
|
selectedDate={selectedDate}
|
|
259
|
+
focusedDate={focusedDate}
|
|
153
260
|
min={min}
|
|
154
261
|
max={max}
|
|
155
262
|
viewYear={viewYear}
|
|
@@ -158,6 +265,16 @@ const Calendar = ({
|
|
|
158
265
|
onViewDateUpdate={handleViewDateUpdate}
|
|
159
266
|
/>
|
|
160
267
|
)}
|
|
268
|
+
{showPrompt && (
|
|
269
|
+
<InlinePrompt
|
|
270
|
+
sentiment={promptSentiment}
|
|
271
|
+
width="full"
|
|
272
|
+
id={disabledReasonId}
|
|
273
|
+
className={promptClassName}
|
|
274
|
+
>
|
|
275
|
+
{promptText}
|
|
276
|
+
</InlinePrompt>
|
|
277
|
+
)}
|
|
161
278
|
</div>
|
|
162
279
|
);
|
|
163
280
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface DisabledDateRange {
|
|
2
|
+
start: Date;
|
|
3
|
+
end: Date;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type WeekDayType = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
|
|
7
|
+
|
|
8
|
+
export interface DisabledRules {
|
|
9
|
+
/** Days of the week to disable (e.g. `['sat', 'sun']` for weekends). */
|
|
10
|
+
weekdays?: WeekDayType[];
|
|
11
|
+
/** Specific calendar dates to disable. */
|
|
12
|
+
dates?: Date[];
|
|
13
|
+
/** Inclusive date ranges to disable. */
|
|
14
|
+
ranges?: DisabledDateRange[];
|
|
15
|
+
/**
|
|
16
|
+
* Human-readable explanation shown in an InlinePrompt below the calendar grid when a
|
|
17
|
+
* disabled date is focused or clicked. Required for accessibility: it is linked to
|
|
18
|
+
* disabled cells via `aria-describedby` so screen reader users understand why a date
|
|
19
|
+
* is unavailable. Also used as the prompt text when `min`/`max` out-of-range dates
|
|
20
|
+
* are focused, overriding the built-in default message.
|
|
21
|
+
*/
|
|
22
|
+
reason: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DateChangeMeta {
|
|
26
|
+
/** Whether the focused date is selectable (not disabled or out of range). */
|
|
27
|
+
isEnabled: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OnDateSelectParams {
|
|
31
|
+
/** The date that was focused or clicked. */
|
|
32
|
+
date: Date;
|
|
33
|
+
/** Whether the date is selectable (not disabled or out of range). */
|
|
34
|
+
isEnabled: boolean;
|
|
35
|
+
}
|
|
@@ -15,6 +15,8 @@ interface DateHeaderProps {
|
|
|
15
15
|
onPreviousClick: () => void;
|
|
16
16
|
onNextClick: () => void;
|
|
17
17
|
dateMode: string;
|
|
18
|
+
isPreviousDisabled?: boolean;
|
|
19
|
+
isNextDisabled?: boolean;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const DateHeader: React.FC<DateHeaderProps> = ({
|
|
@@ -23,6 +25,8 @@ const DateHeader: React.FC<DateHeaderProps> = ({
|
|
|
23
25
|
onPreviousClick,
|
|
24
26
|
onNextClick,
|
|
25
27
|
dateMode,
|
|
28
|
+
isPreviousDisabled,
|
|
29
|
+
isNextDisabled,
|
|
26
30
|
}) => {
|
|
27
31
|
const intl = useIntl();
|
|
28
32
|
const { isMobile } = useLayout();
|
|
@@ -34,6 +38,7 @@ const DateHeader: React.FC<DateHeaderProps> = ({
|
|
|
34
38
|
priority="minimal"
|
|
35
39
|
className="d-inline-flex"
|
|
36
40
|
aria-label={`${intl.formatMessage(messages.previous)} ${dateMode}`}
|
|
41
|
+
disabled={isPreviousDisabled}
|
|
37
42
|
onClick={onPreviousClick}
|
|
38
43
|
>
|
|
39
44
|
<ChevronLeft className="left-single-direction" />
|
|
@@ -61,6 +66,7 @@ const DateHeader: React.FC<DateHeaderProps> = ({
|
|
|
61
66
|
className="d-inline-flex"
|
|
62
67
|
aria-label={`${intl.formatMessage(messages.next)} ${dateMode}`}
|
|
63
68
|
priority="minimal"
|
|
69
|
+
disabled={isNextDisabled}
|
|
64
70
|
onClick={onNextClick}
|
|
65
71
|
>
|
|
66
72
|
<ChevronRight className="right-single-direction" />
|