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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/app/views/govuk_publishing_components/components/_radio.html.erb +2 -0
  3. data/app/views/govuk_publishing_components/components/docs/inverse_header.yml +2 -30
  4. data/app/views/govuk_publishing_components/components/docs/radio.yml +15 -0
  5. data/lib/govuk_publishing_components/version.rb +1 -1
  6. data/node_modules/accessible-autocomplete/CHANGELOG.md +398 -0
  7. data/node_modules/accessible-autocomplete/CODEOWNERS +2 -0
  8. data/node_modules/accessible-autocomplete/CONTRIBUTING.md +161 -0
  9. data/node_modules/accessible-autocomplete/LICENSE.txt +20 -0
  10. data/node_modules/accessible-autocomplete/Procfile +1 -0
  11. data/node_modules/accessible-autocomplete/README.md +490 -0
  12. data/node_modules/accessible-autocomplete/accessibility-criteria.md +43 -0
  13. data/node_modules/accessible-autocomplete/app.json +15 -0
  14. data/node_modules/accessible-autocomplete/babel.config.js +29 -0
  15. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css +3 -0
  16. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.css.map +1 -0
  17. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js +2 -0
  18. data/node_modules/accessible-autocomplete/dist/accessible-autocomplete.min.js.map +1 -0
  19. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js +2 -0
  20. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.preact.min.js.map +1 -0
  21. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js +2 -0
  22. data/node_modules/accessible-autocomplete/dist/lib/accessible-autocomplete.react.min.js.map +1 -0
  23. data/node_modules/accessible-autocomplete/examples/ajax-source.html +300 -0
  24. data/node_modules/accessible-autocomplete/examples/form-single.html +381 -0
  25. data/node_modules/accessible-autocomplete/examples/form.html +673 -0
  26. data/node_modules/accessible-autocomplete/examples/index.html +693 -0
  27. data/node_modules/accessible-autocomplete/examples/preact/index.html +346 -0
  28. data/node_modules/accessible-autocomplete/examples/react/index.html +347 -0
  29. data/node_modules/accessible-autocomplete/examples/suggestions.json +258 -0
  30. data/node_modules/accessible-autocomplete/package.json +93 -0
  31. data/node_modules/accessible-autocomplete/postcss.config.js +16 -0
  32. data/node_modules/accessible-autocomplete/preact.js +1 -0
  33. data/node_modules/accessible-autocomplete/react.js +1 -0
  34. data/node_modules/accessible-autocomplete/scripts/check-staged.mjs +16 -0
  35. data/node_modules/accessible-autocomplete/src/autocomplete.css +167 -0
  36. data/node_modules/accessible-autocomplete/src/autocomplete.js +610 -0
  37. data/node_modules/accessible-autocomplete/src/dropdown-arrow-down.js +11 -0
  38. data/node_modules/accessible-autocomplete/src/status.js +125 -0
  39. data/node_modules/accessible-autocomplete/src/wrapper.js +60 -0
  40. data/node_modules/accessible-autocomplete/test/functional/dropdown-arrow-down.js +46 -0
  41. data/node_modules/accessible-autocomplete/test/functional/index.js +809 -0
  42. data/node_modules/accessible-autocomplete/test/functional/wrapper.js +339 -0
  43. data/node_modules/accessible-autocomplete/test/integration/index.js +309 -0
  44. data/node_modules/accessible-autocomplete/test/karma.config.js +46 -0
  45. data/node_modules/accessible-autocomplete/test/wdio.config.js +123 -0
  46. data/node_modules/accessible-autocomplete/webpack.config.mjs +244 -0
  47. metadata +57 -2
@@ -0,0 +1,610 @@
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-controls': `${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
+ // set aria-labelledby first so that users can override it with menuAttributes
512
+ 'aria-labelledby': id,
513
+ // Copy the attributes passed as props
514
+ ...menuAttributes,
515
+ // And add the values computed for the autocomplete
516
+ id: `${id}__listbox`,
517
+ role: 'listbox',
518
+ className: menuClassList.join(' '),
519
+ onMouseLeave: this.handleListMouseLeave
520
+ }
521
+
522
+ // Preact would override our computed `className`
523
+ // with the `class` from the `menuAttributes` so
524
+ // we need to clean it up from the computed attributes
525
+ delete computedMenuAttributes.class
526
+
527
+ return (
528
+ <div className={wrapperClassName} onKeyDown={this.handleKeyDown}>
529
+ <Status
530
+ id={id}
531
+ length={options.length}
532
+ queryLength={query.length}
533
+ minQueryLength={minLength}
534
+ selectedOption={this.templateInputValue(options[selected])}
535
+ selectedOptionIndex={selected}
536
+ validChoiceMade={validChoiceMade}
537
+ isInFocus={this.state.focused !== null}
538
+ tQueryTooShort={tStatusQueryTooShort}
539
+ tNoResults={tStatusNoResults}
540
+ tSelectedOption={tStatusSelectedOption}
541
+ tResults={tStatusResults}
542
+ className={statusClassName}
543
+ />
544
+
545
+ {hintValue && (
546
+ <span><input className={[hintClassName, hintClasses === null ? inputClasses : hintClasses].filter(Boolean).join(' ')} readonly tabIndex='-1' value={hintValue} /></span>
547
+ )}
548
+
549
+ <input
550
+ {...ariaProps}
551
+ autoComplete='off'
552
+ className={inputClassList.join(' ')}
553
+ id={id}
554
+ onClick={this.handleInputClick}
555
+ onBlur={this.handleInputBlur}
556
+ {...onChangeCrossLibrary(this.handleInputChange)}
557
+ onFocus={this.handleInputFocus}
558
+ name={name}
559
+ placeholder={placeholder}
560
+ ref={(inputElement) => { this.elementReferences[-1] = inputElement }}
561
+ type='text'
562
+ role='combobox'
563
+ required={required}
564
+ value={query}
565
+ />
566
+
567
+ {dropdownArrow}
568
+
569
+ <ul {...computedMenuAttributes}>
570
+ {options.map((option, index) => {
571
+ const showFocused = focused === -1 ? selected === index : focused === index
572
+ const optionModifierFocused = showFocused && hovered === null ? ` ${optionClassName}--focused` : ''
573
+ const optionModifierOdd = (index % 2) ? ` ${optionClassName}--odd` : ''
574
+ const iosPosinsetHtml = (isIosDevice())
575
+ ? `<span id=${id}__option-suffix--${index} style="border:0;clip:rect(0 0 0 0);height:1px;` +
576
+ 'marginBottom:-1px;marginRight:-1px;overflow:hidden;padding:0;position:absolute;' +
577
+ 'whiteSpace:nowrap;width:1px">' + ` ${index + 1} of ${options.length}</span>`
578
+ : ''
579
+
580
+ return (
581
+ <li
582
+ aria-selected={focused === index ? 'true' : 'false'}
583
+ className={`${optionClassName}${optionModifierFocused}${optionModifierOdd}`}
584
+ dangerouslySetInnerHTML={{ __html: this.templateSuggestion(option) + iosPosinsetHtml }}
585
+ id={`${id}__option--${index}`}
586
+ key={index}
587
+ onBlur={(event) => this.handleOptionBlur(event, index)}
588
+ onClick={(event) => this.handleOptionClick(event, index)}
589
+ onMouseDown={this.handleOptionMouseDown}
590
+ onMouseEnter={(event) => this.handleOptionMouseEnter(event, index)}
591
+ ref={(optionEl) => { this.elementReferences[index] = optionEl }}
592
+ role='option'
593
+ tabIndex='-1'
594
+ aria-posinset={index + 1}
595
+ aria-setsize={options.length}
596
+ />
597
+ )
598
+ })}
599
+
600
+ {showNoOptionsFound && (
601
+ <li className={`${optionClassName} ${optionClassName}--no-results`} role='option' aria-disabled='true'>{tNoResults()}</li>
602
+ )}
603
+ </ul>
604
+
605
+ <span id={assistiveHintID} style={{ display: 'none' }}>{tAssistiveHint()}</span>
606
+
607
+ </div>
608
+ )
609
+ }
610
+ }
@@ -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