@ministryofjustice/frontend 2.2.5 → 3.0.1
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/moj/all.jquery.min.js +11 -3
- package/moj/all.js +327 -138
- package/moj/components/button-menu/_button-menu.scss +71 -109
- package/moj/components/button-menu/button-menu.js +292 -123
- package/moj/components/button-menu/button-menu.spec.js +363 -0
- package/moj/components/button-menu/template.njk +32 -6
- package/moj/components/date-picker/date-picker.js +29 -15
- package/moj/components/date-picker/date-picker.spec.js +571 -0
- package/moj/components/identity-bar/_identity-bar.scss +5 -9
- package/moj/components/identity-bar/template.njk +17 -22
- package/moj/components/page-header-actions/_page-header-actions.scss +9 -39
- package/moj/components/page-header-actions/template.njk +16 -16
- package/moj/objects/_all.scss +1 -0
- package/moj/objects/_button-group.scss +37 -0
- package/package.json +1 -1
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
getByText,
|
|
6
|
+
getByRole,
|
|
7
|
+
queryByRole,
|
|
8
|
+
queryByText,
|
|
9
|
+
screen,
|
|
10
|
+
} = require("@testing-library/dom");
|
|
11
|
+
const { userEvent } = require("@testing-library/user-event");
|
|
12
|
+
const { configureAxe, toHaveNoViolations } = require("jest-axe");
|
|
13
|
+
expect.extend(toHaveNoViolations);
|
|
14
|
+
|
|
15
|
+
require("../../../../jest.setup.js");
|
|
16
|
+
require("./date-picker.js");
|
|
17
|
+
|
|
18
|
+
const user = userEvent.setup();
|
|
19
|
+
const axe = configureAxe({
|
|
20
|
+
rules: {
|
|
21
|
+
// disable landmark rules when testing isolated components.
|
|
22
|
+
region: { enabled: false },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const createComponent = () => {
|
|
27
|
+
const html = `
|
|
28
|
+
<div class="moj-datepicker" data-module="moj-date-picker">
|
|
29
|
+
<div class="govuk-form-group">
|
|
30
|
+
<label class="govuk-label" for="date">
|
|
31
|
+
Date
|
|
32
|
+
</label>
|
|
33
|
+
<div id="date-hint" class="govuk-hint">
|
|
34
|
+
For example, 17/5/2024.
|
|
35
|
+
</div>
|
|
36
|
+
<input class="govuk-input moj-js-datepicker-input " id="date" name="date" type="text" aria-describedby="date-hint" autocomplete="off">
|
|
37
|
+
</div>
|
|
38
|
+
</div>`;
|
|
39
|
+
document.body.insertAdjacentHTML("afterbegin", html);
|
|
40
|
+
|
|
41
|
+
component = document.querySelector('[data-module="moj-date-picker"]');
|
|
42
|
+
return component;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const randomIntBetween = (min, max) => {
|
|
46
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const padToTwoDigits = (number) => {
|
|
50
|
+
return number.toString().padStart(2, "0");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getFirstDayOfWeek = (dateObject, firstDayOfWeekIndex) => {
|
|
54
|
+
const dayOfWeek = dateObject.getDay();
|
|
55
|
+
const firstDayOfWeek = new Date(dateObject);
|
|
56
|
+
const diff =
|
|
57
|
+
dayOfWeek >= firstDayOfWeekIndex
|
|
58
|
+
? dayOfWeek - firstDayOfWeekIndex
|
|
59
|
+
: 6 - dayOfWeek;
|
|
60
|
+
|
|
61
|
+
firstDayOfWeek.setDate(dateObject.getDate() - diff);
|
|
62
|
+
firstDayOfWeek.setHours(0, 0, 0, 0);
|
|
63
|
+
|
|
64
|
+
return firstDayOfWeek;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const getLastDayOfWeek = (dateObject, lastDayOfWeekIndex) => {
|
|
68
|
+
const dayOfWeek = dateObject.getDay();
|
|
69
|
+
const lastDayOfWeek = new Date(dateObject);
|
|
70
|
+
const diff =
|
|
71
|
+
dayOfWeek <= lastDayOfWeekIndex
|
|
72
|
+
? lastDayOfWeekIndex - dayOfWeek
|
|
73
|
+
: 7 - dayOfWeek;
|
|
74
|
+
|
|
75
|
+
lastDayOfWeek.setDate(dateObject.getDate() - diff);
|
|
76
|
+
lastDayOfWeek.setHours(0, 0, 0, 0);
|
|
77
|
+
|
|
78
|
+
return lastDayOfWeek;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
describe("Date picker with defaults", () => {
|
|
82
|
+
let component;
|
|
83
|
+
let calendarButton;
|
|
84
|
+
let dialog;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
component = createComponent();
|
|
88
|
+
new MOJFrontend.DatePicker(component, {}).init();
|
|
89
|
+
|
|
90
|
+
calendarButton = queryByText(component, "Choose date")?.closest("button");
|
|
91
|
+
dialog = queryByRole(component, "dialog", { hidden: true });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
document.body.innerHTML = "";
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("initialises calendar calendarButton and dialog", () => {
|
|
99
|
+
expect(calendarButton).not.toBeNull();
|
|
100
|
+
expect(dialog).not.toBeNull();
|
|
101
|
+
expect(component).toContainElement(calendarButton);
|
|
102
|
+
expect(component).toContainElement(dialog);
|
|
103
|
+
expect(dialog).not.toBeVisible();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("calendar button toggles dialog", async () => {
|
|
107
|
+
await user.click(calendarButton);
|
|
108
|
+
expect(dialog).toBeVisible();
|
|
109
|
+
|
|
110
|
+
await user.click(calendarButton);
|
|
111
|
+
expect(dialog).not.toBeVisible();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("dialog has required buttons", async () => {
|
|
115
|
+
await user.click(calendarButton);
|
|
116
|
+
let selectButton = queryByText(dialog, "Select");
|
|
117
|
+
let closeButton = queryByText(dialog, "Close");
|
|
118
|
+
let prevMonthButton = queryByText(dialog, "Previous month");
|
|
119
|
+
let prevYearButton = queryByText(dialog, "Previous year");
|
|
120
|
+
let nextMonthButton = queryByText(dialog, "Next month");
|
|
121
|
+
let nextYearButton = queryByText(dialog, "Next year");
|
|
122
|
+
|
|
123
|
+
expect(selectButton).not.toBeNull();
|
|
124
|
+
expect(closeButton).not.toBeNull();
|
|
125
|
+
expect(prevMonthButton).not.toBeNull();
|
|
126
|
+
expect(prevYearButton).not.toBeNull();
|
|
127
|
+
expect(nextMonthButton).not.toBeNull();
|
|
128
|
+
expect(nextYearButton).not.toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("calendar opens with current month and year", async () => {
|
|
132
|
+
await user.click(calendarButton);
|
|
133
|
+
const today = new Date();
|
|
134
|
+
const currentMonthName = today.toLocaleString("default", { month: "long" });
|
|
135
|
+
const currentYear = today.getFullYear();
|
|
136
|
+
const dialogTitle = `${currentMonthName} ${currentYear}`;
|
|
137
|
+
|
|
138
|
+
expect(dialog).toContainElement(screen.getByText(dialogTitle));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("today is selected", async () => {
|
|
142
|
+
await user.click(calendarButton);
|
|
143
|
+
const today = new Date();
|
|
144
|
+
const todayButton = getByRole(dialog, "button", { current: "date" });
|
|
145
|
+
|
|
146
|
+
expect(todayButton).toHaveFocus();
|
|
147
|
+
expect(todayButton).toHaveClass(
|
|
148
|
+
"moj-datepicker__button--selected",
|
|
149
|
+
"moj-datepicker__button--current",
|
|
150
|
+
"moj-datepicker__button--today",
|
|
151
|
+
);
|
|
152
|
+
expect(todayButton.textContent).toContain(`${today.getDate()}`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("can navigate back in time", async () => {
|
|
156
|
+
const today = new Date();
|
|
157
|
+
const currentMonthName = today.toLocaleString("default", { month: "long" });
|
|
158
|
+
const currentYear = today.getFullYear();
|
|
159
|
+
const currentMonth = today.getMonth();
|
|
160
|
+
const previousMonthName = new Date(
|
|
161
|
+
today.setMonth(currentMonth - 1),
|
|
162
|
+
).toLocaleString("default", { month: "long" });
|
|
163
|
+
const previousYear = currentYear - 1;
|
|
164
|
+
|
|
165
|
+
const currentTitle = `${currentMonthName} ${currentYear}`;
|
|
166
|
+
const previousMonthTitle = `${previousMonthName} ${currentYear}`;
|
|
167
|
+
const previousYearTitle = `${previousMonthName} ${previousYear}`;
|
|
168
|
+
|
|
169
|
+
await user.click(calendarButton);
|
|
170
|
+
let prevMonthButton = getByText(dialog, "Previous month");
|
|
171
|
+
let prevYearButton = getByText(dialog, "Previous year");
|
|
172
|
+
|
|
173
|
+
expect(dialog).toContainElement(screen.getByText(currentTitle));
|
|
174
|
+
await user.click(prevMonthButton);
|
|
175
|
+
expect(dialog).toContainElement(screen.getByText(previousMonthTitle));
|
|
176
|
+
await user.click(prevYearButton);
|
|
177
|
+
expect(dialog).toContainElement(screen.getByText(previousYearTitle));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("can navigate forward in time", async () => {
|
|
181
|
+
const today = new Date();
|
|
182
|
+
const currentMonthName = today.toLocaleString("default", { month: "long" });
|
|
183
|
+
const currentYear = today.getFullYear();
|
|
184
|
+
const currentMonth = today.getMonth();
|
|
185
|
+
const nextMonthName = new Date(
|
|
186
|
+
today.setMonth(currentMonth + 1),
|
|
187
|
+
).toLocaleString("default", { month: "long" });
|
|
188
|
+
const nextYear = currentYear + 1;
|
|
189
|
+
|
|
190
|
+
const currentTitle = `${currentMonthName} ${currentYear}`;
|
|
191
|
+
const nextMonthTitle = `${nextMonthName} ${currentYear}`;
|
|
192
|
+
const nextYearTitle = `${nextMonthName} ${nextYear}`;
|
|
193
|
+
|
|
194
|
+
await user.click(calendarButton);
|
|
195
|
+
let nextMonthButton = getByText(dialog, "Next month");
|
|
196
|
+
let nextYearButton = getByText(dialog, "Next year");
|
|
197
|
+
|
|
198
|
+
expect(dialog).toContainElement(screen.getByText(currentTitle));
|
|
199
|
+
await user.click(nextMonthButton);
|
|
200
|
+
expect(dialog).toContainElement(screen.getByText(nextMonthTitle));
|
|
201
|
+
await user.click(nextYearButton);
|
|
202
|
+
expect(dialog).toContainElement(screen.getByText(nextYearTitle));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("close button closes the calendar popup", async () => {
|
|
206
|
+
await user.click(calendarButton);
|
|
207
|
+
let closeButton = queryByText(dialog, "Close");
|
|
208
|
+
|
|
209
|
+
expect(dialog).toBeVisible();
|
|
210
|
+
await user.click(closeButton);
|
|
211
|
+
expect(dialog).not.toBeVisible();
|
|
212
|
+
expect(calendarButton).toHaveFocus();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("clicking outside closes the calendar popup", async () => {
|
|
216
|
+
let hint = screen.getByText("For example, 17/5/2024.");
|
|
217
|
+
|
|
218
|
+
await user.click(calendarButton);
|
|
219
|
+
expect(dialog).toBeVisible();
|
|
220
|
+
await user.click(hint);
|
|
221
|
+
expect(dialog).not.toBeVisible();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("date picker with initial value", () => {
|
|
225
|
+
let inputString;
|
|
226
|
+
let dateString;
|
|
227
|
+
let input;
|
|
228
|
+
let selectedDate;
|
|
229
|
+
let newDate;
|
|
230
|
+
|
|
231
|
+
beforeEach(async () => {
|
|
232
|
+
inputString = "19/05/2024";
|
|
233
|
+
dateString = "2024-05-19";
|
|
234
|
+
input = screen.getByLabelText("Date");
|
|
235
|
+
selectedDate = new Date(dateString);
|
|
236
|
+
|
|
237
|
+
while (newDate != selectedDate.getDate()) {
|
|
238
|
+
newDate = randomIntBetween(7, 21); //outside this we could have duplicate hidden buttons from prev/next month
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await user.type(input, inputString);
|
|
242
|
+
await user.click(calendarButton);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("opens to date in input field", async () => {
|
|
246
|
+
const selectedDateButton = getByRole(dialog, "button", {
|
|
247
|
+
current: "date",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(selectedDateButton).toHaveFocus();
|
|
251
|
+
expect(selectedDateButton).toHaveClass(
|
|
252
|
+
"moj-datepicker__button--selected",
|
|
253
|
+
"moj-datepicker__button--current",
|
|
254
|
+
);
|
|
255
|
+
expect(selectedDateButton).not.toHaveClass(
|
|
256
|
+
"moj-datepicker__button--today",
|
|
257
|
+
);
|
|
258
|
+
expect(selectedDateButton.textContent).toContain(
|
|
259
|
+
`${selectedDate.getDate()}`,
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("clicking a date selects it, closes dialog, and populates input", async () => {
|
|
264
|
+
const newDateButton = queryByText(dialog, newDate)?.closest("button");
|
|
265
|
+
|
|
266
|
+
await user.click(newDateButton);
|
|
267
|
+
|
|
268
|
+
expect(dialog).not.toBeVisible();
|
|
269
|
+
expect(input).toHaveValue(`${newDate}/5/2024`);
|
|
270
|
+
expect(calendarButton).toHaveFocus();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("clicking select, closes dialog and populates input", async () => {
|
|
274
|
+
const selectButton = getByText(dialog, "Select");
|
|
275
|
+
|
|
276
|
+
await user.keyboard("ArrowRight");
|
|
277
|
+
await user.click(selectButton);
|
|
278
|
+
|
|
279
|
+
expect(dialog).not.toBeVisible();
|
|
280
|
+
expect(input).toHaveValue(`${newDate}/5/2024`);
|
|
281
|
+
expect(calendarButton).toHaveFocus();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("keyboard interaction", () => {
|
|
286
|
+
let inputString;
|
|
287
|
+
let dateString;
|
|
288
|
+
let input;
|
|
289
|
+
let initialDate;
|
|
290
|
+
|
|
291
|
+
beforeEach(async () => {
|
|
292
|
+
inputString = "19/5/2024";
|
|
293
|
+
dateString = "2024-05-19";
|
|
294
|
+
input = screen.getByLabelText("Date");
|
|
295
|
+
initialDate = new Date(dateString);
|
|
296
|
+
|
|
297
|
+
await user.type(input, inputString);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("esc closes calendar dialog", async () => {
|
|
301
|
+
await user.tab();
|
|
302
|
+
expect(calendarButton).toHaveFocus();
|
|
303
|
+
await user.keyboard("{enter}");
|
|
304
|
+
expect(dialog).toBeVisible();
|
|
305
|
+
await user.keyboard("{escape}");
|
|
306
|
+
expect(dialog).not.toBeVisible();
|
|
307
|
+
expect(calendarButton).toHaveFocus();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("calendar dialog is a focus trap", async () => {
|
|
311
|
+
await user.click(calendarButton);
|
|
312
|
+
const selectedDateButton = getByRole(dialog, "button", {
|
|
313
|
+
current: "date",
|
|
314
|
+
});
|
|
315
|
+
const selectButton = queryByText(dialog, "Select");
|
|
316
|
+
const closeButton = queryByText(dialog, "Close");
|
|
317
|
+
const prevMonthButton = getByRole(dialog, "button", {
|
|
318
|
+
name: "Previous month",
|
|
319
|
+
});
|
|
320
|
+
const prevYearButton = getByRole(dialog, "button", {
|
|
321
|
+
name: "Previous year",
|
|
322
|
+
});
|
|
323
|
+
const nextMonthButton = getByRole(dialog, "button", {
|
|
324
|
+
name: "Next month",
|
|
325
|
+
});
|
|
326
|
+
const nextYearButton = getByRole(dialog, "button", { name: "Next year" });
|
|
327
|
+
|
|
328
|
+
expect(selectedDateButton).toHaveFocus();
|
|
329
|
+
await user.tab();
|
|
330
|
+
expect(selectButton).toHaveFocus();
|
|
331
|
+
await user.tab();
|
|
332
|
+
expect(closeButton).toHaveFocus();
|
|
333
|
+
await user.tab();
|
|
334
|
+
expect(prevYearButton).toHaveFocus();
|
|
335
|
+
await user.tab();
|
|
336
|
+
expect(prevMonthButton).toHaveFocus();
|
|
337
|
+
await user.tab();
|
|
338
|
+
expect(nextMonthButton).toHaveFocus();
|
|
339
|
+
await user.tab();
|
|
340
|
+
expect(nextYearButton).toHaveFocus();
|
|
341
|
+
await user.tab();
|
|
342
|
+
expect(selectedDateButton).toHaveFocus();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("rigth arrow navigates to next day", async () => {
|
|
346
|
+
await user.click(calendarButton);
|
|
347
|
+
const initialDateButton = getByRole(dialog, "button", {
|
|
348
|
+
current: "date",
|
|
349
|
+
});
|
|
350
|
+
let selectedDateButton;
|
|
351
|
+
let labelRegex;
|
|
352
|
+
|
|
353
|
+
expect(initialDateButton).toHaveFocus();
|
|
354
|
+
|
|
355
|
+
await user.keyboard("[ArrowRight]");
|
|
356
|
+
labelRegex = new RegExp(` ${initialDate.getDate() + 1} `);
|
|
357
|
+
selectedDateButton = getByRole(dialog, "button", { name: labelRegex });
|
|
358
|
+
expect(selectedDateButton).toHaveClass(
|
|
359
|
+
"moj-datepicker__button--selected",
|
|
360
|
+
);
|
|
361
|
+
expect(selectedDateButton).toHaveFocus();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("left arrow navigates to next day", async () => {
|
|
365
|
+
await user.click(calendarButton);
|
|
366
|
+
const initialDateButton = getByRole(dialog, "button", {
|
|
367
|
+
current: "date",
|
|
368
|
+
});
|
|
369
|
+
let selectedDateButton;
|
|
370
|
+
let labelRegex;
|
|
371
|
+
|
|
372
|
+
expect(initialDateButton).toHaveFocus();
|
|
373
|
+
|
|
374
|
+
await user.keyboard("[ArrowLeft]");
|
|
375
|
+
labelRegex = new RegExp(` ${initialDate.getDate() - 1} `);
|
|
376
|
+
selectedDateButton = getByRole(dialog, "button", { name: labelRegex });
|
|
377
|
+
expect(selectedDateButton).toHaveClass(
|
|
378
|
+
"moj-datepicker__button--selected",
|
|
379
|
+
);
|
|
380
|
+
expect(selectedDateButton).toHaveFocus();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("up arrow navigates to next day", async () => {
|
|
384
|
+
await user.click(calendarButton);
|
|
385
|
+
const initialDateButton = getByRole(dialog, "button", {
|
|
386
|
+
current: "date",
|
|
387
|
+
});
|
|
388
|
+
let selectedDateButton;
|
|
389
|
+
let labelRegex;
|
|
390
|
+
|
|
391
|
+
expect(initialDateButton).toHaveFocus();
|
|
392
|
+
|
|
393
|
+
await user.keyboard("[ArrowUp]");
|
|
394
|
+
labelRegex = new RegExp(` ${initialDate.getDate() - 7} `);
|
|
395
|
+
selectedDateButton = getByRole(dialog, "button", { name: labelRegex });
|
|
396
|
+
expect(selectedDateButton).toHaveClass(
|
|
397
|
+
"moj-datepicker__button--selected",
|
|
398
|
+
);
|
|
399
|
+
expect(selectedDateButton).toHaveFocus();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("down arrow navigates to next day", async () => {
|
|
403
|
+
await user.click(calendarButton);
|
|
404
|
+
const initialDateButton = getByRole(dialog, "button", {
|
|
405
|
+
current: "date",
|
|
406
|
+
});
|
|
407
|
+
let selectedDateButton;
|
|
408
|
+
let labelRegex;
|
|
409
|
+
|
|
410
|
+
expect(initialDateButton).toHaveFocus();
|
|
411
|
+
|
|
412
|
+
await user.keyboard("[ArrowDown]");
|
|
413
|
+
labelRegex = new RegExp(` ${initialDate.getDate() + 7} `);
|
|
414
|
+
selectedDateButton = getByRole(dialog, "button", { name: labelRegex });
|
|
415
|
+
expect(selectedDateButton).toHaveClass(
|
|
416
|
+
"moj-datepicker__button--selected",
|
|
417
|
+
);
|
|
418
|
+
expect(selectedDateButton).toHaveFocus();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("home key focuses first day of the week", async () => {
|
|
422
|
+
await user.click(calendarButton);
|
|
423
|
+
const initialDateButton = getByRole(dialog, "button", {
|
|
424
|
+
current: "date",
|
|
425
|
+
});
|
|
426
|
+
const firstDayOfWeek = getFirstDayOfWeek(initialDate, 1);
|
|
427
|
+
const labelRegex = new RegExp(` ${firstDayOfWeek.getDate()} `);
|
|
428
|
+
const selectedDateButton = getByRole(dialog, "button", {
|
|
429
|
+
name: labelRegex,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
expect(initialDateButton).toHaveFocus();
|
|
433
|
+
await user.keyboard("[Home]");
|
|
434
|
+
expect(selectedDateButton).toHaveClass(
|
|
435
|
+
"moj-datepicker__button--selected",
|
|
436
|
+
);
|
|
437
|
+
expect(selectedDateButton).toHaveFocus();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("end key focuses last day of the week", async () => {
|
|
441
|
+
await user.click(calendarButton);
|
|
442
|
+
const initialDateButton = getByRole(dialog, "button", {
|
|
443
|
+
current: "date",
|
|
444
|
+
});
|
|
445
|
+
const lastDayOfWeek = getLastDayOfWeek(initialDate, 0);
|
|
446
|
+
const labelRegex = new RegExp(` ${lastDayOfWeek.getDate()} `);
|
|
447
|
+
const selectedDateButton = getByRole(dialog, "button", {
|
|
448
|
+
name: labelRegex,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
expect(initialDateButton).toHaveFocus();
|
|
452
|
+
await user.keyboard("[End]");
|
|
453
|
+
expect(selectedDateButton).toHaveClass(
|
|
454
|
+
"moj-datepicker__button--selected",
|
|
455
|
+
);
|
|
456
|
+
expect(selectedDateButton).toHaveFocus();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("pageup focuses previous month and year", async () => {
|
|
460
|
+
await user.click(calendarButton);
|
|
461
|
+
const currentMonthName = initialDate.toLocaleString("default", {
|
|
462
|
+
month: "long",
|
|
463
|
+
});
|
|
464
|
+
const currentYear = initialDate.getFullYear();
|
|
465
|
+
const previousMonthName = new Date(
|
|
466
|
+
new Date(initialDate).setMonth(initialDate.getMonth() - 1, 1),
|
|
467
|
+
).toLocaleString("default", { month: "long" });
|
|
468
|
+
const previousYear = new Date(
|
|
469
|
+
new Date(initialDate).setFullYear(initialDate.getFullYear() - 1),
|
|
470
|
+
).getFullYear();
|
|
471
|
+
const currentTitle = `${currentMonthName} ${currentYear}`;
|
|
472
|
+
const previousMonthTitle = `${previousMonthName} ${currentYear}`;
|
|
473
|
+
const previousYearTitle = `${previousMonthName} ${previousYear}`;
|
|
474
|
+
const dialogTitle = getByRole(dialog, "heading", { level: 2 });
|
|
475
|
+
|
|
476
|
+
expect(dialogTitle.textContent).toEqual(currentTitle);
|
|
477
|
+
|
|
478
|
+
await user.keyboard("{PageUp}");
|
|
479
|
+
expect(dialogTitle.textContent).toEqual(previousMonthTitle);
|
|
480
|
+
|
|
481
|
+
await user.keyboard("{Shift>}{PageUp}");
|
|
482
|
+
expect(dialogTitle.textContent).toEqual(previousYearTitle);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("pagedown focuses next month and year", async () => {
|
|
486
|
+
await user.click(calendarButton);
|
|
487
|
+
const currentMonthName = initialDate.toLocaleString("default", {
|
|
488
|
+
month: "long",
|
|
489
|
+
});
|
|
490
|
+
const currentYear = initialDate.getFullYear();
|
|
491
|
+
const nextMonthName = new Date(
|
|
492
|
+
new Date(initialDate).setMonth(initialDate.getMonth() + 1, 1),
|
|
493
|
+
).toLocaleString("default", { month: "long" });
|
|
494
|
+
const nextYear = new Date(
|
|
495
|
+
new Date(initialDate).setFullYear(initialDate.getFullYear() + 1),
|
|
496
|
+
).getFullYear();
|
|
497
|
+
const currentTitle = `${currentMonthName} ${currentYear}`;
|
|
498
|
+
const nextMonthTitle = `${nextMonthName} ${currentYear}`;
|
|
499
|
+
const nextYearTitle = `${nextMonthName} ${nextYear}`;
|
|
500
|
+
const dialogTitle = getByRole(dialog, "heading", { level: 2 });
|
|
501
|
+
|
|
502
|
+
expect(dialogTitle.textContent).toEqual(currentTitle);
|
|
503
|
+
|
|
504
|
+
await user.keyboard("{PageDown}");
|
|
505
|
+
expect(dialogTitle.textContent).toEqual(nextMonthTitle);
|
|
506
|
+
|
|
507
|
+
await user.keyboard("{Shift>}{PageDown}");
|
|
508
|
+
expect(dialogTitle.textContent).toEqual(nextYearTitle);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("enter selects date and closes dialog", async () => {
|
|
512
|
+
await user.click(calendarButton);
|
|
513
|
+
await user.keyboard("[ArrowRight]");
|
|
514
|
+
await user.keyboard("[Enter]");
|
|
515
|
+
|
|
516
|
+
expect(dialog).not.toBeVisible();
|
|
517
|
+
expect(input).toHaveValue(`20/5/2024`);
|
|
518
|
+
expect(calendarButton).toHaveFocus();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("space selects date and closes dialog", async () => {
|
|
522
|
+
await user.click(calendarButton);
|
|
523
|
+
await user.keyboard("[ArrowRight]");
|
|
524
|
+
await user.keyboard("[Space]");
|
|
525
|
+
|
|
526
|
+
expect(dialog).not.toBeVisible();
|
|
527
|
+
expect(input).toHaveValue(`20/5/2024`);
|
|
528
|
+
expect(calendarButton).toHaveFocus();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("select button selects date and closes dialog", async () => {
|
|
532
|
+
await user.click(calendarButton);
|
|
533
|
+
await user.keyboard("[ArrowRight]");
|
|
534
|
+
await user.tab();
|
|
535
|
+
await user.keyboard("[Enter]");
|
|
536
|
+
|
|
537
|
+
expect(dialog).not.toBeVisible();
|
|
538
|
+
expect(input).toHaveValue(`20/5/2024`);
|
|
539
|
+
expect(calendarButton).toHaveFocus();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("close button, closes dialog", async () => {
|
|
543
|
+
await user.click(calendarButton);
|
|
544
|
+
await user.keyboard("[ArrowRight]");
|
|
545
|
+
await user.tab();
|
|
546
|
+
await user.tab();
|
|
547
|
+
await user.keyboard("[Enter]");
|
|
548
|
+
|
|
549
|
+
expect(dialog).not.toBeVisible();
|
|
550
|
+
expect(input).toHaveValue(`19/5/2024`);
|
|
551
|
+
expect(calendarButton).toHaveFocus();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe("accessibility", () => {
|
|
556
|
+
test("component has no wcag violations", async () => {
|
|
557
|
+
expect(await axe(document.body)).toHaveNoViolations();
|
|
558
|
+
await user.click(calendarButton);
|
|
559
|
+
expect(await axe(document.body)).toHaveNoViolations();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
//test component API - JS and data-attribute
|
|
564
|
+
//open with date in input
|
|
565
|
+
//min date
|
|
566
|
+
//max date
|
|
567
|
+
//excluded dates
|
|
568
|
+
//excluded days
|
|
569
|
+
//leadingZeros - test home and end keys
|
|
570
|
+
//weekStartDay
|
|
571
|
+
});
|
|
@@ -11,18 +11,16 @@
|
|
|
11
11
|
padding-top: govuk-spacing(2);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
.moj-identity-bar__container {
|
|
16
15
|
@include moj-width-container;
|
|
17
16
|
font-size: 0; /* Hide whitespace between elements */
|
|
18
17
|
text-align: justify; /* Trick to remove the need for floats */
|
|
19
18
|
|
|
20
19
|
&:after {
|
|
21
|
-
content:
|
|
20
|
+
content: "";
|
|
22
21
|
display: inline-block;
|
|
23
22
|
width: 100%;
|
|
24
23
|
}
|
|
25
|
-
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
.moj-identity-bar__title {
|
|
@@ -42,18 +40,13 @@
|
|
|
42
40
|
padding-top: govuk-spacing(2) + 1px; /* Alignment tweaks */
|
|
43
41
|
padding-bottom: govuk-spacing(2) - 1px; /* Alignment tweaks */
|
|
44
42
|
}
|
|
45
|
-
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
|
|
49
45
|
.moj-identity-bar__actions {
|
|
50
|
-
margin-bottom: - govuk-spacing(2);
|
|
51
|
-
|
|
52
46
|
@include govuk-media-query($from: tablet) {
|
|
53
47
|
display: inline-block;
|
|
54
48
|
vertical-align: middle;
|
|
55
49
|
}
|
|
56
|
-
|
|
57
50
|
}
|
|
58
51
|
|
|
59
52
|
.moj-identity-bar__menu {
|
|
@@ -64,4 +57,7 @@
|
|
|
64
57
|
margin-right: 0;
|
|
65
58
|
}
|
|
66
59
|
|
|
67
|
-
|
|
60
|
+
.moj-button-menu__toggle-button {
|
|
61
|
+
margin-bottom: 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
{%- from "moj/components/button-menu/macro.njk" import mojButtonMenu %}
|
|
2
|
-
|
|
3
|
-
<div class="moj-identity-bar {{- ' ' + params.classes if params.classes}}" {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
{% from "govuk/macros/attributes.njk" import govukAttributes %}
|
|
3
|
+
<div class="moj-identity-bar {{- ' ' + params.classes if params.classes}}" {{- govukAttributes(params.attributes) -}}>
|
|
4
|
+
<div class="moj-identity-bar__container">
|
|
5
|
+
<div class="moj-identity-bar__details">
|
|
6
|
+
{% if params.title %}
|
|
7
|
+
<span class="moj-identity-bar__title">{{ params.title.html | safe if params.title.html else params.title.text }}</span>
|
|
8
|
+
{% endif %}
|
|
9
|
+
</div>
|
|
10
|
+
{% if params.menus %}
|
|
11
|
+
<div class="moj-identity-bar__actions">
|
|
12
|
+
{% for menu in params.menus %}
|
|
13
|
+
<div class="moj-identity-bar__menu">
|
|
14
|
+
{{ mojButtonMenu(menu) }}
|
|
15
|
+
</div>
|
|
16
|
+
{% endfor %}
|
|
17
|
+
</div>
|
|
18
|
+
{% endif %}
|
|
11
19
|
</div>
|
|
12
|
-
|
|
13
|
-
{% if params.menus %}
|
|
14
|
-
<div class="moj-identity-bar__actions">
|
|
15
|
-
{% for menu in params.menus %}
|
|
16
|
-
<div class="moj-identity-bar__menu">
|
|
17
|
-
{{mojButtonMenu({ items: menu.items, classes: menu.classes })}}
|
|
18
|
-
</div>
|
|
19
|
-
{% endfor %}
|
|
20
|
-
</div>
|
|
21
|
-
{% endif %}
|
|
22
|
-
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
20
|
</div>
|