From de9b6941a1d39007580d1bb8c847001d85bc2c03 Mon Sep 17 00:00:00 2001 From: Raymond C Date: Wed, 6 Nov 2019 03:10:50 +1100 Subject: [PATCH 01/16] make dropdowns searchable --- frontend/package-lock.json | 268 +++++++++++++++++++++-- frontend/package.json | 1 + frontend/src/actions/index.js | 8 +- frontend/src/components/ControlPanel.jsx | 95 ++++---- frontend/src/components/ReactSelect.jsx | 222 +++++++++++++++++++ 5 files changed, 524 insertions(+), 70 deletions(-) create mode 100644 frontend/src/components/ReactSelect.jsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a60ccc9f..8233eaf9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1136,6 +1136,148 @@ "@emotion/weak-memoize": "0.2.2" } }, + "@emotion/core": { + "version": "10.0.22", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.22.tgz", + "integrity": "sha512-7eoP6KQVUyOjAkE6y4fdlxbZRA4ILs7dqkkm6oZUJmihtHv0UBq98VgPirq9T8F9K2gKu0J/au/TpKryKMinaA==", + "requires": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.17", + "@emotion/css": "^10.0.22", + "@emotion/serialize": "^0.11.12", + "@emotion/sheet": "0.9.3", + "@emotion/utils": "0.11.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.1.tgz", + "integrity": "sha512-SQ0sS7KUJDvgCI2cpZG0nJygO6002oTbhgSuw4WcocsnbxLwL5Q8I3fqbJdyBAc3uFrWZiR2JomseuxSuci3SQ==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@emotion/cache": { + "version": "10.0.19", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.19.tgz", + "integrity": "sha512-BoiLlk4vEsGBg2dAqGSJu0vJl/PgVtCYLBFJaEO8RmQzPugXewQCXZJNXTDFaRlfCs0W+quesayav4fvaif5WQ==", + "requires": { + "@emotion/sheet": "0.9.3", + "@emotion/stylis": "0.8.4", + "@emotion/utils": "0.11.2", + "@emotion/weak-memoize": "0.2.4" + } + }, + "@emotion/hash": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.3.tgz", + "integrity": "sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw==" + }, + "@emotion/memoize": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.3.tgz", + "integrity": "sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow==" + }, + "@emotion/serialize": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.14.tgz", + "integrity": "sha512-6hTsySIuQTbDbv00AnUO6O6Xafdwo5GswRlMZ5hHqiFx+4pZ7uGWXUQFW46Kc2taGhP89uXMXn/lWQkdyTosPA==", + "requires": { + "@emotion/hash": "0.7.3", + "@emotion/memoize": "0.7.3", + "@emotion/unitless": "0.7.4", + "@emotion/utils": "0.11.2", + "csstype": "^2.5.7" + } + }, + "@emotion/sheet": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.3.tgz", + "integrity": "sha512-c3Q6V7Df7jfwSq5AzQWbXHa5soeE4F5cbqi40xn0CzXxWW9/6Mxq48WJEtqfWzbZtW9odZdnRAkwCQwN12ob4A==" + }, + "@emotion/stylis": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.4.tgz", + "integrity": "sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ==" + }, + "@emotion/unitless": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.4.tgz", + "integrity": "sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ==" + }, + "@emotion/utils": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.2.tgz", + "integrity": "sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA==" + }, + "@emotion/weak-memoize": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz", + "integrity": "sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA==" + } + } + }, + "@emotion/css": { + "version": "10.0.22", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.22.tgz", + "integrity": "sha512-8phfa5mC/OadBTmGpMpwykIVH0gFCbUoO684LUkyixPq4F1Wwri7fK5Xlm8lURNBrd2TuvTbPUGxFsGxF9UacA==", + "requires": { + "@emotion/serialize": "^0.11.12", + "@emotion/utils": "0.11.2", + "babel-plugin-emotion": "^10.0.22" + }, + "dependencies": { + "@emotion/hash": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.3.tgz", + "integrity": "sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw==" + }, + "@emotion/memoize": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.3.tgz", + "integrity": "sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow==" + }, + "@emotion/serialize": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.14.tgz", + "integrity": "sha512-6hTsySIuQTbDbv00AnUO6O6Xafdwo5GswRlMZ5hHqiFx+4pZ7uGWXUQFW46Kc2taGhP89uXMXn/lWQkdyTosPA==", + "requires": { + "@emotion/hash": "0.7.3", + "@emotion/memoize": "0.7.3", + "@emotion/unitless": "0.7.4", + "@emotion/utils": "0.11.2", + "csstype": "^2.5.7" + } + }, + "@emotion/unitless": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.4.tgz", + "integrity": "sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ==" + }, + "@emotion/utils": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.2.tgz", + "integrity": "sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA==" + }, + "babel-plugin-emotion": { + "version": "10.0.23", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.23.tgz", + "integrity": "sha512-1JiCyXU0t5S2xCbItejCduLGGcKmF3POT0Ujbexog2MI4IlRcIn/kWjkYwCUZlxpON0O5FC635yPl/3slr7cKQ==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.7.3", + "@emotion/memoize": "0.7.3", + "@emotion/serialize": "^0.11.14", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + } + } + }, "@emotion/hash": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.1.tgz", @@ -4754,7 +4896,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -4775,12 +4918,14 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4795,17 +4940,20 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4922,7 +5070,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -4934,6 +5083,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4948,6 +5098,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4955,12 +5106,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4979,6 +5132,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5059,7 +5213,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5071,6 +5226,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -5156,7 +5312,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5192,6 +5349,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5211,6 +5369,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5254,12 +5413,14 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "optional": true } } }, @@ -9665,7 +9826,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -9686,12 +9848,14 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9706,17 +9870,20 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -9833,7 +10000,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -9845,6 +10013,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9859,6 +10028,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9866,12 +10036,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9890,6 +10062,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -9970,7 +10143,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -9982,6 +10156,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -10067,7 +10242,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -10103,6 +10279,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10122,6 +10299,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -10165,12 +10343,14 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "optional": true } } } @@ -11565,6 +11745,11 @@ } } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -13942,6 +14127,14 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.1.tgz", "integrity": "sha512-V9yoTr6MeZXPPd4nV/05eCBvGH9cGzc52FN8fs0O0TVQ3HYYf1n7EgZVtHbldRq5xU9zEzoXIITjYNIfxDDdUw==" }, + "react-input-autosize": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", + "integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-is": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", @@ -13973,6 +14166,11 @@ "resolved": "https://registry.npmjs.org/react-leaflet-control/-/react-leaflet-control-2.1.1.tgz", "integrity": "sha512-3fx4uSRFtDaFNa1yq62DCxdc9kR3Lk4V7R0lHqbKfPTEIOvPR7VFvk3xzcSnSB/0LAYO8R96Pwcs1c6knyL/Hw==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-redux": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.1.tgz", @@ -14199,6 +14397,32 @@ } } }, + "react-select": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.0.8.tgz", + "integrity": "sha512-v9LpOhckLlRmXN5A6/mGGEft4FMrfaBFTGAnuPHcUgVId7Je42kTq9y0Z+Ye5z8/j0XDT3zUqza8gaRaI1PZIg==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/cache": "^10.0.9", + "@emotion/core": "^10.0.9", + "@emotion/css": "^10.0.9", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "react-input-autosize": "^2.2.2", + "react-transition-group": "^2.2.1" + } + }, + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "react-vis": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/react-vis/-/react-vis-1.11.7.tgz", diff --git a/frontend/package.json b/frontend/package.json index 56f7f351..91c3ac17 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "react-leaflet-control": "^2.1.1", "react-redux": "^6.0.1", "react-scripts": "3.1.1", + "react-select": "^3.0.8", "react-vis": "^1.11.7", "redux": "^4.0.1", "redux-first-router": "^2.1.1", diff --git a/frontend/src/actions/index.js b/frontend/src/actions/index.js index 16288db6..3aa01aeb 100644 --- a/frontend/src/actions/index.js +++ b/frontend/src/actions/index.js @@ -10,8 +10,7 @@ import { export function fetchGraphData(params) { return function(dispatch) { - - var query = `query($routeId:String, $startStopId:String, $endStopId:String, + const query = `query($routeId:String, $startStopId:String, $endStopId:String, $directionId:String, $date:String, $startTime:String, $endTime:String) { routeMetrics(routeId:$routeId) { trip(startStopId:$startStopId, endStopId:$endStopId, directionId:$directionId) { @@ -45,8 +44,9 @@ export function fetchGraphData(params) { } }`.replace(/\s+/g, ' '); - axios.get('/api/graphql', { - params: { query: query, variables: JSON.stringify(params) }, + axios + .get('/api/graphql', { + params: { query, variables: JSON.stringify(params) }, baseURL: metricsBaseURL, }) .then(response => { diff --git a/frontend/src/components/ControlPanel.jsx b/frontend/src/components/ControlPanel.jsx index 7622551c..fe197bf0 100644 --- a/frontend/src/components/ControlPanel.jsx +++ b/frontend/src/components/ControlPanel.jsx @@ -10,11 +10,12 @@ import MenuItem from '@material-ui/core/MenuItem'; import FormControl from '@material-ui/core/FormControl'; import Select from '@material-ui/core/Select'; import Grid from '@material-ui/core/Grid'; -import { handleGraphParams } from '../actions'; -import { ROUTE, DIRECTION, FROM_STOP, TO_STOP, Path } from '../routeUtil'; import StartStopIcon from '@material-ui/icons/DirectionsTransit'; import EndStopIcon from '@material-ui/icons/Flag'; +import { handleGraphParams } from '../actions'; +import { ROUTE, DIRECTION, FROM_STOP, TO_STOP, Path } from '../routeUtil'; import { Colors } from '../UIConstants'; +import ReactSelect from './ReactSelect'; const useStyles = makeStyles(theme => ({ root: { @@ -68,8 +69,8 @@ function ControlPanel(props) { return secondStopInfo.stops.slice(secondStopListIndex + 1); } - function onSelectFirstStop(event) { - const stopId = event.target.value; + function onSelectFirstStop(option) { + const stopId = option.value; const directionId = props.graphParams.directionId; const secondStopId = props.graphParams.endStopId; @@ -94,8 +95,8 @@ function ControlPanel(props) { }); } - function onSelectSecondStop(event) { - const endStopId = event.target.value; + function onSelectSecondStop(option) { + const endStopId = option.value; const path = new Path(); path.buildPath(TO_STOP, endStopId).commitPath(); @@ -190,51 +191,57 @@ function ControlPanel(props) { - + - From Stop - + ({ + value: firstStopId, + label: ( + selectedRoute.stops[firstStopId] || { + title: firstStopId, + } + ).title, + }), + )} + stopId={graphParams.startStopId} + /> - + - + - To Stop - + ).title, + }))} + stopId={graphParams.endStopId} + /> diff --git a/frontend/src/components/ReactSelect.jsx b/frontend/src/components/ReactSelect.jsx new file mode 100644 index 00000000..dbcb472a --- /dev/null +++ b/frontend/src/components/ReactSelect.jsx @@ -0,0 +1,222 @@ +import React, { useState, useEffect, useRef } from 'react'; +import Select, { components } from 'react-select'; +import { makeStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Fade from '@material-ui/core/Fade'; +import Grow from '@material-ui/core/Grow'; +import TextField from '@material-ui/core/TextField'; +import MenuItem from '@material-ui/core/MenuItem'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; + +const useStyles = makeStyles({ + input: { + display: 'flex', + }, + valueContainer: { + display: 'flex', + flex: 1, + alignItems: 'center', + }, + singleValue: { + display: 'flex', + flex: 1, + whiteSpace: 'nowrap', + }, + menu: { + position: 'absolute', + zIndex: 1000, + }, +}); + +const selectStyles = { + dropdownIndicator: provided => ({ + ...provided, + paddingLeft: 0, + paddingRight: 0, + }), + input: provided => ({ + ...provided, + marginLeft: 0, + marginRight: 0, + }), +}; + +export default function ReactSelect(selectProps) { + const classes = useStyles(); + const timeout = 400; + let textFieldRect; + + const isInitialMount = useRef(true); + const menuStyle = useRef({}); + const scrollbarWidth = useRef(window.innerWidth - document.body.clientWidth); + const [menuToggle, setMenuToggle] = useState(false); + + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + } else { + let menuWidth = document.getElementById(`${selectProps.inputId}Menu`); + if (menuWidth) { + menuWidth = menuWidth.clientWidth; + const windowWidth = + window.innerWidth || + document.documentElement.clientWidth || + document.body.clientWidth; + const rightWillSlice = + textFieldRect.left + menuWidth + scrollbarWidth.current > windowWidth; + const leftWillSlice = textFieldRect.right - menuWidth < 0; + menuStyle.current = + rightWillSlice && !leftWillSlice + ? { + right: + textFieldRect.right - + document.body.clientWidth + + scrollbarWidth.current, + } + : {}; + } + } + }, [menuToggle, selectProps.inputId, textFieldRect, scrollbarWidth]); + + function inputComponent({ inputRef, ...props }) { + return
; + } + + function Control(props) { + const { + innerRef: ref, + innerProps, + children, + selectProps: { textFieldProps, inputId }, + } = props; + + return ( + + ); + } + + function ValueContainer({ children }) { + const input = children[1]; + const singleValue = children[0]; + + return
{[input, singleValue]}
; + } + + function SingleValue({ children }) { + return
{children}
; + } + + function DropdownIndicator(props) { + return ( + + + + ); + } + + function Menu(props) { + // FIXME: exit timeout not working because component is unmounted? + return ( + + + + {props.children} + + + + ); + } + + function MenuList(props) { + textFieldRect = document + .getElementById(props.selectProps.inputId) + .getBoundingClientRect(); + + return ( + + {props.children} + + ); + } + + function Option(props) { + const focused = (function() { + if (props.isFocused && !props.isSelected) { + return { + backgroundColor: 'rgba(0, 0, 0, 0.08)', + }; + } + return {}; + })(); + + return ( + + {props.children} + + ); + } + + const replacedComponents = { + Control, + ValueContainer, + SingleValue, + IndicatorSeparator: () => null, + DropdownIndicator, + Menu, + MenuList, + Option, + }; + + return ( + { + scrollbarWidth.current = window.innerWidth - document.body.clientWidth; document.body.style.overflow = 'hidden'; document.body.style.paddingRight = `${scrollbarWidth.current}px`; setMenuToggle(true); From db42b6a9d3f2f1e20d9589c1386e31e0d8360be8 Mon Sep 17 00:00:00 2001 From: Raymond C Date: Fri, 8 Nov 2019 20:11:58 +1100 Subject: [PATCH 04/16] limit input width --- frontend/src/components/ControlPanel.jsx | 1 + frontend/src/components/ReactSelect.jsx | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/ControlPanel.jsx b/frontend/src/components/ControlPanel.jsx index fe197bf0..63017ec9 100644 --- a/frontend/src/components/ControlPanel.jsx +++ b/frontend/src/components/ControlPanel.jsx @@ -25,6 +25,7 @@ const useStyles = makeStyles(theme => ({ formControl: { margin: theme.spacing(1), minWidth: 120, + maxWidth: '100%', }, })); diff --git a/frontend/src/components/ReactSelect.jsx b/frontend/src/components/ReactSelect.jsx index 715f8e08..d05e1727 100644 --- a/frontend/src/components/ReactSelect.jsx +++ b/frontend/src/components/ReactSelect.jsx @@ -16,6 +16,8 @@ const useStyles = makeStyles({ display: 'flex', flex: 1, alignItems: 'center', + minWidth: 0, + maxWidth: '100%', }, textContent: { display: 'flex', @@ -41,6 +43,8 @@ const selectStyles = { ...provided, marginLeft: 0, marginRight: 0, + maxWidth: '100%', + overflow: 'hidden', }), }; @@ -78,10 +82,6 @@ export default function ReactSelect(selectProps) { } }, [menuToggle, selectProps.inputId, textFieldRect, scrollbarWidth]); - function inputComponent({ inputRef, ...props }) { - return
; - } - function Control(props) { const { innerProps, @@ -94,7 +94,7 @@ export default function ReactSelect(selectProps) { id={inputId} fullWidth InputProps={{ - inputComponent, + inputComponent: 'div', inputProps: { children, ...innerProps, @@ -134,7 +134,6 @@ export default function ReactSelect(selectProps) { } function Menu(props) { - // FIXME: exit timeout not working because component is unmounted? return ( { - scrollbarWidth.current = window.innerWidth - document.body.clientWidth; + if (!menuToggle) { + scrollbarWidth.current = + window.innerWidth - document.body.clientWidth; + } document.body.style.overflow = 'hidden'; document.body.style.paddingRight = `${scrollbarWidth.current}px`; setMenuToggle(true); @@ -218,6 +220,7 @@ export default function ReactSelect(selectProps) { document.body.style.paddingRight = 0; setMenuToggle(false); }} + menuPlacement="auto" styles={selectStyles} placeholder="Type here to search..." value={selectProps.options.filter( From 9d61735245ca15e31ceb8e8e87a806729855da7e Mon Sep 17 00:00:00 2001 From: Raymond C Date: Tue, 12 Nov 2019 03:28:51 +1100 Subject: [PATCH 05/16] improved menu placement --- frontend/src/components/ReactSelect.jsx | 173 ++++++++++++++++-------- 1 file changed, 117 insertions(+), 56 deletions(-) diff --git a/frontend/src/components/ReactSelect.jsx b/frontend/src/components/ReactSelect.jsx index d05e1727..e5442981 100644 --- a/frontend/src/components/ReactSelect.jsx +++ b/frontend/src/components/ReactSelect.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import Select, { components } from 'react-select'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles, createMuiTheme } from '@material-ui/core/styles'; import Paper from '@material-ui/core/Paper'; import Fade from '@material-ui/core/Fade'; import Grow from '@material-ui/core/Grow'; @@ -29,7 +29,7 @@ const useStyles = makeStyles({ }, menu: { position: 'absolute', - zIndex: 1000, + zIndex: 1200, }, }); @@ -50,48 +50,36 @@ const selectStyles = { export default function ReactSelect(selectProps) { const classes = useStyles(); - const timeout = 400; - let textFieldRect; - - const isInitialMount = useRef(true); - const menuStyle = useRef({}); + const theme = createMuiTheme(); + const transitionDuration = 400; + const eventHandlerDelay = 100; const scrollbarWidth = useRef(0); - const [menuToggle, setMenuToggle] = useState(false); + const isInitialMount = useRef(true); + const menuPlacementTop = useRef(false); + const [textFieldDOMRect, setTextFieldDOMRect] = useState({}); - useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false; - } else { - let menuWidth = document.getElementById(`${selectProps.inputId}Menu`); - if (menuWidth) { - menuWidth = menuWidth.clientWidth; - const windowWidth = window.innerWidth; - const rightWillSlice = - textFieldRect.left + menuWidth + scrollbarWidth.current > windowWidth; - const leftWillSlice = textFieldRect.right - menuWidth < 0; - menuStyle.current = - rightWillSlice && !leftWillSlice - ? { - right: - textFieldRect.right - - document.body.clientWidth + - scrollbarWidth.current, - } - : {}; - } - } - }, [menuToggle, selectProps.inputId, textFieldRect, scrollbarWidth]); + function handleReposition() { + clearTimeout(window[`${selectProps.inputId}Timeout`]); + window[`${selectProps.inputId}Timeout`] = setTimeout(() => { + setTextFieldDOMRect( + document.getElementById(selectProps.inputId).getBoundingClientRect(), + ); + }, eventHandlerDelay); + } - function Control(props) { - const { - innerProps, - children, - selectProps: { textFieldProps, inputId }, - } = props; + function Control({ children, innerProps }) { + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + setTextFieldDOMRect( + document.getElementById(selectProps.inputId).getBoundingClientRect(), + ); + } + }); return ( ); } @@ -110,7 +98,14 @@ export default function ReactSelect(selectProps) { const input = children[1]; const singleValue = children[0]; - return
{[input, singleValue]}
; + return ( +
+ {[input, singleValue]} +
+ ); } function Placeholder({ children }) { @@ -134,16 +129,67 @@ export default function ReactSelect(selectProps) { } function Menu(props) { + const menuStyle = {}; + const [menuStyleRight, setMenuStyleRight] = useState(0); + const [menuStyleBottom, setMenuStyleBottom] = useState(0); + menuPlacementTop.current = + textFieldDOMRect.top > + document.documentElement.clientHeight - textFieldDOMRect.bottom; + + if (menuStyleRight) { + menuStyle.right = menuStyleRight; + } + + if (menuStyleBottom) { + menuStyle.bottom = menuStyleBottom; + } + + useEffect(() => { + const menu = document.getElementById(`${selectProps.inputId}Menu`); + const inputHeight = + textFieldDOMRect.height + + document.getElementById(selectProps.inputId).parentElement + .previousSibling.clientHeight; + const rightWillSlice = + textFieldDOMRect.left + menu.clientWidth + scrollbarWidth.current > + window.innerWidth; + const leftWillSlice = textFieldDOMRect.right - menu.clientWidth < 0; + const idealRightPosition = + textFieldDOMRect.right - + document.documentElement.clientWidth + + scrollbarWidth.current; + + if (rightWillSlice && !leftWillSlice) { + if (menuStyleRight !== idealRightPosition) { + setMenuStyleRight(idealRightPosition); + } + } else if (menuStyleRight) { + setMenuStyleRight(0); + } + + if (menuPlacementTop.current) { + if (menuStyleBottom !== inputHeight) { + setMenuStyleBottom(inputHeight); + } + } else if (menuStyleBottom) { + setMenuStyleBottom(0); + } + + document + .getElementById(`${selectProps.inputId}Value`) + .firstChild.firstChild.firstChild.focus(); + }, [menuStyleRight, menuStyleBottom]); + return ( - + @@ -155,15 +201,23 @@ export default function ReactSelect(selectProps) { } function MenuList(props) { - textFieldRect = document - .getElementById(props.selectProps.inputId) - .getBoundingClientRect(); + let maxHeight; + + if (menuPlacementTop.current) { + maxHeight = + textFieldDOMRect.top - + document.getElementById(selectProps.inputId).parentElement + .previousSibling.clientHeight - + theme.spacing(2); + } else { + maxHeight = + document.documentElement.clientHeight - + textFieldDOMRect.bottom - + theme.spacing(2); + } return ( - + {props.children} ); @@ -203,24 +257,31 @@ export default function ReactSelect(selectProps) { Option, }; + useEffect(() => { + window.addEventListener('scroll', handleReposition); + window.addEventListener('resize', handleReposition); + return () => { + window.removeEventListener('scroll', handleReposition); + window.removeEventListener('resize', handleReposition); + }; + }); + return ( { - const overflow = document.body.style.overflow; - if (overflow === 'visible' || !overflow) { - scrollbarWidth.current = - window.innerWidth - document.documentElement.clientWidth; - } - document.body.style.overflow = 'hidden'; - document.body.style.paddingRight = `${scrollbarWidth.current}px`; - menuTransition.current = true; - }} - onMenuClose={() => { - document.body.style.overflow = 'visible'; - document.body.style.paddingRight = 0; - }} + onMenuOpen={onMenuOpen} + onMenuClose={onMenuClose} styles={selectStyles} placeholder="Type here to search..." value={selectProps.options.filter( From 80b43cef739ebcb3ce31865cca0dbb5208394f27 Mon Sep 17 00:00:00 2001 From: Raymond C Date: Mon, 18 Nov 2019 17:20:02 +1100 Subject: [PATCH 08/16] added comments --- frontend/src/components/ReactSelect.jsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/frontend/src/components/ReactSelect.jsx b/frontend/src/components/ReactSelect.jsx index 5dd021a8..90177dbe 100644 --- a/frontend/src/components/ReactSelect.jsx +++ b/frontend/src/components/ReactSelect.jsx @@ -56,10 +56,15 @@ export default function ReactSelect(selectProps) { const focusedOption = useRef(); const scrollbarWidth = useRef(0); const isInitialMount = useRef(true); + // determines whether transitionDuration is used, otherwise 0. Set to false on resize const menuTransition = useRef(true); const menuPlacementTop = useRef(false); const [textFieldDOMRect, setTextFieldDOMRect] = useState({}); + /** + * updates textfield location after resize/scroll + * re-renders menu if open which updates menu placement and max height + */ function handleReposition() { clearTimeout(window[`${selectProps.inputId}Timeout`]); window[`${selectProps.inputId}Timeout`] = setTimeout(() => { @@ -163,6 +168,11 @@ export default function ReactSelect(selectProps) { document.documentElement.clientWidth + scrollbarWidth.current; + /** + * check if the right side of the menu will be outside the view + * if so change the 'right' property of the style object + * but only if the left side is not cut off in the process, otherwise default positon + */ if (rightWillSlice && !leftWillSlice) { if (menuStyleRight !== idealRightPosition) { setMenuStyleRight(idealRightPosition); @@ -179,6 +189,10 @@ export default function ReactSelect(selectProps) { setMenuStyleBottom(0); } + /** + * temporary fix to react-select issue not setting focus to selected value by default + * may or may not be needed after fixing issue with default focus option + */ if (focusedOption.current) { focusedOption.current.parentNode.scrollTop = focusedOption.current.offsetTop - @@ -186,6 +200,10 @@ export default function ReactSelect(selectProps) { focusedOption.current.clientHeight / 2; } + /** + * set focus back to input element after resize + * currently doesn't allow virtual keyboard to close without closing menu via blur + */ document .getElementById(`${selectProps.inputId}Value`) .firstChild.firstChild.firstChild.focus(); @@ -214,6 +232,7 @@ export default function ReactSelect(selectProps) { function MenuList(props) { let maxHeight; + // calculates appropriate max height depending on top or bottom menu placement if (menuPlacementTop.current) { maxHeight = textFieldDOMRect.top - From 4b41a8e570a669309f9071bef7c02b5dacf8151c Mon Sep 17 00:00:00 2001 From: Raymond C Date: Fri, 22 Nov 2019 00:43:58 +1100 Subject: [PATCH 09/16] fix focus issue after re-render --- frontend/src/components/ReactSelect.jsx | 459 +++++++++++++----------- 1 file changed, 251 insertions(+), 208 deletions(-) diff --git a/frontend/src/components/ReactSelect.jsx b/frontend/src/components/ReactSelect.jsx index 90177dbe..450b6113 100644 --- a/frontend/src/components/ReactSelect.jsx +++ b/frontend/src/components/ReactSelect.jsx @@ -8,6 +8,9 @@ import TextField from '@material-ui/core/TextField'; import MenuItem from '@material-ui/core/MenuItem'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +const transitionDuration = 400; +const eventHandlerDelay = 100; +const theme = createMuiTheme(); const useStyles = makeStyles({ input: { display: 'flex', @@ -48,235 +51,265 @@ const selectStyles = { }), }; -export default function ReactSelect(selectProps) { - const classes = useStyles(); - const theme = createMuiTheme(); - const transitionDuration = 400; - const eventHandlerDelay = 100; - const focusedOption = useRef(); - const scrollbarWidth = useRef(0); - const isInitialMount = useRef(true); - // determines whether transitionDuration is used, otherwise 0. Set to false on resize - const menuTransition = useRef(true); - const menuPlacementTop = useRef(false); - const [textFieldDOMRect, setTextFieldDOMRect] = useState({}); +function Control(props) { + const { + children, + innerProps, + selectProps: { + inputId, + classes, + textFieldProps, + isInitialMount, + onInitialMount, + }, + } = props; - /** - * updates textfield location after resize/scroll - * re-renders menu if open which updates menu placement and max height - */ - function handleReposition() { - clearTimeout(window[`${selectProps.inputId}Timeout`]); - window[`${selectProps.inputId}Timeout`] = setTimeout(() => { - menuTransition.current = false; - setTextFieldDOMRect( - document.getElementById(selectProps.inputId).getBoundingClientRect(), - ); - }, eventHandlerDelay); - } + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + onInitialMount(document.getElementById(inputId).getBoundingClientRect()); + } + }); - function Control({ children, innerProps }) { - useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false; - setTextFieldDOMRect( - document.getElementById(selectProps.inputId).getBoundingClientRect(), - ); - } - }); - - return ( - - ); - } + return ( + + ); +} - function ValueContainer({ children }) { - const input = children[1]; - const singleValue = children[0]; - - return ( -
- {[input, singleValue]} -
- ); - } +function ValueContainer({ children, selectProps: { inputId, classes } }) { + const input = children[1]; + const singleValue = children[0]; - function Placeholder({ children }) { - return ( -
- {children} -
- ); - } + return ( +
+ {[input, singleValue]} +
+ ); +} - function SingleValue({ children }) { - return
{children}
; - } +function Placeholder({ children, selectProps: { classes } }) { + return ( +
+ {children} +
+ ); +} - function DropdownIndicator(props) { - return ( - - - - ); - } +function SingleValue({ children, selectProps: { classes } }) { + return
{children}
; +} - function Menu(props) { - const menuStyle = {}; - const timeout = menuTransition.current ? transitionDuration : 0; - const [menuStyleRight, setMenuStyleRight] = useState(0); - const [menuStyleBottom, setMenuStyleBottom] = useState(0); +function DropdownIndicator(props) { + return ( + + + + ); +} - menuPlacementTop.current = - textFieldDOMRect.top > - document.documentElement.clientHeight - textFieldDOMRect.bottom; +function Menu(props) { + const { + children, + innerProps, + selectProps: { + classes, + focusedOption, + inputId, + menuIsOpen, + menuPlacementTop, + menuTransition, + scrollbarWidth, + textFieldDOMRect, + }, + } = props; + const menuStyle = {}; + const timeout = menuTransition.current ? transitionDuration : 0; + const [menuStyleRight, setMenuStyleRight] = useState(0); + const [menuStyleBottom, setMenuStyleBottom] = useState(0); + + menuPlacementTop.current = + textFieldDOMRect.top > + document.documentElement.clientHeight - textFieldDOMRect.bottom; + + if (menuStyleRight) { + menuStyle.right = menuStyleRight; + } - if (menuStyleRight) { - menuStyle.right = menuStyleRight; - } + if (menuStyleBottom) { + menuStyle.bottom = menuStyleBottom; + } - if (menuStyleBottom) { - menuStyle.bottom = menuStyleBottom; + useEffect(() => { + const menu = document.getElementById(`${inputId}Menu`); + // TODO: get label ref via callback? also in menu list + const labelHeight = document.getElementById(inputId).parentElement + .previousSibling.clientHeight; + const inputHeight = textFieldDOMRect.height + labelHeight; + const rightWillSlice = + textFieldDOMRect.left + menu.clientWidth + scrollbarWidth.current > + window.innerWidth; + const leftWillSlice = textFieldDOMRect.right - menu.clientWidth < 0; + const idealRightPosition = + textFieldDOMRect.right - + document.documentElement.clientWidth + + scrollbarWidth.current; + + /** + * check if the right side of the menu will be outside the view + * if so change the 'right' property of the style object + * but only if the left side is not cut off in the process, otherwise default positon + */ + if (rightWillSlice && !leftWillSlice) { + if (menuStyleRight !== idealRightPosition) { + setMenuStyleRight(idealRightPosition); + } + } else if (menuStyleRight) { + setMenuStyleRight(0); } - useEffect(() => { - const menu = document.getElementById(`${selectProps.inputId}Menu`); - const labelHeight = document.getElementById(selectProps.inputId) - .parentElement.previousSibling.clientHeight; - const inputHeight = textFieldDOMRect.height + labelHeight; - const rightWillSlice = - textFieldDOMRect.left + menu.clientWidth + scrollbarWidth.current > - window.innerWidth; - const leftWillSlice = textFieldDOMRect.right - menu.clientWidth < 0; - const idealRightPosition = - textFieldDOMRect.right - - document.documentElement.clientWidth + - scrollbarWidth.current; - - /** - * check if the right side of the menu will be outside the view - * if so change the 'right' property of the style object - * but only if the left side is not cut off in the process, otherwise default positon - */ - if (rightWillSlice && !leftWillSlice) { - if (menuStyleRight !== idealRightPosition) { - setMenuStyleRight(idealRightPosition); - } - } else if (menuStyleRight) { - setMenuStyleRight(0); + if (menuPlacementTop.current) { + if (menuStyleBottom !== inputHeight) { + setMenuStyleBottom(inputHeight); } + } else if (menuStyleBottom) { + setMenuStyleBottom(0); + } - if (menuPlacementTop.current) { - if (menuStyleBottom !== inputHeight) { - setMenuStyleBottom(inputHeight); - } - } else if (menuStyleBottom) { - setMenuStyleBottom(0); - } + /** + * temporary fix to react-select issue not setting focus to selected value by default + * may or may not be needed after fixing issue with default focus option + */ + if (focusedOption.current) { + focusedOption.current.parentNode.scrollTop = + focusedOption.current.offsetTop - + menu.clientHeight / 2 + + focusedOption.current.clientHeight / 2; + } + }, [ + focusedOption, + inputId, + menuPlacementTop, + menuStyleBottom, + menuStyleRight, + scrollbarWidth, + textFieldDOMRect, + ]); - /** - * temporary fix to react-select issue not setting focus to selected value by default - * may or may not be needed after fixing issue with default focus option - */ - if (focusedOption.current) { - focusedOption.current.parentNode.scrollTop = - focusedOption.current.offsetTop - - menu.clientHeight / 2 + - focusedOption.current.clientHeight / 2; - } + return ( + + + + {children} + + + + ); +} - /** - * set focus back to input element after resize - * currently doesn't allow virtual keyboard to close without closing menu via blur - */ - document - .getElementById(`${selectProps.inputId}Value`) - .firstChild.firstChild.firstChild.focus(); - }, [menuStyleRight, menuStyleBottom]); - - return ( - - - - {props.children} - - - - ); +function MenuList(props) { + const { + children, + selectProps: { inputId, menuPlacementTop, textFieldDOMRect }, + } = props; + let maxHeight; + + // calculates appropriate max height depending on top or bottom menu placement + if (menuPlacementTop.current) { + maxHeight = + textFieldDOMRect.top - + document.getElementById(inputId).parentElement.previousSibling + .clientHeight - + theme.spacing(2); + } else { + maxHeight = + document.documentElement.clientHeight - + textFieldDOMRect.bottom - + theme.spacing(2); } - function MenuList(props) { - let maxHeight; + return ( + + {children} + + ); +} - // calculates appropriate max height depending on top or bottom menu placement - if (menuPlacementTop.current) { - maxHeight = - textFieldDOMRect.top - - document.getElementById(selectProps.inputId).parentElement - .previousSibling.clientHeight - - theme.spacing(2); - } else { - maxHeight = - document.documentElement.clientHeight - - textFieldDOMRect.bottom - - theme.spacing(2); +function Option(props) { + const { + children, + innerProps, + isFocused, + isSelected, + selectProps: { focusedOption }, + } = props; + const focused = (function() { + if (isFocused && !isSelected) { + return { + backgroundColor: theme.palette.action.hover, + }; } + return {}; + })(); - return ( - - {props.children} - - ); - } + return ( + { + if (isSelected) { + focusedOption.current = element; + } + }} + selected={isSelected} + style={focused} + {...innerProps} + > + {children} + + ); +} - function Option(props) { - const focused = (function() { - if (props.isFocused && !props.isSelected) { - return { - backgroundColor: theme.palette.action.hover, - }; - } - return {}; - })(); - - return ( - { - if (props.isSelected) { - focusedOption.current = element; - } - }} - selected={props.isSelected} - style={focused} - {...props.innerProps} - > - {props.children} - - ); +export default function ReactSelect(selectProps) { + const classes = useStyles(); + const focusedOption = useRef(); + const scrollbarWidth = useRef(0); + const isInitialMount = useRef(true); + // determines whether transitionDuration is used, otherwise 0. Set to false on resize + const menuTransition = useRef(true); + const menuPlacementTop = useRef(false); + const [textFieldDOMRect, setTextFieldDOMRect] = useState({}); + + /** + * updates textfield location on input focus or window resize + * re-renders menu if open which updates menu placement and max height + */ + function handleReposition() { + clearTimeout(window[`${selectProps.inputId}Timeout`]); + window[`${selectProps.inputId}Timeout`] = setTimeout(() => { + menuTransition.current = false; + setTextFieldDOMRect( + document.getElementById(selectProps.inputId).getBoundingClientRect(), + ); + }, eventHandlerDelay); } const replacedComponents = { @@ -307,6 +340,8 @@ export default function ReactSelect(selectProps) { function onMenuClose() { document.body.style.overflow = 'visible'; document.body.style.paddingRight = 0; + menuTransition.current = true; + document.activeElement.blur(); } useEffect(() => { @@ -320,11 +355,19 @@ export default function ReactSelect(selectProps) { return ( Date: Mon, 9 Dec 2019 16:36:11 +1100 Subject: [PATCH 13/16] port menu option hover --- frontend/src/components/ControlPanel.jsx | 5 ++--- frontend/src/components/ReactSelect.jsx | 24 ++++++++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/ControlPanel.jsx b/frontend/src/components/ControlPanel.jsx index c9cbe4b2..6166c95a 100644 --- a/frontend/src/components/ControlPanel.jsx +++ b/frontend/src/components/ControlPanel.jsx @@ -83,7 +83,7 @@ function ControlPanel(props) { } function onSelectFirstStop(option) { - const stopId = option.value; + const stopId = option.value.stopId; const directionId = props.graphParams.directionId; const secondStopId = props.graphParams.endStopId; @@ -110,7 +110,7 @@ function ControlPanel(props) { } function onSelectSecondStop(option) { - const endStopId = option.value; + const endStopId = option.value.stopId; const path = new Path(); path.buildPath(TO_STOP, endStopId).commitPath(); @@ -251,7 +251,6 @@ function ControlPanel(props) { title: firstStopId, } ).title, - icon: }), )} stopId={graphParams.startStopId} diff --git a/frontend/src/components/ReactSelect.jsx b/frontend/src/components/ReactSelect.jsx index c8b6b1c5..52e40e22 100644 --- a/frontend/src/components/ReactSelect.jsx +++ b/frontend/src/components/ReactSelect.jsx @@ -282,7 +282,11 @@ function Option(props) { innerRef, isFocused, isSelected, - selectProps: { focusedOption }, + data: { + label, + value: { icon }, + }, + selectProps: { focusedOption, handleItemMouseOver, handleItemMouseOut }, } = props; const focused = (function() { if (isFocused && !isSelected) { @@ -293,6 +297,12 @@ function Option(props) { return {}; })(); + if (isFocused) { + handleItemMouseOver(icon, label); + } else { + handleItemMouseOut(icon); + } + return ( { @@ -346,15 +356,17 @@ export default function ReactSelect(selectProps) { Option, }; - function onMenuOpen() { + function handleMenuOpen() { menuTransition.current = true; setMenuIsOpen(true); + selectProps.onOpen(); } - function onMenuClose() { + function handleMenuClose() { menuTransition.current = true; document.activeElement.blur(); setMenuIsOpen(false); + selectProps.onClose(); } useEffect(() => { @@ -379,13 +391,13 @@ export default function ReactSelect(selectProps) { menuPlacementTop={menuPlacementTop} menuTransition={menuTransition} onInitialMount={setTextFieldDOMRect} - onMenuOpen={onMenuOpen} - onMenuClose={onMenuClose} + onMenuOpen={handleMenuOpen} + onMenuClose={handleMenuClose} placeholder="Type here to search..." styles={selectStyles} textFieldDOMRect={textFieldDOMRect} value={selectProps.options.filter( - option => option.value === selectProps.stopId, + option => option.value.stopId === selectProps.stopId, )} {...selectProps} /> From 489c804c721a9f89968370143d58cc03604938d2 Mon Sep 17 00:00:00 2001 From: Raymond C Date: Thu, 12 Dec 2019 17:31:25 +1100 Subject: [PATCH 14/16] input has min width when typing --- frontend/src/components/ReactSelect.jsx | 62 ++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/ReactSelect.jsx b/frontend/src/components/ReactSelect.jsx index 52e40e22..f42b688d 100644 --- a/frontend/src/components/ReactSelect.jsx +++ b/frontend/src/components/ReactSelect.jsx @@ -24,6 +24,9 @@ const useStyles = makeStyles({ backgroundColor: theme.palette.background.focus, }, }, + selectInput: { + minWidth: '100%', + }, valueContainer: { display: 'flex', flex: 1, @@ -51,12 +54,13 @@ const selectStyles = { paddingLeft: 0, paddingRight: 0, }), - input: provided => ({ + input: (provided, state) => ({ ...provided, marginLeft: 0, marginRight: 0, maxWidth: '100%', overflow: 'hidden', + minWidth: state.minWidth, }), }; @@ -69,7 +73,7 @@ function Control(props) { classes, textFieldProps, isInitialMount, - onInitialMount, + setTextFieldDOMRect, select, setMenuIsOpen, }, @@ -78,7 +82,9 @@ function Control(props) { useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; - onInitialMount(document.getElementById(inputId).getBoundingClientRect()); + setTextFieldDOMRect( + document.getElementById(inputId).getBoundingClientRect(), + ); } }); @@ -117,8 +123,49 @@ function ValueContainer({ children, selectProps: { inputId, classes } }) { ); } +function handleInputKeyUp(inputProps) { + return () => inputProps.selectProps.setTextFieldDOMRect( + document.getElementById(inputProps.selectProps.inputId).getBoundingClientRect() + ); +} + +function handleInputChange(inputProps) { + const { + onChange, + selectProps: { setInputMinWidth }, + } = inputProps; + + return e => { + onChange(e); + if (e.target.value) { + setInputMinWidth('100%'); + e.target.style.minWidth = '100%'; + } else { + setInputMinWidth(0); + } + }; +} + +function handleInputBlur(inputProps) { + return e => { + inputProps.onBlur(e); + inputProps.selectProps.setInputMinWidth(0); + }; +} + function Input(props) { - return ; + return ( + + ); } function Placeholder({ children, selectProps: { classes } }) { @@ -328,6 +375,7 @@ export default function ReactSelect(selectProps) { const menuPlacementTop = useRef(false); const [textFieldDOMRect, setTextFieldDOMRect] = useState({}); const [menuIsOpen, setMenuIsOpen] = useState(false); + const [inputMinWidth, setInputMinWidth] = useState(0); /** * updates textfield location on scroll/resize @@ -338,7 +386,7 @@ export default function ReactSelect(selectProps) { window[`${selectProps.inputId}Timeout`] = setTimeout(() => { menuTransition.current = false; setTextFieldDOMRect( - document.getElementById(selectProps.inputId).getBoundingClientRect(), + document.getElementById(selectProps.inputId).getBoundingClientRect() ); }, eventHandlerDelay); } @@ -387,10 +435,12 @@ export default function ReactSelect(selectProps) { classes={classes} components={replacedComponents} focusedOption={focusedOption} + inputMinWidth={inputMinWidth} + setInputMinWidth={setInputMinWidth} isInitialMount={isInitialMount} menuPlacementTop={menuPlacementTop} menuTransition={menuTransition} - onInitialMount={setTextFieldDOMRect} + setTextFieldDOMRect={setTextFieldDOMRect} onMenuOpen={handleMenuOpen} onMenuClose={handleMenuClose} placeholder="Type here to search..." From d9e65f327a5575042a6000341b39b566c1624659 Mon Sep 17 00:00:00 2001 From: Raymond C Date: Sat, 14 Dec 2019 22:04:18 +1100 Subject: [PATCH 15/16] move functions out of renders, clean up code --- frontend/src/components/ReactSelect.jsx | 342 ++++++++++++++---------- 1 file changed, 206 insertions(+), 136 deletions(-) diff --git a/frontend/src/components/ReactSelect.jsx b/frontend/src/components/ReactSelect.jsx index f42b688d..13e4a784 100644 --- a/frontend/src/components/ReactSelect.jsx +++ b/frontend/src/components/ReactSelect.jsx @@ -9,7 +9,7 @@ import MenuItem from '@material-ui/core/MenuItem'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; const transitionDuration = 300; -const eventHandlerDelay = 30; +const scrollHandlerDelay = 30; const theme = createMuiTheme({ palette: { background: { @@ -64,33 +64,31 @@ const selectStyles = { }), }; +/** + * handles keyup when textfield is focused via tab key + * input element is focused on Enter/ArrowDown + */ +function handleTextKeyUp(controlProps) { + return e => { + if (e.key === 'Enter' || e.key === 'ArrowDown') { + controlProps.selectProps.selectRef.current.focus(); + controlProps.selectProps.setMenuIsOpen(true); + } + }; +} + function Control(props) { const { children, innerProps, - selectProps: { - inputId, - classes, - textFieldProps, - isInitialMount, - setTextFieldDOMRect, - select, - setMenuIsOpen, - }, + selectProps: { labelRef, textRef, classes, textFieldProps }, } = props; - - useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false; - setTextFieldDOMRect( - document.getElementById(inputId).getBoundingClientRect(), - ); - } - }); + const inputLabelProps = textFieldProps.InputLabelProps; + inputLabelProps.ref = labelRef; return ( { - if (e.key === 'Enter' || e.key === 'ArrowDown') { - select.current.focus(); - setMenuIsOpen(true); - } - }, + onKeyUp: handleTextKeyUp(props), }, }} - {...textFieldProps} + label={textFieldProps.label} + InputLabelProps={inputLabelProps} /> ); } -function ValueContainer({ children, selectProps: { inputId, classes } }) { +function ValueContainer(props) { + const { + children, + selectProps: { classes }, + } = props; const input = children[1]; const singleValue = children[0]; - return ( -
- {[input, singleValue]} -
- ); + return
{[input, singleValue]}
; } function handleInputKeyUp(inputProps) { - return () => inputProps.selectProps.setTextFieldDOMRect( - document.getElementById(inputProps.selectProps.inputId).getBoundingClientRect() - ); + const { + selectProps: { setTextFieldDOMRect, textRef }, + } = inputProps; + return () => setTextFieldDOMRect(textRef.current.getBoundingClientRect()); } +/** + * sets min-width of select input wrapper to 100% when input value exists + * allows input cursor to be moved by clicking on blank space within textfield + */ function handleInputChange(inputProps) { const { onChange, @@ -158,7 +157,7 @@ function Input(props) { {children} @@ -176,8 +180,12 @@ function Placeholder({ children, selectProps: { classes } }) { ); } -function SingleValue({ children, selectProps: { classes } }) { - return
{children}
; +function SingleValue(props) { + return ( +
+ {props.children} +
+ ); } function DropdownIndicator(props) { @@ -194,8 +202,9 @@ function Menu(props) { innerProps, selectProps: { classes, - focusedOption, - inputId, + focusedOptionRef, + labelRef, + menuRef, menuIsOpen, menuPlacementTop, menuTransition, @@ -207,6 +216,7 @@ function Menu(props) { const [menuStyleRight, setMenuStyleRight] = useState(0); const [menuStyleBottom, setMenuStyleBottom] = useState(0); + // evaluates true if there is more space for the menu above the textfield rather than below menuPlacementTop.current = textFieldDOMRect.top > document.documentElement.clientHeight - textFieldDOMRect.bottom; @@ -220,14 +230,12 @@ function Menu(props) { } useEffect(() => { - const menu = document.getElementById(`${inputId}Menu`); - // TODO: get label ref via callback? also in menu list - const labelHeight = document.getElementById(inputId).parentElement - .previousSibling.clientHeight; + const labelHeight = labelRef.current.clientHeight; const inputHeight = textFieldDOMRect.height + labelHeight; const rightWillSlice = - textFieldDOMRect.left + menu.clientWidth > window.innerWidth; - const leftWillSlice = textFieldDOMRect.right - menu.clientWidth < 0; + textFieldDOMRect.left + menuRef.current.clientWidth > window.innerWidth; + const leftWillSlice = + textFieldDOMRect.right - menuRef.current.clientWidth < 0; const idealRightPosition = textFieldDOMRect.right - document.documentElement.clientWidth; @@ -252,19 +260,17 @@ function Menu(props) { setMenuStyleBottom(0); } - /** - * temporary fix to react-select issue not setting focus to selected value by default - * may or may not be needed after fixing issue with default focus option - */ - if (focusedOption.current) { - focusedOption.current.parentNode.scrollTop = - focusedOption.current.offsetTop - - menu.clientHeight / 2 + - focusedOption.current.clientHeight / 2; + // temporary fix to react-select issue not setting focus to selected value by default + if (focusedOptionRef.current) { + focusedOptionRef.current.parentNode.scrollTop = + focusedOptionRef.current.offsetTop - + menuRef.current.clientHeight / 2 + + focusedOptionRef.current.clientHeight / 2; } }, [ - focusedOption, - inputId, + focusedOptionRef, + labelRef, + menuRef, menuPlacementTop, menuStyleBottom, menuStyleRight, @@ -279,7 +285,7 @@ function Menu(props) { > { + if (isSelected) focusedOptionRef.current = element; + if (innerRef) innerRef(element); + }; +} + function Option(props) { const { children, innerProps, - innerRef, isFocused, isSelected, data: { label, value: { icon }, }, - selectProps: { focusedOption, handleItemMouseOver, handleItemMouseOut }, + selectProps: { handleItemMouseOver, handleItemMouseOut }, } = props; - const focused = (function() { - if (isFocused && !isSelected) { - return { - backgroundColor: theme.palette.action.hover, - }; - } - return {}; - })(); + const focusedStyle = {}; if (isFocused) { handleItemMouseOver(icon, label); + if (!isSelected) focusedStyle.backgroundColor = theme.palette.action.hover; } else { handleItemMouseOut(icon); } return ( { - if (isSelected) focusedOption.current = element; - if (innerRef) innerRef(element); - }} + ref={optionRef(props)} selected={isSelected} - style={focused} + style={focusedStyle} {...innerProps} > {children} @@ -365,91 +371,155 @@ function Option(props) { ); } -export default function ReactSelect(selectProps) { +function handleMenuOpen(menuTransition, setMenuIsOpen, onOpen) { + const allowTransition = menuTransition; + + return () => { + allowTransition.current = true; + setMenuIsOpen(true); + onOpen(); + }; +} + +function handleMenuClose(menuTransition, setMenuIsOpen, onClose) { + const allowTransition = menuTransition; + + return () => { + allowTransition.current = true; + document.activeElement.blur(); + setMenuIsOpen(false); + onClose(); + }; +} + +function filterValue(stopId) { + return option => option.value.stopId === stopId; +} + +/** + * updates textfield location on scroll/resize + * re-renders menu if open which updates menu placement and max height + */ +const reposition = {}; +function handleReposition( + eventType, + inputId, + menuTransition, + setTextFieldDOMRect, + textRef, +) { + const allowTransition = menuTransition; + reposition[eventType] = () => { + clearTimeout(window[`${inputId}Timeout`]); + window[`${inputId}Timeout`] = setTimeout( + () => { + allowTransition.current = false; + setTextFieldDOMRect(textRef.current.getBoundingClientRect()); + }, + eventType === 'scroll' ? scrollHandlerDelay : 0 + ); + }; + + return reposition[eventType]; +} + +export default function ReactSelect(props) { const classes = useStyles(); - const focusedOption = useRef(); - const select = useRef(); - const isInitialMount = useRef(true); - // determines whether transitionDuration is used, otherwise 0. Set to false on resize - const menuTransition = useRef(true); + const labelRef = useRef(); + const menuRef = useRef(); + const selectRef = useRef(); + const textRef = useRef(); + const focusedOptionRef = useRef(); + const [menuIsOpen, setMenuIsOpen] = useState(false); const menuPlacementTop = useRef(false); + // determines whether transitionDuration is used. Set to false on resize/scroll + const menuTransition = useRef(true); + // contains position and dimensions of textfield const [textFieldDOMRect, setTextFieldDOMRect] = useState({}); - const [menuIsOpen, setMenuIsOpen] = useState(false); const [inputMinWidth, setInputMinWidth] = useState(0); - - /** - * updates textfield location on scroll/resize - * re-renders menu if open which updates menu placement and max height - */ - function handleReposition() { - clearTimeout(window[`${selectProps.inputId}Timeout`]); - window[`${selectProps.inputId}Timeout`] = setTimeout(() => { - menuTransition.current = false; - setTextFieldDOMRect( - document.getElementById(selectProps.inputId).getBoundingClientRect() - ); - }, eventHandlerDelay); - } - const replacedComponents = { Control, ValueContainer, Input, Placeholder, SingleValue, - IndicatorSeparator: () => null, + IndicatorSeparator: null, DropdownIndicator, Menu, MenuList, Option, }; - function handleMenuOpen() { - menuTransition.current = true; - setMenuIsOpen(true); - selectProps.onOpen(); - } - - function handleMenuClose() { - menuTransition.current = true; - document.activeElement.blur(); - setMenuIsOpen(false); - selectProps.onClose(); - } - useEffect(() => { - window.addEventListener('scroll', handleReposition); - window.addEventListener('resize', handleReposition); + const inputEl = document.getElementById(props.inputId); + window.addEventListener( + 'scroll', + handleReposition( + 'scroll', + props.inputId, + menuTransition, + setTextFieldDOMRect, + textRef, + ) + ); + window.addEventListener( + 'resize', + handleReposition( + 'resize', + props.inputId, + menuTransition, + setTextFieldDOMRect, + textRef, + ) + ); + inputEl.addEventListener( + 'focus', + handleReposition( + 'focus', + props.inputId, + menuTransition, + setTextFieldDOMRect, + textRef, + ) + ); + return () => { - window.removeEventListener('scroll', handleReposition); - window.removeEventListener('resize', handleReposition); + window.removeEventListener('scroll', reposition.scroll); + window.removeEventListener('resize', reposition.resize); + inputEl.removeEventListener('focus', reposition.focus); }; - }); + }, [props.inputId, setTextFieldDOMRect]); return (