@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,129 +0,0 @@
1
- /* eslint-disable no-new */
2
-
3
- const { queryByRole } = require('@testing-library/dom')
4
- const { userEvent } = require('@testing-library/user-event')
5
- const $ = require('jquery')
6
-
7
- require('./search-toggle.js')
8
-
9
- const user = userEvent.setup()
10
-
11
- const createComponent = () => {
12
- const html = `
13
- <div class="moj-search-toggle" data-module="moj-search-toggle" data-moj-search-toggle-text="Find case">
14
- <div class="moj-search-toggle__toggle"></div>
15
- <div class="moj-search-toggle__search">
16
-
17
- <div class="moj-search moj-search--ondark moj-search--toggle moj-js-hidden">
18
-
19
- <form action="" method="get">
20
-
21
- <div class="govuk-form-group">
22
- <label class="govuk-label moj-search__label govuk-visually-hidden" for="search2">
23
- Search
24
- </label>
25
-
26
- <div id="search2-hint" class="govuk-hint moj-search__hint ">
27
- Enter case number, for example 123456
28
- </div>
29
-
30
- <input class="govuk-input moj-search__input " id="search2" name="search2" type="search" aria-describedby="search2-hint">
31
-
32
- </div>
33
-
34
- <button type="submit" class="govuk-button moj-search__button " data-module="govuk-button">
35
- Search
36
- </button>
37
-
38
- </form>
39
- </div>
40
-
41
- </div>
42
- </div>
43
- <a href="#">link</a>`
44
- document.body.insertAdjacentHTML('afterbegin', html)
45
- const component = document.querySelector('.moj-search-toggle')
46
- return component
47
- }
48
-
49
- describe('search toggle', () => {
50
- let component, buttonContainer, searchContainer
51
-
52
- beforeEach(() => {
53
- component = createComponent()
54
- searchContainer = component.querySelector('.moj-search')
55
- buttonContainer = component.querySelector('.moj-search-toggle__toggle')
56
-
57
- new MOJFrontend.SearchToggle({
58
- toggleButton: {
59
- container: $(buttonContainer),
60
- text: component.getAttribute('data-moj-search-toggle-text')
61
- },
62
- search: {
63
- container: $(searchContainer)
64
- }
65
- })
66
- })
67
-
68
- afterEach(() => {
69
- document.body.innerHTML = ''
70
- })
71
-
72
- test('initialises component', () => {
73
- const toggleButton = queryByRole(buttonContainer, 'button')
74
-
75
- expect(toggleButton).toBeInTheDocument()
76
- expect(toggleButton).toHaveTextContent('Find case')
77
- expect(toggleButton).toHaveAttribute('aria-haspopup', 'true')
78
- expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
79
-
80
- expect(searchContainer).toHaveClass('moj-js-hidden')
81
- })
82
-
83
- test('clicking button toggles search container', async () => {
84
- const toggleButton = queryByRole(buttonContainer, 'button')
85
-
86
- await user.click(toggleButton)
87
-
88
- expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
89
- expect(searchContainer).not.toHaveClass('moj-js-hidden')
90
-
91
- const input = queryByRole(searchContainer, 'searchbox')
92
- expect(input).toHaveFocus()
93
-
94
- await user.click(toggleButton)
95
-
96
- expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
97
- expect(searchContainer).toHaveClass('moj-js-hidden')
98
- expect(toggleButton).toHaveFocus()
99
- })
100
-
101
- test('clicking outside closes the search container', async () => {
102
- const toggleButton = queryByRole(buttonContainer, 'button')
103
-
104
- await user.click(toggleButton)
105
-
106
- expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
107
- expect(searchContainer).not.toHaveClass('moj-js-hidden')
108
-
109
- await user.click(document.body)
110
-
111
- expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
112
- expect(searchContainer).toHaveClass('moj-js-hidden')
113
- })
114
-
115
- test('tabbing closes the search container', async () => {
116
- const toggleButton = queryByRole(buttonContainer, 'button')
117
-
118
- await user.click(toggleButton)
119
-
120
- expect(toggleButton).toHaveAttribute('aria-expanded', 'true')
121
- expect(searchContainer).not.toHaveClass('moj-js-hidden')
122
-
123
- await user.tab()
124
- await user.tab()
125
-
126
- expect(toggleButton).toHaveAttribute('aria-expanded', 'false')
127
- expect(searchContainer).toHaveClass('moj-js-hidden')
128
- })
129
- })
@@ -1,362 +0,0 @@
1
- /* eslint-disable no-new */
2
-
3
- const { queryByRole } = require('@testing-library/dom')
4
- const { userEvent } = require('@testing-library/user-event')
5
-
6
- require('./sortable-table.js')
7
-
8
- const user = userEvent.setup()
9
-
10
- const createComponent = (options = {}) => {
11
- const html = `
12
- <div>
13
- <table class="govuk-table" data-module="moj-sortable-table">
14
- <thead class="govuk-table__head">
15
- <tr class="govuk-table__row">
16
- <th scope="col" class="govuk-table__header" aria-sort="ascending">Name</th>
17
- <th scope="col" class="govuk-table__header" aria-sort="none">Elevation</th>
18
- <th scope="col" class="govuk-table__header" aria-sort="none">Continent</th>
19
- <th scope="col" class="govuk-table__header govuk-table__header--numeric" aria-sort="none">First summit</th>
20
- <th scope="col" class="govuk-table__header" aria-sort="none">Test nickname</th>
21
- </tr>
22
- </thead>
23
- <tbody class="govuk-table__body">
24
- <tr class="govuk-table__row">
25
- <td class="govuk-table__cell">Aconcagua</td>
26
- <td class="govuk-table__cell" data-sort-value="6961">6,961 meters</td>
27
- <td class="govuk-table__cell">South America</td>
28
- <td class="govuk-table__cell govuk-table__cell--numeric" data-sort-value="1897">1897</td>
29
- <td class="govuk-table__cell"></td>
30
- </tr>
31
- <tr class="govuk-table__row">
32
- <td class="govuk-table__cell">Everest</td>
33
- <td class="govuk-table__cell" data-sort-value="8850">8,850 meters</td>
34
- <td class="govuk-table__cell">Asia</td>
35
- <td class="govuk-table__cell govuk-table__cell--numeric" data-sort-value="1953">1953</td>
36
- <td class="govuk-table__cell">1Tallest</td>
37
- </tr>
38
- <tr class="govuk-table__row">
39
- <td class="govuk-table__cell">Kilimanjaro</td>
40
- <td class="govuk-table__cell" data-sort-value="5895">5,895 meters</td>
41
- <td class="govuk-table__cell">Africa</td>
42
- <td class="govuk-table__cell govuk-table__cell--numeric" data-sort-value="1889">1889</td>
43
- <td class="govuk-table__cell">KiliJ89</td>
44
- </tr>
45
- <tr class="govuk-table__row">
46
- <td class="govuk-table__cell">K2</td>
47
- <td class="govuk-table__cell" data-sort-value="8611">8,611 meters</td>
48
- <td class="govuk-table__cell">Asia</td>
49
- <td class="govuk-table__cell govuk-table__cell--numeric" data-sort-value="1889">1954</td>
50
- <td class="govuk-table__cell">1NearlyTallest</td>
51
- </tr>
52
- </tbody>
53
- </table>
54
- </div>`
55
-
56
- document.body.insertAdjacentHTML('afterbegin', html)
57
- const component = document.querySelector('[data-module="moj-sortable-table"]')
58
- options.table = component
59
- return { component, options }
60
- }
61
-
62
- describe('sortable table', () => {
63
- let component
64
-
65
- beforeEach(() => {
66
- ;({ component } = createComponent())
67
- new MOJFrontend.SortableTable({
68
- table: component
69
- })
70
- })
71
-
72
- afterEach(() => {
73
- document.body.innerHTML = ''
74
- })
75
-
76
- test('initialises with buttons in headers', () => {
77
- const headers = Array.from(component.querySelectorAll('th')).filter(
78
- (header) => header.getAttribute('aria-sort')
79
- )
80
-
81
- for (const header of headers) {
82
- const button = header.querySelector('button')
83
- expect(button).toBeInTheDocument()
84
- expect(button).toHaveTextContent(header.textContent)
85
- }
86
- })
87
-
88
- test('creates status box for announcements', () => {
89
- const statusBox = queryByRole(component.parentElement, 'status')
90
- expect(statusBox).toBeInTheDocument()
91
- expect(statusBox).toHaveClass('govuk-visually-hidden')
92
- })
93
-
94
- test('sorts ascending by Name on initial load', () => {
95
- const tbody = component.querySelector('tbody')
96
- const cells = tbody.querySelectorAll('tr td:first-child')
97
- const values = Array.from(cells).map((cell) => cell.textContent.trim())
98
-
99
- expect(component.querySelector('th')).toHaveAttribute(
100
- 'aria-sort',
101
- 'ascending'
102
- )
103
- expect(values).toEqual(['Aconcagua', 'Everest', 'K2', 'Kilimanjaro'])
104
- })
105
-
106
- test('sorts string column in descending order when clicked', async () => {
107
- const nameHeaderButton = queryByRole(component, 'button', { name: 'Name' })
108
- const tbody = component.querySelector('tbody')
109
-
110
- await user.click(nameHeaderButton)
111
-
112
- const descCells = tbody.querySelectorAll('tr td:first-child')
113
- const descValues = Array.from(descCells).map((cell) =>
114
- cell.textContent.trim()
115
- )
116
-
117
- expect(descValues).toEqual(['Kilimanjaro', 'K2', 'Everest', 'Aconcagua'])
118
- expect(nameHeaderButton.parentElement).toHaveAttribute(
119
- 'aria-sort',
120
- 'descending'
121
- )
122
-
123
- await user.click(nameHeaderButton)
124
-
125
- const ascCells = tbody.querySelectorAll('tr td:first-child')
126
- const ascValues = Array.from(ascCells).map((cell) =>
127
- cell.textContent.trim()
128
- )
129
-
130
- expect(ascValues).toEqual(['Aconcagua', 'Everest', 'K2', 'Kilimanjaro'])
131
- expect(nameHeaderButton.parentElement).toHaveAttribute(
132
- 'aria-sort',
133
- 'ascending'
134
- )
135
- })
136
-
137
- test('sorts numeric column using data-sort-value', async () => {
138
- const elevationHeaderButton = queryByRole(component, 'button', {
139
- name: 'Elevation'
140
- })
141
- const tbody = component.querySelector('tbody')
142
-
143
- await user.click(elevationHeaderButton)
144
-
145
- const ascCells = tbody.querySelectorAll('tr td:nth-child(2)')
146
- const ascValues = Array.from(ascCells).map((cell) =>
147
- parseInt(cell.getAttribute('data-sort-value'))
148
- )
149
-
150
- expect(ascValues).toEqual([5895, 6961, 8611, 8850])
151
- expect(elevationHeaderButton.parentElement).toHaveAttribute(
152
- 'aria-sort',
153
- 'ascending'
154
- )
155
-
156
- await user.click(elevationHeaderButton)
157
-
158
- const descCells = tbody.querySelectorAll('tr td:nth-child(2)')
159
- const descValues = Array.from(descCells).map((cell) =>
160
- parseInt(cell.getAttribute('data-sort-value'))
161
- )
162
-
163
- expect(descValues).toEqual([8850, 8611, 6961, 5895])
164
- expect(elevationHeaderButton.parentElement).toHaveAttribute(
165
- 'aria-sort',
166
- 'descending'
167
- )
168
- })
169
-
170
- test('sorts mixed data column without specified data-sort-value', async () => {
171
- const nicknameHeaderButton = queryByRole(component, 'button', {
172
- name: 'Test nickname'
173
- })
174
- const tbody = component.querySelector('tbody')
175
-
176
- await user.click(nicknameHeaderButton)
177
-
178
- const ascCells = tbody.querySelectorAll('tr td:nth-child(5)')
179
- const ascValues = Array.from(ascCells).map((cell) =>
180
- cell.textContent.trim()
181
- )
182
- // Values converted to numbers in getCellValue function for comparison
183
-
184
- expect(ascValues).toEqual(['', '1NearlyTallest', '1Tallest', 'KiliJ89'])
185
- expect(nicknameHeaderButton.parentElement).toHaveAttribute(
186
- 'aria-sort',
187
- 'ascending'
188
- )
189
- })
190
-
191
- test('updates status message when sorting', async () => {
192
- const elevationHeaderButton = queryByRole(component, 'button', {
193
- name: 'Elevation'
194
- })
195
- const statusBox = queryByRole(component.parentElement, 'status')
196
-
197
- await user.click(elevationHeaderButton)
198
-
199
- expect(statusBox).toHaveTextContent('Sort by Elevation (ascending)')
200
-
201
- await user.click(elevationHeaderButton)
202
-
203
- expect(statusBox).toHaveTextContent('Sort by Elevation (descending)')
204
- })
205
-
206
- test('removes sort state from other columns when sorting a new column', async () => {
207
- const nameHeaderButton = queryByRole(component, 'button', { name: 'Name' })
208
- const elevationHeaderButton = queryByRole(component, 'button', {
209
- name: 'Elevation'
210
- })
211
-
212
- await user.click(elevationHeaderButton)
213
-
214
- expect(nameHeaderButton.parentElement).toHaveAttribute('aria-sort', 'none')
215
- expect(elevationHeaderButton.parentElement).toHaveAttribute(
216
- 'aria-sort',
217
- 'ascending'
218
- )
219
- })
220
-
221
- test('cycles through sort states: none -> ascending -> descending', async () => {
222
- const headerButton = queryByRole(component, 'button', { name: 'Continent' })
223
- const header = headerButton.parentElement
224
-
225
- expect(header).toHaveAttribute('aria-sort', 'none')
226
-
227
- await user.click(headerButton)
228
- expect(header).toHaveAttribute('aria-sort', 'ascending')
229
-
230
- await user.click(headerButton)
231
- expect(header).toHaveAttribute('aria-sort', 'descending')
232
-
233
- await user.click(headerButton)
234
- expect(header).toHaveAttribute('aria-sort', 'ascending')
235
- })
236
- })
237
-
238
- describe('sortable table options', () => {
239
- let component
240
- let options
241
-
242
- beforeEach(() => {
243
- ;({ component, options } = createComponent())
244
- })
245
-
246
- afterEach(() => {
247
- document.body.innerHTML = ''
248
- })
249
-
250
- test('uses default status message when no options provided', async () => {
251
- new MOJFrontend.SortableTable(options)
252
-
253
- const elevationHeaderButton = queryByRole(component, 'button', {
254
- name: 'Elevation'
255
- })
256
- const statusBox = queryByRole(component.parentElement, 'status')
257
-
258
- await user.click(elevationHeaderButton)
259
- expect(statusBox).toHaveTextContent('Sort by Elevation (ascending)')
260
- })
261
-
262
- test('uses custom status message when provided', async () => {
263
- options.statusMessage = 'Sorted column: %heading% (order: %direction%)'
264
- new MOJFrontend.SortableTable(options)
265
-
266
- const elevationHeaderButton = queryByRole(component, 'button', {
267
- name: 'Elevation'
268
- })
269
- const statusBox = queryByRole(component.parentElement, 'status')
270
-
271
- await user.click(elevationHeaderButton)
272
- expect(statusBox).toHaveTextContent(
273
- 'Sorted column: Elevation (order: ascending)'
274
- )
275
- })
276
-
277
- test('uses custom ascending text when provided', async () => {
278
- options.ascendingText = 'A to Z'
279
- new MOJFrontend.SortableTable(options)
280
-
281
- const nameHeaderButton = queryByRole(component, 'button', { name: 'Name' })
282
- const statusBox = queryByRole(component.parentElement, 'status')
283
-
284
- await user.click(nameHeaderButton)
285
- await user.click(nameHeaderButton)
286
-
287
- expect(statusBox).toHaveTextContent('Sort by Name (A to Z)')
288
- })
289
-
290
- test('uses custom descending text when provided', async () => {
291
- options.descendingText = 'Z to A'
292
- new MOJFrontend.SortableTable(options)
293
-
294
- const nameHeaderButton = queryByRole(component, 'button', { name: 'Name' })
295
- const statusBox = queryByRole(component.parentElement, 'status')
296
-
297
- await user.click(nameHeaderButton)
298
-
299
- expect(statusBox).toHaveTextContent('Sort by Name (Z to A)')
300
- })
301
-
302
- test('uses all custom options together', async () => {
303
- options = {
304
- table: component,
305
- statusMessage: 'component sorted by %heading% in %direction% order',
306
- ascendingText: 'lowest to highest',
307
- descendingText: 'highest to lowest'
308
- }
309
- new MOJFrontend.SortableTable(options)
310
-
311
- const elevationHeaderButton = queryByRole(component, 'button', {
312
- name: 'Elevation'
313
- })
314
- const statusBox = queryByRole(component.parentElement, 'status')
315
-
316
- await user.click(elevationHeaderButton)
317
- expect(statusBox).toHaveTextContent(
318
- 'component sorted by Elevation in lowest to highest order'
319
- )
320
-
321
- await user.click(elevationHeaderButton)
322
- expect(statusBox).toHaveTextContent(
323
- 'component sorted by Elevation in highest to lowest order'
324
- )
325
- })
326
-
327
- test('allows reinitialization of component without duplicating functionality', () => {
328
- const { component, options } = createComponent()
329
- new MOJFrontend.SortableTable(options)
330
- new MOJFrontend.SortableTable(options) // Initialize again
331
-
332
- // Check that we don't have duplicate buttons in headers
333
- const headers = component.querySelectorAll('th[aria-sort]')
334
- headers.forEach((header) => {
335
- const buttons = header.querySelectorAll('button')
336
- expect(buttons).toHaveLength(1)
337
- })
338
- })
339
-
340
- test('maintains original sort when reinitialized', async () => {
341
- const { component, options } = createComponent()
342
- new MOJFrontend.SortableTable(options)
343
-
344
- const elevationHeaderButton = queryByRole(component, 'button', {
345
- name: 'Elevation'
346
- })
347
- await user.click(elevationHeaderButton)
348
-
349
- new MOJFrontend.SortableTable(options) // Reinitialize
350
-
351
- const cells = component.querySelectorAll('tbody tr td:nth-child(2)')
352
- const values = Array.from(cells).map((cell) =>
353
- parseInt(cell.getAttribute('data-sort-value'))
354
- )
355
-
356
- expect(values).toEqual([5895, 6961, 8611, 8850])
357
- expect(elevationHeaderButton.parentElement).toHaveAttribute(
358
- 'aria-sort',
359
- 'ascending'
360
- )
361
- })
362
- })