govuk_publishing_components 43.4.0 → 43.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.
- checksums.yaml +4 -4
- data/app/views/govuk_publishing_components/components/_radio.html.erb +2 -0
- data/app/views/govuk_publishing_components/components/docs/inverse_header.yml +2 -30
- data/app/views/govuk_publishing_components/components/docs/radio.yml +15 -0
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/node_modules/accessible-autocomplete/CHANGELOG.md +398 -0
- data/node_modules/accessible-autocomplete/CODEOWNERS +2 -0
- data/node_modules/accessible-autocomplete/CONTRIBUTING.md +161 -0
- data/node_modules/accessible-autocomplete/LICENSE.txt +20 -0
- data/node_modules/accessible-autocomplete/Procfile +1 -0
- data/node_modules/accessible-autocomplete/README.md +490 -0
- data/node_modules/accessible-autocomplete/accessibility-criteria.md +43 -0
- data/node_modules/accessible-autocomplete/app.json +15 -0
- data/node_modules/accessible-autocomplete/babel.config.js +29 -0
- data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css +3 -0
- data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css.map +1 -0
- data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js +2 -0
- data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js.map +1 -0
- data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js +2 -0
- data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js.map +1 -0
- data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js +2 -0
- data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js.map +1 -0
- data/node_modules/accessible-autocomplete/examples/ajax-source.html +300 -0
- data/node_modules/accessible-autocomplete/examples/form-single.html +381 -0
- data/node_modules/accessible-autocomplete/examples/form.html +673 -0
- data/node_modules/accessible-autocomplete/examples/index.html +693 -0
- data/node_modules/accessible-autocomplete/examples/preact/index.html +346 -0
- data/node_modules/accessible-autocomplete/examples/react/index.html +347 -0
- data/node_modules/accessible-autocomplete/examples/suggestions.json +258 -0
- data/node_modules/accessible-autocomplete/package.json +93 -0
- data/node_modules/accessible-autocomplete/postcss.config.js +16 -0
- data/node_modules/accessible-autocomplete/preact.js +1 -0
- data/node_modules/accessible-autocomplete/react.js +1 -0
- data/node_modules/accessible-autocomplete/scripts/check-staged.mjs +16 -0
- data/node_modules/accessible-autocomplete/src/autocomplete.css +167 -0
- data/node_modules/accessible-autocomplete/src/autocomplete.js +610 -0
- data/node_modules/accessible-autocomplete/src/dropdown-arrow-down.js +11 -0
- data/node_modules/accessible-autocomplete/src/status.js +125 -0
- data/node_modules/accessible-autocomplete/src/wrapper.js +60 -0
- data/node_modules/accessible-autocomplete/test/functional/dropdown-arrow-down.js +46 -0
- data/node_modules/accessible-autocomplete/test/functional/index.js +809 -0
- data/node_modules/accessible-autocomplete/test/functional/wrapper.js +339 -0
- data/node_modules/accessible-autocomplete/test/integration/index.js +309 -0
- data/node_modules/accessible-autocomplete/test/karma.config.js +46 -0
- data/node_modules/accessible-autocomplete/test/wdio.config.js +123 -0
- data/node_modules/accessible-autocomplete/webpack.config.mjs +244 -0
- 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
|
+
})
|