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,192 @@
1
+ {
2
+ "_args": [
3
+ [
4
+ {
5
+ "raw": "accessible-autocomplete@^1.6.2",
6
+ "scope": null,
7
+ "escapedName": "accessible-autocomplete",
8
+ "name": "accessible-autocomplete",
9
+ "rawSpec": "^1.6.2",
10
+ "spec": ">=1.6.2 <2.0.0",
11
+ "type": "range"
12
+ },
13
+ "/var/lib/jenkins/workspace/ublishing_components_master-N4FWJIUY4CIFHKGZOAAEVVXODRY3YBORQOPIBBXWX72VUPSGJRRQ"
14
+ ]
15
+ ],
16
+ "_from": "accessible-autocomplete@>=1.6.2 <2.0.0",
17
+ "_hasShrinkwrap": false,
18
+ "_id": "accessible-autocomplete@1.6.2",
19
+ "_inCache": true,
20
+ "_location": "/accessible-autocomplete",
21
+ "_nodeVersion": "8.9.4",
22
+ "_npmOperationalInternal": {
23
+ "host": "s3://npm-registry-packages",
24
+ "tmp": "tmp/accessible-autocomplete_1.6.2_1542120962811_0.125232612907197"
25
+ },
26
+ "_npmUser": {
27
+ "name": "alphagov",
28
+ "email": "govuk-dev@digital.cabinet-office.gov.uk"
29
+ },
30
+ "_npmVersion": "5.6.0",
31
+ "_phantomChildren": {},
32
+ "_requested": {
33
+ "raw": "accessible-autocomplete@^1.6.2",
34
+ "scope": null,
35
+ "escapedName": "accessible-autocomplete",
36
+ "name": "accessible-autocomplete",
37
+ "rawSpec": "^1.6.2",
38
+ "spec": ">=1.6.2 <2.0.0",
39
+ "type": "range"
40
+ },
41
+ "_requiredBy": [
42
+ "/"
43
+ ],
44
+ "_resolved": "https://registry.npmjs.org/accessible-autocomplete/-/accessible-autocomplete-1.6.2.tgz",
45
+ "_shasum": "cb69d37748ba5b0351c84422468538890e25759c",
46
+ "_shrinkwrap": null,
47
+ "_spec": "accessible-autocomplete@^1.6.2",
48
+ "_where": "/var/lib/jenkins/workspace/ublishing_components_master-N4FWJIUY4CIFHKGZOAAEVVXODRY3YBORQOPIBBXWX72VUPSGJRRQ",
49
+ "author": {
50
+ "name": "Government Digital Service",
51
+ "url": "https://www.gov.uk/government/organisations/government-digital-service"
52
+ },
53
+ "browserslist": [
54
+ ">0.1%",
55
+ "last 2 Chrome versions",
56
+ "last 2 Firefox versions",
57
+ "last 2 Edge versions",
58
+ "last 2 Samsung versions",
59
+ "Safari >= 9",
60
+ "ie 8-11",
61
+ "iOS >= 9"
62
+ ],
63
+ "bugs": {
64
+ "url": "https://github.com/alphagov/accessible-autocomplete/issues"
65
+ },
66
+ "dependencies": {
67
+ "preact": "^8.3.1"
68
+ },
69
+ "description": "An autocomplete component, built to be accessible.",
70
+ "devDependencies": {
71
+ "@babel/core": "^7.1.5",
72
+ "@babel/plugin-proposal-class-properties": "^7.1.0",
73
+ "@babel/plugin-proposal-decorators": "^7.1.2",
74
+ "@babel/plugin-transform-member-expression-literals": "^7.0.0",
75
+ "@babel/plugin-transform-modules-commonjs": "^7.1.0",
76
+ "@babel/plugin-transform-property-literals": "^7.0.0",
77
+ "@babel/plugin-transform-react-jsx": "^7.0.0",
78
+ "@babel/preset-env": "^7.1.5",
79
+ "@babel/register": "^7.0.0",
80
+ "babel-eslint": "^10.0.1",
81
+ "babel-loader": "^8.0.4",
82
+ "babel-plugin-istanbul": "^5.1.0",
83
+ "chai": "^4.2.0",
84
+ "chalk": "^2.4.1",
85
+ "copy-webpack-plugin": "^4.6.0",
86
+ "coveralls": "^3.0.2",
87
+ "cross-env": "^5.2.0",
88
+ "csso-cli": "^1.1.0",
89
+ "dotenv": "^6.1.0",
90
+ "husky": "^1.1.3",
91
+ "karma": "^3.1.1",
92
+ "karma-chai": "^0.1.0",
93
+ "karma-chai-sinon": "^0.1.5",
94
+ "karma-chrome-launcher": "^2.2.0",
95
+ "karma-coverage": "^1.1.2",
96
+ "karma-mocha": "^1.0.1",
97
+ "karma-mocha-reporter": "^2.2.5",
98
+ "karma-sourcemap-loader": "^0.3.7",
99
+ "karma-webpack": "^4.0.0-rc.2",
100
+ "mocha": "^5.2.0",
101
+ "npm-run-all": "^4.1.3",
102
+ "puppeteer": "^1.10.0",
103
+ "replace-bundle-webpack-plugin": "^1.0.0",
104
+ "sinon": "^6.3.5",
105
+ "sinon-chai": "^3.2.0",
106
+ "source-map-loader": "^0.2.4",
107
+ "standard": "^12.0.1",
108
+ "uglifyjs-webpack-plugin": "^2.0.1",
109
+ "wdio-mocha-framework": "^0.6.4",
110
+ "wdio-sauce-service": "^0.4.13",
111
+ "wdio-selenium-standalone-service": "^0.0.10",
112
+ "wdio-spec-reporter": "^0.1.5",
113
+ "wdio-static-server-service": "^1.0.1",
114
+ "wdio-webpack-dev-server-service": "^2.0.2",
115
+ "webdriverio": "^4.14.0",
116
+ "webpack": "^4.25.1",
117
+ "webpack-cli": "^3.1.2",
118
+ "webpack-dev-middleware": "^3.4.0",
119
+ "webpack-dev-server": "^3.1.10",
120
+ "webpack-sources": "^1.3.0"
121
+ },
122
+ "directories": {},
123
+ "dist": {
124
+ "integrity": "sha512-7S+6Vi82LQFSSd5feKedu46tiY2/DShpdXiRp0NY3cLwc+DKe1ayWd66mb3JVi8LTQubRM7jco+u92e6w0bbvg==",
125
+ "shasum": "cb69d37748ba5b0351c84422468538890e25759c",
126
+ "tarball": "https://registry.npmjs.org/accessible-autocomplete/-/accessible-autocomplete-1.6.2.tgz",
127
+ "fileCount": 37,
128
+ "unpackedSize": 540441,
129
+ "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJb6uYDCRA9TVsSAnZWagAAo6sP/igz39UbvmkraVAOGsF2\nqyHQ8DzJv6pIU9qXyZ7mEVk9gTpBtZQVKLtK2X9Pi9ZitKOO4P9+R04CuYur\n6EYrD1YAwrIcuK4Rr7nsnmfsRQxijdmuXTSRQI/nXCeRsqPMuaCphpVdnBkV\nsuOV40I+zuWDQn+2tK9JS/SyW0UF9kAf54uk/VntpricZK9BPESt/40uUYKn\nqSxG3grkfwvE7DoJlI2HuZ0Q1/CR9xHBYtmUu7oYbyO1KWSEzjdZvNf7+zb9\nuEHWTzueLO1O572XxEp8rjLJB1U5W/cu7GLQBDD1/5ShGK3ghJnea2zpCEGn\nEZAK/llomb6X4jdUqsh8glyhhq4qwm/7JGPRrraTODzSIAXgpCZ/a88FSJ+x\n58ZEzB+bCA/h+YRpG5yehWiObLCz0lHMcSW38vZwX0xgrtVBgxzFS88r+QR8\ntKLfoTTJWa7cjlbWfFCIip6H3oSYKKg3uW8wKCqFt91sMp2//9ctFUfmavJm\nkQzvSpDcCz5C7Na11XDlsdz3x5RoYUGnhBXlNEtpZ/SN5cA7XZFyEa6hSu9J\nZzCpaxK7HdXx01i7yCx83oZxPkwXcOzwEVB1B/3kssKZgtuO6pAW+17s8HTA\nwpVJX6g2Wrrfgr/6uBqznlS4E8LhMwceJLSy2gbTD/8Akn9Lv8ntwyNyGomA\nQ6iu\r\n=i6eo\r\n-----END PGP SIGNATURE-----\r\n"
130
+ },
131
+ "gitHead": "70af963ba97f840025a6ee0569b2e9a687e904f9",
132
+ "homepage": "https://github.com/alphagov/accessible-autocomplete#readme",
133
+ "husky": {
134
+ "hooks": {
135
+ "pre-push": "npm run build && node scripts/check-staged.js"
136
+ }
137
+ },
138
+ "keywords": [
139
+ "a11y",
140
+ "accessibility",
141
+ "autocomplete",
142
+ "component",
143
+ "plugin",
144
+ "typeahead",
145
+ "widget"
146
+ ],
147
+ "license": "MIT",
148
+ "main": "dist/accessible-autocomplete.min.js",
149
+ "maintainers": [
150
+ {
151
+ "name": "alphagov",
152
+ "email": "govuk-dev@digital.cabinet-office.gov.uk"
153
+ },
154
+ {
155
+ "name": "carolinegreen",
156
+ "email": "caroline.green@digital.cabinet-office.gov.uk"
157
+ },
158
+ {
159
+ "name": "danmitchell-",
160
+ "email": "dan@digitalreflow.co.uk"
161
+ },
162
+ {
163
+ "name": "tvararu",
164
+ "email": "theo@vararu.org"
165
+ }
166
+ ],
167
+ "name": "accessible-autocomplete",
168
+ "optionalDependencies": {},
169
+ "readme": "ERROR: No README data found!",
170
+ "repository": {
171
+ "type": "git",
172
+ "url": "git+https://github.com/alphagov/accessible-autocomplete.git"
173
+ },
174
+ "scripts": {
175
+ "build": "run-s 'build:js' 'build:css'",
176
+ "build:css": "csso src/autocomplete.css -o dist/accessible-autocomplete.min.css",
177
+ "build:js": "cross-env NODE_ENV=production webpack --progress --display-modules",
178
+ "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --progress",
179
+ "karma": "npm run karma:dev -- --single-run",
180
+ "karma:dev": "cross-env NODE_ENV=test karma start test/karma.config.js",
181
+ "preversion": "npm test",
182
+ "standard": "standard",
183
+ "test": "run-p standard karma wdio",
184
+ "version": "npm run build && git add -A dist",
185
+ "wdio": "cross-env NODE_ENV=test wdio test/wdio.config.js && git checkout dist/"
186
+ },
187
+ "standard": {
188
+ "parser": "babel-eslint"
189
+ },
190
+ "style": "dist/accessible-autocomplete.min.css",
191
+ "version": "1.6.2"
192
+ }
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/lib/accessible-autocomplete.preact.min')
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/lib/accessible-autocomplete.react.min')
@@ -0,0 +1,14 @@
1
+ const cp = require('child_process')
2
+ const chalk = require('chalk')
3
+
4
+ cp.exec('git diff --name-only dist/', (err, stdout) => {
5
+ if (err) {
6
+ console.log(chalk.red('ERROR:'), err)
7
+ return process.exit(1)
8
+ }
9
+ if (stdout.toString().length) {
10
+ console.log(chalk.red('ERROR:'), 'There are unstaged changes in `dist/` after running `npm run build`. Please commit them.')
11
+ return process.exit(1)
12
+ }
13
+ process.exit()
14
+ })
@@ -0,0 +1,141 @@
1
+ .autocomplete__wrapper {
2
+ position: relative;
3
+ }
4
+
5
+ .autocomplete__hint,
6
+ .autocomplete__input {
7
+ -webkit-appearance: none;
8
+ border: 2px solid;
9
+ border-radius: 0; /* Safari 10 on iOS adds implicit border rounding. */
10
+ box-sizing: border-box;
11
+ -moz-box-sizing: border-box;
12
+ -webkit-box-sizing: border-box;
13
+ margin-bottom: 0; /* BUG: Safari 10 on macOS seems to add an implicit margin. */
14
+ width: 100%;
15
+ }
16
+
17
+ .autocomplete__input {
18
+ background-color: transparent;
19
+ position: relative;
20
+ }
21
+
22
+ .autocomplete__hint {
23
+ color: #BFC1C3;
24
+ position: absolute;
25
+ }
26
+
27
+ .autocomplete__input--default{
28
+ padding: 4px;
29
+ }
30
+
31
+ .autocomplete__input--focused {
32
+ outline-offset: 0;
33
+ outline: 3px solid #ffbf47;
34
+ }
35
+
36
+ .autocomplete__input--show-all-values {
37
+ padding: 4px 34px 4px 4px;
38
+ cursor: pointer;
39
+ }
40
+
41
+ .autocomplete__dropdown-arrow-down{
42
+ z-index: -1;
43
+ display: inline-block;
44
+ position: absolute;
45
+ right: 8px;
46
+ width: 24px;
47
+ height: 24px;
48
+ top: 10px;
49
+ }
50
+
51
+ .autocomplete__menu {
52
+ background-color: #fff;
53
+ border: 2px solid #0B0C0C;
54
+ border-top: 0;
55
+ color: #34384B;
56
+ margin: 0;
57
+ max-height: 342px;
58
+ overflow-x: hidden;
59
+ padding: 0;
60
+ width: 100%;
61
+ width: calc(100% - 4px);
62
+ }
63
+
64
+ .autocomplete__menu--visible {
65
+ display: block;
66
+ }
67
+
68
+ .autocomplete__menu--hidden {
69
+ display: none;
70
+ }
71
+
72
+ .autocomplete__menu--overlay {
73
+ box-shadow: rgba(0, 0, 0, 0.256863) 0px 2px 6px;
74
+ left: 0;
75
+ position: absolute;
76
+ top: 100%;
77
+ z-index: 100;
78
+ }
79
+
80
+ .autocomplete__menu--inline {
81
+ position: relative;
82
+ }
83
+
84
+ .autocomplete__option {
85
+ border-bottom: solid #BFC1C3;
86
+ border-width: 1px 0;
87
+ cursor: pointer;
88
+ display: block;
89
+ position: relative;
90
+ }
91
+
92
+ .autocomplete__option > * {
93
+ pointer-events: none;
94
+ }
95
+
96
+ .autocomplete__option:first-of-type {
97
+ border-top-width: 0;
98
+ }
99
+
100
+ .autocomplete__option:last-of-type {
101
+ border-bottom-width: 0;
102
+ }
103
+
104
+ .autocomplete__option--odd {
105
+ background-color: #FAFAFA;
106
+ }
107
+
108
+ .autocomplete__option--focused,
109
+ .autocomplete__option:hover {
110
+ background-color: #005EA5;
111
+ border-color: #005EA5;
112
+ color: white;
113
+ outline: none;
114
+ }
115
+
116
+ .autocomplete__option--no-results {
117
+ background-color: #FAFAFA;
118
+ color: #646b6f;
119
+ cursor: not-allowed;
120
+ }
121
+
122
+ .autocomplete__hint,
123
+ .autocomplete__input,
124
+ .autocomplete__option {
125
+ font-size: 16px;
126
+ line-height: 1.25;
127
+ }
128
+
129
+ .autocomplete__hint,
130
+ .autocomplete__option {
131
+ padding: 4px;
132
+ }
133
+
134
+ @media (min-width: 641px) {
135
+ .autocomplete__hint,
136
+ .autocomplete__input,
137
+ .autocomplete__option {
138
+ font-size: 19px;
139
+ line-height: 1.31579;
140
+ }
141
+ }
@@ -0,0 +1,524 @@
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
+ // Based on https://github.com/ausi/Feature-detection-technique-for-pointer-events
17
+ const hasPointerEvents = (() => {
18
+ const element = document.createElement('x')
19
+ element.style.cssText = 'pointer-events:auto'
20
+ return element.style.pointerEvents === 'auto'
21
+ })()
22
+
23
+ function isIosDevice () {
24
+ return !!(navigator.userAgent.match(/(iPod|iPhone|iPad)/g) && navigator.userAgent.match(/AppleWebKit/g))
25
+ }
26
+
27
+ function isPrintableKeyCode (keyCode) {
28
+ return (
29
+ (keyCode > 47 && keyCode < 58) || // number keys
30
+ keyCode === 32 || keyCode === 8 || // spacebar or backspace
31
+ (keyCode > 64 && keyCode < 91) || // letter keys
32
+ (keyCode > 95 && keyCode < 112) || // numpad keys
33
+ (keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
34
+ (keyCode > 218 && keyCode < 223) // [\]' (in order)
35
+ )
36
+ }
37
+
38
+ // Preact does not implement onChange on inputs, but React does.
39
+ function onChangeCrossLibrary (handler) {
40
+ if (IS_PREACT) { return { onInput: handler } }
41
+ if (IS_REACT) { return { onChange: handler } }
42
+ }
43
+
44
+ export default class Autocomplete extends Component {
45
+ static defaultProps = {
46
+ autoselect: false,
47
+ cssNamespace: 'autocomplete',
48
+ defaultValue: '',
49
+ displayMenu: 'inline',
50
+ minLength: 0,
51
+ name: 'input-autocomplete',
52
+ placeholder: '',
53
+ onConfirm: () => {},
54
+ confirmOnBlur: true,
55
+ showNoOptionsFound: true,
56
+ showAllValues: false,
57
+ required: false,
58
+ tNoResults: () => 'No results found',
59
+ dropdownArrow: DropdownArrowDown
60
+ }
61
+
62
+ elementReferences = {}
63
+
64
+ constructor (props) {
65
+ super(props)
66
+
67
+ this.state = {
68
+ focused: null,
69
+ hovered: null,
70
+ clicked: null,
71
+ menuOpen: false,
72
+ options: props.defaultValue ? [props.defaultValue] : [],
73
+ query: props.defaultValue,
74
+ selected: null
75
+ }
76
+
77
+ this.handleComponentBlur = this.handleComponentBlur.bind(this)
78
+ this.handleKeyDown = this.handleKeyDown.bind(this)
79
+ this.handleUpArrow = this.handleUpArrow.bind(this)
80
+ this.handleDownArrow = this.handleDownArrow.bind(this)
81
+ this.handleEnter = this.handleEnter.bind(this)
82
+ this.handlePrintableKey = this.handlePrintableKey.bind(this)
83
+
84
+ this.handleListMouseLeave = this.handleListMouseLeave.bind(this)
85
+
86
+ this.handleOptionBlur = this.handleOptionBlur.bind(this)
87
+ this.handleOptionClick = this.handleOptionClick.bind(this)
88
+ this.handleOptionFocus = this.handleOptionFocus.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.handleInputFocus = this.handleInputFocus.bind(this)
94
+
95
+ this.pollInputElement = this.pollInputElement.bind(this)
96
+ this.getDirectInputChanges = this.getDirectInputChanges.bind(this)
97
+ }
98
+
99
+ componentDidMount () {
100
+ this.pollInputElement()
101
+ }
102
+
103
+ componentWillUnmount () {
104
+ clearTimeout(this.$pollInput)
105
+ clearTimeout(this.$blurInput)
106
+ }
107
+
108
+ // Applications like Dragon NaturallySpeaking will modify the
109
+ // `input` field by directly changing its `.value`. These events
110
+ // don't trigger our JavaScript event listeners, so we need to poll
111
+ // to handle when and if they occur.
112
+ pollInputElement () {
113
+ this.getDirectInputChanges()
114
+ this.$pollInput = setTimeout(() => {
115
+ this.pollInputElement()
116
+ }, 100)
117
+ }
118
+
119
+ getDirectInputChanges () {
120
+ const inputReference = this.elementReferences[-1]
121
+ const queryHasChanged = inputReference && inputReference.value !== this.state.query
122
+
123
+ if (queryHasChanged) {
124
+ this.handleInputChange({ target: { value: inputReference.value } })
125
+ }
126
+ }
127
+
128
+ componentDidUpdate (prevProps, prevState) {
129
+ const { focused, clicked } = this.state
130
+ const componentLostFocus = focused === null
131
+ const focusedChanged = prevState.focused !== focused
132
+ const focusDifferentElement = (focusedChanged && !componentLostFocus) || clicked !== null
133
+ if (focusDifferentElement) {
134
+ this.elementReferences[focused].focus()
135
+ }
136
+ const focusedInput = focused === -1
137
+ const componentGainedFocus = focusedChanged && prevState.focused === null
138
+ const selectAllText = focusedInput && componentGainedFocus
139
+ if (selectAllText) {
140
+ const inputElement = this.elementReferences[focused]
141
+ inputElement.setSelectionRange(0, inputElement.value.length)
142
+ }
143
+ }
144
+
145
+ hasAutoselect () {
146
+ return isIosDevice() ? false : this.props.autoselect
147
+ }
148
+
149
+ // This template is used when converting from a state.options object into a state.query.
150
+ templateInputValue (value) {
151
+ const inputValueTemplate = this.props.templates && this.props.templates.inputValue
152
+ return inputValueTemplate ? inputValueTemplate(value) : value
153
+ }
154
+
155
+ // This template is used when displaying results / suggestions.
156
+ templateSuggestion (value) {
157
+ const suggestionTemplate = this.props.templates && this.props.templates.suggestion
158
+ return suggestionTemplate ? suggestionTemplate(value) : value
159
+ }
160
+
161
+ handleComponentBlur (newState) {
162
+ const { options, query, selected } = this.state
163
+ let newQuery
164
+ if (this.props.confirmOnBlur) {
165
+ newQuery = newState.query || query
166
+ this.props.onConfirm(options[selected])
167
+ } else {
168
+ newQuery = query
169
+ }
170
+ this.setState({
171
+ focused: null,
172
+ clicked: null,
173
+ menuOpen: newState.menuOpen || false,
174
+ query: newQuery,
175
+ selected: null
176
+ })
177
+ }
178
+
179
+ handleListMouseLeave (event) {
180
+ this.setState({
181
+ hovered: null
182
+ })
183
+ }
184
+
185
+ handleOptionBlur (event, index) {
186
+ const { focused, clicked, menuOpen, options, selected } = this.state
187
+ const focusingOutsideComponent = event.relatedTarget === null && clicked === null
188
+ const focusingInput = event.relatedTarget === this.elementReferences[-1]
189
+ const focusingAnotherOption = focused !== index && focused !== -1
190
+ const blurComponent = (!focusingAnotherOption && focusingOutsideComponent) || !(focusingAnotherOption || focusingInput)
191
+ if (blurComponent) {
192
+ const keepMenuOpen = menuOpen && isIosDevice()
193
+ this.handleComponentBlur({
194
+ menuOpen: keepMenuOpen,
195
+ query: this.templateInputValue(options[selected])
196
+ })
197
+ }
198
+ }
199
+
200
+ handleInputBlur (event) {
201
+ const { focused, menuOpen, options, query, selected } = this.state
202
+ const focusingAnOption = focused !== -1
203
+ clearTimeout(this.$blurInput)
204
+ if (!focusingAnOption) {
205
+ const keepMenuOpen = menuOpen && isIosDevice()
206
+ const newQuery = isIosDevice() ? query : this.templateInputValue(options[selected])
207
+ this.$blurInput = setTimeout(() => this.handleComponentBlur({
208
+ menuOpen: keepMenuOpen,
209
+ query: newQuery
210
+ }), 200)
211
+ }
212
+ }
213
+
214
+ handleInputChange (event) {
215
+ const { minLength, source, showAllValues } = this.props
216
+ const autoselect = this.hasAutoselect()
217
+ const query = event.target.value
218
+ const queryEmpty = query.length === 0
219
+ const queryChanged = this.state.query.length !== query.length
220
+ const queryLongEnough = query.length >= minLength
221
+
222
+ this.setState({ query })
223
+
224
+ const searchForOptions = showAllValues || (!queryEmpty && queryChanged && queryLongEnough)
225
+ if (searchForOptions) {
226
+ source(query, (options) => {
227
+ const optionsAvailable = options.length > 0
228
+ this.setState({
229
+ menuOpen: optionsAvailable,
230
+ options,
231
+ selected: (autoselect && optionsAvailable) ? 0 : -1
232
+ })
233
+ })
234
+ } else if (queryEmpty || !queryLongEnough) {
235
+ this.setState({
236
+ menuOpen: false,
237
+ options: []
238
+ })
239
+ }
240
+ }
241
+
242
+ handleInputClick (event) {
243
+ this.handleInputChange(event)
244
+ }
245
+
246
+ handleInputFocus (event) {
247
+ this.setState({
248
+ focused: -1
249
+ })
250
+ }
251
+
252
+ handleOptionFocus (index) {
253
+ this.setState({
254
+ focused: index,
255
+ hovered: null,
256
+ selected: index
257
+ })
258
+ }
259
+
260
+ handleOptionMouseEnter (event, index) {
261
+ // iOS Safari prevents click event if mouseenter adds hover background colour
262
+ // See: https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html#//apple_ref/doc/uid/TP40006511-SW4
263
+ if (!isIosDevice()) {
264
+ this.setState({
265
+ hovered: index
266
+ })
267
+ }
268
+ }
269
+
270
+ handleOptionClick (event, index) {
271
+ const selectedOption = this.state.options[index]
272
+ const newQuery = this.templateInputValue(selectedOption)
273
+ clearTimeout(this.$blurInput)
274
+ this.props.onConfirm(selectedOption)
275
+ this.setState({
276
+ focused: -1,
277
+ clicked: index,
278
+ hovered: null,
279
+ menuOpen: false,
280
+ query: newQuery,
281
+ selected: -1
282
+ })
283
+ this.forceUpdate()
284
+ }
285
+
286
+ handleUpArrow (event) {
287
+ event.preventDefault()
288
+ const { menuOpen, selected } = this.state
289
+ const isNotAtTop = selected !== -1
290
+ const allowMoveUp = isNotAtTop && menuOpen
291
+ if (allowMoveUp) {
292
+ this.handleOptionFocus(selected - 1)
293
+ }
294
+ }
295
+
296
+ handleDownArrow (event) {
297
+ event.preventDefault()
298
+ // if not open, open
299
+ if (this.props.showAllValues && this.state.menuOpen === false) {
300
+ event.preventDefault()
301
+ this.props.source('', (options) => {
302
+ this.setState({
303
+ menuOpen: true,
304
+ options,
305
+ selected: 0,
306
+ focused: 0,
307
+ hovered: null
308
+ })
309
+ })
310
+ } else if (this.state.menuOpen === true) {
311
+ const { menuOpen, options, selected } = this.state
312
+ const isNotAtBottom = selected !== options.length - 1
313
+ const allowMoveDown = isNotAtBottom && menuOpen
314
+ if (allowMoveDown) {
315
+ this.handleOptionFocus(selected + 1)
316
+ }
317
+ }
318
+ }
319
+
320
+ handleSpace (event) {
321
+ // if not open, open
322
+ if (this.props.showAllValues && this.state.menuOpen === false && this.state.query === '') {
323
+ event.preventDefault()
324
+ this.props.source('', (options) => {
325
+ this.setState({
326
+ menuOpen: true,
327
+ options
328
+ })
329
+ })
330
+ }
331
+ const focusIsOnOption = this.state.focused !== -1
332
+ if (focusIsOnOption) {
333
+ event.preventDefault()
334
+ this.handleOptionClick(event, this.state.focused)
335
+ }
336
+ }
337
+
338
+ handleEnter (event) {
339
+ if (this.state.menuOpen) {
340
+ event.preventDefault()
341
+ const hasSelectedOption = this.state.selected >= 0
342
+ if (hasSelectedOption) {
343
+ this.handleOptionClick(event, this.state.selected)
344
+ }
345
+ }
346
+ }
347
+
348
+ handlePrintableKey (event) {
349
+ const inputElement = this.elementReferences[-1]
350
+ const eventIsOnInput = event.target === inputElement
351
+ if (!eventIsOnInput) {
352
+ // FIXME: This would be better if it was in componentDidUpdate,
353
+ // but using setState to trigger that seems to not work correctly
354
+ // in preact@8.1.0.
355
+ inputElement.focus()
356
+ }
357
+ }
358
+
359
+ handleKeyDown (event) {
360
+ switch (keyCodes[event.keyCode]) {
361
+ case 'up':
362
+ this.handleUpArrow(event)
363
+ break
364
+ case 'down':
365
+ this.handleDownArrow(event)
366
+ break
367
+ case 'space':
368
+ this.handleSpace(event)
369
+ break
370
+ case 'enter':
371
+ this.handleEnter(event)
372
+ break
373
+ case 'escape':
374
+ this.handleComponentBlur({
375
+ query: this.state.query
376
+ })
377
+ break
378
+ default:
379
+ if (isPrintableKeyCode(event.keyCode)) {
380
+ this.handlePrintableKey(event)
381
+ }
382
+ break
383
+ }
384
+ }
385
+
386
+ render () {
387
+ const {
388
+ cssNamespace,
389
+ displayMenu,
390
+ id,
391
+ minLength,
392
+ name,
393
+ placeholder,
394
+ required,
395
+ showAllValues,
396
+ tNoResults,
397
+ tStatusQueryTooShort,
398
+ tStatusNoResults,
399
+ tStatusSelectedOption,
400
+ tStatusResults,
401
+ dropdownArrow: dropdownArrowFactory
402
+ } = this.props
403
+ const { focused, hovered, menuOpen, options, query, selected } = this.state
404
+ const autoselect = this.hasAutoselect()
405
+
406
+ const inputFocused = focused === -1
407
+ const noOptionsAvailable = options.length === 0
408
+ const queryNotEmpty = query.length !== 0
409
+ const queryLongEnough = query.length >= minLength
410
+ const showNoOptionsFound = this.props.showNoOptionsFound &&
411
+ inputFocused && noOptionsAvailable && queryNotEmpty && queryLongEnough
412
+
413
+ const wrapperClassName = `${cssNamespace}__wrapper`
414
+
415
+ const inputClassName = `${cssNamespace}__input`
416
+ const componentIsFocused = focused !== null
417
+ const inputModifierFocused = componentIsFocused ? ` ${inputClassName}--focused` : ''
418
+ const inputModifierType = this.props.showAllValues ? ` ${inputClassName}--show-all-values` : ` ${inputClassName}--default`
419
+ const dropdownArrowClassName = `${cssNamespace}__dropdown-arrow-down`
420
+ const optionFocused = focused !== -1 && focused !== null
421
+
422
+ const menuClassName = `${cssNamespace}__menu`
423
+ const menuModifierDisplayMenu = `${menuClassName}--${displayMenu}`
424
+ const menuIsVisible = menuOpen || showNoOptionsFound
425
+ const menuModifierVisibility = `${menuClassName}--${(menuIsVisible) ? 'visible' : 'hidden'}`
426
+
427
+ const optionClassName = `${cssNamespace}__option`
428
+
429
+ const hintClassName = `${cssNamespace}__hint`
430
+ const selectedOptionText = this.templateInputValue(options[selected])
431
+ const optionBeginsWithQuery = selectedOptionText &&
432
+ selectedOptionText.toLowerCase().indexOf(query.toLowerCase()) === 0
433
+ const hintValue = (optionBeginsWithQuery && autoselect)
434
+ ? query + selectedOptionText.substr(query.length)
435
+ : ''
436
+ const showHint = hasPointerEvents && hintValue
437
+
438
+ let dropdownArrow
439
+
440
+ // we only need a dropdown arrow if showAllValues is set to a truthy value
441
+ if (showAllValues) {
442
+ dropdownArrow = dropdownArrowFactory({ className: dropdownArrowClassName })
443
+
444
+ // if the factory returns a string we'll render this as HTML (usage w/o (P)React)
445
+ if (typeof dropdownArrow === 'string') {
446
+ dropdownArrow = <div className={`${cssNamespace}__dropdown-arrow-down-wrapper`} dangerouslySetInnerHTML={{ __html: dropdownArrow }} />
447
+ }
448
+ }
449
+
450
+ return (
451
+ <div className={wrapperClassName} onKeyDown={this.handleKeyDown} role='combobox' aria-expanded={menuOpen ? 'true' : 'false'}>
452
+ <Status
453
+ length={options.length}
454
+ queryLength={query.length}
455
+ minQueryLength={minLength}
456
+ selectedOption={this.templateInputValue(options[selected])}
457
+ selectedOptionIndex={selected}
458
+ tQueryTooShort={tStatusQueryTooShort}
459
+ tNoResults={tStatusNoResults}
460
+ tSelectedOption={tStatusSelectedOption}
461
+ tResults={tStatusResults}
462
+ />
463
+
464
+ {showHint && (
465
+ <span><input className={hintClassName} readonly tabIndex='-1' value={hintValue} /></span>
466
+ )}
467
+
468
+ <input
469
+ aria-activedescendant={optionFocused ? `${id}__option--${focused}` : false}
470
+ aria-owns={`${id}__listbox`}
471
+ autoComplete='off'
472
+ className={`${inputClassName}${inputModifierFocused}${inputModifierType}`}
473
+ id={id}
474
+ onClick={(event) => this.handleInputClick(event)}
475
+ onBlur={this.handleInputBlur}
476
+ {...onChangeCrossLibrary(this.handleInputChange)}
477
+ onFocus={this.handleInputFocus}
478
+ name={name}
479
+ placeholder={placeholder}
480
+ ref={(inputElement) => { this.elementReferences[-1] = inputElement }}
481
+ type='text'
482
+ role='textbox'
483
+ required={required}
484
+ value={query}
485
+ />
486
+
487
+ {dropdownArrow}
488
+
489
+ <ul
490
+ className={`${menuClassName} ${menuModifierDisplayMenu} ${menuModifierVisibility}`}
491
+ onMouseLeave={(event) => this.handleListMouseLeave(event)}
492
+ id={`${id}__listbox`}
493
+ role='listbox'
494
+ >
495
+ {options.map((option, index) => {
496
+ const showFocused = focused === -1 ? selected === index : focused === index
497
+ const optionModifierFocused = showFocused && hovered === null ? ` ${optionClassName}--focused` : ''
498
+ const optionModifierOdd = (index % 2) ? ` ${optionClassName}--odd` : ''
499
+
500
+ return (
501
+ <li
502
+ aria-selected={focused === index}
503
+ className={`${optionClassName}${optionModifierFocused}${optionModifierOdd}`}
504
+ dangerouslySetInnerHTML={{ __html: this.templateSuggestion(option) }}
505
+ id={`${id}__option--${index}`}
506
+ key={index}
507
+ onBlur={(event) => this.handleOptionBlur(event, index)}
508
+ onClick={(event) => this.handleOptionClick(event, index)}
509
+ onMouseEnter={(event) => this.handleOptionMouseEnter(event, index)}
510
+ ref={(optionEl) => { this.elementReferences[index] = optionEl }}
511
+ role='option'
512
+ tabIndex='-1'
513
+ />
514
+ )
515
+ })}
516
+
517
+ {showNoOptionsFound && (
518
+ <li className={`${optionClassName} ${optionClassName}--no-results`}>{tNoResults()}</li>
519
+ )}
520
+ </ul>
521
+ </div>
522
+ )
523
+ }
524
+ }