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,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
|