govuk_publishing_components 43.4.0 → 43.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/app/views/govuk_publishing_components/components/_radio.html.erb +2 -0
  3. data/app/views/govuk_publishing_components/components/docs/inverse_header.yml +2 -30
  4. data/app/views/govuk_publishing_components/components/docs/radio.yml +15 -0
  5. data/lib/govuk_publishing_components/version.rb +1 -1
  6. data/node_modules/accessible-autocomplete/CHANGELOG.md +398 -0
  7. data/node_modules/accessible-autocomplete/CODEOWNERS +2 -0
  8. data/node_modules/accessible-autocomplete/CONTRIBUTING.md +161 -0
  9. data/node_modules/accessible-autocomplete/LICENSE.txt +20 -0
  10. data/node_modules/accessible-autocomplete/Procfile +1 -0
  11. data/node_modules/accessible-autocomplete/README.md +490 -0
  12. data/node_modules/accessible-autocomplete/accessibility-criteria.md +43 -0
  13. data/node_modules/accessible-autocomplete/app.json +15 -0
  14. data/node_modules/accessible-autocomplete/babel.config.js +29 -0
  15. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css +3 -0
  16. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css.map +1 -0
  17. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js +2 -0
  18. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js.map +1 -0
  19. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js +2 -0
  20. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js.map +1 -0
  21. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js +2 -0
  22. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js.map +1 -0
  23. data/node_modules/accessible-autocomplete/examples/ajax-source.html +300 -0
  24. data/node_modules/accessible-autocomplete/examples/form-single.html +381 -0
  25. data/node_modules/accessible-autocomplete/examples/form.html +673 -0
  26. data/node_modules/accessible-autocomplete/examples/index.html +693 -0
  27. data/node_modules/accessible-autocomplete/examples/preact/index.html +346 -0
  28. data/node_modules/accessible-autocomplete/examples/react/index.html +347 -0
  29. data/node_modules/accessible-autocomplete/examples/suggestions.json +258 -0
  30. data/node_modules/accessible-autocomplete/package.json +93 -0
  31. data/node_modules/accessible-autocomplete/postcss.config.js +16 -0
  32. data/node_modules/accessible-autocomplete/preact.js +1 -0
  33. data/node_modules/accessible-autocomplete/react.js +1 -0
  34. data/node_modules/accessible-autocomplete/scripts/check-staged.mjs +16 -0
  35. data/node_modules/accessible-autocomplete/src/autocomplete.css +167 -0
  36. data/node_modules/accessible-autocomplete/src/autocomplete.js +610 -0
  37. data/node_modules/accessible-autocomplete/src/dropdown-arrow-down.js +11 -0
  38. data/node_modules/accessible-autocomplete/src/status.js +125 -0
  39. data/node_modules/accessible-autocomplete/src/wrapper.js +60 -0
  40. data/node_modules/accessible-autocomplete/test/functional/dropdown-arrow-down.js +46 -0
  41. data/node_modules/accessible-autocomplete/test/functional/index.js +809 -0
  42. data/node_modules/accessible-autocomplete/test/functional/wrapper.js +339 -0
  43. data/node_modules/accessible-autocomplete/test/integration/index.js +309 -0
  44. data/node_modules/accessible-autocomplete/test/karma.config.js +46 -0
  45. data/node_modules/accessible-autocomplete/test/wdio.config.js +123 -0
  46. data/node_modules/accessible-autocomplete/webpack.config.mjs +244 -0
  47. metadata +57 -2
@@ -0,0 +1,809 @@
1
+ /* global after, describe, before, beforeEach, it */
2
+
3
+ import { expect } from 'chai'
4
+ import { createElement, render } from 'preact' /** @jsx createElement */
5
+ import Autocomplete from '../../src/autocomplete'
6
+ import Status from '../../src/status'
7
+
8
+ function suggest (query, syncResults) {
9
+ const results = [
10
+ 'France',
11
+ 'Germany',
12
+ 'United Kingdom'
13
+ ]
14
+ syncResults(query
15
+ ? results.filter(function (result) {
16
+ return result.toLowerCase().indexOf(query.toLowerCase()) !== -1
17
+ })
18
+ : []
19
+ )
20
+ }
21
+
22
+ describe('Autocomplete', () => {
23
+ describe('rendering', () => {
24
+ let scratch
25
+
26
+ before(() => {
27
+ scratch = document.createElement('div');
28
+ (document.body || document.documentElement).appendChild(scratch)
29
+ })
30
+
31
+ beforeEach(() => {
32
+ scratch.innerHTML = ''
33
+ })
34
+
35
+ after(() => {
36
+ scratch.parentNode.removeChild(scratch)
37
+ scratch = null
38
+ })
39
+
40
+ describe('basic usage', () => {
41
+ it('renders an input', () => {
42
+ render(<Autocomplete />, scratch)
43
+
44
+ expect(scratch.innerHTML).to.contain('input')
45
+ expect(scratch.innerHTML).to.contain('class="autocomplete__input')
46
+ expect(scratch.innerHTML).to.contain('class="autocomplete__menu')
47
+ expect(scratch.innerHTML).to.contain('name="input-autocomplete"')
48
+ })
49
+
50
+ it('renders an input with a required attribute', () => {
51
+ render(<Autocomplete required />, scratch)
52
+
53
+ expect(scratch.innerHTML).to.contain('required')
54
+ })
55
+
56
+ it('renders an input without a required attribute', () => {
57
+ render(<Autocomplete required={false} />, scratch)
58
+
59
+ expect(scratch.innerHTML).to.not.contain('required')
60
+ })
61
+
62
+ it('renders an input with a name attribute', () => {
63
+ render(<Autocomplete name='bob' />, scratch)
64
+
65
+ expect(scratch.innerHTML).to.contain('name="bob"')
66
+ })
67
+
68
+ it('renders an input with a custom CSS namespace', () => {
69
+ render(<Autocomplete cssNamespace='bob' />, scratch)
70
+
71
+ expect(scratch.innerHTML).to.contain('class="bob__input')
72
+ expect(scratch.innerHTML).to.contain('class="bob__menu')
73
+ })
74
+
75
+ it('renders with an aria-expanded attribute', () => {
76
+ render(<Autocomplete required />, scratch)
77
+
78
+ const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
79
+ const inputElement = wrapperElement.getElementsByTagName('input')[0]
80
+
81
+ expect(inputElement.getAttribute('aria-expanded')).to.equal('false')
82
+ })
83
+
84
+ it('renders with an aria-describedby attribute', () => {
85
+ render(<Autocomplete id='autocomplete-default' />, scratch)
86
+
87
+ const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
88
+ const inputElement = wrapperElement.getElementsByTagName('input')[0]
89
+
90
+ expect(inputElement.getAttribute('aria-describedby')).to.equal('autocomplete-default__assistiveHint')
91
+ })
92
+
93
+ describe('renders with an aria-autocomplete attribute', () => {
94
+ it('of value "list", when autoselect is not enabled', () => {
95
+ render(<Autocomplete required />, scratch)
96
+
97
+ const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
98
+ const inputElement = wrapperElement.getElementsByTagName('input')[0]
99
+
100
+ expect(inputElement.getAttribute('aria-autocomplete')).to.equal('list')
101
+ })
102
+
103
+ it('of value "both", when autoselect is enabled', () => {
104
+ render(<Autocomplete required autoselect />, scratch)
105
+
106
+ const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
107
+ const inputElement = wrapperElement.getElementsByTagName('input')[0]
108
+
109
+ expect(inputElement.getAttribute('aria-autocomplete')).to.equal('both')
110
+ })
111
+ })
112
+
113
+ describe('menuAttributes', () => {
114
+ it('renders with extra attributes on the menu', () => {
115
+ render(<Autocomplete menuAttributes={{ 'data-test': 'test' }} id='autocomplete-default' />, scratch)
116
+
117
+ const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
118
+ const dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
119
+
120
+ expect(dropdownElement.getAttribute('data-test')).to.equal('test')
121
+ })
122
+
123
+ describe('attributes computed by the component', () => {
124
+ it('does not override attributes computed by the component', () => {
125
+ const menuAttributes = {
126
+ id: 'custom-id',
127
+ role: 'custom-role'
128
+ }
129
+
130
+ render(<Autocomplete menuAttributes={menuAttributes} id='autocomplete-default' />, scratch)
131
+
132
+ // Check that the computed values are the ones expected in the HTML
133
+ const menuElement = scratch.getElementsByClassName('autocomplete__menu')[0]
134
+ expect(menuElement.id).to.equal('autocomplete-default__listbox', 'HTML id')
135
+ expect(menuElement.role).to.equal('listbox', 'HTML role')
136
+
137
+ // Check that in protecting the menu, we don't affect the object passed as option
138
+ expect(menuAttributes.id).to.equal('custom-id', 'options id')
139
+ expect(menuAttributes.role).to.equal('custom-role', 'options role')
140
+ })
141
+ })
142
+
143
+ it('adds `className` to the computed value of the `class` attribute', () => {
144
+ const menuAttributes = {
145
+ className: 'custom-className'
146
+ }
147
+
148
+ render(<Autocomplete menuAttributes={menuAttributes} id='autocomplete-default' />, scratch)
149
+
150
+ // Check that the computed values are the ones expected in the HTML
151
+ const menuElement = scratch.getElementsByClassName('autocomplete__menu')[0]
152
+ expect(menuElement.getAttribute('class')).to.equal('autocomplete__menu autocomplete__menu--inline autocomplete__menu--hidden custom-className')
153
+
154
+ // Check that in protecting the menu, we don't affect the object passed as option
155
+ expect(menuAttributes.className).to.equal('custom-className')
156
+ })
157
+
158
+ // Align with Preact's behaviour where `class` takes precedence
159
+ it('adds `class` to the computed value of the `class` attribute, ignoring `className` if present', () => {
160
+ const menuAttributes = {
161
+ className: 'custom-className',
162
+ class: 'custom-class'
163
+ }
164
+
165
+ render(<Autocomplete menuAttributes={menuAttributes} id='autocomplete-default' />, scratch)
166
+
167
+ // Check that the computed values are the ones expected in the HTML
168
+ const menuElement = scratch.getElementsByClassName('autocomplete__menu')[0]
169
+ expect(menuElement.getAttribute('class')).to.equal('autocomplete__menu autocomplete__menu--inline autocomplete__menu--hidden custom-class')
170
+
171
+ // Check that in protecting the menu, we don't affect the object passed as option
172
+ expect(menuAttributes.className).to.equal('custom-className')
173
+ expect(menuAttributes.class).to.equal('custom-class')
174
+ })
175
+
176
+ it('adds `aria-labelledby` by default, based on the ID', () => {
177
+ render(<Autocomplete id='autocomplete-default' />, scratch)
178
+
179
+ const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
180
+ const dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
181
+
182
+ expect(dropdownElement.getAttribute('aria-labelledby')).to.equal('autocomplete-default')
183
+ })
184
+
185
+ it('overrides `aria-labelledby` if passed in menuAttributes', () => {
186
+ render(<Autocomplete menuAttributes={{ 'aria-labelledby': 'test' }} id='autocomplete-default' />, scratch)
187
+
188
+ const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
189
+ const dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
190
+
191
+ expect(dropdownElement.getAttribute('aria-labelledby')).to.equal('test')
192
+ })
193
+ })
194
+
195
+ it('renders with extra class on the input', () => {
196
+ render(<Autocomplete inputClasses='custom-class' id='autocomplete-default' />, scratch)
197
+
198
+ const inputElement = scratch.getElementsByClassName('autocomplete__input')[0]
199
+
200
+ expect(inputElement.getAttribute('class')).to.contain(' custom-class')
201
+ })
202
+
203
+ it('renders with extra class on the menu', () => {
204
+ render(<Autocomplete menuClasses='custom-class' id='autocomplete-default' />, scratch)
205
+
206
+ const menuElement = scratch.getElementsByClassName('autocomplete__menu')[0]
207
+
208
+ expect(menuElement.getAttribute('class')).to.contain('custom-class')
209
+ })
210
+
211
+ it('renders with the correct roles', () => {
212
+ render(<Autocomplete required />, scratch)
213
+
214
+ const wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
215
+ const inputElement = wrapperElement.getElementsByTagName('input')[0]
216
+ const dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
217
+
218
+ expect(inputElement.getAttribute('role')).to.equal('combobox', 'input should have combobox role')
219
+ expect(dropdownElement.getAttribute('role')).to.equal('listbox', 'menu should have listbox role')
220
+ })
221
+ })
222
+ })
223
+
224
+ describe('behaviour', () => {
225
+ let autocomplete, autoselectAutocomplete, onConfirmAutocomplete, onConfirmTriggered,
226
+ autoselectOnSelectAutocomplete, confirmOnBlurAutocomplete
227
+
228
+ beforeEach(() => {
229
+ autocomplete = new Autocomplete({
230
+ ...Autocomplete.defaultProps,
231
+ id: 'test',
232
+ source: suggest
233
+ })
234
+
235
+ autoselectAutocomplete = new Autocomplete({
236
+ ...Autocomplete.defaultProps,
237
+ autoselect: true,
238
+ id: 'test2',
239
+ source: suggest
240
+ })
241
+
242
+ onConfirmTriggered = false
243
+ onConfirmAutocomplete = new Autocomplete({
244
+ ...Autocomplete.defaultProps,
245
+ id: 'test3',
246
+ onConfirm: () => { onConfirmTriggered = true },
247
+ source: suggest
248
+ })
249
+
250
+ autoselectOnSelectAutocomplete = new Autocomplete({
251
+ ...Autocomplete.defaultProps,
252
+ autoselect: true,
253
+ id: 'test4',
254
+ onConfirm: () => { onConfirmTriggered = true },
255
+ source: suggest
256
+ })
257
+
258
+ confirmOnBlurAutocomplete = new Autocomplete({
259
+ ...Autocomplete.defaultProps,
260
+ id: 'test5',
261
+ onConfirm: () => { onConfirmTriggered = true },
262
+ confirmOnBlur: false,
263
+ source: suggest
264
+ })
265
+ })
266
+
267
+ describe('typing', () => {
268
+ it('searches for options', () => {
269
+ autocomplete.handleInputChange({ target: { value: 'f' } })
270
+ expect(autocomplete.state.menuOpen).to.equal(true)
271
+ expect(autocomplete.state.options).to.contain('France')
272
+ })
273
+
274
+ it('hides menu when no options are available', () => {
275
+ autocomplete.handleInputChange({ target: { value: 'aa' } })
276
+ expect(autocomplete.state.menuOpen).to.equal(false)
277
+ expect(autocomplete.state.options.length).to.equal(0)
278
+ })
279
+
280
+ it('hides menu when query becomes empty', () => {
281
+ autocomplete.setState({ query: 'f', options: ['France'], menuOpen: true })
282
+ autocomplete.handleInputChange({ target: { value: '' } })
283
+ expect(autocomplete.state.menuOpen).to.equal(false)
284
+ })
285
+
286
+ it('searches with the new term when query length changes', () => {
287
+ autocomplete.setState({ query: 'fr', options: ['France'] })
288
+ autocomplete.handleInputChange({ target: { value: 'fb' } })
289
+ expect(autocomplete.state.options.length).to.equal(0)
290
+ })
291
+
292
+ it('removes the aria-describedby attribute when query is non empty', () => {
293
+ expect(autocomplete.state.ariaHint).to.equal(true)
294
+ autocomplete.handleInputChange({ target: { value: 'a' } })
295
+ expect(autocomplete.state.ariaHint).to.equal(false)
296
+ autocomplete.handleInputChange({ target: { value: '' } })
297
+ expect(autocomplete.state.ariaHint).to.equal(true)
298
+ })
299
+
300
+ describe('with minLength', () => {
301
+ beforeEach(() => {
302
+ autocomplete = new Autocomplete({
303
+ ...Autocomplete.defaultProps,
304
+ id: 'test',
305
+ source: suggest,
306
+ minLength: 2
307
+ })
308
+ })
309
+
310
+ it('doesn\'t search when under limit', () => {
311
+ autocomplete.handleInputChange({ target: { value: 'f' } })
312
+ expect(autocomplete.state.menuOpen).to.equal(false)
313
+ expect(autocomplete.state.options.length).to.equal(0)
314
+ })
315
+
316
+ it('does search when over limit', () => {
317
+ autocomplete.handleInputChange({ target: { value: 'fra' } })
318
+ expect(autocomplete.state.menuOpen).to.equal(true)
319
+ expect(autocomplete.state.options).to.contain('France')
320
+ })
321
+
322
+ it('hides results when going under limit', () => {
323
+ autocomplete.setState({ menuOpen: true, query: 'fr', options: ['France'] })
324
+ autocomplete.handleInputChange({ target: { value: 'f' } })
325
+ expect(autocomplete.state.menuOpen).to.equal(false)
326
+ expect(autocomplete.state.options.length).to.equal(0)
327
+ })
328
+ })
329
+ })
330
+
331
+ describe('focusing input', () => {
332
+ describe('when no query is present', () => {
333
+ it('does not display menu', () => {
334
+ autocomplete.setState({ query: '' })
335
+ autocomplete.handleInputFocus()
336
+ expect(autocomplete.state.menuOpen).to.equal(false)
337
+ expect(autocomplete.state.focused).to.equal(-1)
338
+ })
339
+ })
340
+
341
+ describe('when a non-matched query is present (no matching options are present)', () => {
342
+ it('does not display menu', () => {
343
+ autocomplete.setState({ query: 'f' })
344
+ autocomplete.handleInputFocus()
345
+ expect(autocomplete.state.menuOpen).to.equal(false)
346
+ expect(autocomplete.state.focused).to.equal(-1)
347
+ })
348
+ })
349
+
350
+ describe('when a matched query is present (matching options exist)', () => {
351
+ describe('and no user choice has yet been made', () => {
352
+ it('displays menu', () => {
353
+ autocomplete.setState({
354
+ menuOpen: false,
355
+ options: ['France'],
356
+ query: 'fr',
357
+ focused: null,
358
+ selected: null,
359
+ validChoiceMade: false
360
+ })
361
+ autocomplete.handleInputFocus()
362
+ expect(autocomplete.state.focused).to.equal(-1)
363
+ expect(autocomplete.state.menuOpen).to.equal(true)
364
+ expect(autocomplete.state.selected).to.equal(-1)
365
+ })
366
+ })
367
+ describe('and a user choice HAS been made', () => {
368
+ it('does not display menu', () => {
369
+ autocomplete.setState({
370
+ menuOpen: false,
371
+ options: ['France'],
372
+ query: 'fr',
373
+ focused: null,
374
+ selected: null,
375
+ validChoiceMade: true
376
+ })
377
+ autocomplete.handleInputFocus()
378
+ expect(autocomplete.state.focused).to.equal(-1)
379
+ expect(autocomplete.state.menuOpen).to.equal(false)
380
+ })
381
+ })
382
+ })
383
+
384
+ describe('with option selected', () => {
385
+ it('leaves menu open, does not change query', () => {
386
+ autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
387
+ autocomplete.handleInputFocus()
388
+ expect(autocomplete.state.focused).to.equal(-1)
389
+ expect(autocomplete.state.menuOpen).to.equal(true)
390
+ expect(autocomplete.state.query).to.equal('fr')
391
+ })
392
+ })
393
+
394
+ describe('with defaultValue', () => {
395
+ beforeEach(() => {
396
+ autocomplete = new Autocomplete({
397
+ ...Autocomplete.defaultProps,
398
+ defaultValue: 'France',
399
+ id: 'test',
400
+ source: suggest
401
+ })
402
+ })
403
+
404
+ it('is prefilled', () => {
405
+ expect(autocomplete.state.options.length).to.equal(1)
406
+ expect(autocomplete.state.options[0]).to.equal('France')
407
+ expect(autocomplete.state.query).to.equal('France')
408
+ })
409
+ })
410
+ })
411
+
412
+ describe('blurring input', () => {
413
+ it('unfocuses component', () => {
414
+ autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: -1 })
415
+ autocomplete.handleInputBlur({ relatedTarget: null })
416
+ expect(autocomplete.state.focused).to.equal(null)
417
+ expect(autocomplete.state.menuOpen).to.equal(false)
418
+ expect(autocomplete.state.query).to.equal('fr')
419
+ })
420
+
421
+ describe('with autoselect and onConfirm', () => {
422
+ it('unfocuses component, updates query, triggers onConfirm', () => {
423
+ autoselectOnSelectAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: 0 })
424
+ autoselectOnSelectAutocomplete.handleInputBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
425
+ expect(autoselectOnSelectAutocomplete.state.focused).to.equal(null)
426
+ expect(autoselectOnSelectAutocomplete.state.menuOpen).to.equal(false)
427
+ expect(autoselectOnSelectAutocomplete.state.query).to.equal('France')
428
+ expect(onConfirmTriggered).to.equal(true)
429
+ })
430
+ })
431
+
432
+ describe('with confirmOnBlur false', () => {
433
+ it('unfocuses component, does not touch query, does not trigger onConfirm', () => {
434
+ confirmOnBlurAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: 0 })
435
+ confirmOnBlurAutocomplete.handleInputBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
436
+ expect(confirmOnBlurAutocomplete.state.focused).to.equal(null)
437
+ expect(confirmOnBlurAutocomplete.state.menuOpen).to.equal(false)
438
+ expect(confirmOnBlurAutocomplete.state.query).to.equal('fr')
439
+ expect(onConfirmTriggered).to.equal(false)
440
+ })
441
+ })
442
+ })
443
+
444
+ describe('focusing option', () => {
445
+ it('sets the option as focused', () => {
446
+ autocomplete.setState({ options: ['France'] })
447
+ autocomplete.handleOptionFocus(0)
448
+ expect(autocomplete.state.focused).to.equal(0)
449
+ })
450
+ })
451
+
452
+ describe('focusing out option', () => {
453
+ describe('with input selected', () => {
454
+ it('unfocuses component, does not change query', () => {
455
+ autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: -1 })
456
+ autocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
457
+ expect(autocomplete.state.focused).to.equal(null)
458
+ expect(autocomplete.state.menuOpen).to.equal(false)
459
+ expect(autocomplete.state.query).to.equal('fr')
460
+ })
461
+ })
462
+
463
+ describe('with option selected', () => {
464
+ describe('with confirmOnBlur true', () => {
465
+ it('unfocuses component, updates query', () => {
466
+ autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
467
+ autocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
468
+ expect(autocomplete.state.focused).to.equal(null)
469
+ expect(autocomplete.state.menuOpen).to.equal(false)
470
+ expect(autocomplete.state.query).to.equal('France')
471
+ })
472
+ })
473
+ describe('with confirmOnBlur false', () => {
474
+ it('unfocuses component, does not update query', () => {
475
+ confirmOnBlurAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
476
+ confirmOnBlurAutocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
477
+ expect(confirmOnBlurAutocomplete.state.focused).to.equal(null)
478
+ expect(confirmOnBlurAutocomplete.state.menuOpen).to.equal(false)
479
+ expect(confirmOnBlurAutocomplete.state.query).to.equal('fr')
480
+ })
481
+ })
482
+ })
483
+ })
484
+
485
+ describe('hovering option', () => {
486
+ it('sets the option as hovered, does not change focused, does not change selected', () => {
487
+ autocomplete.setState({ options: ['France'], hovered: null, focused: -1, selected: -1 })
488
+ autocomplete.handleOptionMouseEnter({}, 0)
489
+ expect(autocomplete.state.hovered).to.equal(0)
490
+ expect(autocomplete.state.focused).to.equal(-1)
491
+ expect(autocomplete.state.selected).to.equal(-1)
492
+ })
493
+ })
494
+
495
+ describe('hovering out option', () => {
496
+ it('sets focus back on selected, sets hovered to null', () => {
497
+ autocomplete.setState({ options: ['France'], hovered: 0, focused: -1, selected: -1 })
498
+ autocomplete.handleListMouseLeave({ toElement: 'mock' }, 0)
499
+ expect(autocomplete.state.hovered).to.equal(null)
500
+ expect(autocomplete.state.focused).to.equal(-1)
501
+ expect(autocomplete.state.selected).to.equal(-1)
502
+ })
503
+ })
504
+
505
+ describe('up key', () => {
506
+ it('focuses previous element', () => {
507
+ autocomplete.setState({ menuOpen: true, options: ['France'], focused: 0 })
508
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 38 })
509
+ expect(autocomplete.state.focused).to.equal(-1)
510
+ })
511
+ })
512
+
513
+ describe('down key', () => {
514
+ describe('0 options available', () => {
515
+ it('does nothing', () => {
516
+ autocomplete.setState({ menuOpen: false, options: [], focused: -1 })
517
+ const stateBefore = autocomplete.state
518
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
519
+ expect(autocomplete.state).to.equal(stateBefore)
520
+ })
521
+ })
522
+
523
+ describe('1 option available', () => {
524
+ it('focuses next element', () => {
525
+ autocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: -1 })
526
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
527
+ expect(autocomplete.state.focused).to.equal(0)
528
+ expect(autocomplete.state.selected).to.equal(0)
529
+ })
530
+ })
531
+
532
+ describe('2 or more option available', () => {
533
+ it('focuses next element', () => {
534
+ autocomplete.setState({ menuOpen: true, options: ['France', 'Germany'], focused: 0, selected: 0 })
535
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
536
+ expect(autocomplete.state.focused).to.equal(1)
537
+ expect(autocomplete.state.selected).to.equal(1)
538
+ })
539
+ })
540
+
541
+ describe('autoselect', () => {
542
+ describe('0 options available', () => {
543
+ it('does nothing', () => {
544
+ autoselectAutocomplete.setState({ menuOpen: false, options: [], focused: -1, selected: -1 })
545
+ const stateBefore = autoselectAutocomplete.state
546
+ autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
547
+ expect(autoselectAutocomplete.state).to.equal(stateBefore)
548
+ })
549
+ })
550
+
551
+ describe('1 option available', () => {
552
+ it('does nothing', () => {
553
+ autoselectAutocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: 0 })
554
+ const stateBefore = autoselectAutocomplete.state
555
+ autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
556
+ expect(autoselectAutocomplete.state).to.equal(stateBefore)
557
+ })
558
+ })
559
+
560
+ describe('2 or more option available', () => {
561
+ it('on input, focuses second element', () => {
562
+ autoselectAutocomplete.setState({ menuOpen: true, options: ['France', 'Germany'], focused: -1, selected: 0 })
563
+ autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
564
+ expect(autoselectAutocomplete.state.focused).to.equal(1)
565
+ expect(autoselectAutocomplete.state.selected).to.equal(1)
566
+ })
567
+ })
568
+ })
569
+ })
570
+
571
+ describe('escape key', () => {
572
+ it('unfocuses component', () => {
573
+ autocomplete.setState({ menuOpen: true, options: ['France'], focused: -1 })
574
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 27 })
575
+ expect(autocomplete.state.menuOpen).to.equal(false)
576
+ expect(autocomplete.state.focused).to.equal(null)
577
+ })
578
+ })
579
+
580
+ describe('enter key', () => {
581
+ describe('on an option', () => {
582
+ it('prevents default, closes the menu, sets the query, focuses the input, triggers onConfirm', () => {
583
+ let preventedDefault = false
584
+ onConfirmAutocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
585
+ onConfirmAutocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
586
+ expect(onConfirmAutocomplete.state.menuOpen).to.equal(false)
587
+ expect(onConfirmAutocomplete.state.query).to.equal('France')
588
+ expect(onConfirmAutocomplete.state.focused).to.equal(-1)
589
+ expect(onConfirmAutocomplete.state.selected).to.equal(-1)
590
+ expect(preventedDefault).to.equal(true)
591
+ expect(onConfirmTriggered).to.equal(true)
592
+ })
593
+ })
594
+
595
+ describe('on the input', () => {
596
+ describe('with menu opened', () => {
597
+ it('prevents default, does nothing', () => {
598
+ let preventedDefault = false
599
+ autocomplete.setState({ menuOpen: true, options: [], query: 'asd', focused: -1, selected: -1 })
600
+ const stateBefore = autocomplete.state
601
+ autocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
602
+ expect(autocomplete.state).to.equal(stateBefore)
603
+ expect(preventedDefault).to.equal(true)
604
+ })
605
+ })
606
+
607
+ describe('with menu closed', () => {
608
+ it('bubbles, does not prevent default', () => {
609
+ let preventedDefault = false
610
+ autocomplete.setState({ menuOpen: false, options: ['France'], focused: -1, selected: -1 })
611
+ const stateBefore = autocomplete.state
612
+ autocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
613
+ expect(autocomplete.state).to.equal(stateBefore)
614
+ expect(preventedDefault).to.equal(false)
615
+ })
616
+ })
617
+
618
+ describe('autoselect', () => {
619
+ it('closes the menu, selects the first option, keeps input focused', () => {
620
+ autoselectAutocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: 0 })
621
+ autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 13 })
622
+ expect(autoselectAutocomplete.state.menuOpen).to.equal(false)
623
+ expect(autoselectAutocomplete.state.query).to.equal('France')
624
+ expect(autoselectAutocomplete.state.focused).to.equal(-1)
625
+ expect(autoselectAutocomplete.state.selected).to.equal(-1)
626
+ })
627
+ })
628
+ })
629
+ })
630
+
631
+ describe('space key', () => {
632
+ describe('on an option', () => {
633
+ it('prevents default, closes the menu, sets the query, focuses the input, triggers onConfirm', () => {
634
+ let preventedDefault = false
635
+ onConfirmAutocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
636
+ onConfirmAutocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 32 })
637
+ expect(onConfirmAutocomplete.state.menuOpen).to.equal(false)
638
+ expect(onConfirmAutocomplete.state.query).to.equal('France')
639
+ expect(onConfirmAutocomplete.state.focused).to.equal(-1)
640
+ expect(onConfirmAutocomplete.state.selected).to.equal(-1)
641
+ expect(preventedDefault).to.equal(true)
642
+ expect(onConfirmTriggered).to.equal(true)
643
+ })
644
+ })
645
+ })
646
+
647
+ describe('an unrecognised key', () => {
648
+ it('does nothing', () => {
649
+ autocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
650
+ autocomplete.elementReferences[-1] = 'input element'
651
+ autocomplete.handleKeyDown({ target: 'not the input element', keyCode: 4242 })
652
+ expect(autocomplete.state.focused).to.equal(0)
653
+ expect(autocomplete.state.selected).to.equal(0)
654
+ })
655
+ })
656
+
657
+ describe('derived state', () => {
658
+ it('initially assumes no valid choice on each new input', () => {
659
+ autocomplete.handleInputChange({ target: { value: 'F' } })
660
+ expect(autocomplete.state.validChoiceMade).to.equal(false)
661
+ })
662
+
663
+ describe('identifies that the user has made a valid choice', () => {
664
+ it('when an option is actively clicked', () => {
665
+ autocomplete.setState({ query: 'f', options: ['France'], validChoiceMade: false })
666
+ autocomplete.handleOptionClick({}, 0)
667
+ expect(autocomplete.state.validChoiceMade).to.equal(true)
668
+ })
669
+
670
+ it('when the input is blurred, autoselect is disabled, and the current query exactly matches an option', () => {
671
+ autocomplete.setState({ query: 'France', options: ['France'], validChoiceMade: false })
672
+ autocomplete.handleComponentBlur({})
673
+ expect(autocomplete.state.validChoiceMade).to.equal(true)
674
+ })
675
+
676
+ it('when in the same scenario, but the match differs only by case sensitivity', () => {
677
+ autocomplete.setState({ query: 'fraNCe', options: ['France'], validChoiceMade: false })
678
+ autocomplete.handleComponentBlur({})
679
+ expect(autocomplete.state.validChoiceMade).to.equal(true)
680
+ })
681
+
682
+ it('when the input is blurred, autoselect is enabled, and the current query results in at least one option', () => {
683
+ autoselectAutocomplete.setState({ options: ['France'], validChoiceMade: false })
684
+ autoselectAutocomplete.handleInputChange({ target: { value: 'France' } })
685
+ autoselectAutocomplete.handleComponentBlur({})
686
+ expect(autoselectAutocomplete.state.validChoiceMade).to.equal(true)
687
+ })
688
+ })
689
+
690
+ describe('identifies that the user has not made a valid choice', () => {
691
+ it('when the input is blurred, autoselect is disabled, and the current query does not match an option', () => {
692
+ autocomplete.setState({ query: 'Fracne', options: ['France'], validChoiceMade: false })
693
+ autocomplete.handleComponentBlur({})
694
+ expect(autocomplete.state.validChoiceMade).to.equal(false)
695
+ })
696
+
697
+ it('when the input is blurred, autoselect is enabled, but no options exist for the current query', () => {
698
+ autoselectAutocomplete.setState({ options: [], validChoiceMade: false })
699
+ autoselectAutocomplete.handleInputChange({ target: { value: 'gpvx' } })
700
+ autoselectAutocomplete.handleComponentBlur({})
701
+ expect(autoselectAutocomplete.state.validChoiceMade).to.equal(false)
702
+ })
703
+ })
704
+
705
+ describe('identifies that the valid choice situation has changed', () => {
706
+ it('when the user amends a previously matched query such that it no longer matches an option', () => {
707
+ autocomplete.setState({ query: 'France', options: ['France'], validChoiceMade: false })
708
+ autocomplete.handleComponentBlur({})
709
+ expect(autocomplete.state.validChoiceMade).to.equal(true)
710
+ autocomplete.handleInputChange({ target: { value: 'Francey' } })
711
+ autocomplete.handleComponentBlur({})
712
+ expect(autocomplete.state.validChoiceMade).to.equal(false)
713
+ autocomplete.handleInputChange({ target: { value: 'France' } })
714
+ autocomplete.handleComponentBlur({})
715
+ expect(autocomplete.state.validChoiceMade).to.equal(true)
716
+ autocomplete.handleInputChange({ target: { value: 'Franc' } })
717
+ autocomplete.handleComponentBlur({})
718
+ expect(autocomplete.state.validChoiceMade).to.equal(false)
719
+ })
720
+ })
721
+ })
722
+ })
723
+ })
724
+
725
+ describe('Status', () => {
726
+ describe('rendering', () => {
727
+ let scratch
728
+
729
+ before(() => {
730
+ scratch = document.createElement('div');
731
+ (document.body || document.documentElement).appendChild(scratch)
732
+ })
733
+
734
+ beforeEach(() => {
735
+ scratch.innerHTML = ''
736
+ })
737
+
738
+ after(() => {
739
+ scratch.parentNode.removeChild(scratch)
740
+ scratch = null
741
+ })
742
+
743
+ it('renders a pair of aria live regions', () => {
744
+ render(<Status />, scratch)
745
+ expect(scratch.innerHTML).to.contain('div')
746
+
747
+ const wrapperElement = scratch.getElementsByTagName('div')[0]
748
+ const ariaLiveA = wrapperElement.getElementsByTagName('div')[0]
749
+ const ariaLiveB = wrapperElement.getElementsByTagName('div')[1]
750
+
751
+ expect(ariaLiveA.getAttribute('role')).to.equal('status', 'first aria live region should be marked as role=status')
752
+ expect(ariaLiveA.getAttribute('aria-atomic')).to.equal('true', 'first aria live region should be marked as atomic')
753
+ expect(ariaLiveA.getAttribute('aria-live')).to.equal('polite', 'first aria live region should be marked as polite')
754
+ expect(ariaLiveB.getAttribute('role')).to.equal('status', 'second aria live region should be marked as role=status')
755
+ expect(ariaLiveB.getAttribute('aria-atomic')).to.equal('true', 'second aria live region should be marked as atomic')
756
+ expect(ariaLiveB.getAttribute('aria-live')).to.equal('polite', 'second aria live region should be marked as polite')
757
+ })
758
+
759
+ describe('behaviour', () => {
760
+ describe('silences aria live announcement', () => {
761
+ it('when a valid choice has been made and the input has focus', (done) => {
762
+ const status = new Status({
763
+ ...Status.defaultProps,
764
+ validChoiceMade: true,
765
+ isInFocus: true
766
+ })
767
+ status.componentWillMount()
768
+ status.render()
769
+
770
+ setTimeout(() => {
771
+ expect(status.state.silenced).to.equal(true)
772
+ done()
773
+ }, 1500)
774
+ })
775
+
776
+ it('when the input no longer has focus', (done) => {
777
+ const status = new Status({
778
+ ...Status.defaultProps,
779
+ validChoiceMade: false,
780
+ isInFocus: false
781
+ })
782
+ status.componentWillMount()
783
+ status.render()
784
+
785
+ setTimeout(() => {
786
+ expect(status.state.silenced).to.equal(true)
787
+ done()
788
+ }, 1500)
789
+ })
790
+ })
791
+ describe('does not silence aria live announcement', () => {
792
+ it('when a valid choice has not been made and the input has focus', (done) => {
793
+ const status = new Status({
794
+ ...Status.defaultProps,
795
+ validChoiceMade: false,
796
+ isInFocus: true
797
+ })
798
+ status.componentWillMount()
799
+ status.render()
800
+
801
+ setTimeout(() => {
802
+ expect(status.state.silenced).to.equal(false)
803
+ done()
804
+ }, 1500)
805
+ })
806
+ })
807
+ })
808
+ })
809
+ })