From 0ab226ca16a9089fdbb32f4da1b9ab190712f24b Mon Sep 17 00:00:00 2001 From: James Prado Date: Thu, 8 Feb 2018 12:51:15 -0500 Subject: [PATCH] Add nonce loading indicator & refresh button (#1021) * Add InlineSpinner component * Add 'what-input' module * Add input style overrides * Add new refresh icon * Update footer styles * Add nonce refresh button & loading indicator * Center InlineSpinner * Add types * Lock version * prettify package.json * prettify package.json --- common/Root.tsx | 1 + common/components/Footer/index.scss | 4 +- common/components/GasLimitField.scss | 11 +++ common/components/GasLimitField.tsx | 78 +++++----------- common/components/NonceField.scss | 36 ++++++++ common/components/NonceField.tsx | 90 +++++++++++++------ .../TXMetaDataPanel/TXMetaDataPanel.scss | 2 +- .../components/AdvancedGas.scss | 6 -- .../components/AdvancedGas.tsx | 6 +- .../TXMetaDataPanel/components/SimpleGas.scss | 23 ----- .../TXMetaDataPanel/components/SimpleGas.tsx | 30 +++++-- common/components/index.ts | 2 +- common/components/ui/InlineSpinner.scss | 24 +++++ common/components/ui/InlineSpinner.tsx | 17 ++++ common/sass/styles.scss | 4 + common/sass/styles/overrides.scss | 1 + common/sass/styles/overrides/inputs.scss | 9 ++ common/selectors/transaction/network.ts | 3 + package.json | 27 ++++-- 19 files changed, 236 insertions(+), 138 deletions(-) create mode 100644 common/components/GasLimitField.scss create mode 100644 common/components/NonceField.scss create mode 100644 common/components/ui/InlineSpinner.scss create mode 100644 common/components/ui/InlineSpinner.tsx create mode 100644 common/sass/styles/overrides/inputs.scss diff --git a/common/Root.tsx b/common/Root.tsx index 5a82cc54..7ae3f165 100644 --- a/common/Root.tsx +++ b/common/Root.tsx @@ -17,6 +17,7 @@ import { Store } from 'redux'; import { pollOfflineStatus } from 'actions/config'; import { AppState } from 'reducers'; import { RouteNotFound } from 'components/RouteNotFound'; +import 'what-input'; interface Props { store: Store; diff --git a/common/components/Footer/index.scss b/common/components/Footer/index.scss index 92a9c282..406c70f5 100644 --- a/common/components/Footer/index.scss +++ b/common/components/Footer/index.scss @@ -146,8 +146,8 @@ margin: 0 0 $space-md 0; } - li, - p { + > li, + > p { font-size: 0.8rem; margin: $space-sm 0; } diff --git a/common/components/GasLimitField.scss b/common/components/GasLimitField.scss new file mode 100644 index 00000000..cfc36f7a --- /dev/null +++ b/common/components/GasLimitField.scss @@ -0,0 +1,11 @@ +@import 'common/sass/variables'; + +.gaslimit { + &-label-wrapper { + align-items: center; + margin-bottom: $space-xs; + > label { + margin-bottom: 0; + } + } +} diff --git a/common/components/GasLimitField.tsx b/common/components/GasLimitField.tsx index afec6682..f5b1c8af 100644 --- a/common/components/GasLimitField.tsx +++ b/common/components/GasLimitField.tsx @@ -1,66 +1,34 @@ import React from 'react'; import { GasLimitFieldFactory } from './GasLimitFieldFactory'; import translate from 'translations'; -import { CSSTransition } from 'react-transition-group'; -import { Spinner } from 'components/ui'; import { gasLimitValidator } from 'libs/validators'; +import { InlineSpinner } from 'components/ui/InlineSpinner'; +import './GasLimitField.scss'; interface Props { - includeLabel: boolean; - onlyIncludeLoader: boolean; customLabel?: string; disabled?: boolean; } -export const GaslimitLoading: React.SFC<{ - gasEstimationPending: boolean; - onlyIncludeLoader?: boolean; -}> = ({ gasEstimationPending, onlyIncludeLoader }) => ( - -
- {onlyIncludeLoader ? 'Calculating gas limit' : 'Calculating'} - -
-
-); - -export const GasLimitField: React.SFC = ({ - includeLabel, - onlyIncludeLoader, - customLabel, - disabled -}) => ( - - ( - -
- {includeLabel ? ( - customLabel ? ( - - ) : ( - - ) - ) : null} -
- -
- {onlyIncludeLoader ? null : ( - - )} - - )} - /> - +export const GasLimitField: React.SFC = ({ customLabel, disabled }) => ( + ( + +
+ {customLabel ? : } +
+ +
+ + + )} + /> ); diff --git a/common/components/NonceField.scss b/common/components/NonceField.scss new file mode 100644 index 00000000..0082106a --- /dev/null +++ b/common/components/NonceField.scss @@ -0,0 +1,36 @@ +@import 'common/sass/variables'; + +.nonce { + &-label-wrapper { + align-items: center; + margin-bottom: $space-xs; + > label { + margin-bottom: 0; + } + } + &-input-wrapper { + position: relative; + } + &-refresh { + position: absolute; + right: 0; + top: 0; + border: none; + background: transparent; + padding: 0; + margin: 0 1rem; + height: 2.55rem; + opacity: 0.3; + transition: opacity 300ms; + > img { + height: 1.4rem; + } + &:hover { + opacity: 0.54; + } + &:active { + transition: opacity 120ms; + opacity: 1; + } + } +} diff --git a/common/components/NonceField.tsx b/common/components/NonceField.tsx index ffdd34aa..3bf41a78 100644 --- a/common/components/NonceField.tsx +++ b/common/components/NonceField.tsx @@ -1,38 +1,72 @@ import React from 'react'; import { NonceFieldFactory } from 'components/NonceFieldFactory'; import Help from 'components/ui/Help'; +import RefreshIcon from 'assets/images/refresh.svg'; +import './NonceField.scss'; +import { InlineSpinner } from 'components/ui/InlineSpinner'; +import { connect } from 'react-redux'; +import { getNonceRequested, TGetNonceRequested } from 'actions/transaction'; +import { nonceRequestPending } from 'selectors/transaction'; +import { AppState } from 'reducers'; -interface Props { +interface OwnProps { alwaysDisplay: boolean; } -const nonceHelp = ( - -); +interface StateProps { + nonePending: boolean; +} -export const NonceField: React.SFC = ({ alwaysDisplay }) => ( - { - const content = ( -
- - {nonceHelp} +interface DispatchProps { + requestNonce: TGetNonceRequested; +} - -
- ); +type Props = OwnProps & DispatchProps & StateProps; - return alwaysDisplay || shouldDisplay ? content : null; - }} - /> -); +class NonceField extends React.Component { + public render() { + const { alwaysDisplay, requestNonce, nonePending } = this.props; + return ( + { + return alwaysDisplay || shouldDisplay ? ( + +
+ + +
+ +
+
+ + +
+ + ) : null; + }} + /> + ); + } +} + +const mapStateToProps = (state: AppState) => { + return { + nonePending: nonceRequestPending(state) + }; +}; + +export default connect(mapStateToProps, { requestNonce: getNonceRequested })(NonceField); diff --git a/common/components/TXMetaDataPanel/TXMetaDataPanel.scss b/common/components/TXMetaDataPanel/TXMetaDataPanel.scss index 5601f3b6..ecf38b5e 100644 --- a/common/components/TXMetaDataPanel/TXMetaDataPanel.scss +++ b/common/components/TXMetaDataPanel/TXMetaDataPanel.scss @@ -12,7 +12,7 @@ .Calculating-limit { color: rgba(51, 51, 51, 0.7); display: flex; - align-items: baseline; + align-items: center; font-weight: 400; opacity: 0; pointer-events: none; diff --git a/common/components/TXMetaDataPanel/components/AdvancedGas.scss b/common/components/TXMetaDataPanel/components/AdvancedGas.scss index dcc7d50d..c4029e04 100644 --- a/common/components/TXMetaDataPanel/components/AdvancedGas.scss +++ b/common/components/TXMetaDataPanel/components/AdvancedGas.scss @@ -40,10 +40,4 @@ width: 100%; } } - - &-data { - } - - &-fee-summary { - } } diff --git a/common/components/TXMetaDataPanel/components/AdvancedGas.tsx b/common/components/TXMetaDataPanel/components/AdvancedGas.tsx index 92ea3333..e34acabb 100644 --- a/common/components/TXMetaDataPanel/components/AdvancedGas.tsx +++ b/common/components/TXMetaDataPanel/components/AdvancedGas.tsx @@ -87,11 +87,7 @@ class AdvancedGas extends React.Component { {gasLimitField && (
- +
)} {nonceField && ( diff --git a/common/components/TXMetaDataPanel/components/SimpleGas.scss b/common/components/TXMetaDataPanel/components/SimpleGas.scss index 2d0dc934..4a7a3f1c 100644 --- a/common/components/TXMetaDataPanel/components/SimpleGas.scss +++ b/common/components/TXMetaDataPanel/components/SimpleGas.scss @@ -53,26 +53,3 @@ } } } - -.fade { - &-enter, - &-exit { - transition: opacity 300ms; - } - - &-enter { - opacity: 0; - - &-active { - opacity: 1; - } - } - - &-exit { - opacity: 1; - - &-active { - opacity: 0; - } - } -} diff --git a/common/components/TXMetaDataPanel/components/SimpleGas.tsx b/common/components/TXMetaDataPanel/components/SimpleGas.tsx index c3018620..7dd88361 100644 --- a/common/components/TXMetaDataPanel/components/SimpleGas.tsx +++ b/common/components/TXMetaDataPanel/components/SimpleGas.tsx @@ -5,15 +5,21 @@ import { gasPriceDefaults } from 'config'; import FeeSummary from './FeeSummary'; import './SimpleGas.scss'; import { AppState } from 'reducers'; -import { getGasLimitEstimationTimedOut } from 'selectors/transaction'; +import { + getGasLimitEstimationTimedOut, + getGasEstimationPending, + nonceRequestPending +} from 'selectors/transaction'; import { connect } from 'react-redux'; -import { GasLimitField } from 'components/GasLimitField'; import { getIsWeb3Node } from 'selectors/config'; import { Wei, fromWei } from 'libs/units'; +import { InlineSpinner } from 'components/ui/InlineSpinner'; const SliderWithTooltip = Slider.createSliderWithTooltip(Slider); interface OwnProps { gasPrice: AppState['transaction']['fields']['gasPrice']; + noncePending: boolean; + gasLimitPending: boolean; inputGasPrice(rawGas: string); setGasPrice(rawGas: string); } @@ -31,16 +37,22 @@ class SimpleGas extends React.Component { } public render() { - const { gasPrice, gasLimitEstimationTimedOut, isWeb3Node } = this.props; + const { + gasPrice, + gasLimitEstimationTimedOut, + isWeb3Node, + noncePending, + gasLimitPending + } = this.props; return (
- +
+ +
+ +
{gasLimitEstimationTimedOut && ( @@ -101,6 +113,8 @@ class SimpleGas extends React.Component { } export default connect((state: AppState) => ({ + noncePending: nonceRequestPending(state), + gasLimitPending: getGasEstimationPending(state), gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state), isWeb3Node: getIsWeb3Node(state) }))(SimpleGas); diff --git a/common/components/index.ts b/common/components/index.ts index 76c99908..7515dae2 100644 --- a/common/components/index.ts +++ b/common/components/index.ts @@ -1,7 +1,6 @@ export * from './AddressField'; export * from './DataField'; export * from './GasLimitField'; -export * from './NonceField'; export * from './AmountField'; export * from './SendEverything'; export * from './UnitDropDown'; @@ -9,6 +8,7 @@ export * from './CurrentCustomMessage'; export * from './GenerateTransaction'; export * from './SendButton'; export * from './SigningStatus'; +export { default as NonceField } from './NonceField'; export { default as Header } from './Header'; export { default as Footer } from './Footer'; export { default as BalanceSidebar } from './BalanceSidebar'; diff --git a/common/components/ui/InlineSpinner.scss b/common/components/ui/InlineSpinner.scss new file mode 100644 index 00000000..aad696ba --- /dev/null +++ b/common/components/ui/InlineSpinner.scss @@ -0,0 +1,24 @@ +.inline-spinner { + &--fade { + &-enter, + &-exit { + transition: opacity 300ms; + } + + &-enter { + opacity: 0; + + &-active { + opacity: 1; + } + } + + &-exit { + opacity: 1; + + &-active { + opacity: 0; + } + } + } +} diff --git a/common/components/ui/InlineSpinner.tsx b/common/components/ui/InlineSpinner.tsx new file mode 100644 index 00000000..193c3d10 --- /dev/null +++ b/common/components/ui/InlineSpinner.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { CSSTransition } from 'react-transition-group'; +import { Spinner } from 'components/ui'; +import './InlineSpinner.scss'; + +export const InlineSpinner: React.SFC<{ + active: boolean; + text?: string; +}> = ({ active, text }) => ( + + {/* TODO: when react-transition-group v2.3 releases, use '-done' classes instead of conditional 'active' class https://github.com/reactjs/react-transition-group/issues/274 */} +
+ {text} + +
+
+); diff --git a/common/sass/styles.scss b/common/sass/styles.scss index 2a5f8773..6df1acd6 100644 --- a/common/sass/styles.scss +++ b/common/sass/styles.scss @@ -35,3 +35,7 @@ @import './styles/tab'; @import './styles/flexbox'; @import './fonts'; + +[data-whatintent='mouse'] *:focus { + outline: none; +} diff --git a/common/sass/styles/overrides.scss b/common/sass/styles/overrides.scss index 23e593be..ba3171c6 100644 --- a/common/sass/styles/overrides.scss +++ b/common/sass/styles/overrides.scss @@ -9,6 +9,7 @@ @import './overrides/input-groups'; @import './overrides/type'; @import './overrides/tables'; +@import './overrides/inputs'; // Other overrides @import './overrides/react-select'; diff --git a/common/sass/styles/overrides/inputs.scss b/common/sass/styles/overrides/inputs.scss new file mode 100644 index 00000000..a35d3179 --- /dev/null +++ b/common/sass/styles/overrides/inputs.scss @@ -0,0 +1,9 @@ +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type='number'] { + -moz-appearance: textfield; +} diff --git a/common/selectors/transaction/network.ts b/common/selectors/transaction/network.ts index d6ec4890..d368f8c6 100644 --- a/common/selectors/transaction/network.ts +++ b/common/selectors/transaction/network.ts @@ -4,6 +4,9 @@ import { RequestStatus } from 'reducers/transaction/network'; export const getNetworkStatus = (state: AppState) => getTransactionState(state).network; +export const nonceRequestPending = (state: AppState) => + getNetworkStatus(state).getNonceStatus === RequestStatus.REQUESTED; + export const nonceRequestFailed = (state: AppState) => getNetworkStatus(state).getNonceStatus === RequestStatus.FAILED; diff --git a/package.json b/package.json index 20373ba6..d27fbe70 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,8 @@ "webpack-hot-middleware": "2.21.0", "webpack-sources": "1.0.1", "webpack-subresource-integrity": "1.0.3", - "worker-loader": "1.1.0" + "worker-loader": "1.1.0", + "what-input": "5.0.5" }, "scripts": { "freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js", @@ -140,10 +141,14 @@ "prebuild": "check-node-version --package", "build:downloadable": "webpack --config webpack_config/webpack.html.js", "prebuild:downloadable": "check-node-version --package", - "build:electron": "webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js", - "build:electron:osx": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js", - "build:electron:windows": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js", - "build:electron:linux": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js", + "build:electron": + "webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js", + "build:electron:osx": + "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js", + "build:electron:windows": + "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js", + "build:electron:linux": + "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js", "prebuild:electron": "check-node-version --package", "test:coverage": "jest --config=jest_config/jest.config.json --coverage", "test": "jest --config=jest_config/jest.config.json", @@ -155,14 +160,18 @@ "predev": "check-node-version --package", "dev:https": "HTTPS=true node webpack_config/devServer.js", "predev:https": "check-node-version --package", - "dev:electron": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'", - "dev:electron:https": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'", + "dev:electron": + "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'", + "dev:electron:https": + "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'", "tslint": "tslint --project . --exclude common/vendor/**/*", "tscheck": "tsc --noEmit", "start": "npm run dev", "precommit": "lint-staged", - "formatAll": "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override", - "prettier:diff": "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"", + "formatAll": + "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override", + "prettier:diff": + "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"", "prepush": "npm run tslint && npm run tscheck" }, "lint-staged": {