@ministryofjustice/frontend 3.4.0 → 3.5.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.
Files changed (33) hide show
  1. package/moj/all.jquery.min.js +7 -70
  2. package/moj/all.js +2856 -2865
  3. package/moj/components/add-another/add-another.js +135 -104
  4. package/moj/components/alert/alert.js +482 -247
  5. package/moj/components/alert/alert.spec.helper.js +30 -5
  6. package/moj/components/button-menu/button-menu.js +346 -319
  7. package/moj/components/date-picker/date-picker.js +925 -900
  8. package/moj/components/filter-toggle-button/filter-toggle-button.js +122 -91
  9. package/moj/components/form-validator/form-validator.js +399 -164
  10. package/moj/components/multi-file-upload/multi-file-upload.js +445 -210
  11. package/moj/components/multi-select/multi-select.js +106 -75
  12. package/moj/components/password-reveal/password-reveal.js +64 -33
  13. package/moj/components/rich-text-editor/rich-text-editor.js +186 -153
  14. package/moj/components/search-toggle/search-toggle.js +77 -46
  15. package/moj/components/sortable-table/sortable-table.js +167 -146
  16. package/moj/helpers/_links.scss +1 -1
  17. package/moj/helpers.js +218 -180
  18. package/moj/moj-frontend.min.js +7 -70
  19. package/moj/version.js +28 -1
  20. package/package.json +1 -1
  21. package/moj/all.spec.js +0 -24
  22. package/moj/components/add-another/add-another.spec.js +0 -165
  23. package/moj/components/alert/alert.spec.js +0 -229
  24. package/moj/components/button-menu/button-menu.spec.js +0 -360
  25. package/moj/components/date-picker/date-picker.spec.js +0 -1178
  26. package/moj/components/filter-toggle-button/filter-toggle-button.spec.js +0 -302
  27. package/moj/components/multi-file-upload/multi-file-upload.spec.js +0 -510
  28. package/moj/components/multi-select/multi-select.spec.js +0 -128
  29. package/moj/components/password-reveal/password-reveal.spec.js +0 -57
  30. package/moj/components/search-toggle/search-toggle.spec.js +0 -129
  31. package/moj/components/sortable-table/sortable-table.spec.js +0 -362
  32. package/moj/helpers.spec.js +0 -235
  33. package/moj/namespace.js +0 -2
@@ -1,229 +0,0 @@
1
- /* eslint-disable no-new */
2
- const { getByRole, queryByRole, screen } = require('@testing-library/dom')
3
- const { userEvent } = require('@testing-library/user-event')
4
-
5
- const { pageTemplate } = require('./alert.spec.helper.js')
6
- require('../../helpers.js')
7
- require('./alert.js')
8
-
9
- const user = userEvent.setup()
10
-
11
- const kebabize = (str) => {
12
- return str.replace(
13
- /[A-Z]+(?![a-z])|[A-Z]/g,
14
- ($, ofset) => (ofset ? '-' : '') + $.toLowerCase()
15
- )
16
- }
17
-
18
- const configToDataAttributes = (config) => {
19
- let attributes = ''
20
- for (const [key, value] of Object.entries(config)) {
21
- attributes += `data-${kebabize(key)}="${value}" `
22
- }
23
- return attributes
24
- }
25
-
26
- const createComponent = (options = {}, role = 'region') => {
27
- const dataAttributes = configToDataAttributes(options)
28
- const html = `<div role="${role}" class="moj-alert moj-alert--information moj-alert--with-heading" aria-label="information: The finance section has moved" data-module="moj-alert" ${dataAttributes}>
29
- <div>
30
- <svg class="moj-alert__icon" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" height="30" width="30">
31
- <path fill-rule="evenodd" clip-rule="evenodd" d="M10.2165 3.45151C11.733 2.82332 13.3585 2.5 15 2.5C16.6415 2.5 18.267 2.82332 19.7835 3.45151C21.3001 4.07969 22.6781 5.00043 23.8388 6.16117C24.9996 7.3219 25.9203 8.69989 26.5485 10.2165C27.1767 11.733 27.5 13.3585 27.5 15C27.5 18.3152 26.183 21.4946 23.8388 23.8388C21.4946 26.183 18.3152 27.5 15 27.5C13.3585 27.5 11.733 27.1767 10.2165 26.5485C8.69989 25.9203 7.3219 24.9996 6.16117 23.8388C3.81696 21.4946 2.5 18.3152 2.5 15C2.5 11.6848 3.81696 8.50537 6.16117 6.16117C7.3219 5.00043 8.69989 4.07969 10.2165 3.45151ZM16.3574 22.4121H13.6621V12.95H16.3574V22.4121ZM13.3789 9.20898C13.3789 8.98763 13.4212 8.7793 13.5059 8.58398C13.5905 8.38216 13.7044 8.20964 13.8477 8.06641C13.9974 7.91667 14.1699 7.79948 14.3652 7.71484C14.5605 7.63021 14.7721 7.58789 15 7.58789C15.2214 7.58789 15.4297 7.63021 15.625 7.71484C15.8268 7.79948 15.9993 7.91667 16.1426 8.06641C16.2923 8.20964 16.4095 8.38216 16.4941 8.58398C16.5788 8.7793 16.6211 8.98763 16.6211 9.20898C16.6211 9.43685 16.5788 9.64844 16.4941 9.84375C16.4095 10.0391 16.2923 10.2116 16.1426 10.3613C15.9993 10.5046 15.8268 10.6185 15.625 10.7031C15.4297 10.7878 15.2214 10.8301 15 10.8301C14.7721 10.8301 14.5605 10.7878 14.3652 10.7031C14.1699 10.6185 13.9974 10.5046 13.8477 10.3613C13.7044 10.2116 13.5905 10.0391 13.5059 9.84375C13.4212 9.64844 13.3789 9.43685 13.3789 9.20898Z" fill="currentColor" />
32
- </svg>
33
- </div>
34
- <div class="moj-alert__content">
35
- <h2 class="govuk-heading-m">The finance section has moved
36
- </h2>You can now find it in the <a href="#">dashboard</a>.
37
- </div>
38
- <div class="moj-alert__action">
39
- <button class="moj-alert__dismiss" hidden>Dismiss</button>
40
- </div>
41
- </div>
42
- `
43
-
44
- document.body.insertAdjacentHTML('afterbegin', html)
45
- const component = document.querySelector('[data-module="moj-alert"]')
46
- return component
47
- }
48
-
49
- describe('alert', () => {
50
- let component
51
- let dismissButton
52
-
53
- afterEach(() => {
54
- document.body.innerHTML = ''
55
- })
56
-
57
- test('non-dismissible', () => {
58
- component = createComponent()
59
- new MOJFrontend.Alert(component).init()
60
- dismissButton = queryByRole(component, 'button', { hidden: true })
61
-
62
- expect(dismissButton).toHaveAttribute('hidden')
63
- })
64
-
65
- test('dismissible', () => {
66
- component = createComponent({ dismissible: 'true' })
67
- new MOJFrontend.Alert(component).init()
68
- dismissButton = queryByRole(component, 'button', { hidden: true })
69
-
70
- expect(dismissButton).not.toHaveAttribute('hidden')
71
- })
72
-
73
- test('non-dismissible "false" string', () => {
74
- component = createComponent({ dismissible: 'false' })
75
- new MOJFrontend.Alert(component).init()
76
- dismissButton = queryByRole(component, 'button', { hidden: true })
77
-
78
- expect(dismissButton).toHaveAttribute('hidden')
79
- })
80
-
81
- test('applies custom dismiss text', () => {
82
- component = createComponent({
83
- dismissible: true,
84
- dismissText: 'Close'
85
- })
86
- new MOJFrontend.Alert(component).init()
87
- dismissButton = queryByRole(component, 'button', { name: 'Close' })
88
-
89
- expect(dismissButton).toBeInTheDocument()
90
- })
91
-
92
- test('region role does not receive focus', () => {
93
- component = createComponent({}, 'region')
94
- new MOJFrontend.Alert(component).init()
95
-
96
- expect(component).not.toHaveFocus()
97
- })
98
-
99
- test('alert role autofocuses', () => {
100
- component = createComponent({}, 'alert')
101
- new MOJFrontend.Alert(component).init()
102
-
103
- expect(component).toHaveFocus()
104
- })
105
-
106
- test('disableAutoFocus prevents autofocus', () => {
107
- component = createComponent(
108
- {
109
- disableAutoFocus: true
110
- },
111
- 'alert'
112
- )
113
- new MOJFrontend.Alert(component).init()
114
-
115
- expect(component).not.toHaveFocus()
116
- })
117
-
118
- test('dismiss button removes component', async () => {
119
- component = createComponent({
120
- dismissible: true
121
- })
122
- new MOJFrontend.Alert(component).init()
123
- dismissButton = queryByRole(component, 'button')
124
-
125
- await user.click(dismissButton)
126
-
127
- expect(component).not.toBeInTheDocument()
128
- })
129
-
130
- describe('focus on dismiss', () => {
131
- let firstAlert
132
- let secondAlert
133
- let thirdAlert
134
- let fourthAlert
135
- let fifthAlert
136
- let heading1
137
- let heading2
138
-
139
- beforeEach(() => {
140
- document.body.insertAdjacentHTML('afterbegin', pageTemplate)
141
- const alerts = document.querySelectorAll('[data-module="moj-alert"]')
142
- alerts.forEach((alert) => {
143
- new MOJFrontend.Alert(alert).init()
144
- })
145
-
146
- firstAlert = document.querySelector('#alert-1')
147
- secondAlert = document.querySelector('#alert-2')
148
- thirdAlert = document.querySelector('#alert-3')
149
- fourthAlert = document.querySelector('#alert-4')
150
- fifthAlert = document.querySelector('#alert-5')
151
-
152
- heading1 = document.querySelector('#h1')
153
- heading2 = document.querySelector('#h2')
154
- })
155
-
156
- afterEach(() => {
157
- document.body.innerHTML = ''
158
- })
159
-
160
- test('it moves focus to the element provide by focusOnDismiss', async () => {
161
- const alert = fifthAlert
162
- const focusedElement = document.querySelector('#focusOnMe')
163
- const dismissButton = getByRole(alert, 'button')
164
-
165
- await user.click(dismissButton)
166
-
167
- expect(focusedElement).toHaveFocus()
168
- })
169
-
170
- test('it moves focus to the next sibling alert if present', async () => {
171
- const alert = thirdAlert
172
- const dismissButton = getByRole(alert, 'button')
173
-
174
- await user.click(dismissButton)
175
-
176
- expect(fourthAlert).toHaveFocus()
177
- })
178
-
179
- test('it moves focus to the previous sibling alert if present', async () => {
180
- fourthAlert.remove()
181
- fifthAlert.remove()
182
- const alert = thirdAlert
183
- const dismissButton = getByRole(alert, 'button')
184
-
185
- await user.click(dismissButton)
186
-
187
- expect(secondAlert).toHaveFocus()
188
- })
189
-
190
- test('it moves focus to the previous heading if present', async () => {
191
- thirdAlert.remove()
192
- fourthAlert.remove()
193
- fifthAlert.remove()
194
- const alert = secondAlert
195
- const dismissButton = getByRole(alert, 'button')
196
-
197
- await user.click(dismissButton)
198
-
199
- expect(heading2).toHaveFocus()
200
- })
201
-
202
- test('it moves focus to the parent heading if present', async () => {
203
- heading2.remove()
204
- thirdAlert.remove()
205
- fourthAlert.remove()
206
- fifthAlert.remove()
207
- const alert = secondAlert
208
- const dismissButton = getByRole(alert, 'button')
209
-
210
- await user.click(dismissButton)
211
-
212
- expect(heading1).toHaveFocus()
213
- })
214
-
215
- test('it moves focus to main if no other element matches', async () => {
216
- heading2.remove()
217
- thirdAlert.remove()
218
- fourthAlert.remove()
219
- fifthAlert.remove()
220
- const alert = firstAlert
221
- const main = screen.getByRole('main')
222
- const dismissButton = getByRole(alert, 'button')
223
-
224
- await user.click(dismissButton)
225
-
226
- expect(main).toHaveFocus()
227
- })
228
- })
229
- })
@@ -1,360 +0,0 @@
1
- const { queryByRole, screen } = require('@testing-library/dom')
2
- const { userEvent } = require('@testing-library/user-event')
3
- const { configureAxe } = require('jest-axe')
4
-
5
- require('./button-menu.js')
6
-
7
- const user = userEvent.setup()
8
- const axe = configureAxe({
9
- rules: {
10
- // disable landmark rules when testing isolated components.
11
- region: { enabled: false }
12
- }
13
- })
14
-
15
- const kebabize = (str) => {
16
- return str.replace(
17
- /[A-Z]+(?![a-z])|[A-Z]/g,
18
- ($, ofset) => (ofset ? '-' : '') + $.toLowerCase()
19
- )
20
- }
21
-
22
- const configToDataAttributes = (config) => {
23
- let attributes = ''
24
- for (const [key, value] of Object.entries(config)) {
25
- attributes += `data-${kebabize(key)}="${value}" `
26
- }
27
- return attributes
28
- }
29
-
30
- const createComponent = (config = {}, html) => {
31
- const dataAttributes = configToDataAttributes(config)
32
- if (typeof html === 'undefined') {
33
- html = `
34
- <div class="moj-button-menu" data-module="moj-button-menu" ${dataAttributes}>
35
- <a href="#one" role="button">First action</a>
36
- <a href="#two" role="button" class="govuk-button--warning">Second action</a>
37
- <a href="#three" role="button" class="custom-class">Third action</a>
38
- </div>`
39
- }
40
- document.body.insertAdjacentHTML('afterbegin', html)
41
- const component = document.querySelector('[data-module="moj-button-menu"]')
42
- return component
43
- }
44
-
45
- describe('Button menu with defaults', () => {
46
- let component
47
- let toggleButton
48
- let menu
49
- let items
50
-
51
- beforeEach(() => {
52
- component = createComponent()
53
- new MOJFrontend.ButtonMenu(component).init()
54
-
55
- toggleButton = queryByRole(component, 'button', { hidden: false })
56
- menu = screen.queryByRole('list', { hidden: true })
57
- items = menu?.querySelectorAll('a, button')
58
- })
59
-
60
- afterEach(() => {
61
- document.body.innerHTML = ''
62
- })
63
-
64
- test('initialises component elements', () => {
65
- expect(toggleButton).not.toBeNull()
66
- expect(menu).not.toBeNull()
67
- expect(items).not.toBeNull()
68
- })
69
-
70
- test('intialises toggle button', () => {
71
- expect(component).toContainElement(toggleButton)
72
- expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
73
- expect(toggleButton).toHaveAttribute('aria-haspopup', 'true')
74
- })
75
-
76
- test('intialises menu', () => {
77
- expect(component).toContainElement(menu)
78
- expect(menu).not.toBeVisible()
79
- })
80
-
81
- test('creates menuitems', () => {
82
- expect(items).toHaveLength(3)
83
- })
84
-
85
- test('removes other govuk-button classes from menuitems', () => {
86
- expect(items[1]).not.toHaveClass('govuk-button--warning')
87
- })
88
-
89
- test('keeps custom classes on items', () => {
90
- expect(items[2]).toHaveClass('custom-class')
91
- })
92
-
93
- test('clicking toggle button shows menu', async () => {
94
- await user.click(toggleButton)
95
-
96
- expect(menu).toBeVisible()
97
- expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
98
- })
99
-
100
- test('clicking a link in the menu', async () => {
101
- await user.click(toggleButton)
102
-
103
- expect(menu).toBeVisible()
104
- await user.click(items[0])
105
- expect(global.window.location.hash).toContain('#one')
106
- await user.click(items[2])
107
- expect(global.window.location.hash).toContain('#three')
108
- })
109
-
110
- test('clicking outside closes menu', async () => {
111
- await user.click(toggleButton)
112
- expect(menu).toBeVisible()
113
-
114
- await user.click(document.body)
115
- expect(menu).not.toBeVisible()
116
- })
117
-
118
- describe('keyboard interactions', () => {
119
- test('enter on toggle button opens menu', async () => {
120
- toggleButton.focus()
121
- await user.keyboard('[Enter]')
122
-
123
- expect(menu).toBeVisible()
124
- expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
125
- expect(items[0]).toHaveFocus()
126
- })
127
-
128
- test('space on toggle button opens menu', async () => {
129
- toggleButton.focus()
130
- await user.keyboard('[Space]')
131
-
132
- expect(menu).toBeVisible()
133
- expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
134
- expect(items[0]).toHaveFocus()
135
- })
136
-
137
- test('esc closes menu', async () => {
138
- toggleButton.focus()
139
- await user.keyboard('[Space]')
140
- expect(menu).toBeVisible()
141
- await user.keyboard('[Escape]')
142
-
143
- expect(menu).not.toBeVisible()
144
- expect(toggleButton).toHaveFocus()
145
- })
146
-
147
- test('down arrow on toggle button opens menu with focus on first item', async () => {
148
- toggleButton.focus()
149
- await user.keyboard('[ArrowDown]')
150
-
151
- expect(menu).toBeVisible()
152
- expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
153
- expect(items[0]).toHaveFocus()
154
- })
155
-
156
- test('up arrow on toggle button opens menu with focus on last item', async () => {
157
- toggleButton.focus()
158
- await user.keyboard('[ArrowUp]')
159
-
160
- expect(menu).toBeVisible()
161
- expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
162
- expect(items[items.length - 1]).toHaveFocus()
163
- })
164
-
165
- test('down arrow on menu item navigates to next item with looping', async () => {
166
- toggleButton.focus()
167
- await user.keyboard('[Enter]')
168
- expect(items[0]).toHaveFocus()
169
-
170
- await user.keyboard('[ArrowDown]')
171
- expect(items[1]).toHaveFocus()
172
-
173
- await user.keyboard('[ArrowDown]')
174
- expect(items[2]).toHaveFocus()
175
-
176
- await user.keyboard('[ArrowDown]')
177
- expect(items[0]).toHaveFocus()
178
- })
179
-
180
- test('up arrow on menu item navigates to previous item with looping', async () => {
181
- toggleButton.focus()
182
- await user.keyboard('[ArrowUp]')
183
- expect(items[items.length - 1]).toHaveFocus()
184
-
185
- await user.keyboard('[ArrowUp]')
186
- expect(items[1]).toHaveFocus()
187
-
188
- await user.keyboard('[ArrowUp]')
189
- expect(items[0]).toHaveFocus()
190
-
191
- await user.keyboard('[ArrowUp]')
192
- expect(items[items.length - 1]).toHaveFocus()
193
- })
194
-
195
- test('home navigates to first item', async () => {
196
- toggleButton.focus()
197
- await user.keyboard('[ArrowUp]')
198
- expect(items[items.length - 1]).toHaveFocus()
199
-
200
- await user.keyboard('[Home]')
201
- expect(items[0]).toHaveFocus()
202
- })
203
-
204
- test('end navigates to last item', async () => {
205
- toggleButton.focus()
206
- await user.keyboard('[Enter]')
207
- expect(items[0]).toHaveFocus()
208
-
209
- await user.keyboard('[End]')
210
- expect(items[items.length - 1]).toHaveFocus()
211
- })
212
-
213
- test('tab moves focus out of the menu', async () => {
214
- toggleButton.focus()
215
- await user.keyboard('[Enter]')
216
- expect(menu).toBeVisible()
217
- expect(items[0]).toHaveFocus()
218
- await user.tab()
219
-
220
- expect(document.body).toHaveFocus()
221
- expect(menu).not.toBeVisible()
222
- })
223
- })
224
-
225
- describe('accessibility', () => {
226
- test('component has no wcag violations', async () => {
227
- expect(await axe(document.body)).toHaveNoViolations()
228
- await user.click(toggleButton)
229
- expect(await axe(document.body)).toHaveNoViolations()
230
- })
231
- })
232
- })
233
-
234
- describe('Button menu javascript API', () => {
235
- let component
236
-
237
- beforeEach(() => {
238
- component = createComponent()
239
- })
240
-
241
- afterEach(() => {
242
- document.body.innerHTML = ''
243
- })
244
-
245
- test('setting toggle button text', () => {
246
- const label = 'click me'
247
- new MOJFrontend.ButtonMenu(component, { buttonText: label }).init()
248
- const toggleButton = queryByRole(component, 'button', { name: label })
249
-
250
- expect(toggleButton).toBeInTheDocument()
251
- })
252
-
253
- test('setting menu alignment', () => {
254
- new MOJFrontend.ButtonMenu(component, { alignMenu: 'right' }).init()
255
- const menu = screen.queryByRole('list', { hidden: true })
256
-
257
- expect(menu).toHaveClass('moj-button-menu__wrapper--right')
258
- })
259
-
260
- test('setting button classes', () => {
261
- const defaultClassNames = 'govuk-button moj-button-menu__toggle-button'
262
- const classNames = 'classOne classTwo'
263
-
264
- new MOJFrontend.ButtonMenu(component, { buttonClasses: classNames }).init()
265
- const toggleButton = queryByRole(component, 'button', { hidden: false })
266
-
267
- expect(toggleButton).toHaveClass(defaultClassNames)
268
- expect(toggleButton).toHaveClass(classNames)
269
- })
270
- })
271
-
272
- describe('Button menu data-attributes API', () => {
273
- let component
274
-
275
- beforeEach(() => {})
276
-
277
- afterEach(() => {
278
- document.body.innerHTML = ''
279
- })
280
-
281
- test('setting toggle button text', () => {
282
- const label = 'click me'
283
-
284
- component = createComponent({ buttonText: label })
285
- new MOJFrontend.ButtonMenu(component).init()
286
- const toggleButton = queryByRole(component, 'button', { name: label })
287
-
288
- expect(toggleButton).toBeInTheDocument()
289
- })
290
-
291
- test('setting menu alignment', () => {
292
- component = createComponent({ alignMenu: 'right' })
293
- new MOJFrontend.ButtonMenu(component).init()
294
- const menu = screen.queryByRole('list', { hidden: true })
295
-
296
- expect(menu).toHaveClass('moj-button-menu__wrapper--right')
297
- })
298
-
299
- test('setting button classes', () => {
300
- const defaultClassNames = 'govuk-button moj-button-menu__toggle-button'
301
- const classNames = 'classOne classTwo'
302
-
303
- component = createComponent({ buttonClasses: classNames })
304
- new MOJFrontend.ButtonMenu(component).init()
305
- const toggleButton = queryByRole(component, 'button', { hidden: false })
306
-
307
- expect(toggleButton).toHaveClass(defaultClassNames)
308
- expect(toggleButton).toHaveClass(classNames)
309
- })
310
- })
311
-
312
- describe('menu button with a single item', () => {
313
- let component
314
- let toggleButton
315
- let menu
316
- let items
317
-
318
- beforeEach(() => {
319
- const html = `
320
- <div class="moj-button-menu" data-module="moj-button-menu" data-button-classes="govuk-button--warning custom-class">
321
- <a href="#one" role="button" class="govuk-button--inverse">First action</a>
322
- </div>`
323
-
324
- component = createComponent({}, html)
325
- new MOJFrontend.ButtonMenu(component).init()
326
-
327
- toggleButton = queryByRole(component, 'button', { name: 'Actions' })
328
- menu = screen.queryByRole('list', { hidden: true })
329
- items = menu?.queryByRole('button', { hidden: true })
330
- })
331
-
332
- afterEach(() => {
333
- document.body.innerHTML = ''
334
- })
335
-
336
- test('menu is not created', () => {
337
- expect(menu).toBeNull()
338
- })
339
-
340
- test('there are no items', () => {
341
- expect(items).toBeUndefined()
342
- })
343
-
344
- test('there is no toggle button', () => {
345
- expect(toggleButton).toBeNull()
346
- })
347
-
348
- test('first item has become button', () => {
349
- const button = screen.queryByRole('button', { name: 'First action' })
350
-
351
- expect(button).toBeInTheDocument()
352
- })
353
-
354
- test('first item has buttonClasses config applied', () => {
355
- const button = screen.queryByRole('button', { name: 'First action' })
356
-
357
- expect(button).toHaveClass('govuk-button--warning', 'custom-class')
358
- expect(button).not.toHaveClass('govuk-button--inverse')
359
- })
360
- })