govuk_publishing_components 12.13.0 → 12.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/govuk_publishing_components/components/accessible-autocomplete.js +37 -0
  3. data/app/assets/javascripts/govuk_publishing_components/components/checkboxes.js +6 -5
  4. data/app/assets/javascripts/govuk_publishing_components/components/copy-to-clipboard.js +15 -15
  5. data/app/assets/javascripts/govuk_publishing_components/components/feedback.js +2 -4
  6. data/app/assets/javascripts/govuk_publishing_components/components/initial-focus.js +8 -8
  7. data/app/assets/javascripts/govuk_publishing_components/components/step-by-step-nav.js +3 -3
  8. data/app/assets/stylesheets/govuk_publishing_components/_all_components.scss +1 -0
  9. data/app/assets/stylesheets/govuk_publishing_components/components/_accessible-autocomplete.scss +16 -0
  10. data/app/views/govuk_publishing_components/components/_accessible_autocomplete.html.erb +24 -0
  11. data/app/views/govuk_publishing_components/components/docs/accessible_autocomplete.yml +42 -0
  12. data/config/initializers/assets.rb +1 -0
  13. data/lib/govuk_publishing_components/version.rb +1 -1
  14. data/node_modules/accessible-autocomplete/CHANGELOG.md +269 -0
  15. data/node_modules/accessible-autocomplete/CONTRIBUTING.md +150 -0
  16. data/node_modules/accessible-autocomplete/LICENSE.txt +20 -0
  17. data/node_modules/accessible-autocomplete/Procfile +1 -0
  18. data/node_modules/accessible-autocomplete/README.md +416 -0
  19. data/node_modules/accessible-autocomplete/accessibility-criteria.md +42 -0
  20. data/node_modules/accessible-autocomplete/app.json +15 -0
  21. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css +1 -0
  22. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js +2 -0
  23. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js.map +1 -0
  24. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js +2 -0
  25. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js.map +1 -0
  26. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js +2 -0
  27. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js.map +1 -0
  28. data/node_modules/accessible-autocomplete/examples/form.html +671 -0
  29. data/node_modules/accessible-autocomplete/examples/index.html +616 -0
  30. data/node_modules/accessible-autocomplete/examples/preact/index.html +346 -0
  31. data/node_modules/accessible-autocomplete/examples/react/index.html +347 -0
  32. data/node_modules/accessible-autocomplete/package.json +192 -0
  33. data/node_modules/accessible-autocomplete/preact.js +1 -0
  34. data/node_modules/accessible-autocomplete/react.js +1 -0
  35. data/node_modules/accessible-autocomplete/scripts/check-staged.js +14 -0
  36. data/node_modules/accessible-autocomplete/src/autocomplete.css +141 -0
  37. data/node_modules/accessible-autocomplete/src/autocomplete.js +524 -0
  38. data/node_modules/accessible-autocomplete/src/dropdown-arrow-down.js +11 -0
  39. data/node_modules/accessible-autocomplete/src/status.js +80 -0
  40. data/node_modules/accessible-autocomplete/src/wrapper.js +60 -0
  41. data/node_modules/accessible-autocomplete/test/functional/dropdown-arrow-down.js +44 -0
  42. data/node_modules/accessible-autocomplete/test/functional/index.js +485 -0
  43. data/node_modules/accessible-autocomplete/test/functional/wrapper.js +267 -0
  44. data/node_modules/accessible-autocomplete/test/integration/index.js +188 -0
  45. data/node_modules/accessible-autocomplete/test/karma.config.js +42 -0
  46. data/node_modules/accessible-autocomplete/test/wdio.config.js +80 -0
  47. data/node_modules/accessible-autocomplete/webpack.config.babel.js +193 -0
  48. data/node_modules/preact/LICENSE +21 -0
  49. data/node_modules/preact/README.md +580 -0
  50. data/node_modules/preact/debug.js +112 -0
  51. data/node_modules/preact/debug.js.map +1 -0
  52. data/node_modules/preact/debug/index.js +121 -0
  53. data/node_modules/preact/devtools.js +403 -0
  54. data/node_modules/preact/devtools.js.map +1 -0
  55. data/node_modules/preact/devtools/devtools.js +395 -0
  56. data/node_modules/preact/devtools/index.js +4 -0
  57. data/node_modules/preact/dist/preact.d.ts +891 -0
  58. data/node_modules/preact/dist/preact.dev.js +718 -0
  59. data/node_modules/preact/dist/preact.dev.js.map +1 -0
  60. data/node_modules/preact/dist/preact.js +408 -0
  61. data/node_modules/preact/dist/preact.js.flow +13 -0
  62. data/node_modules/preact/dist/preact.js.map +1 -0
  63. data/node_modules/preact/dist/preact.min.js +2 -0
  64. data/node_modules/preact/dist/preact.min.js.map +1 -0
  65. data/node_modules/preact/dist/preact.mjs +715 -0
  66. data/node_modules/preact/dist/preact.mjs.map +1 -0
  67. data/node_modules/preact/package.json +218 -0
  68. data/node_modules/preact/src/clone-element.js +18 -0
  69. data/node_modules/preact/src/component.js +90 -0
  70. data/node_modules/preact/src/constants.js +17 -0
  71. data/node_modules/preact/src/dom/index.js +138 -0
  72. data/node_modules/preact/src/h.js +86 -0
  73. data/node_modules/preact/src/options.js +22 -0
  74. data/node_modules/preact/src/preact.d.ts +891 -0
  75. data/node_modules/preact/src/preact.js +26 -0
  76. data/node_modules/preact/src/preact.js.flow +13 -0
  77. data/node_modules/preact/src/render-queue.js +28 -0
  78. data/node_modules/preact/src/render.js +22 -0
  79. data/node_modules/preact/src/util.js +19 -0
  80. data/node_modules/preact/src/vdom/component-recycler.js +48 -0
  81. data/node_modules/preact/src/vdom/component.js +296 -0
  82. data/node_modules/preact/src/vdom/diff.js +336 -0
  83. data/node_modules/preact/src/vdom/index.js +54 -0
  84. data/node_modules/preact/src/vnode.js +9 -0
  85. data/node_modules/preact/typings.json +5 -0
  86. metadata +78 -2
@@ -0,0 +1,11 @@
1
+ import { createElement } from 'preact' /** @jsx createElement */
2
+
3
+ const DropdownArrowDown = ({ className }) => (
4
+ <svg version='1.1' xmlns='http://www.w3.org/2000/svg' className={className} focusable='false'>
5
+ <g stroke='none' fill='none' fill-rule='evenodd'>
6
+ <polygon fill='#000000' points='0 0 22 0 11 17' />
7
+ </g>
8
+ </svg>
9
+ )
10
+
11
+ export default DropdownArrowDown
@@ -0,0 +1,80 @@
1
+ import { createElement, Component } from 'preact' /** @jsx createElement */
2
+
3
+ export default class Status extends Component {
4
+ static defaultProps = {
5
+ tQueryTooShort: (minQueryLength) => `Type in ${minQueryLength} or more characters for results.`,
6
+ tNoResults: () => 'No search results.',
7
+ tSelectedOption: (selectedOption, length, index) => `${selectedOption} (${index + 1} of ${length}) is selected.`,
8
+ tResults: (length, contentSelectedOption) => {
9
+ const words = {
10
+ result: (length === 1) ? 'result' : 'results',
11
+ is: (length === 1) ? 'is' : 'are'
12
+ }
13
+
14
+ return `${length} ${words.result} ${words.is} available. ${contentSelectedOption}`
15
+ }
16
+ };
17
+
18
+ state = {
19
+ bump: false
20
+ }
21
+
22
+ componentWillReceiveProps ({ queryLength }) {
23
+ const hasChanged = queryLength !== this.props.queryLength
24
+ if (hasChanged) {
25
+ this.setState(({ bump }) => ({ bump: !bump }))
26
+ }
27
+ }
28
+
29
+ render () {
30
+ const {
31
+ length,
32
+ queryLength,
33
+ minQueryLength,
34
+ selectedOption,
35
+ selectedOptionIndex,
36
+ tQueryTooShort,
37
+ tNoResults,
38
+ tSelectedOption,
39
+ tResults
40
+ } = this.props
41
+ const { bump } = this.state
42
+
43
+ const queryTooShort = queryLength < minQueryLength
44
+ const noResults = length === 0
45
+
46
+ const contentSelectedOption = selectedOption
47
+ ? tSelectedOption(selectedOption, length, selectedOptionIndex)
48
+ : ''
49
+
50
+ let content = null
51
+ if (queryTooShort) {
52
+ content = tQueryTooShort(minQueryLength)
53
+ } else if (noResults) {
54
+ content = tNoResults()
55
+ } else {
56
+ content = tResults(length, contentSelectedOption)
57
+ }
58
+
59
+ return <div
60
+ aria-atomic='true'
61
+ aria-live='polite'
62
+ role='status'
63
+ style={{
64
+ border: '0',
65
+ clip: 'rect(0 0 0 0)',
66
+ height: '1px',
67
+ marginBottom: '-1px',
68
+ marginRight: '-1px',
69
+ overflow: 'hidden',
70
+ padding: '0',
71
+ position: 'absolute',
72
+ whiteSpace: 'nowrap',
73
+ width: '1px'
74
+ }}
75
+ >
76
+ {content}
77
+ <span>{bump ? ',' : ',,'}</span>
78
+ </div>
79
+ }
80
+ }
@@ -0,0 +1,60 @@
1
+ import { createElement, render } from 'preact' /** @jsx createElement */
2
+ import Autocomplete from './autocomplete'
3
+
4
+ function accessibleAutocomplete (options) {
5
+ if (!options.element) { throw new Error('element is not defined') }
6
+ if (!options.id) { throw new Error('id is not defined') }
7
+ if (!options.source) { throw new Error('source is not defined') }
8
+ if (Array.isArray(options.source)) {
9
+ options.source = createSimpleEngine(options.source)
10
+ }
11
+ render(<Autocomplete {...options} />, options.element)
12
+ }
13
+
14
+ const createSimpleEngine = (values) => (query, syncResults) => {
15
+ var matches = values.filter(r => r.toLowerCase().indexOf(query.toLowerCase()) !== -1)
16
+ syncResults(matches)
17
+ }
18
+
19
+ accessibleAutocomplete.enhanceSelectElement = (configurationOptions) => {
20
+ if (!configurationOptions.selectElement) { throw new Error('selectElement is not defined') }
21
+
22
+ // Set defaults.
23
+ if (!configurationOptions.source) {
24
+ let availableOptions = [].filter.call(configurationOptions.selectElement.options, option => (option.value || configurationOptions.preserveNullOptions))
25
+ configurationOptions.source = availableOptions.map(option => option.textContent || option.innerText)
26
+ }
27
+ configurationOptions.onConfirm = configurationOptions.onConfirm || (query => {
28
+ const requestedOption = [].filter.call(configurationOptions.selectElement.options, option => (option.textContent || option.innerText) === query)[0]
29
+ if (requestedOption) { requestedOption.selected = true }
30
+ })
31
+
32
+ if (configurationOptions.selectElement.value || configurationOptions.defaultValue === undefined) {
33
+ const option = configurationOptions.selectElement.options[configurationOptions.selectElement.options.selectedIndex]
34
+ configurationOptions.defaultValue = option.textContent || option.innerText
35
+ }
36
+
37
+ if (configurationOptions.name === undefined) configurationOptions.name = ''
38
+ if (configurationOptions.id === undefined) {
39
+ if (configurationOptions.selectElement.id === undefined) {
40
+ configurationOptions.id = ''
41
+ } else {
42
+ configurationOptions.id = configurationOptions.selectElement.id
43
+ }
44
+ }
45
+ if (configurationOptions.autoselect === undefined) configurationOptions.autoselect = true
46
+
47
+ const element = document.createElement('span')
48
+
49
+ configurationOptions.selectElement.parentNode.insertBefore(element, configurationOptions.selectElement)
50
+
51
+ accessibleAutocomplete({
52
+ ...configurationOptions,
53
+ element: element
54
+ })
55
+
56
+ configurationOptions.selectElement.style.display = 'none'
57
+ configurationOptions.selectElement.id = configurationOptions.selectElement.id + '-select'
58
+ }
59
+
60
+ export default accessibleAutocomplete
@@ -0,0 +1,44 @@
1
+ /* global before, beforeEach, after, describe, expect, it */
2
+ import { createElement, render } from 'preact' /** @jsx createElement */
3
+ import DropdownArrowDown from '../../src/dropdown-arrow-down'
4
+
5
+ describe('DropdownArrowDown', () => {
6
+ describe('rendering', () => {
7
+ let scratch
8
+
9
+ before(() => {
10
+ scratch = document.createElement('div');
11
+ (document.body || document.documentElement).appendChild(scratch)
12
+ })
13
+
14
+ beforeEach(() => {
15
+ scratch.innerHTML = ''
16
+ })
17
+
18
+ after(() => {
19
+ scratch.parentNode.removeChild(scratch)
20
+ scratch = null
21
+ })
22
+
23
+ describe('basic usage', () => {
24
+ it('renders an svg', () => {
25
+ render(<DropdownArrowDown />, scratch)
26
+
27
+ expect(scratch.innerHTML).to.contain('svg')
28
+ })
29
+
30
+ it('renders with a given custom class', () => {
31
+ render(<DropdownArrowDown className='foo' />, scratch)
32
+
33
+ expect(scratch.innerHTML).to.contain('class="foo"')
34
+ })
35
+
36
+ // IE issue so the dropdown svg is not focusable (tabindex won't work for this)
37
+ it('renders an svg where focusable attribute is false', () => {
38
+ render(<DropdownArrowDown />, scratch)
39
+
40
+ expect(scratch.innerHTML).to.contain('focusable="false"')
41
+ })
42
+ })
43
+ })
44
+ })
@@ -0,0 +1,485 @@
1
+ /* global after, describe, before, beforeEach, expect, it */
2
+ import { createElement, render } from 'preact' /** @jsx createElement */
3
+ import Autocomplete from '../../src/autocomplete'
4
+
5
+ function suggest (query, syncResults) {
6
+ var results = [
7
+ 'France',
8
+ 'Germany',
9
+ 'United Kingdom'
10
+ ]
11
+ syncResults(query
12
+ ? results.filter(function (result) {
13
+ return result.toLowerCase().indexOf(query.toLowerCase()) !== -1
14
+ })
15
+ : []
16
+ )
17
+ }
18
+
19
+ describe('Autocomplete', () => {
20
+ describe('rendering', () => {
21
+ let scratch
22
+
23
+ before(() => {
24
+ scratch = document.createElement('div');
25
+ (document.body || document.documentElement).appendChild(scratch)
26
+ })
27
+
28
+ beforeEach(() => {
29
+ scratch.innerHTML = ''
30
+ })
31
+
32
+ after(() => {
33
+ scratch.parentNode.removeChild(scratch)
34
+ scratch = null
35
+ })
36
+
37
+ describe('basic usage', () => {
38
+ it('renders an input', () => {
39
+ render(<Autocomplete />, scratch)
40
+
41
+ expect(scratch.innerHTML).to.contain('input')
42
+ expect(scratch.innerHTML).to.contain('class="autocomplete__input')
43
+ expect(scratch.innerHTML).to.contain('class="autocomplete__menu')
44
+ expect(scratch.innerHTML).to.contain('name="input-autocomplete"')
45
+ })
46
+
47
+ it('renders an input with a required attribute', () => {
48
+ render(<Autocomplete required />, scratch)
49
+
50
+ expect(scratch.innerHTML).to.contain('required')
51
+ })
52
+
53
+ it('renders an input without a required attribute', () => {
54
+ render(<Autocomplete required={false} />, scratch)
55
+
56
+ expect(scratch.innerHTML).to.not.contain('required')
57
+ })
58
+
59
+ it('renders an input with a name attribute', () => {
60
+ render(<Autocomplete name='bob' />, scratch)
61
+
62
+ expect(scratch.innerHTML).to.contain('name="bob"')
63
+ })
64
+
65
+ it('renders an input with a custom CSS namespace', () => {
66
+ render(<Autocomplete cssNamespace='bob' />, scratch)
67
+
68
+ expect(scratch.innerHTML).to.contain('class="bob__input')
69
+ expect(scratch.innerHTML).to.contain('class="bob__menu')
70
+ })
71
+
72
+ it('renders with the correct aria attributes', () => {
73
+ render(<Autocomplete required />, scratch)
74
+
75
+ let wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
76
+
77
+ expect(wrapperElement.getAttribute('aria-expanded')).to.equal('false')
78
+ })
79
+
80
+ it('renders with the correct roles', () => {
81
+ render(<Autocomplete required />, scratch)
82
+
83
+ let wrapperElement = scratch.getElementsByClassName('autocomplete__wrapper')[0]
84
+ let inputElement = wrapperElement.getElementsByTagName('input')[0]
85
+ let dropdownElement = wrapperElement.getElementsByTagName('ul')[0]
86
+
87
+ expect(inputElement.getAttribute('role')).to.equal('textbox', 'input should have textbox role')
88
+ expect(dropdownElement.getAttribute('role')).to.equal('listbox', 'menu should have listbox role')
89
+ })
90
+ })
91
+ })
92
+
93
+ describe('behaviour', () => {
94
+ let autocomplete, autoselectAutocomplete, onConfirmAutocomplete, onConfirmTriggered,
95
+ autoselectOnSelectAutocomplete, confirmOnBlurAutocomplete
96
+
97
+ beforeEach(() => {
98
+ autocomplete = new Autocomplete({
99
+ ...Autocomplete.defaultProps,
100
+ id: 'test',
101
+ source: suggest
102
+ })
103
+
104
+ autoselectAutocomplete = new Autocomplete({
105
+ ...Autocomplete.defaultProps,
106
+ autoselect: true,
107
+ id: 'test2',
108
+ source: suggest
109
+ })
110
+
111
+ onConfirmTriggered = false
112
+ onConfirmAutocomplete = new Autocomplete({
113
+ ...Autocomplete.defaultProps,
114
+ id: 'test3',
115
+ onConfirm: () => { onConfirmTriggered = true },
116
+ source: suggest
117
+ })
118
+
119
+ autoselectOnSelectAutocomplete = new Autocomplete({
120
+ ...Autocomplete.defaultProps,
121
+ autoselect: true,
122
+ id: 'test4',
123
+ onConfirm: () => { onConfirmTriggered = true },
124
+ source: suggest
125
+ })
126
+
127
+ confirmOnBlurAutocomplete = new Autocomplete({
128
+ ...Autocomplete.defaultProps,
129
+ id: 'test5',
130
+ onConfirm: () => { onConfirmTriggered = true },
131
+ confirmOnBlur: false,
132
+ source: suggest
133
+ })
134
+ })
135
+
136
+ describe('typing', () => {
137
+ it('searches for options', () => {
138
+ autocomplete.handleInputChange({ target: { value: 'f' } })
139
+ expect(autocomplete.state.menuOpen).to.equal(true)
140
+ expect(autocomplete.state.options).to.contain('France')
141
+ })
142
+
143
+ it('hides menu when no options are available', () => {
144
+ autocomplete.handleInputChange({ target: { value: 'aa' } })
145
+ expect(autocomplete.state.menuOpen).to.equal(false)
146
+ expect(autocomplete.state.options.length).to.equal(0)
147
+ })
148
+
149
+ it('hides menu when query becomes empty', () => {
150
+ autocomplete.setState({ query: 'f', options: ['France'], menuOpen: true })
151
+ autocomplete.handleInputChange({ target: { value: '' } })
152
+ expect(autocomplete.state.menuOpen).to.equal(false)
153
+ })
154
+
155
+ describe('with minLength', () => {
156
+ beforeEach(() => {
157
+ autocomplete = new Autocomplete({
158
+ ...Autocomplete.defaultProps,
159
+ id: 'test',
160
+ source: suggest,
161
+ minLength: 2
162
+ })
163
+ })
164
+
165
+ it('doesn\'t search when under limit', () => {
166
+ autocomplete.handleInputChange({ target: { value: 'f' } })
167
+ expect(autocomplete.state.menuOpen).to.equal(false)
168
+ expect(autocomplete.state.options.length).to.equal(0)
169
+ })
170
+
171
+ it('does search when over limit', () => {
172
+ autocomplete.handleInputChange({ target: { value: 'fra' } })
173
+ expect(autocomplete.state.menuOpen).to.equal(true)
174
+ expect(autocomplete.state.options).to.contain('France')
175
+ })
176
+
177
+ it('hides results when going under limit', () => {
178
+ autocomplete.setState({ menuOpen: true, query: 'fr', options: ['France'] })
179
+ autocomplete.handleInputChange({ target: { value: 'f' } })
180
+ expect(autocomplete.state.menuOpen).to.equal(false)
181
+ expect(autocomplete.state.options.length).to.equal(0)
182
+ })
183
+ })
184
+ })
185
+
186
+ describe('focusing input', () => {
187
+ it('does not display menu when something is typed in', () => {
188
+ autocomplete.setState({ query: 'f' })
189
+ autocomplete.handleInputFocus()
190
+ expect(autocomplete.state.menuOpen).to.equal(false)
191
+ expect(autocomplete.state.focused).to.equal(-1)
192
+ })
193
+
194
+ it('hides menu when query is empty', () => {
195
+ autocomplete.setState({ query: '' })
196
+ autocomplete.handleInputFocus()
197
+ expect(autocomplete.state.menuOpen).to.equal(false)
198
+ expect(autocomplete.state.focused).to.equal(-1)
199
+ })
200
+
201
+ describe('with option selected', () => {
202
+ it('leaves menu open, does not change query', () => {
203
+ autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
204
+ autocomplete.handleInputFocus()
205
+ expect(autocomplete.state.focused).to.equal(-1)
206
+ expect(autocomplete.state.menuOpen).to.equal(true)
207
+ expect(autocomplete.state.query).to.equal('fr')
208
+ })
209
+ })
210
+
211
+ describe('with defaultValue', () => {
212
+ beforeEach(() => {
213
+ autocomplete = new Autocomplete({
214
+ ...Autocomplete.defaultProps,
215
+ defaultValue: 'France',
216
+ id: 'test',
217
+ source: suggest
218
+ })
219
+ })
220
+
221
+ it('is prefilled', () => {
222
+ expect(autocomplete.state.options.length).to.equal(1)
223
+ expect(autocomplete.state.options[0]).to.equal('France')
224
+ expect(autocomplete.state.query).to.equal('France')
225
+ })
226
+ })
227
+ })
228
+
229
+ describe('blurring input', () => {
230
+ it('unfocuses component', (done) => {
231
+ autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: -1 })
232
+ autocomplete.handleInputBlur({ relatedTarget: null })
233
+ // Using setTimeouts here since changes in values take a while to reflect in lists
234
+ setTimeout(() => {
235
+ expect(autocomplete.state.focused).to.equal(null)
236
+ expect(autocomplete.state.menuOpen).to.equal(false)
237
+ expect(autocomplete.state.query).to.equal('fr')
238
+ done()
239
+ }, 250)
240
+ })
241
+
242
+ describe('with autoselect and onConfirm', () => {
243
+ it('unfocuses component, updates query, triggers onConfirm', (done) => {
244
+ autoselectOnSelectAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: 0 })
245
+ autoselectOnSelectAutocomplete.handleInputBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
246
+ // Using setTimeouts here since changes in values take a while to reflect in lists
247
+ setTimeout(() => {
248
+ expect(autoselectOnSelectAutocomplete.state.focused).to.equal(null)
249
+ expect(autoselectOnSelectAutocomplete.state.menuOpen).to.equal(false)
250
+ expect(autoselectOnSelectAutocomplete.state.query).to.equal('France')
251
+ expect(onConfirmTriggered).to.equal(true)
252
+ done()
253
+ }, 250)
254
+ })
255
+ })
256
+
257
+ describe('with confirmOnBlur false', () => {
258
+ it('unfocuses component, does not touch query, does not trigger onConfirm', (done) => {
259
+ confirmOnBlurAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: -1, selected: 0 })
260
+ confirmOnBlurAutocomplete.handleInputBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
261
+ setTimeout(() => {
262
+ expect(confirmOnBlurAutocomplete.state.focused).to.equal(null)
263
+ expect(confirmOnBlurAutocomplete.state.menuOpen).to.equal(false)
264
+ expect(confirmOnBlurAutocomplete.state.query).to.equal('fr')
265
+ expect(onConfirmTriggered).to.equal(false)
266
+ done()
267
+ }, 250)
268
+ })
269
+ })
270
+ })
271
+
272
+ describe('focusing option', () => {
273
+ it('sets the option as focused', () => {
274
+ autocomplete.setState({ options: ['France'] })
275
+ autocomplete.handleOptionFocus(0)
276
+ expect(autocomplete.state.focused).to.equal(0)
277
+ })
278
+ })
279
+
280
+ describe('focusing out option', () => {
281
+ describe('with input selected', () => {
282
+ it('unfocuses component, does not change query', () => {
283
+ autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: -1 })
284
+ autocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
285
+ expect(autocomplete.state.focused).to.equal(null)
286
+ expect(autocomplete.state.menuOpen).to.equal(false)
287
+ expect(autocomplete.state.query).to.equal('fr')
288
+ })
289
+ })
290
+
291
+ describe('with option selected', () => {
292
+ describe('with confirmOnBlur true', () => {
293
+ it('unfocuses component, updates query', () => {
294
+ autocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
295
+ autocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
296
+ expect(autocomplete.state.focused).to.equal(null)
297
+ expect(autocomplete.state.menuOpen).to.equal(false)
298
+ expect(autocomplete.state.query).to.equal('France')
299
+ })
300
+ })
301
+ describe('with confirmOnBlur false', () => {
302
+ it('unfocuses component, does not update query', () => {
303
+ confirmOnBlurAutocomplete.setState({ menuOpen: true, options: ['France'], query: 'fr', focused: 0, selected: 0 })
304
+ confirmOnBlurAutocomplete.handleOptionBlur({ target: 'mock', relatedTarget: 'relatedMock' }, 0)
305
+ expect(confirmOnBlurAutocomplete.state.focused).to.equal(null)
306
+ expect(confirmOnBlurAutocomplete.state.menuOpen).to.equal(false)
307
+ expect(confirmOnBlurAutocomplete.state.query).to.equal('fr')
308
+ })
309
+ })
310
+ })
311
+ })
312
+
313
+ describe('hovering option', () => {
314
+ it('sets the option as hovered, does not change focused, does not change selected', () => {
315
+ autocomplete.setState({ options: ['France'], hovered: null, focused: -1, selected: -1 })
316
+ autocomplete.handleOptionMouseEnter({}, 0)
317
+ expect(autocomplete.state.hovered).to.equal(0)
318
+ expect(autocomplete.state.focused).to.equal(-1)
319
+ expect(autocomplete.state.selected).to.equal(-1)
320
+ })
321
+ })
322
+
323
+ describe('hovering out option', () => {
324
+ it('sets focus back on selected, sets hovered to null', () => {
325
+ autocomplete.setState({ options: ['France'], hovered: 0, focused: -1, selected: -1 })
326
+ autocomplete.handleListMouseLeave({ toElement: 'mock' }, 0)
327
+ expect(autocomplete.state.hovered).to.equal(null)
328
+ expect(autocomplete.state.focused).to.equal(-1)
329
+ expect(autocomplete.state.selected).to.equal(-1)
330
+ })
331
+ })
332
+
333
+ describe('up key', () => {
334
+ it('focuses previous element', () => {
335
+ autocomplete.setState({ menuOpen: true, options: ['France'], focused: 0 })
336
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 38 })
337
+ expect(autocomplete.state.focused).to.equal(-1)
338
+ })
339
+ })
340
+
341
+ describe('down key', () => {
342
+ describe('0 options available', () => {
343
+ it('does nothing', () => {
344
+ autocomplete.setState({ menuOpen: false, options: [], focused: -1 })
345
+ const stateBefore = autocomplete.state
346
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
347
+ expect(autocomplete.state).to.equal(stateBefore)
348
+ })
349
+ })
350
+
351
+ describe('1 option available', () => {
352
+ it('focuses next element', () => {
353
+ autocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: -1 })
354
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
355
+ expect(autocomplete.state.focused).to.equal(0)
356
+ expect(autocomplete.state.selected).to.equal(0)
357
+ })
358
+ })
359
+
360
+ describe('2 or more option available', () => {
361
+ it('focuses next element', () => {
362
+ autocomplete.setState({ menuOpen: true, options: ['France', 'Germany'], focused: 0, selected: 0 })
363
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
364
+ expect(autocomplete.state.focused).to.equal(1)
365
+ expect(autocomplete.state.selected).to.equal(1)
366
+ })
367
+ })
368
+
369
+ describe('autoselect', () => {
370
+ describe('0 options available', () => {
371
+ it('does nothing', () => {
372
+ autoselectAutocomplete.setState({ menuOpen: false, options: [], focused: -1, selected: -1 })
373
+ const stateBefore = autoselectAutocomplete.state
374
+ autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
375
+ expect(autoselectAutocomplete.state).to.equal(stateBefore)
376
+ })
377
+ })
378
+
379
+ describe('1 option available', () => {
380
+ it('does nothing', () => {
381
+ autoselectAutocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: 0 })
382
+ const stateBefore = autoselectAutocomplete.state
383
+ autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
384
+ expect(autoselectAutocomplete.state).to.equal(stateBefore)
385
+ })
386
+ })
387
+
388
+ describe('2 or more option available', () => {
389
+ it('on input, focuses second element', () => {
390
+ autoselectAutocomplete.setState({ menuOpen: true, options: ['France', 'Germany'], focused: -1, selected: 0 })
391
+ autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 40 })
392
+ expect(autoselectAutocomplete.state.focused).to.equal(1)
393
+ expect(autoselectAutocomplete.state.selected).to.equal(1)
394
+ })
395
+ })
396
+ })
397
+ })
398
+
399
+ describe('escape key', () => {
400
+ it('unfocuses component', () => {
401
+ autocomplete.setState({ menuOpen: true, options: ['France'], focused: -1 })
402
+ autocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 27 })
403
+ expect(autocomplete.state.menuOpen).to.equal(false)
404
+ expect(autocomplete.state.focused).to.equal(null)
405
+ })
406
+ })
407
+
408
+ describe('enter key', () => {
409
+ describe('on an option', () => {
410
+ it('prevents default, closes the menu, sets the query, focuses the input, triggers onConfirm', () => {
411
+ let preventedDefault = false
412
+ onConfirmAutocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
413
+ onConfirmAutocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
414
+ expect(onConfirmAutocomplete.state.menuOpen).to.equal(false)
415
+ expect(onConfirmAutocomplete.state.query).to.equal('France')
416
+ expect(onConfirmAutocomplete.state.focused).to.equal(-1)
417
+ expect(onConfirmAutocomplete.state.selected).to.equal(-1)
418
+ expect(preventedDefault).to.equal(true)
419
+ expect(onConfirmTriggered).to.equal(true)
420
+ })
421
+ })
422
+
423
+ describe('on the input', () => {
424
+ describe('with menu opened', () => {
425
+ it('prevents default, does nothing', () => {
426
+ let preventedDefault = false
427
+ autocomplete.setState({ menuOpen: true, options: [], query: 'asd', focused: -1, selected: -1 })
428
+ const stateBefore = autocomplete.state
429
+ autocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
430
+ expect(autocomplete.state).to.equal(stateBefore)
431
+ expect(preventedDefault).to.equal(true)
432
+ })
433
+ })
434
+
435
+ describe('with menu closed', () => {
436
+ it('bubbles, does not prevent default', () => {
437
+ let preventedDefault = false
438
+ autocomplete.setState({ menuOpen: false, options: ['France'], focused: -1, selected: -1 })
439
+ const stateBefore = autocomplete.state
440
+ autocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 13 })
441
+ expect(autocomplete.state).to.equal(stateBefore)
442
+ expect(preventedDefault).to.equal(false)
443
+ })
444
+ })
445
+
446
+ describe('autoselect', () => {
447
+ it('closes the menu, selects the first option, keeps input focused', () => {
448
+ autoselectAutocomplete.setState({ menuOpen: true, options: ['France'], focused: -1, selected: 0 })
449
+ autoselectAutocomplete.handleKeyDown({ preventDefault: () => {}, keyCode: 13 })
450
+ expect(autoselectAutocomplete.state.menuOpen).to.equal(false)
451
+ expect(autoselectAutocomplete.state.query).to.equal('France')
452
+ expect(autoselectAutocomplete.state.focused).to.equal(-1)
453
+ expect(autoselectAutocomplete.state.selected).to.equal(-1)
454
+ })
455
+ })
456
+ })
457
+ })
458
+
459
+ describe('space key', () => {
460
+ describe('on an option', () => {
461
+ it('prevents default, closes the menu, sets the query, focuses the input, triggers onConfirm', () => {
462
+ let preventedDefault = false
463
+ onConfirmAutocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
464
+ onConfirmAutocomplete.handleKeyDown({ preventDefault: () => { preventedDefault = true }, keyCode: 32 })
465
+ expect(onConfirmAutocomplete.state.menuOpen).to.equal(false)
466
+ expect(onConfirmAutocomplete.state.query).to.equal('France')
467
+ expect(onConfirmAutocomplete.state.focused).to.equal(-1)
468
+ expect(onConfirmAutocomplete.state.selected).to.equal(-1)
469
+ expect(preventedDefault).to.equal(true)
470
+ expect(onConfirmTriggered).to.equal(true)
471
+ })
472
+ })
473
+ })
474
+
475
+ describe('an unrecognised key', () => {
476
+ it('does nothing', () => {
477
+ autocomplete.setState({ menuOpen: true, options: ['France'], focused: 0, selected: 0 })
478
+ autocomplete.elementReferences[-1] = 'input element'
479
+ autocomplete.handleKeyDown({ target: 'not the input element', keyCode: 4242 })
480
+ expect(autocomplete.state.focused).to.equal(0)
481
+ expect(autocomplete.state.selected).to.equal(0)
482
+ })
483
+ })
484
+ })
485
+ })