@wordpress/components 29.11.0 → 29.13.1-next.719a03cbe.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/CHANGELOG.md +22 -0
- package/build/box-control/input-control.js +2 -2
- package/build/box-control/input-control.js.map +1 -1
- package/build/calendar/date-calendar/index.js +60 -0
- package/build/calendar/date-calendar/index.js.map +1 -0
- package/build/calendar/date-range-calendar/index.js +168 -0
- package/build/calendar/date-range-calendar/index.js.map +1 -0
- package/build/calendar/index.js +27 -0
- package/build/calendar/index.js.map +1 -0
- package/build/calendar/types.js +6 -0
- package/build/calendar/types.js.map +1 -0
- package/build/calendar/utils/constants.js +68 -0
- package/build/calendar/utils/constants.js.map +1 -0
- package/build/calendar/utils/day-cell.js +137 -0
- package/build/calendar/utils/day-cell.js.map +1 -0
- package/build/calendar/utils/misc.js +10 -0
- package/build/calendar/utils/misc.js.map +1 -0
- package/build/calendar/utils/use-controlled-value.js +58 -0
- package/build/calendar/utils/use-controlled-value.js.map +1 -0
- package/build/calendar/utils/use-localization-props.js +162 -0
- package/build/calendar/utils/use-localization-props.js.map +1 -0
- package/build/custom-gradient-picker/gradient-bar/control-points.js +1 -1
- package/build/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
- package/build/custom-select-control-v2/custom-select.js +3 -3
- package/build/custom-select-control-v2/custom-select.js.map +1 -1
- package/build/date-time/date/index.js +1 -1
- package/build/date-time/date/index.js.map +1 -1
- package/build/form-file-upload/index.js +4 -6
- package/build/form-file-upload/index.js.map +1 -1
- package/build/form-token-field/index.js +11 -1
- package/build/form-token-field/index.js.map +1 -1
- package/build/form-token-field/token.js +1 -1
- package/build/form-token-field/token.js.map +1 -1
- package/build/index.js +19 -0
- package/build/index.js.map +1 -1
- package/build/mobile/bottom-sheet/cell.native.js +2 -2
- package/build/mobile/bottom-sheet/cell.native.js.map +1 -1
- package/build/mobile/image/index.native.js +1 -1
- package/build/mobile/image/index.native.js.map +1 -1
- package/build/mobile/link-picker/index.native.js +1 -1
- package/build/mobile/link-picker/index.native.js.map +1 -1
- package/build/navigation/menu/menu-title-search.js +1 -1
- package/build/navigation/menu/menu-title-search.js.map +1 -1
- package/build/palette-edit/index.js +4 -4
- package/build/palette-edit/index.js.map +1 -1
- package/build-module/box-control/input-control.js +2 -2
- package/build-module/box-control/input-control.js.map +1 -1
- package/build-module/calendar/date-calendar/index.js +51 -0
- package/build-module/calendar/date-calendar/index.js.map +1 -0
- package/build-module/calendar/date-range-calendar/index.js +157 -0
- package/build-module/calendar/date-range-calendar/index.js.map +1 -0
- package/build-module/calendar/index.js +4 -0
- package/build-module/calendar/index.js.map +1 -0
- package/build-module/calendar/types.js +2 -0
- package/build-module/calendar/types.js.map +1 -0
- package/build-module/calendar/utils/constants.js +61 -0
- package/build-module/calendar/utils/constants.js.map +1 -0
- package/build-module/calendar/utils/day-cell.js +131 -0
- package/build-module/calendar/utils/day-cell.js.map +1 -0
- package/build-module/calendar/utils/misc.js +4 -0
- package/build-module/calendar/utils/misc.js.map +1 -0
- package/build-module/calendar/utils/use-controlled-value.js +51 -0
- package/build-module/calendar/utils/use-controlled-value.js.map +1 -0
- package/build-module/calendar/utils/use-localization-props.js +154 -0
- package/build-module/calendar/utils/use-localization-props.js.map +1 -0
- package/build-module/custom-gradient-picker/gradient-bar/control-points.js +1 -1
- package/build-module/custom-gradient-picker/gradient-bar/control-points.js.map +1 -1
- package/build-module/custom-select-control-v2/custom-select.js +4 -4
- package/build-module/custom-select-control-v2/custom-select.js.map +1 -1
- package/build-module/date-time/date/index.js +1 -1
- package/build-module/date-time/date/index.js.map +1 -1
- package/build-module/form-file-upload/index.js +4 -6
- package/build-module/form-file-upload/index.js.map +1 -1
- package/build-module/form-token-field/index.js +11 -1
- package/build-module/form-token-field/index.js.map +1 -1
- package/build-module/form-token-field/token.js +1 -1
- package/build-module/form-token-field/token.js.map +1 -1
- package/build-module/index.js +1 -0
- package/build-module/index.js.map +1 -1
- package/build-module/mobile/bottom-sheet/cell.native.js +2 -2
- package/build-module/mobile/bottom-sheet/cell.native.js.map +1 -1
- package/build-module/mobile/image/index.native.js +1 -1
- package/build-module/mobile/image/index.native.js.map +1 -1
- package/build-module/mobile/link-picker/index.native.js +1 -1
- package/build-module/mobile/link-picker/index.native.js.map +1 -1
- package/build-module/navigation/menu/menu-title-search.js +1 -1
- package/build-module/navigation/menu/menu-title-search.js.map +1 -1
- package/build-module/palette-edit/index.js +4 -4
- package/build-module/palette-edit/index.js.map +1 -1
- package/build-style/style-rtl.css +358 -5
- package/build-style/style.css +358 -5
- package/build-types/box-control/input-control.d.ts.map +1 -1
- package/build-types/box-control/utils.d.ts +7 -7
- package/build-types/calendar/date-calendar/index.d.ts +11 -0
- package/build-types/calendar/date-calendar/index.d.ts.map +1 -0
- package/build-types/calendar/date-range-calendar/index.d.ts +14 -0
- package/build-types/calendar/date-range-calendar/index.d.ts.map +1 -0
- package/build-types/calendar/index.d.ts +4 -0
- package/build-types/calendar/index.d.ts.map +1 -0
- package/build-types/calendar/stories/date-calendar.story.d.ts +16 -0
- package/build-types/calendar/stories/date-calendar.story.d.ts.map +1 -0
- package/build-types/calendar/stories/date-range-calendar.story.d.ts +16 -0
- package/build-types/calendar/stories/date-range-calendar.story.d.ts.map +1 -0
- package/build-types/calendar/test/__utils__/index.d.ts +10 -0
- package/build-types/calendar/test/__utils__/index.d.ts.map +1 -0
- package/build-types/calendar/test/date-calendar.d.ts +2 -0
- package/build-types/calendar/test/date-calendar.d.ts.map +1 -0
- package/build-types/calendar/test/date-range-calendar.d.ts +2 -0
- package/build-types/calendar/test/date-range-calendar.d.ts.map +1 -0
- package/build-types/calendar/types.d.ts +317 -0
- package/build-types/calendar/types.d.ts.map +1 -0
- package/build-types/calendar/utils/constants.d.ts +52 -0
- package/build-types/calendar/utils/constants.d.ts.map +1 -0
- package/build-types/calendar/utils/day-cell.d.ts +21 -0
- package/build-types/calendar/utils/day-cell.d.ts.map +1 -0
- package/build-types/calendar/utils/misc.d.ts +2 -0
- package/build-types/calendar/utils/misc.d.ts.map +1 -0
- package/build-types/calendar/utils/use-controlled-value.d.ts +27 -0
- package/build-types/calendar/utils/use-controlled-value.d.ts.map +1 -0
- package/build-types/calendar/utils/use-localization-props.d.ts +64 -0
- package/build-types/calendar/utils/use-localization-props.d.ts.map +1 -0
- package/build-types/custom-gradient-picker/constants.d.ts +6 -3
- package/build-types/custom-gradient-picker/constants.d.ts.map +1 -1
- package/build-types/custom-select-control-v2/custom-select.d.ts.map +1 -1
- package/build-types/dimension-control/sizes.d.ts +15 -3
- package/build-types/dimension-control/sizes.d.ts.map +1 -1
- package/build-types/font-size-picker/constants.d.ts +2 -2
- package/build-types/font-size-picker/constants.d.ts.map +1 -1
- package/build-types/form-file-upload/index.d.ts.map +1 -1
- package/build-types/form-token-field/index.d.ts.map +1 -1
- package/build-types/index.d.ts +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/popover/overlay-middlewares.d.ts +6 -1
- package/build-types/popover/overlay-middlewares.d.ts.map +1 -1
- package/package.json +21 -20
- package/src/box-control/input-control.tsx +14 -5
- package/src/calendar/date-calendar/README.md +250 -0
- package/src/calendar/date-calendar/index.tsx +55 -0
- package/src/calendar/date-range-calendar/README.md +287 -0
- package/src/calendar/date-range-calendar/index.tsx +203 -0
- package/src/calendar/index.tsx +3 -0
- package/src/calendar/stories/date-calendar.story.tsx +221 -0
- package/src/calendar/stories/date-range-calendar.story.tsx +230 -0
- package/src/calendar/style.scss +431 -0
- package/src/calendar/test/__utils__/index.ts +56 -0
- package/src/calendar/test/date-calendar.tsx +975 -0
- package/src/calendar/test/date-range-calendar.tsx +1701 -0
- package/src/calendar/types.ts +342 -0
- package/src/calendar/utils/constants.ts +62 -0
- package/src/calendar/utils/day-cell.tsx +133 -0
- package/src/calendar/utils/misc.ts +3 -0
- package/src/calendar/utils/use-controlled-value.ts +61 -0
- package/src/calendar/utils/use-localization-props.ts +169 -0
- package/src/circular-option-picker/stories/index.story.tsx +2 -2
- package/src/custom-gradient-picker/gradient-bar/control-points.tsx +1 -1
- package/src/custom-select-control-v2/custom-select.tsx +6 -3
- package/src/date-time/date/index.tsx +1 -1
- package/src/form-file-upload/index.tsx +6 -12
- package/src/form-token-field/index.tsx +12 -1
- package/src/form-token-field/token.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/mobile/bottom-sheet/cell.native.js +2 -2
- package/src/mobile/image/index.native.js +1 -1
- package/src/mobile/link-picker/index.native.js +1 -1
- package/src/navigation/menu/menu-title-search.tsx +1 -1
- package/src/palette-edit/index.tsx +4 -4
- package/src/select-control/style.scss +0 -6
- package/src/style.scss +1 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen, within } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import {
|
|
7
|
+
startOfDay,
|
|
8
|
+
startOfWeek,
|
|
9
|
+
startOfMonth,
|
|
10
|
+
endOfWeek,
|
|
11
|
+
addDays,
|
|
12
|
+
subDays,
|
|
13
|
+
addWeeks,
|
|
14
|
+
addMonths,
|
|
15
|
+
subMonths,
|
|
16
|
+
subYears,
|
|
17
|
+
addHours,
|
|
18
|
+
} from 'date-fns';
|
|
19
|
+
import { ar } from 'date-fns/locale';
|
|
20
|
+
/**
|
|
21
|
+
* WordPress dependencies
|
|
22
|
+
*/
|
|
23
|
+
import { useState } from '@wordpress/element';
|
|
24
|
+
/**
|
|
25
|
+
* Internal dependencies
|
|
26
|
+
*/
|
|
27
|
+
import { DateCalendar, TZDate } from '..';
|
|
28
|
+
import {
|
|
29
|
+
getDateButton,
|
|
30
|
+
getDateCell,
|
|
31
|
+
queryDateCell,
|
|
32
|
+
monthNameFormatter,
|
|
33
|
+
} from './__utils__';
|
|
34
|
+
import type { DateCalendarProps } from '../types';
|
|
35
|
+
|
|
36
|
+
const UncontrolledDateCalendar = (
|
|
37
|
+
props: DateCalendarProps & {
|
|
38
|
+
initialSelected?: Date | undefined | null;
|
|
39
|
+
initialMonth?: Date | undefined;
|
|
40
|
+
}
|
|
41
|
+
) => {
|
|
42
|
+
return (
|
|
43
|
+
<DateCalendar
|
|
44
|
+
{ ...props }
|
|
45
|
+
defaultSelected={ props.initialSelected ?? undefined }
|
|
46
|
+
defaultMonth={ props.initialMonth }
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const ControlledDateCalendar = (
|
|
52
|
+
props: DateCalendarProps & {
|
|
53
|
+
initialSelected?: Date | undefined | null;
|
|
54
|
+
initialMonth?: Date | undefined;
|
|
55
|
+
}
|
|
56
|
+
) => {
|
|
57
|
+
const [ selected, setSelected ] = useState< Date | undefined | null >(
|
|
58
|
+
props.initialSelected
|
|
59
|
+
);
|
|
60
|
+
const [ month, setMonth ] = useState< Date | undefined >(
|
|
61
|
+
props.initialMonth
|
|
62
|
+
);
|
|
63
|
+
return (
|
|
64
|
+
<DateCalendar
|
|
65
|
+
{ ...props }
|
|
66
|
+
selected={ selected ?? null }
|
|
67
|
+
onSelect={ ( ...args ) => {
|
|
68
|
+
setSelected( args[ 0 ] );
|
|
69
|
+
props.onSelect?.( ...args );
|
|
70
|
+
} }
|
|
71
|
+
month={ month }
|
|
72
|
+
onMonthChange={ ( newMonth ) => {
|
|
73
|
+
setMonth( newMonth );
|
|
74
|
+
props.onMonthChange?.( newMonth );
|
|
75
|
+
} }
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function setupUserEvent() {
|
|
81
|
+
// The `advanceTimersByTime` is needed since we're using jest
|
|
82
|
+
// fake timers to simulate a fixed date for tests.
|
|
83
|
+
const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
|
|
84
|
+
return user;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe( 'DateCalendar', () => {
|
|
88
|
+
let today: Date;
|
|
89
|
+
let tomorrow: Date;
|
|
90
|
+
let yesterday: Date;
|
|
91
|
+
let currentMonth: Date;
|
|
92
|
+
let nextMonth: Date;
|
|
93
|
+
let nextNextMonth: Date;
|
|
94
|
+
let prevMonth: Date;
|
|
95
|
+
let prevPrevMonth: Date;
|
|
96
|
+
|
|
97
|
+
beforeAll( () => {
|
|
98
|
+
jest.useFakeTimers();
|
|
99
|
+
// For consistent tests, set the system time to a fixed date:
|
|
100
|
+
// Thursday, May 15, 2025, 20:00 UTC
|
|
101
|
+
jest.setSystemTime( 1747339200000 );
|
|
102
|
+
today = startOfDay( new Date() );
|
|
103
|
+
tomorrow = startOfDay( addDays( today, 1 ) );
|
|
104
|
+
yesterday = startOfDay( subDays( today, 1 ) );
|
|
105
|
+
currentMonth = startOfMonth( today );
|
|
106
|
+
nextMonth = startOfMonth( addMonths( today, 1 ) );
|
|
107
|
+
nextNextMonth = startOfMonth( addMonths( today, 2 ) );
|
|
108
|
+
prevMonth = startOfMonth( subMonths( today, 1 ) );
|
|
109
|
+
prevPrevMonth = startOfMonth( subMonths( today, 2 ) );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
afterAll( () => {
|
|
113
|
+
jest.useRealTimers();
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
describe( 'Semantics and basic behavior', () => {
|
|
117
|
+
it( 'should apply the correct roles, semantics and attributes', async () => {
|
|
118
|
+
render( <DateCalendar /> );
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
screen.getByRole( 'application', { name: 'Date calendar' } )
|
|
122
|
+
).toBeVisible();
|
|
123
|
+
|
|
124
|
+
const tableGrid = screen.getByRole( 'grid', {
|
|
125
|
+
name: monthNameFormatter( 'en-US' ).format( today ),
|
|
126
|
+
} );
|
|
127
|
+
expect( tableGrid ).toBeVisible();
|
|
128
|
+
expect( tableGrid ).toHaveAttribute(
|
|
129
|
+
'aria-multiselectable',
|
|
130
|
+
'false'
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const todayButton = getDateButton( today );
|
|
134
|
+
expect( todayButton ).toBeVisible();
|
|
135
|
+
expect( todayButton ).toHaveAccessibleName( /today/i );
|
|
136
|
+
} );
|
|
137
|
+
|
|
138
|
+
it( 'should show multiple months at once via the `numberOfMonths` prop', () => {
|
|
139
|
+
render( <DateCalendar numberOfMonths={ 2 } /> );
|
|
140
|
+
|
|
141
|
+
const grids = screen.getAllByRole( 'grid' );
|
|
142
|
+
expect( grids ).toHaveLength( 2 );
|
|
143
|
+
expect( grids[ 0 ] ).toHaveAccessibleName(
|
|
144
|
+
monthNameFormatter( 'en-US' ).format( today )
|
|
145
|
+
);
|
|
146
|
+
expect( grids[ 1 ] ).toHaveAccessibleName(
|
|
147
|
+
monthNameFormatter( 'en-US' ).format( nextMonth )
|
|
148
|
+
);
|
|
149
|
+
} );
|
|
150
|
+
} );
|
|
151
|
+
|
|
152
|
+
describe( 'Date selection', () => {
|
|
153
|
+
it( 'should select an initial date in uncontrolled mode via the `defaultSelected` prop', () => {
|
|
154
|
+
render( <DateCalendar defaultSelected={ today } /> );
|
|
155
|
+
|
|
156
|
+
expect( getDateCell( today, { selected: true } ) ).toBeVisible();
|
|
157
|
+
|
|
158
|
+
const todayButton = getDateButton( today );
|
|
159
|
+
expect( todayButton ).toBeVisible();
|
|
160
|
+
expect( todayButton ).toHaveAccessibleName( /selected/i );
|
|
161
|
+
} );
|
|
162
|
+
|
|
163
|
+
it( 'should select an initial date in controlled mode via the `selected` prop', () => {
|
|
164
|
+
// Note: the `defaultSelected` prop is ignored when the `selected` prop is set.
|
|
165
|
+
render(
|
|
166
|
+
<DateCalendar defaultSelected={ tomorrow } selected={ today } />
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect( getDateCell( today, { selected: true } ) ).toBeVisible();
|
|
170
|
+
|
|
171
|
+
const todayButton = getDateButton( today );
|
|
172
|
+
expect( todayButton ).toBeVisible();
|
|
173
|
+
expect( todayButton ).toHaveAccessibleName( /selected/i );
|
|
174
|
+
} );
|
|
175
|
+
|
|
176
|
+
it( 'should have no date selected in uncontrolled mode when the `selected` and `defaultSelected` props are set to `undefined`', () => {
|
|
177
|
+
render( <DateCalendar /> );
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
screen.queryByRole( 'gridcell', { selected: true } )
|
|
181
|
+
).not.toBeInTheDocument();
|
|
182
|
+
expect(
|
|
183
|
+
screen.queryByRole( 'button', { name: /selected/i } )
|
|
184
|
+
).not.toBeInTheDocument();
|
|
185
|
+
} );
|
|
186
|
+
|
|
187
|
+
it( 'should have no date selected in controlled mode when the `selected` prop is set to `null`', () => {
|
|
188
|
+
// Note: the `defaultSelected` prop is ignored when the `selected` prop is set.
|
|
189
|
+
render(
|
|
190
|
+
<DateCalendar defaultSelected={ tomorrow } selected={ null } />
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(
|
|
194
|
+
screen.queryByRole( 'gridcell', { selected: true } )
|
|
195
|
+
).not.toBeInTheDocument();
|
|
196
|
+
expect(
|
|
197
|
+
screen.queryByRole( 'button', { name: /selected/i } )
|
|
198
|
+
).not.toBeInTheDocument();
|
|
199
|
+
} );
|
|
200
|
+
|
|
201
|
+
it( 'should select a date in uncontrolled mode via the `defaultSelected` prop even if the date is disabled`', () => {
|
|
202
|
+
render(
|
|
203
|
+
<DateCalendar
|
|
204
|
+
defaultSelected={ tomorrow }
|
|
205
|
+
disabled={ tomorrow }
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect( getDateCell( tomorrow, { selected: true } ) ).toBeVisible();
|
|
210
|
+
|
|
211
|
+
const tomorrowButton = getDateButton( tomorrow );
|
|
212
|
+
expect( tomorrowButton ).toBeVisible();
|
|
213
|
+
expect( tomorrowButton ).toHaveAccessibleName( /selected/i );
|
|
214
|
+
expect( tomorrowButton ).toBeDisabled();
|
|
215
|
+
} );
|
|
216
|
+
|
|
217
|
+
it( 'should select a date in controlled mode via the `selected` prop even if the date is disabled`', () => {
|
|
218
|
+
render(
|
|
219
|
+
<DateCalendar selected={ tomorrow } disabled={ tomorrow } />
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect( getDateCell( tomorrow, { selected: true } ) ).toBeVisible();
|
|
223
|
+
|
|
224
|
+
const tomorrowButton = getDateButton( tomorrow );
|
|
225
|
+
expect( tomorrowButton ).toBeVisible();
|
|
226
|
+
expect( tomorrowButton ).toHaveAccessibleName( /selected/i );
|
|
227
|
+
expect( tomorrowButton ).toBeDisabled();
|
|
228
|
+
} );
|
|
229
|
+
|
|
230
|
+
describe.each( [
|
|
231
|
+
[ 'Uncontrolled', UncontrolledDateCalendar ],
|
|
232
|
+
[ 'Controlled', ControlledDateCalendar ],
|
|
233
|
+
] )( '[`%s`]', ( _mode, Component ) => {
|
|
234
|
+
it( 'should select a date when a date button is clicked', async () => {
|
|
235
|
+
const user = setupUserEvent();
|
|
236
|
+
const onSelect = jest.fn();
|
|
237
|
+
|
|
238
|
+
render( <Component onSelect={ onSelect } /> );
|
|
239
|
+
|
|
240
|
+
const todayButton = getDateButton( today );
|
|
241
|
+
await user.click( todayButton );
|
|
242
|
+
|
|
243
|
+
expect( onSelect ).toHaveBeenCalledTimes( 1 );
|
|
244
|
+
expect( onSelect ).toHaveBeenCalledWith(
|
|
245
|
+
today,
|
|
246
|
+
today,
|
|
247
|
+
expect.objectContaining( { today: true } ),
|
|
248
|
+
expect.objectContaining( {
|
|
249
|
+
type: 'click',
|
|
250
|
+
target: todayButton,
|
|
251
|
+
} )
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(
|
|
255
|
+
getDateCell( today, { selected: true } )
|
|
256
|
+
).toBeVisible();
|
|
257
|
+
} );
|
|
258
|
+
|
|
259
|
+
it( 'should not select a disabled date when a date button is clicked', async () => {
|
|
260
|
+
const user = setupUserEvent();
|
|
261
|
+
const onSelect = jest.fn();
|
|
262
|
+
|
|
263
|
+
render(
|
|
264
|
+
<Component onSelect={ onSelect } disabled={ tomorrow } />
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await user.click( getDateButton( tomorrow ) );
|
|
268
|
+
|
|
269
|
+
expect( onSelect ).not.toHaveBeenCalled();
|
|
270
|
+
expect(
|
|
271
|
+
screen.queryByRole( 'button', { name: /selected/i } )
|
|
272
|
+
).not.toBeInTheDocument();
|
|
273
|
+
} );
|
|
274
|
+
|
|
275
|
+
it( 'should select a new date when a different date button is clicked', async () => {
|
|
276
|
+
const user = setupUserEvent();
|
|
277
|
+
const onSelect = jest.fn();
|
|
278
|
+
|
|
279
|
+
render(
|
|
280
|
+
<Component
|
|
281
|
+
initialSelected={ today }
|
|
282
|
+
onSelect={ onSelect }
|
|
283
|
+
/>
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const tomorrowButton = getDateButton( tomorrow );
|
|
287
|
+
await user.click( tomorrowButton );
|
|
288
|
+
|
|
289
|
+
expect( onSelect ).toHaveBeenCalledTimes( 1 );
|
|
290
|
+
expect( onSelect ).toHaveBeenCalledWith(
|
|
291
|
+
tomorrow,
|
|
292
|
+
tomorrow,
|
|
293
|
+
expect.objectContaining( { today: false } ),
|
|
294
|
+
expect.objectContaining( {
|
|
295
|
+
type: 'click',
|
|
296
|
+
target: tomorrowButton,
|
|
297
|
+
} )
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(
|
|
301
|
+
getDateCell( tomorrow, { selected: true } )
|
|
302
|
+
).toBeVisible();
|
|
303
|
+
} );
|
|
304
|
+
|
|
305
|
+
it( 'should de-select the selected date when the selected date button is clicked', async () => {
|
|
306
|
+
const user = setupUserEvent();
|
|
307
|
+
const onSelect = jest.fn();
|
|
308
|
+
|
|
309
|
+
render(
|
|
310
|
+
<Component
|
|
311
|
+
initialSelected={ today }
|
|
312
|
+
onSelect={ onSelect }
|
|
313
|
+
/>
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const todayButton = getDateButton( today );
|
|
317
|
+
await user.click( todayButton );
|
|
318
|
+
|
|
319
|
+
expect( onSelect ).toHaveBeenCalledTimes( 1 );
|
|
320
|
+
expect( onSelect ).toHaveBeenCalledWith(
|
|
321
|
+
undefined,
|
|
322
|
+
today,
|
|
323
|
+
expect.objectContaining( { today: true, selected: true } ),
|
|
324
|
+
expect.objectContaining( {
|
|
325
|
+
type: 'click',
|
|
326
|
+
target: todayButton,
|
|
327
|
+
} )
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(
|
|
331
|
+
queryDateCell( today, { selected: true } )
|
|
332
|
+
).not.toBeInTheDocument();
|
|
333
|
+
} );
|
|
334
|
+
|
|
335
|
+
it( 'should not de-select the selected date when the selected date button is clicked if the `required` prop is set to `true`', async () => {
|
|
336
|
+
const user = setupUserEvent();
|
|
337
|
+
const onSelect = jest.fn();
|
|
338
|
+
|
|
339
|
+
render(
|
|
340
|
+
<Component
|
|
341
|
+
initialSelected={ today }
|
|
342
|
+
onSelect={ onSelect }
|
|
343
|
+
required
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const todayButton = getDateButton( today );
|
|
348
|
+
await user.click( todayButton );
|
|
349
|
+
|
|
350
|
+
expect( onSelect ).toHaveBeenCalledTimes( 1 );
|
|
351
|
+
expect( onSelect ).toHaveBeenCalledWith(
|
|
352
|
+
today,
|
|
353
|
+
today,
|
|
354
|
+
expect.objectContaining( { today: true, selected: true } ),
|
|
355
|
+
expect.objectContaining( {
|
|
356
|
+
type: 'click',
|
|
357
|
+
target: todayButton,
|
|
358
|
+
} )
|
|
359
|
+
);
|
|
360
|
+
expect(
|
|
361
|
+
queryDateCell( today, { selected: true } )
|
|
362
|
+
).toBeVisible();
|
|
363
|
+
} );
|
|
364
|
+
} );
|
|
365
|
+
} );
|
|
366
|
+
|
|
367
|
+
describe( 'Month navigation', () => {
|
|
368
|
+
it( 'should select an initial month in uncontrolled mode via the `defaultMonth` prop', () => {
|
|
369
|
+
render( <DateCalendar defaultMonth={ nextMonth } /> );
|
|
370
|
+
|
|
371
|
+
expect(
|
|
372
|
+
screen.getByRole( 'grid', {
|
|
373
|
+
name: monthNameFormatter( 'en-US' ).format( nextMonth ),
|
|
374
|
+
} )
|
|
375
|
+
).toBeVisible();
|
|
376
|
+
expect( getDateCell( nextMonth ) ).toBeVisible();
|
|
377
|
+
expect( getDateButton( nextMonth ) ).toBeVisible();
|
|
378
|
+
} );
|
|
379
|
+
|
|
380
|
+
it( 'should select an initial month in controlled mode via the `month` prop', () => {
|
|
381
|
+
render( <DateCalendar month={ nextMonth } /> );
|
|
382
|
+
|
|
383
|
+
expect(
|
|
384
|
+
screen.getByRole( 'grid', {
|
|
385
|
+
name: monthNameFormatter( 'en-US' ).format( nextMonth ),
|
|
386
|
+
} )
|
|
387
|
+
).toBeVisible();
|
|
388
|
+
expect( getDateCell( nextMonth ) ).toBeVisible();
|
|
389
|
+
expect( getDateButton( nextMonth ) ).toBeVisible();
|
|
390
|
+
} );
|
|
391
|
+
|
|
392
|
+
describe.each( [
|
|
393
|
+
[ 'Uncontrolled', UncontrolledDateCalendar ],
|
|
394
|
+
[ 'Controlled', ControlledDateCalendar ],
|
|
395
|
+
] )( '[`%s`]', ( _mode, Component ) => {
|
|
396
|
+
it( 'should navigate to the previous and next months when the previous and next month buttons are clicked', async () => {
|
|
397
|
+
const user = setupUserEvent();
|
|
398
|
+
const onMonthChange = jest.fn();
|
|
399
|
+
|
|
400
|
+
render( <Component onMonthChange={ onMonthChange } /> );
|
|
401
|
+
|
|
402
|
+
const prevButton = screen.getByRole( 'button', {
|
|
403
|
+
name: /previous month/i,
|
|
404
|
+
} );
|
|
405
|
+
const nextButton = screen.getByRole( 'button', {
|
|
406
|
+
name: /next month/i,
|
|
407
|
+
} );
|
|
408
|
+
await user.click( prevButton );
|
|
409
|
+
|
|
410
|
+
expect( onMonthChange ).toHaveBeenCalledTimes( 1 );
|
|
411
|
+
expect( onMonthChange ).toHaveBeenCalledWith( prevMonth );
|
|
412
|
+
|
|
413
|
+
expect(
|
|
414
|
+
screen.getByRole( 'grid', {
|
|
415
|
+
name: monthNameFormatter( 'en-US' ).format( prevMonth ),
|
|
416
|
+
} )
|
|
417
|
+
).toBeVisible();
|
|
418
|
+
expect( getDateCell( prevMonth ) ).toBeVisible();
|
|
419
|
+
expect( getDateButton( prevMonth ) ).toBeVisible();
|
|
420
|
+
|
|
421
|
+
await user.click( nextButton );
|
|
422
|
+
|
|
423
|
+
expect( onMonthChange ).toHaveBeenCalledTimes( 2 );
|
|
424
|
+
expect( onMonthChange ).toHaveBeenCalledWith( currentMonth );
|
|
425
|
+
|
|
426
|
+
expect(
|
|
427
|
+
screen.getByRole( 'grid', {
|
|
428
|
+
name: monthNameFormatter( 'en-US' ).format(
|
|
429
|
+
currentMonth
|
|
430
|
+
),
|
|
431
|
+
} )
|
|
432
|
+
).toBeVisible();
|
|
433
|
+
expect( getDateCell( currentMonth ) ).toBeVisible();
|
|
434
|
+
expect( getDateButton( currentMonth ) ).toBeVisible();
|
|
435
|
+
|
|
436
|
+
await user.click( nextButton );
|
|
437
|
+
|
|
438
|
+
expect( onMonthChange ).toHaveBeenCalledTimes( 3 );
|
|
439
|
+
expect( onMonthChange ).toHaveBeenCalledWith( nextMonth );
|
|
440
|
+
|
|
441
|
+
expect(
|
|
442
|
+
screen.getByRole( 'grid', {
|
|
443
|
+
name: monthNameFormatter( 'en-US' ).format( nextMonth ),
|
|
444
|
+
} )
|
|
445
|
+
).toBeVisible();
|
|
446
|
+
expect( getDateCell( nextMonth ) ).toBeVisible();
|
|
447
|
+
expect( getDateButton( nextMonth ) ).toBeVisible();
|
|
448
|
+
} );
|
|
449
|
+
|
|
450
|
+
it( 'should not navigate to a month that is before the `startMonth` prop', async () => {
|
|
451
|
+
const user = setupUserEvent();
|
|
452
|
+
const onMonthChange = jest.fn();
|
|
453
|
+
|
|
454
|
+
render(
|
|
455
|
+
<Component
|
|
456
|
+
startMonth={ nextMonth }
|
|
457
|
+
onMonthChange={ onMonthChange }
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const prevButton = screen.getByRole( 'button', {
|
|
462
|
+
name: /previous month/i,
|
|
463
|
+
} );
|
|
464
|
+
const nextButton = screen.getByRole( 'button', {
|
|
465
|
+
name: /next month/i,
|
|
466
|
+
} );
|
|
467
|
+
|
|
468
|
+
expect(
|
|
469
|
+
screen.getByRole( 'grid', {
|
|
470
|
+
name: monthNameFormatter( 'en-US' ).format( nextMonth ),
|
|
471
|
+
} )
|
|
472
|
+
).toBeVisible();
|
|
473
|
+
expect( getDateCell( nextMonth ) ).toBeVisible();
|
|
474
|
+
expect( getDateButton( nextMonth ) ).toBeVisible();
|
|
475
|
+
|
|
476
|
+
expect( prevButton ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
477
|
+
|
|
478
|
+
await user.click( prevButton );
|
|
479
|
+
|
|
480
|
+
expect( onMonthChange ).not.toHaveBeenCalled();
|
|
481
|
+
|
|
482
|
+
await user.click( nextButton );
|
|
483
|
+
|
|
484
|
+
expect( onMonthChange ).toHaveBeenCalledTimes( 1 );
|
|
485
|
+
expect( onMonthChange ).toHaveBeenCalledWith( nextNextMonth );
|
|
486
|
+
|
|
487
|
+
expect(
|
|
488
|
+
screen.getByRole( 'grid', {
|
|
489
|
+
name: monthNameFormatter( 'en-US' ).format(
|
|
490
|
+
nextNextMonth
|
|
491
|
+
),
|
|
492
|
+
} )
|
|
493
|
+
).toBeVisible();
|
|
494
|
+
expect( getDateCell( nextNextMonth ) ).toBeVisible();
|
|
495
|
+
expect( getDateButton( nextNextMonth ) ).toBeVisible();
|
|
496
|
+
|
|
497
|
+
expect( prevButton ).not.toHaveAttribute( 'aria-disabled' );
|
|
498
|
+
} );
|
|
499
|
+
|
|
500
|
+
it( 'should not navigate to a month that is after the `endMonth` prop', async () => {
|
|
501
|
+
const user = setupUserEvent();
|
|
502
|
+
const onMonthChange = jest.fn();
|
|
503
|
+
|
|
504
|
+
render(
|
|
505
|
+
<Component
|
|
506
|
+
endMonth={ prevMonth }
|
|
507
|
+
onMonthChange={ onMonthChange }
|
|
508
|
+
/>
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const prevButton = screen.getByRole( 'button', {
|
|
512
|
+
name: /previous month/i,
|
|
513
|
+
} );
|
|
514
|
+
const nextButton = screen.getByRole( 'button', {
|
|
515
|
+
name: /next month/i,
|
|
516
|
+
} );
|
|
517
|
+
|
|
518
|
+
expect(
|
|
519
|
+
screen.getByRole( 'grid', {
|
|
520
|
+
name: monthNameFormatter( 'en-US' ).format( prevMonth ),
|
|
521
|
+
} )
|
|
522
|
+
).toBeVisible();
|
|
523
|
+
expect( getDateCell( prevMonth ) ).toBeVisible();
|
|
524
|
+
expect( getDateButton( prevMonth ) ).toBeVisible();
|
|
525
|
+
|
|
526
|
+
expect( nextButton ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
527
|
+
|
|
528
|
+
await user.click( nextButton );
|
|
529
|
+
|
|
530
|
+
expect( onMonthChange ).not.toHaveBeenCalled();
|
|
531
|
+
|
|
532
|
+
await user.click( prevButton );
|
|
533
|
+
|
|
534
|
+
expect( onMonthChange ).toHaveBeenCalledTimes( 1 );
|
|
535
|
+
expect( onMonthChange ).toHaveBeenCalledWith( prevPrevMonth );
|
|
536
|
+
|
|
537
|
+
expect(
|
|
538
|
+
screen.getByRole( 'grid', {
|
|
539
|
+
name: monthNameFormatter( 'en-US' ).format(
|
|
540
|
+
prevPrevMonth
|
|
541
|
+
),
|
|
542
|
+
} )
|
|
543
|
+
).toBeVisible();
|
|
544
|
+
expect( getDateCell( prevPrevMonth ) ).toBeVisible();
|
|
545
|
+
expect( getDateButton( prevPrevMonth ) ).toBeVisible();
|
|
546
|
+
|
|
547
|
+
expect( nextButton ).not.toHaveAttribute( 'aria-disabled' );
|
|
548
|
+
} );
|
|
549
|
+
} );
|
|
550
|
+
} );
|
|
551
|
+
|
|
552
|
+
describe( 'Keyboard focus and navigation', () => {
|
|
553
|
+
it( 'should auto-focus the selected day when the `autoFocus` prop is set to `true`', async () => {
|
|
554
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
555
|
+
render( <DateCalendar autoFocus defaultSelected={ tomorrow } /> );
|
|
556
|
+
expect( getDateButton( tomorrow ) ).toHaveFocus();
|
|
557
|
+
} );
|
|
558
|
+
|
|
559
|
+
it( "should auto-focus today's date if there is not selected date when the `autoFocus` prop is set to `true`", async () => {
|
|
560
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
561
|
+
render( <DateCalendar autoFocus /> );
|
|
562
|
+
expect( getDateButton( today ) ).toHaveFocus();
|
|
563
|
+
} );
|
|
564
|
+
|
|
565
|
+
it( 'should focus each arrow as a tab stop, but treat the grid as a 2d composite widget', async () => {
|
|
566
|
+
const user = setupUserEvent();
|
|
567
|
+
render( <DateCalendar /> );
|
|
568
|
+
|
|
569
|
+
// Focus previous month button
|
|
570
|
+
await user.tab();
|
|
571
|
+
expect(
|
|
572
|
+
screen.getByRole( 'button', { name: /previous month/i } )
|
|
573
|
+
).toHaveFocus();
|
|
574
|
+
|
|
575
|
+
// Focus next month button
|
|
576
|
+
await user.tab();
|
|
577
|
+
expect(
|
|
578
|
+
screen.getByRole( 'button', { name: /next month/i } )
|
|
579
|
+
).toHaveFocus();
|
|
580
|
+
|
|
581
|
+
// Focus today button
|
|
582
|
+
await user.tab();
|
|
583
|
+
expect( getDateButton( today ) ).toHaveFocus();
|
|
584
|
+
|
|
585
|
+
// Focus next day
|
|
586
|
+
await user.keyboard( '{ArrowRight}' );
|
|
587
|
+
expect( getDateButton( addDays( today, 1 ) ) ).toHaveFocus();
|
|
588
|
+
|
|
589
|
+
// Focus to next week
|
|
590
|
+
await user.keyboard( '{ArrowDown}' );
|
|
591
|
+
expect( getDateButton( addDays( today, 8 ) ) ).toHaveFocus();
|
|
592
|
+
|
|
593
|
+
// Focus previous day
|
|
594
|
+
await user.keyboard( '{ArrowLeft}' );
|
|
595
|
+
expect( getDateButton( addDays( today, 7 ) ) ).toHaveFocus();
|
|
596
|
+
|
|
597
|
+
// Focus previous week
|
|
598
|
+
await user.keyboard( '{ArrowUp}' );
|
|
599
|
+
expect( getDateButton( today ) ).toHaveFocus();
|
|
600
|
+
|
|
601
|
+
// Focus first day of week
|
|
602
|
+
await user.keyboard( '{Home}' );
|
|
603
|
+
expect( getDateButton( startOfWeek( today ) ) ).toHaveFocus();
|
|
604
|
+
|
|
605
|
+
// Focus last day of week
|
|
606
|
+
await user.keyboard( '{End}' );
|
|
607
|
+
expect( getDateButton( endOfWeek( today ) ) ).toHaveFocus();
|
|
608
|
+
|
|
609
|
+
// Focus previous month
|
|
610
|
+
await user.keyboard( '{PageUp}' );
|
|
611
|
+
expect(
|
|
612
|
+
getDateButton( subMonths( endOfWeek( today ), 1 ) )
|
|
613
|
+
).toHaveFocus();
|
|
614
|
+
|
|
615
|
+
expect(
|
|
616
|
+
screen.getByRole( 'grid', {
|
|
617
|
+
name: monthNameFormatter( 'en-US' ).format(
|
|
618
|
+
subMonths( endOfWeek( today ), 1 )
|
|
619
|
+
),
|
|
620
|
+
} )
|
|
621
|
+
).toBeVisible();
|
|
622
|
+
|
|
623
|
+
// Navigate to next month
|
|
624
|
+
await user.keyboard( '{PageDown}' );
|
|
625
|
+
expect( getDateButton( endOfWeek( today ) ) ).toHaveFocus();
|
|
626
|
+
expect(
|
|
627
|
+
screen.getByRole( 'grid', {
|
|
628
|
+
name: monthNameFormatter( 'en-US' ).format(
|
|
629
|
+
endOfWeek( today )
|
|
630
|
+
),
|
|
631
|
+
} )
|
|
632
|
+
).toBeVisible();
|
|
633
|
+
|
|
634
|
+
// Focus previous year
|
|
635
|
+
await user.keyboard( '{Shift>}{PageUp}{/Shift}' );
|
|
636
|
+
expect(
|
|
637
|
+
getDateButton( subYears( endOfWeek( today ), 1 ) )
|
|
638
|
+
).toHaveFocus();
|
|
639
|
+
|
|
640
|
+
expect(
|
|
641
|
+
screen.getByRole( 'grid', {
|
|
642
|
+
name: monthNameFormatter( 'en-US' ).format(
|
|
643
|
+
subYears( endOfWeek( today ), 1 )
|
|
644
|
+
),
|
|
645
|
+
} )
|
|
646
|
+
).toBeVisible();
|
|
647
|
+
|
|
648
|
+
// Focus next year
|
|
649
|
+
await user.keyboard( '{Shift>}{PageDown}{/Shift}' );
|
|
650
|
+
expect( getDateButton( endOfWeek( today ) ) ).toHaveFocus();
|
|
651
|
+
|
|
652
|
+
expect(
|
|
653
|
+
screen.getByRole( 'grid', {
|
|
654
|
+
name: monthNameFormatter( 'en-US' ).format(
|
|
655
|
+
endOfWeek( today )
|
|
656
|
+
),
|
|
657
|
+
} )
|
|
658
|
+
).toBeVisible();
|
|
659
|
+
} );
|
|
660
|
+
|
|
661
|
+
// Note: the following test is not testing advanced keyboard interactions
|
|
662
|
+
// (pageUp, pageDown, shift+pageUp, shift+pageDown, home, end)
|
|
663
|
+
it( 'should not focus disabled dates and skip over them when navigating using arrow keys', async () => {
|
|
664
|
+
const user = setupUserEvent();
|
|
665
|
+
|
|
666
|
+
render(
|
|
667
|
+
<DateCalendar
|
|
668
|
+
disabled={ [
|
|
669
|
+
tomorrow,
|
|
670
|
+
addWeeks( addDays( tomorrow, 1 ), 1 ),
|
|
671
|
+
addWeeks( today, 2 ),
|
|
672
|
+
addWeeks( tomorrow, 2 ),
|
|
673
|
+
] }
|
|
674
|
+
/>
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
await user.tab();
|
|
678
|
+
await user.tab();
|
|
679
|
+
await user.tab();
|
|
680
|
+
expect( getDateButton( today ) ).toHaveFocus();
|
|
681
|
+
|
|
682
|
+
await user.keyboard( '{ArrowRight}' );
|
|
683
|
+
expect( getDateButton( addDays( tomorrow, 1 ) ) ).toHaveFocus();
|
|
684
|
+
|
|
685
|
+
await user.keyboard( '{ArrowDown}' );
|
|
686
|
+
expect(
|
|
687
|
+
getDateButton( addWeeks( addDays( tomorrow, 1 ), 2 ) )
|
|
688
|
+
).toHaveFocus();
|
|
689
|
+
|
|
690
|
+
await user.keyboard( '{ArrowLeft}' );
|
|
691
|
+
expect( getDateButton( addWeeks( yesterday, 2 ) ) ).toHaveFocus();
|
|
692
|
+
|
|
693
|
+
await user.keyboard( '{ArrowUp}' );
|
|
694
|
+
expect( getDateButton( addWeeks( yesterday, 1 ) ) ).toHaveFocus();
|
|
695
|
+
} );
|
|
696
|
+
|
|
697
|
+
it( 'should focus the selected date when tabbing into the calendar', async () => {
|
|
698
|
+
const user = setupUserEvent();
|
|
699
|
+
render( <DateCalendar selected={ tomorrow } /> );
|
|
700
|
+
|
|
701
|
+
// Tab to the calendar grid
|
|
702
|
+
await user.tab();
|
|
703
|
+
await user.tab();
|
|
704
|
+
await user.tab();
|
|
705
|
+
|
|
706
|
+
expect( getDateButton( tomorrow ) ).toHaveFocus();
|
|
707
|
+
} );
|
|
708
|
+
} );
|
|
709
|
+
|
|
710
|
+
describe( 'Disabled states', () => {
|
|
711
|
+
it( 'should support disabling all dates via the `disabled` prop', async () => {
|
|
712
|
+
const user = setupUserEvent();
|
|
713
|
+
|
|
714
|
+
render( <DateCalendar disabled /> );
|
|
715
|
+
|
|
716
|
+
within( screen.getByRole( 'grid' ) )
|
|
717
|
+
.getAllByRole( 'button' )
|
|
718
|
+
.forEach( ( button ) => {
|
|
719
|
+
expect( button ).toBeDisabled();
|
|
720
|
+
} );
|
|
721
|
+
|
|
722
|
+
await user.click(
|
|
723
|
+
screen.getByRole( 'button', { name: /previous/i } )
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
within( screen.getByRole( 'grid' ) )
|
|
727
|
+
.getAllByRole( 'button' )
|
|
728
|
+
.forEach( ( button ) => {
|
|
729
|
+
expect( button ).toBeDisabled();
|
|
730
|
+
} );
|
|
731
|
+
|
|
732
|
+
await user.click( screen.getByRole( 'button', { name: /next/i } ) );
|
|
733
|
+
await user.click( screen.getByRole( 'button', { name: /next/i } ) );
|
|
734
|
+
|
|
735
|
+
within( screen.getByRole( 'grid' ) )
|
|
736
|
+
.getAllByRole( 'button' )
|
|
737
|
+
.forEach( ( button ) => {
|
|
738
|
+
expect( button ).toBeDisabled();
|
|
739
|
+
} );
|
|
740
|
+
} );
|
|
741
|
+
|
|
742
|
+
it( 'should support disabling single dates via the `disabled` prop', async () => {
|
|
743
|
+
render( <DateCalendar disabled={ tomorrow } /> );
|
|
744
|
+
|
|
745
|
+
expect( getDateButton( tomorrow ) ).toBeDisabled();
|
|
746
|
+
} );
|
|
747
|
+
|
|
748
|
+
it( 'should support passing a custom function via the `disabled` prop', async () => {
|
|
749
|
+
const primeNumbers = [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 ];
|
|
750
|
+
render(
|
|
751
|
+
<DateCalendar
|
|
752
|
+
disabled={ ( date ) =>
|
|
753
|
+
primeNumbers.includes( date.getDate() )
|
|
754
|
+
}
|
|
755
|
+
/>
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
for ( const date of primeNumbers ) {
|
|
759
|
+
expect(
|
|
760
|
+
getDateButton(
|
|
761
|
+
new Date( today.getFullYear(), today.getMonth(), date )
|
|
762
|
+
)
|
|
763
|
+
).toBeDisabled();
|
|
764
|
+
}
|
|
765
|
+
} );
|
|
766
|
+
|
|
767
|
+
it( 'should support disabling all dates before a certain date via the `disabled` prop', async () => {
|
|
768
|
+
render( <DateCalendar disabled={ { before: today } } /> );
|
|
769
|
+
|
|
770
|
+
for ( let date = 1; date < today.getDate(); date++ ) {
|
|
771
|
+
expect(
|
|
772
|
+
getDateButton(
|
|
773
|
+
new Date( today.getFullYear(), today.getMonth(), date )
|
|
774
|
+
)
|
|
775
|
+
).toBeDisabled();
|
|
776
|
+
}
|
|
777
|
+
expect( getDateButton( today ) ).toBeEnabled();
|
|
778
|
+
} );
|
|
779
|
+
|
|
780
|
+
it( 'should support disabling all dates after a certain date via the `disabled` prop', async () => {
|
|
781
|
+
render( <DateCalendar disabled={ { after: today } } /> );
|
|
782
|
+
|
|
783
|
+
for ( let date = today.getDate() + 1; date < 32; date++ ) {
|
|
784
|
+
expect(
|
|
785
|
+
getDateButton(
|
|
786
|
+
new Date( today.getFullYear(), today.getMonth(), date )
|
|
787
|
+
)
|
|
788
|
+
).toBeDisabled();
|
|
789
|
+
}
|
|
790
|
+
expect( getDateButton( today ) ).toBeEnabled();
|
|
791
|
+
} );
|
|
792
|
+
|
|
793
|
+
it( 'should support disabling all dates before a certain date and after a certain date via the `disabled` prop', async () => {
|
|
794
|
+
render(
|
|
795
|
+
<DateCalendar
|
|
796
|
+
disabled={ {
|
|
797
|
+
before: yesterday,
|
|
798
|
+
after: addDays( today, 1 ),
|
|
799
|
+
} }
|
|
800
|
+
/>
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
let date;
|
|
804
|
+
|
|
805
|
+
for ( date = 1; date < today.getDate() - 1; date++ ) {
|
|
806
|
+
expect(
|
|
807
|
+
getDateButton(
|
|
808
|
+
new Date( today.getFullYear(), today.getMonth(), date )
|
|
809
|
+
)
|
|
810
|
+
).toBeDisabled();
|
|
811
|
+
}
|
|
812
|
+
expect( getDateButton( yesterday ) ).toBeEnabled();
|
|
813
|
+
expect( getDateButton( today ) ).toBeEnabled();
|
|
814
|
+
expect( getDateButton( addDays( today, 1 ) ) ).toBeEnabled();
|
|
815
|
+
|
|
816
|
+
for ( date = today.getDate() + 2; date < 32; date++ ) {
|
|
817
|
+
expect(
|
|
818
|
+
getDateButton(
|
|
819
|
+
new Date( today.getFullYear(), today.getMonth(), date )
|
|
820
|
+
)
|
|
821
|
+
).toBeDisabled();
|
|
822
|
+
}
|
|
823
|
+
} );
|
|
824
|
+
|
|
825
|
+
it( 'should support disabling all dates within a certain date range via the `disabled` prop', async () => {
|
|
826
|
+
render(
|
|
827
|
+
<DateCalendar
|
|
828
|
+
disabled={ { from: yesterday, to: addDays( today, 1 ) } }
|
|
829
|
+
/>
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
let date;
|
|
833
|
+
|
|
834
|
+
for ( date = 1; date < today.getDate() - 1; date++ ) {
|
|
835
|
+
expect(
|
|
836
|
+
getDateButton(
|
|
837
|
+
new Date( today.getFullYear(), today.getMonth(), date )
|
|
838
|
+
)
|
|
839
|
+
).toBeEnabled();
|
|
840
|
+
}
|
|
841
|
+
expect( getDateButton( yesterday ) ).toBeDisabled();
|
|
842
|
+
expect( getDateButton( today ) ).toBeDisabled();
|
|
843
|
+
expect( getDateButton( addDays( today, 1 ) ) ).toBeDisabled();
|
|
844
|
+
|
|
845
|
+
for ( date = today.getDate() + 2; date < 32; date++ ) {
|
|
846
|
+
expect(
|
|
847
|
+
getDateButton(
|
|
848
|
+
new Date( today.getFullYear(), today.getMonth(), date )
|
|
849
|
+
)
|
|
850
|
+
).toBeEnabled();
|
|
851
|
+
}
|
|
852
|
+
} );
|
|
853
|
+
|
|
854
|
+
it( 'should support disabling specific days of the week via the `disabled` prop', async () => {
|
|
855
|
+
const weekendsInMay = [ 3, 4, 10, 11, 17, 18, 24, 25, 31 ];
|
|
856
|
+
render( <DateCalendar disabled={ { dayOfWeek: [ 0, 6 ] } } /> );
|
|
857
|
+
|
|
858
|
+
for ( const date of weekendsInMay ) {
|
|
859
|
+
expect(
|
|
860
|
+
getDateButton(
|
|
861
|
+
new Date( today.getFullYear(), today.getMonth(), date )
|
|
862
|
+
)
|
|
863
|
+
).toBeDisabled();
|
|
864
|
+
}
|
|
865
|
+
} );
|
|
866
|
+
|
|
867
|
+
it( 'should disable the previous and next months buttons if the `disableNavigation` is set to `true`', async () => {
|
|
868
|
+
const user = setupUserEvent();
|
|
869
|
+
|
|
870
|
+
render( <DateCalendar disableNavigation /> );
|
|
871
|
+
|
|
872
|
+
expect(
|
|
873
|
+
screen.getByRole( 'button', { name: /previous month/i } )
|
|
874
|
+
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
875
|
+
expect(
|
|
876
|
+
screen.getByRole( 'button', { name: /next month/i } )
|
|
877
|
+
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
878
|
+
|
|
879
|
+
await user.tab();
|
|
880
|
+
expect(
|
|
881
|
+
screen.getByRole( 'button', { name: /today/i } )
|
|
882
|
+
).toHaveFocus();
|
|
883
|
+
} );
|
|
884
|
+
} );
|
|
885
|
+
|
|
886
|
+
// Note: we're not testing localization of strings. We're only testing
|
|
887
|
+
// that the date formatting, computed dir, and calendar format are correct.
|
|
888
|
+
describe( 'Localization', () => {
|
|
889
|
+
it( 'should localize the calendar based on the `locale` prop', async () => {
|
|
890
|
+
const user = setupUserEvent();
|
|
891
|
+
|
|
892
|
+
render( <DateCalendar locale={ ar } /> );
|
|
893
|
+
|
|
894
|
+
// Check computed writing direction
|
|
895
|
+
expect(
|
|
896
|
+
screen.getByRole( 'application', { name: 'Date calendar' } )
|
|
897
|
+
).toHaveAttribute( 'dir', 'rtl' );
|
|
898
|
+
|
|
899
|
+
// Check month name
|
|
900
|
+
const grid = screen.getByRole( 'grid', {
|
|
901
|
+
name: monthNameFormatter( 'ar' ).format( today ),
|
|
902
|
+
} );
|
|
903
|
+
expect( grid ).toBeVisible();
|
|
904
|
+
|
|
905
|
+
// Check today button
|
|
906
|
+
expect( getDateButton( today, {}, 'ar' ) ).toHaveAccessibleName(
|
|
907
|
+
/today/i
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
await user.tab();
|
|
911
|
+
await user.tab();
|
|
912
|
+
await user.tab();
|
|
913
|
+
expect( getDateButton( today, {}, 'ar' ) ).toHaveFocus();
|
|
914
|
+
|
|
915
|
+
await user.keyboard( '{Home}' );
|
|
916
|
+
expect(
|
|
917
|
+
getDateButton( startOfWeek( today, { locale: ar } ), {}, 'ar' )
|
|
918
|
+
).toHaveFocus();
|
|
919
|
+
} );
|
|
920
|
+
|
|
921
|
+
it( 'should support timezones according to the `timeZone` prop', async () => {
|
|
922
|
+
const user = setupUserEvent();
|
|
923
|
+
const onSelect = jest.fn();
|
|
924
|
+
|
|
925
|
+
render(
|
|
926
|
+
<DateCalendar timeZone="Asia/Tokyo" onSelect={ onSelect } />
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
// For someone in Tokyo, the current time simulated in the test
|
|
930
|
+
// (ie. 20:00 UTC) is the next day.
|
|
931
|
+
expect( getDateButton( tomorrow ) ).toHaveAccessibleName(
|
|
932
|
+
/today/i
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
// Select tomorrow's button (which is today in Tokyo)
|
|
936
|
+
const tomorrowButton = getDateButton( tomorrow );
|
|
937
|
+
await user.click( tomorrowButton );
|
|
938
|
+
|
|
939
|
+
const tomorrowFromTokyoTimezone = addHours(
|
|
940
|
+
tomorrow,
|
|
941
|
+
new TZDate( tomorrow, 'Asia/Tokyo' ).getTimezoneOffset() / 60
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
expect( onSelect ).toHaveBeenCalledWith(
|
|
945
|
+
tomorrowFromTokyoTimezone,
|
|
946
|
+
tomorrowFromTokyoTimezone,
|
|
947
|
+
expect.objectContaining( { today: true } ),
|
|
948
|
+
expect.objectContaining( {
|
|
949
|
+
type: 'click',
|
|
950
|
+
target: tomorrowButton,
|
|
951
|
+
} )
|
|
952
|
+
);
|
|
953
|
+
} );
|
|
954
|
+
|
|
955
|
+
it( 'should handle timezoned dates and convert them to the calendar timezone', async () => {
|
|
956
|
+
// Still the same time from UTC's POV, just expressed in Tokyo time.
|
|
957
|
+
const tomorrowAtMidnightInTokyo = new TZDate(
|
|
958
|
+
tomorrow,
|
|
959
|
+
'Asia/Tokyo'
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
render(
|
|
963
|
+
<DateCalendar
|
|
964
|
+
defaultSelected={ tomorrowAtMidnightInTokyo }
|
|
965
|
+
// Note: using "Etc/GMT+2" instead of "-02:00" because support for raw offsets was introduced in Node v22 (while currently the repository still targets Node v20).
|
|
966
|
+
timeZone="Etc/GMT+2"
|
|
967
|
+
/>
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
// Changing the calendar timezone to UTC-2 makes the dates become
|
|
971
|
+
// earlier by 1 day (from midnight to 10pm the previous day).
|
|
972
|
+
expect( getDateCell( today, { selected: true } ) ).toBeVisible();
|
|
973
|
+
} );
|
|
974
|
+
} );
|
|
975
|
+
} );
|