From a2be02dfeb0564f25a89561eb787685c2fbe6216 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 26 Jul 2021 10:24:44 -0500 Subject: [PATCH] EIP-1559 - Show gas estimate updating animation in transaction detail (#11566) --- .../edit-gas-popover.component.js | 79 ++++++++++--------- .../app/transaction-detail/index.scss | 2 + .../transaction-detail.component.js | 11 ++- ui/components/ui/loading-heartbeat/index.js | 36 +++++++++ ui/components/ui/loading-heartbeat/index.scss | 23 ++++++ ui/components/ui/ui-components.scss | 1 + ui/hooks/useShouldAnimateGasEstimations.js | 28 +++++++ 7 files changed, 138 insertions(+), 42 deletions(-) create mode 100644 ui/components/ui/loading-heartbeat/index.js create mode 100644 ui/components/ui/loading-heartbeat/index.scss create mode 100644 ui/hooks/useShouldAnimateGasEstimations.js diff --git a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js index 4e84ec5bf..7a862e42d 100644 --- a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js +++ b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useGasFeeInputs } from '../../../hooks/useGasFeeInputs'; +import { useShouldAnimateGasEstimations } from '../../../hooks/useShouldAnimateGasEstimations'; import { GAS_ESTIMATE_TYPES, @@ -27,6 +28,7 @@ import { hideSidebar, updateTransaction, } from '../../../store/actions'; +import LoadingHeartBeat from '../../ui/loading-heartbeat'; export default function EditGasPopover({ popoverTitle = '', @@ -41,6 +43,8 @@ export default function EditGasPopover({ const dispatch = useDispatch(); const showSidebar = useSelector((state) => state.appState.sidebar.isOpen); + const shouldAnimate = useShouldAnimateGasEstimations(); + const showEducationButton = mode === EDIT_GAS_MODES.MODIFY_IN_PLACE && process.env.SHOW_EIP_1559_UI; const [showEducationContent, setShowEducationContent] = useState(false); @@ -181,45 +185,48 @@ export default function EditGasPopover({ ) } > -
+
{showEducationContent ? ( ) : ( - setShowEducationContent(true)} - mode={mode} - transaction={transaction} - gasErrors={gasErrors} - onManualChange={onManualChange} - {...editGasDisplayProps} - /> + <> + + setShowEducationContent(true)} + mode={mode} + transaction={transaction} + hasGasErrors={hasGasErrors} + gasErrors={gasErrors} + onManualChange={onManualChange} + {...editGasDisplayProps} + /> + )}
diff --git a/ui/components/app/transaction-detail/index.scss b/ui/components/app/transaction-detail/index.scss index 538f469d8..d5e732d5e 100644 --- a/ui/components/app/transaction-detail/index.scss +++ b/ui/components/app/transaction-detail/index.scss @@ -1,4 +1,6 @@ .transaction-detail { + position: relative; + .transaction-detail-edit { text-align: end; padding-top: 20px; diff --git a/ui/components/app/transaction-detail/transaction-detail.component.js b/ui/components/app/transaction-detail/transaction-detail.component.js index aacb71b8c..63594be25 100644 --- a/ui/components/app/transaction-detail/transaction-detail.component.js +++ b/ui/components/app/transaction-detail/transaction-detail.component.js @@ -1,20 +1,19 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import { I18nContext } from '../../../contexts/i18n'; +import { useShouldAnimateGasEstimations } from '../../../hooks/useShouldAnimateGasEstimations'; import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component'; +import LoadingHeartBeat from '../../ui/loading-heartbeat'; export default function TransactionDetail({ rows = [], onEdit }) { const t = useContext(I18nContext); + const shouldAnimate = useShouldAnimateGasEstimations(); return ( -
+
+ {onEdit && (
diff --git a/ui/components/ui/loading-heartbeat/index.js b/ui/components/ui/loading-heartbeat/index.js new file mode 100644 index 000000000..fae2e886d --- /dev/null +++ b/ui/components/ui/loading-heartbeat/index.js @@ -0,0 +1,36 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { useEffect, useRef } from 'react'; + +export default function LoadingHeartBeat({ active }) { + const heartNode = useRef(null); + + const LOADING_CLASS = 'loading-heartbeat--active'; + + // When the loading animation completes, remove the className to disappear again + useEffect(() => { + const eventName = 'animationend'; + const node = heartNode?.current; + const eventHandler = () => { + node?.classList.remove(LOADING_CLASS); + }; + + node?.addEventListener(eventName, eventHandler); + return () => { + node?.removeEventListener(eventName, eventHandler); + }; + }, [heartNode]); + + return ( +
+ ); +} + +LoadingHeartBeat.propTypes = { + active: PropTypes.bool, +}; diff --git a/ui/components/ui/loading-heartbeat/index.scss b/ui/components/ui/loading-heartbeat/index.scss new file mode 100644 index 000000000..0b1b095f2 --- /dev/null +++ b/ui/components/ui/loading-heartbeat/index.scss @@ -0,0 +1,23 @@ +.loading-heartbeat { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0; + background: #fff; + display: none; + + &--active { + display: block; + animation: heartbeat 2s ease-in-out; + } +} + +@keyframes heartbeat { + 0% { opacity: 0; } + 25% { opacity: 1; } + 50% { opacity: 0.5; } + 75% { opacity: 1; } + 100% { opacity: 0; } +} diff --git a/ui/components/ui/ui-components.scss b/ui/components/ui/ui-components.scss index 0f95c6af8..f585f9052 100644 --- a/ui/components/ui/ui-components.scss +++ b/ui/components/ui/ui-components.scss @@ -31,6 +31,7 @@ @import 'identicon/index'; @import 'info-tooltip/index'; @import 'list-item/index'; +@import 'loading-heartbeat/index'; @import 'loading-indicator/loading-indicator'; @import 'loading-screen/index'; @import 'menu/menu'; diff --git a/ui/hooks/useShouldAnimateGasEstimations.js b/ui/hooks/useShouldAnimateGasEstimations.js new file mode 100644 index 000000000..df588c0ec --- /dev/null +++ b/ui/hooks/useShouldAnimateGasEstimations.js @@ -0,0 +1,28 @@ +import { useRef } from 'react'; +import { isEqual } from 'lodash'; + +import { useGasFeeEstimates } from './useGasFeeEstimates'; + +export function useShouldAnimateGasEstimations() { + const { isGasEstimatesLoading, gasFeeEstimates } = useGasFeeEstimates(); + + // Do the animation only when gas prices have changed... + const lastGasEstimates = useRef(gasFeeEstimates); + const gasEstimatesChanged = !isEqual( + lastGasEstimates.current, + gasFeeEstimates, + ); + + // ... and only if gas didn't just load + // Removing this line will cause the initial loading screen to stay empty + const gasJustLoaded = isEqual(lastGasEstimates.current, {}); + + if (gasEstimatesChanged) { + lastGasEstimates.current = gasFeeEstimates; + } + + const showLoadingAnimation = + isGasEstimatesLoading || (gasEstimatesChanged && !gasJustLoaded); + + return showLoadingAnimation; +}