govuk_publishing_components 43.3.0 → 43.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/govuk_publishing_components/icon-autocomplete-search-suggestion.svg +4 -0
  3. data/app/assets/javascripts/govuk_publishing_components/components/search-with-autocomplete.js +123 -0
  4. data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +5 -4
  5. data/app/assets/stylesheets/govuk_publishing_components/components/_phase-banner.scss +0 -2
  6. data/app/assets/stylesheets/govuk_publishing_components/components/_search-with-autocomplete.scss +200 -0
  7. data/app/views/govuk_publishing_components/components/_layout_header.html.erb +16 -40
  8. data/app/views/govuk_publishing_components/components/_search.html.erb +16 -14
  9. data/app/views/govuk_publishing_components/components/_search_with_autocomplete.html.erb +27 -0
  10. data/app/views/govuk_publishing_components/components/docs/layout_header.yml +0 -41
  11. data/app/views/govuk_publishing_components/components/docs/phase_banner.yml +4 -0
  12. data/app/views/govuk_publishing_components/components/docs/search_with_autocomplete.yml +61 -0
  13. data/lib/govuk_publishing_components/version.rb +1 -1
  14. data/node_modules/accessible-autocomplete/CHANGELOG.md +390 -0
  15. data/node_modules/accessible-autocomplete/CODEOWNERS +2 -0
  16. data/node_modules/accessible-autocomplete/CONTRIBUTING.md +161 -0
  17. data/node_modules/accessible-autocomplete/LICENSE.txt +20 -0
  18. data/node_modules/accessible-autocomplete/Procfile +1 -0
  19. data/node_modules/accessible-autocomplete/README.md +490 -0
  20. data/node_modules/accessible-autocomplete/accessibility-criteria.md +42 -0
  21. data/node_modules/accessible-autocomplete/app.json +15 -0
  22. data/node_modules/accessible-autocomplete/babel.config.js +29 -0
  23. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css +3 -0
  24. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css.map +1 -0
  25. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js +2 -0
  26. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js.map +1 -0
  27. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js +2 -0
  28. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js.map +1 -0
  29. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js +2 -0
  30. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js.map +1 -0
  31. data/node_modules/accessible-autocomplete/examples/form-single.html +379 -0
  32. data/node_modules/accessible-autocomplete/examples/form.html +673 -0
  33. data/node_modules/accessible-autocomplete/examples/index.html +738 -0
  34. data/node_modules/accessible-autocomplete/examples/preact/index.html +346 -0
  35. data/node_modules/accessible-autocomplete/examples/react/index.html +347 -0
  36. data/node_modules/accessible-autocomplete/package.json +93 -0
  37. data/node_modules/accessible-autocomplete/postcss.config.js +16 -0
  38. data/node_modules/accessible-autocomplete/preact.js +1 -0
  39. data/node_modules/accessible-autocomplete/react.js +1 -0
  40. data/node_modules/accessible-autocomplete/scripts/check-staged.mjs +16 -0
  41. data/node_modules/accessible-autocomplete/src/autocomplete.css +167 -0
  42. data/node_modules/accessible-autocomplete/src/autocomplete.js +608 -0
  43. data/node_modules/accessible-autocomplete/src/dropdown-arrow-down.js +11 -0
  44. data/node_modules/accessible-autocomplete/src/status.js +125 -0
  45. data/node_modules/accessible-autocomplete/src/wrapper.js +60 -0
  46. data/node_modules/accessible-autocomplete/test/functional/dropdown-arrow-down.js +46 -0
  47. data/node_modules/accessible-autocomplete/test/functional/index.js +793 -0
  48. data/node_modules/accessible-autocomplete/test/functional/wrapper.js +339 -0
  49. data/node_modules/accessible-autocomplete/test/integration/index.js +309 -0
  50. data/node_modules/accessible-autocomplete/test/karma.config.js +46 -0
  51. data/node_modules/accessible-autocomplete/test/wdio.config.js +123 -0
  52. data/node_modules/accessible-autocomplete/webpack.config.mjs +244 -0
  53. metadata +46 -2
@@ -0,0 +1,608 @@
1
+ import { createElement, Component } from 'preact' /** @jsx createElement */
2
+ import Status from './status'
3
+ import DropdownArrowDown from './dropdown-arrow-down'
4
+
5
+ const IS_PREACT = process.env.COMPONENT_LIBRARY === 'PREACT'
6
+ const IS_REACT = process.env.COMPONENT_LIBRARY === 'REACT'
7
+
8
+ const keyCodes = {
9
+ 13: 'enter',
10
+ 27: 'escape',
11
+ 32: 'space',
12
+ 38: 'up',
13
+ 40: 'down'
14
+ }
15
+
16
+ function isIosDevice () {
17
+ return typeof navigator !== 'undefined' && !!(navigator.userAgent.match(/(iPod|iPhone|iPad)/g) && navigator.userAgent.match(/AppleWebKit/g))
18
+ }
19
+
20
+ function isPrintableKeyCode (keyCode) {
21
+ return (
22
+ (keyCode > 47 && keyCode < 58) || // number keys
23
+ keyCode === 32 || keyCode === 8 || // spacebar or backspace
24
+ (keyCode > 64 && keyCode < 91) || // letter keys
25
+ (keyCode > 95 && keyCode < 112) || // numpad keys
26
+ (keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
27
+ (keyCode > 218 && keyCode < 223) // [\]' (in order)
28
+ )
29
+ }
30
+
31
+ // Preact does not implement onChange on inputs, but React does.
32
+ function onChangeCrossLibrary (handler) {
33
+ if (IS_PREACT) { return { onInput: handler } }
34
+ if (IS_REACT) { return { onChange: handler } }
35
+ }
36
+
37
+ export default class Autocomplete extends Component {
38
+ static defaultProps = {
39
+ autoselect: false,
40
+ cssNamespace: 'autocomplete',
41
+ defaultValue: '',
42
+ displayMenu: 'inline',
43
+ minLength: 0,
44
+ name: 'input-autocomplete',
45
+ placeholder: '',
46
+ onConfirm: () => {},
47
+ confirmOnBlur: true,
48
+ showNoOptionsFound: true,
49
+ showAllValues: false,
50
+ required: false,
51
+ tNoResults: () => 'No results found',
52
+ tAssistiveHint: () => 'When autocomplete results are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures.',
53
+ dropdownArrow: DropdownArrowDown,
54
+ menuAttributes: {},
55
+ inputClasses: null,
56
+ hintClasses: null,
57
+ menuClasses: null
58
+ }
59
+
60
+ elementReferences = {}
61
+
62
+ constructor (props) {
63
+ super(props)
64
+
65
+ this.state = {
66
+ focused: null,
67
+ hovered: null,
68
+ menuOpen: false,
69
+ options: props.defaultValue ? [props.defaultValue] : [],
70
+ query: props.defaultValue,
71
+ validChoiceMade: false,
72
+ selected: null,
73
+ ariaHint: true
74
+ }
75
+
76
+ this.handleComponentBlur = this.handleComponentBlur.bind(this)
77
+ this.handleKeyDown = this.handleKeyDown.bind(this)
78
+ this.handleUpArrow = this.handleUpArrow.bind(this)
79
+ this.handleDownArrow = this.handleDownArrow.bind(this)
80
+ this.handleEnter = this.handleEnter.bind(this)
81
+ this.handlePrintableKey = this.handlePrintableKey.bind(this)
82
+
83
+ this.handleListMouseLeave = this.handleListMouseLeave.bind(this)
84
+
85
+ this.handleOptionBlur = this.handleOptionBlur.bind(this)
86
+ this.handleOptionClick = this.handleOptionClick.bind(this)
87
+ this.handleOptionFocus = this.handleOptionFocus.bind(this)
88
+ this.handleOptionMouseDown = this.handleOptionMouseDown.bind(this)
89
+ this.handleOptionMouseEnter = this.handleOptionMouseEnter.bind(this)
90
+
91
+ this.handleInputBlur = this.handleInputBlur.bind(this)
92
+ this.handleInputChange = this.handleInputChange.bind(this)
93
+ this.handleInputClick = this.handleInputClick.bind(this)
94
+ this.handleInputFocus = this.handleInputFocus.bind(this)
95
+
96
+ this.pollInputElement = this.pollInputElement.bind(this)
97
+ this.getDirectInputChanges = this.getDirectInputChanges.bind(this)
98
+ }
99
+
100
+ isQueryAnOption (query, options) {
101
+ return options.map(entry => this.templateInputValue(entry).toLowerCase()).indexOf(query.toLowerCase()) !== -1
102
+ }
103
+
104
+ componentDidMount () {
105
+ this.pollInputElement()
106
+ }
107
+
108
+ componentWillUnmount () {
109
+ clearTimeout(this.$pollInput)
110
+ }
111
+
112
+ // Applications like Dragon NaturallySpeaking will modify the
113
+ // `input` field by directly changing its `.value`. These events
114
+ // don't trigger our JavaScript event listeners, so we need to poll
115
+ // to handle when and if they occur.
116
+ pollInputElement () {
117
+ this.getDirectInputChanges()
118
+ this.$pollInput = setTimeout(() => {
119
+ this.pollInputElement()
120
+ }, 100)
121
+ }
122
+
123
+ getDirectInputChanges () {
124
+ const inputReference = this.elementReferences[-1]
125
+ const queryHasChanged = inputReference && inputReference.value !== this.state.query
126
+
127
+ if (queryHasChanged) {
128
+ this.handleInputChange({ target: { value: inputReference.value } })
129
+ }
130
+ }
131
+
132
+ componentDidUpdate (prevProps, prevState) {
133
+ const { focused } = this.state
134
+ const componentLostFocus = focused === null
135
+ const focusedChanged = prevState.focused !== focused
136
+ const focusDifferentElement = focusedChanged && !componentLostFocus
137
+ if (focusDifferentElement) {
138
+ this.elementReferences[focused].focus()
139
+ }
140
+ const focusedInput = focused === -1
141
+ const componentGainedFocus = focusedChanged && prevState.focused === null
142
+ const selectAllText = focusedInput && componentGainedFocus
143
+ if (selectAllText) {
144
+ const inputElement = this.elementReferences[focused]
145
+ inputElement.setSelectionRange(0, inputElement.value.length)
146
+ }
147
+ }
148
+
149
+ hasAutoselect () {
150
+ return isIosDevice() ? false : this.props.autoselect
151
+ }
152
+
153
+ // This template is used when converting from a state.options object into a state.query.
154
+ templateInputValue (value) {
155
+ const inputValueTemplate = this.props.templates && this.props.templates.inputValue
156
+ return inputValueTemplate ? inputValueTemplate(value) : value
157
+ }
158
+
159
+ // This template is used when displaying results / suggestions.
160
+ templateSuggestion (value) {
161
+ const suggestionTemplate = this.props.templates && this.props.templates.suggestion
162
+ return suggestionTemplate ? suggestionTemplate(value) : value
163
+ }
164
+
165
+ handleComponentBlur (newState) {
166
+ const { options, query, selected } = this.state
167
+ let newQuery
168
+ if (this.props.confirmOnBlur) {
169
+ newQuery = newState.query || query
170
+ this.props.onConfirm(options[selected])
171
+ } else {
172
+ newQuery = query
173
+ }
174
+ this.setState({
175
+ focused: null,
176
+ menuOpen: newState.menuOpen || false,
177
+ query: newQuery,
178
+ selected: null,
179
+ validChoiceMade: this.isQueryAnOption(newQuery, options)
180
+ })
181
+ }
182
+
183
+ handleListMouseLeave (event) {
184
+ this.setState({
185
+ hovered: null
186
+ })
187
+ }
188
+
189
+ handleOptionBlur (event, index) {
190
+ const { focused, menuOpen, options, selected } = this.state
191
+ const focusingOutsideComponent = event.relatedTarget === null
192
+ const focusingInput = event.relatedTarget === this.elementReferences[-1]
193
+ const focusingAnotherOption = focused !== index && focused !== -1
194
+ const blurComponent = (!focusingAnotherOption && focusingOutsideComponent) || !(focusingAnotherOption || focusingInput)
195
+ if (blurComponent) {
196
+ const keepMenuOpen = menuOpen && isIosDevice()
197
+ this.handleComponentBlur({
198
+ menuOpen: keepMenuOpen,
199
+ query: this.templateInputValue(options[selected])
200
+ })
201
+ }
202
+ }
203
+
204
+ handleInputBlur (event) {
205
+ const { focused, menuOpen, options, query, selected } = this.state
206
+ const focusingAnOption = focused !== -1
207
+ if (!focusingAnOption) {
208
+ const keepMenuOpen = menuOpen && isIosDevice()
209
+ const newQuery = isIosDevice() ? query : this.templateInputValue(options[selected])
210
+ this.handleComponentBlur({
211
+ menuOpen: keepMenuOpen,
212
+ query: newQuery
213
+ })
214
+ }
215
+ }
216
+
217
+ handleInputChange (event) {
218
+ const { minLength, source, showAllValues } = this.props
219
+ const autoselect = this.hasAutoselect()
220
+ const query = event.target.value
221
+ const queryEmpty = query.length === 0
222
+ const queryChanged = this.state.query !== query
223
+ const queryLongEnough = query.length >= minLength
224
+
225
+ this.setState({
226
+ query,
227
+ ariaHint: queryEmpty
228
+ })
229
+
230
+ const searchForOptions = showAllValues || (!queryEmpty && queryChanged && queryLongEnough)
231
+ if (searchForOptions) {
232
+ source(query, (options) => {
233
+ const optionsAvailable = options.length > 0
234
+ this.setState({
235
+ menuOpen: optionsAvailable,
236
+ options,
237
+ selected: (autoselect && optionsAvailable) ? 0 : -1,
238
+ validChoiceMade: false
239
+ })
240
+ })
241
+ } else if (queryEmpty || !queryLongEnough) {
242
+ this.setState({
243
+ menuOpen: false,
244
+ options: []
245
+ })
246
+ }
247
+ }
248
+
249
+ handleInputClick (event) {
250
+ this.handleInputChange(event)
251
+ }
252
+
253
+ handleInputFocus (event) {
254
+ const { query, validChoiceMade, options } = this.state
255
+ const { minLength } = this.props
256
+ const shouldReopenMenu = !validChoiceMade && query.length >= minLength && options.length > 0
257
+
258
+ if (shouldReopenMenu) {
259
+ this.setState(({ menuOpen }) => ({ focused: -1, menuOpen: shouldReopenMenu || menuOpen, selected: -1 }))
260
+ } else {
261
+ this.setState({ focused: -1 })
262
+ }
263
+ }
264
+
265
+ handleOptionFocus (index) {
266
+ this.setState({
267
+ focused: index,
268
+ hovered: null,
269
+ selected: index
270
+ })
271
+ }
272
+
273
+ handleOptionMouseEnter (event, index) {
274
+ // iOS Safari prevents click event if mouseenter adds hover background colour
275
+ // See: https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html#//apple_ref/doc/uid/TP40006511-SW4
276
+ if (!isIosDevice()) {
277
+ this.setState({
278
+ hovered: index
279
+ })
280
+ }
281
+ }
282
+
283
+ handleOptionClick (event, index) {
284
+ const selectedOption = this.state.options[index]
285
+ const newQuery = this.templateInputValue(selectedOption)
286
+ this.props.onConfirm(selectedOption)
287
+ this.setState({
288
+ focused: -1,
289
+ hovered: null,
290
+ menuOpen: false,
291
+ query: newQuery,
292
+ selected: -1,
293
+ validChoiceMade: true
294
+ })
295
+ this.forceUpdate()
296
+ }
297
+
298
+ handleOptionMouseDown (event) {
299
+ // Safari triggers focusOut before click, but if you
300
+ // preventDefault on mouseDown, you can stop that from happening.
301
+ // If this is removed, clicking on an option in Safari will trigger
302
+ // `handleOptionBlur`, which closes the menu, and the click will
303
+ // trigger on the element underneath instead.
304
+ // See: http://stackoverflow.com/questions/7621711/how-to-prevent-blur-running-when-clicking-a-link-in-jquery
305
+ event.preventDefault()
306
+ }
307
+
308
+ handleUpArrow (event) {
309
+ event.preventDefault()
310
+ const { menuOpen, selected } = this.state
311
+ const isNotAtTop = selected !== -1
312
+ const allowMoveUp = isNotAtTop && menuOpen
313
+ if (allowMoveUp) {
314
+ this.handleOptionFocus(selected - 1)
315
+ }
316
+ }
317
+
318
+ handleDownArrow (event) {
319
+ event.preventDefault()
320
+ // if not open, open
321
+ if (this.props.showAllValues && this.state.menuOpen === false) {
322
+ event.preventDefault()
323
+ this.props.source('', (options) => {
324
+ this.setState({
325
+ menuOpen: true,
326
+ options,
327
+ selected: 0,
328
+ focused: 0,
329
+ hovered: null
330
+ })
331
+ })
332
+ } else if (this.state.menuOpen === true) {
333
+ const { menuOpen, options, selected } = this.state
334
+ const isNotAtBottom = selected !== options.length - 1
335
+ const allowMoveDown = isNotAtBottom && menuOpen
336
+ if (allowMoveDown) {
337
+ this.handleOptionFocus(selected + 1)
338
+ }
339
+ }
340
+ }
341
+
342
+ handleSpace (event) {
343
+ // if not open, open
344
+ if (this.props.showAllValues && this.state.menuOpen === false && this.state.query === '') {
345
+ event.preventDefault()
346
+ this.props.source('', (options) => {
347
+ this.setState({
348
+ menuOpen: true,
349
+ options
350
+ })
351
+ })
352
+ }
353
+ const focusIsOnOption = this.state.focused !== -1
354
+ if (focusIsOnOption) {
355
+ event.preventDefault()
356
+ this.handleOptionClick(event, this.state.focused)
357
+ }
358
+ }
359
+
360
+ handleEnter (event) {
361
+ if (this.state.menuOpen) {
362
+ event.preventDefault()
363
+ const hasSelectedOption = this.state.selected >= 0
364
+ if (hasSelectedOption) {
365
+ this.handleOptionClick(event, this.state.selected)
366
+ }
367
+ }
368
+ }
369
+
370
+ handlePrintableKey (event) {
371
+ const inputElement = this.elementReferences[-1]
372
+ const eventIsOnInput = event.target === inputElement
373
+ if (!eventIsOnInput) {
374
+ // FIXME: This would be better if it was in componentDidUpdate,
375
+ // but using setState to trigger that seems to not work correctly
376
+ // in preact@8.1.0.
377
+ inputElement.focus()
378
+ }
379
+ }
380
+
381
+ handleKeyDown (event) {
382
+ switch (keyCodes[event.keyCode]) {
383
+ case 'up':
384
+ this.handleUpArrow(event)
385
+ break
386
+ case 'down':
387
+ this.handleDownArrow(event)
388
+ break
389
+ case 'space':
390
+ this.handleSpace(event)
391
+ break
392
+ case 'enter':
393
+ this.handleEnter(event)
394
+ break
395
+ case 'escape':
396
+ this.handleComponentBlur({
397
+ query: this.state.query
398
+ })
399
+ break
400
+ default:
401
+ if (isPrintableKeyCode(event.keyCode)) {
402
+ this.handlePrintableKey(event)
403
+ }
404
+ break
405
+ }
406
+ }
407
+
408
+ render () {
409
+ const {
410
+ cssNamespace,
411
+ displayMenu,
412
+ id,
413
+ minLength,
414
+ name,
415
+ placeholder,
416
+ required,
417
+ showAllValues,
418
+ tNoResults,
419
+ tStatusQueryTooShort,
420
+ tStatusNoResults,
421
+ tStatusSelectedOption,
422
+ tStatusResults,
423
+ tAssistiveHint,
424
+ dropdownArrow: dropdownArrowFactory,
425
+ menuAttributes,
426
+ inputClasses,
427
+ hintClasses,
428
+ menuClasses
429
+ } = this.props
430
+ const { focused, hovered, menuOpen, options, query, selected, ariaHint, validChoiceMade } = this.state
431
+ const autoselect = this.hasAutoselect()
432
+
433
+ const inputFocused = focused === -1
434
+ const noOptionsAvailable = options.length === 0
435
+ const queryNotEmpty = query.length !== 0
436
+ const queryLongEnough = query.length >= minLength
437
+ const showNoOptionsFound = this.props.showNoOptionsFound &&
438
+ inputFocused && noOptionsAvailable && queryNotEmpty && queryLongEnough
439
+
440
+ const wrapperClassName = `${cssNamespace}__wrapper`
441
+ const statusClassName = `${cssNamespace}__status`
442
+ const dropdownArrowClassName = `${cssNamespace}__dropdown-arrow-down`
443
+ const optionFocused = focused !== -1 && focused !== null
444
+
445
+ const optionClassName = `${cssNamespace}__option`
446
+
447
+ const hintClassName = `${cssNamespace}__hint`
448
+ const selectedOptionText = this.templateInputValue(options[selected])
449
+ const optionBeginsWithQuery = selectedOptionText &&
450
+ selectedOptionText.toLowerCase().indexOf(query.toLowerCase()) === 0
451
+ const hintValue = (optionBeginsWithQuery && autoselect)
452
+ ? query + selectedOptionText.substr(query.length)
453
+ : ''
454
+
455
+ const assistiveHintID = id + '__assistiveHint'
456
+ const ariaProps = {
457
+ 'aria-describedby': ariaHint ? assistiveHintID : null,
458
+ 'aria-expanded': menuOpen ? 'true' : 'false',
459
+ 'aria-activedescendant': optionFocused ? `${id}__option--${focused}` : null,
460
+ 'aria-owns': `${id}__listbox`,
461
+ 'aria-autocomplete': (this.hasAutoselect()) ? 'both' : 'list'
462
+ }
463
+
464
+ let dropdownArrow
465
+
466
+ // we only need a dropdown arrow if showAllValues is set to a truthy value
467
+ if (showAllValues) {
468
+ dropdownArrow = dropdownArrowFactory({ className: dropdownArrowClassName })
469
+
470
+ // if the factory returns a string we'll render this as HTML (usage w/o (P)React)
471
+ if (typeof dropdownArrow === 'string') {
472
+ dropdownArrow = <div className={`${cssNamespace}__dropdown-arrow-down-wrapper`} dangerouslySetInnerHTML={{ __html: dropdownArrow }} />
473
+ }
474
+ }
475
+
476
+ const inputClassName = `${cssNamespace}__input`
477
+ const inputClassList = [
478
+ inputClassName,
479
+ this.props.showAllValues ? `${inputClassName}--show-all-values` : `${inputClassName}--default`
480
+ ]
481
+
482
+ const componentIsFocused = focused !== null
483
+ if (componentIsFocused) {
484
+ inputClassList.push(`${inputClassName}--focused`)
485
+ }
486
+
487
+ if (inputClasses) {
488
+ inputClassList.push(inputClasses)
489
+ }
490
+
491
+ const menuClassName = `${cssNamespace}__menu`
492
+ const menuModifierDisplayMenu = `${menuClassName}--${displayMenu}`
493
+ const menuIsVisible = menuOpen || showNoOptionsFound
494
+ const menuModifierVisibility = `${menuClassName}--${(menuIsVisible) ? 'visible' : 'hidden'}`
495
+
496
+ const menuClassList = [
497
+ menuClassName,
498
+ menuModifierDisplayMenu,
499
+ menuModifierVisibility
500
+ ]
501
+
502
+ if (menuClasses) {
503
+ menuClassList.push(menuClasses)
504
+ }
505
+
506
+ if (menuAttributes?.class || menuAttributes?.className) {
507
+ menuClassList.push(menuAttributes?.class || menuAttributes?.className)
508
+ }
509
+
510
+ const computedMenuAttributes = {
511
+ // Copy the attributes passed as props
512
+ ...menuAttributes,
513
+ // And add the values computed for the autocomplete
514
+ id: `${id}__listbox`,
515
+ role: 'listbox',
516
+ className: menuClassList.join(' '),
517
+ onMouseLeave: this.handleListMouseLeave
518
+ }
519
+
520
+ // Preact would override our computed `className`
521
+ // with the `class` from the `menuAttributes` so
522
+ // we need to clean it up from the computed attributes
523
+ delete computedMenuAttributes.class
524
+
525
+ return (
526
+ <div className={wrapperClassName} onKeyDown={this.handleKeyDown}>
527
+ <Status
528
+ id={id}
529
+ length={options.length}
530
+ queryLength={query.length}
531
+ minQueryLength={minLength}
532
+ selectedOption={this.templateInputValue(options[selected])}
533
+ selectedOptionIndex={selected}
534
+ validChoiceMade={validChoiceMade}
535
+ isInFocus={this.state.focused !== null}
536
+ tQueryTooShort={tStatusQueryTooShort}
537
+ tNoResults={tStatusNoResults}
538
+ tSelectedOption={tStatusSelectedOption}
539
+ tResults={tStatusResults}
540
+ className={statusClassName}
541
+ />
542
+
543
+ {hintValue && (
544
+ <span><input className={[hintClassName, hintClasses === null ? inputClasses : hintClasses].filter(Boolean).join(' ')} readonly tabIndex='-1' value={hintValue} /></span>
545
+ )}
546
+
547
+ <input
548
+ {...ariaProps}
549
+ autoComplete='off'
550
+ className={inputClassList.join(' ')}
551
+ id={id}
552
+ onClick={this.handleInputClick}
553
+ onBlur={this.handleInputBlur}
554
+ {...onChangeCrossLibrary(this.handleInputChange)}
555
+ onFocus={this.handleInputFocus}
556
+ name={name}
557
+ placeholder={placeholder}
558
+ ref={(inputElement) => { this.elementReferences[-1] = inputElement }}
559
+ type='text'
560
+ role='combobox'
561
+ required={required}
562
+ value={query}
563
+ />
564
+
565
+ {dropdownArrow}
566
+
567
+ <ul {...computedMenuAttributes}>
568
+ {options.map((option, index) => {
569
+ const showFocused = focused === -1 ? selected === index : focused === index
570
+ const optionModifierFocused = showFocused && hovered === null ? ` ${optionClassName}--focused` : ''
571
+ const optionModifierOdd = (index % 2) ? ` ${optionClassName}--odd` : ''
572
+ const iosPosinsetHtml = (isIosDevice())
573
+ ? `<span id=${id}__option-suffix--${index} style="border:0;clip:rect(0 0 0 0);height:1px;` +
574
+ 'marginBottom:-1px;marginRight:-1px;overflow:hidden;padding:0;position:absolute;' +
575
+ 'whiteSpace:nowrap;width:1px">' + ` ${index + 1} of ${options.length}</span>`
576
+ : ''
577
+
578
+ return (
579
+ <li
580
+ aria-selected={focused === index ? 'true' : 'false'}
581
+ className={`${optionClassName}${optionModifierFocused}${optionModifierOdd}`}
582
+ dangerouslySetInnerHTML={{ __html: this.templateSuggestion(option) + iosPosinsetHtml }}
583
+ id={`${id}__option--${index}`}
584
+ key={index}
585
+ onBlur={(event) => this.handleOptionBlur(event, index)}
586
+ onClick={(event) => this.handleOptionClick(event, index)}
587
+ onMouseDown={this.handleOptionMouseDown}
588
+ onMouseEnter={(event) => this.handleOptionMouseEnter(event, index)}
589
+ ref={(optionEl) => { this.elementReferences[index] = optionEl }}
590
+ role='option'
591
+ tabIndex='-1'
592
+ aria-posinset={index + 1}
593
+ aria-setsize={options.length}
594
+ />
595
+ )
596
+ })}
597
+
598
+ {showNoOptionsFound && (
599
+ <li className={`${optionClassName} ${optionClassName}--no-results`}>{tNoResults()}</li>
600
+ )}
601
+ </ul>
602
+
603
+ <span id={assistiveHintID} style={{ display: 'none' }}>{tAssistiveHint()}</span>
604
+
605
+ </div>
606
+ )
607
+ }
608
+ }
@@ -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