From 87224fa94f8cfad0df9f95915a34ffc24fe75b4c Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Mon, 24 Sep 2018 21:00:21 +0200 Subject: [PATCH] fixed and refacored LeftNavigation --- src/utils/windowUtils.js | 10 + .../components/LeftNavigation/Container.js | 3 +- .../components/StickyContainer/index.js | 254 +++++++++--------- .../Wallet/components/LeftNavigation/index.js | 61 +++-- 4 files changed, 183 insertions(+), 145 deletions(-) diff --git a/src/utils/windowUtils.js b/src/utils/windowUtils.js index f7e37334..7eb29cf2 100644 --- a/src/utils/windowUtils.js +++ b/src/utils/windowUtils.js @@ -6,6 +6,16 @@ export const getViewportHeight = (): number => ( window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight // $FlowIssue ); +export const getScrollX = (): number => { + if (window.pageXOffset !== undefined) { + return window.pageXOffset; + } if (window.scrollLeft !== undefined) { + return window.scrollLeft; + } + // $FlowIssue + return (document.documentElement || document.body.parentNode || document.body).scrollLeft; // $FlowIssue +}; + export const getScrollY = (): number => { if (window.pageYOffset !== undefined) { return window.pageYOffset; diff --git a/src/views/Wallet/components/LeftNavigation/Container.js b/src/views/Wallet/components/LeftNavigation/Container.js index 77b7514e..e297c59c 100644 --- a/src/views/Wallet/components/LeftNavigation/Container.js +++ b/src/views/Wallet/components/LeftNavigation/Container.js @@ -17,7 +17,7 @@ type OwnProps = { } -const mapStateToProps: MapStateToProps = (state: State/* , own: OwnProps */): StateProps => ({ +const mapStateToProps: MapStateToProps = (state: State): StateProps => ({ connect: state.connect, accounts: state.accounts, router: state.router, @@ -31,7 +31,6 @@ const mapStateToProps: MapStateToProps = (state: St }); const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - //onAccountSelect: bindActionCreators(AccountActions.onAccountSelect, dispatch), toggleDeviceDropdown: bindActionCreators(toggleDeviceDropdown, dispatch), addAccount: bindActionCreators(TrezorConnectActions.addAccount, dispatch), acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch), diff --git a/src/views/Wallet/components/LeftNavigation/components/StickyContainer/index.js b/src/views/Wallet/components/LeftNavigation/components/StickyContainer/index.js index 9a360f14..6e983cbb 100644 --- a/src/views/Wallet/components/LeftNavigation/components/StickyContainer/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/StickyContainer/index.js @@ -1,152 +1,74 @@ /* @flow */ - -// https://github.com/KyleAMathews/react-headroom/blob/master/src/shouldUpdate.js - import * as React from 'react'; import raf from 'raf'; -import { getViewportHeight, getScrollY } from 'utils/windowUtils'; +import { getViewportHeight, getScrollX, getScrollY } from 'utils/windowUtils'; import styled from 'styled-components'; import colors from 'config/colors'; type Props = { - location: string, - deviceSelection: boolean, children?: React.Node, } -const AsideWrapper = styled.aside` +type State = { + prevScrollY: number; + asideMinHeight: number, + wrapperTopOffset: number, + wrapperLeftOffset: number, + wrapperBottomPadding: number, + footerFixed: boolean, +} + +const AsideWrapper = styled.aside.attrs({ + style: ({ minHeight }) => ({ + minHeight, + }), +})` position: relative; - top: 0; + top: 0px; width: 320px; min-width: 320px; overflow: hidden; background: ${colors.MAIN}; border-right: 1px solid ${colors.DIVIDER}; - - .fixed { - position: fixed; - border-right: 1px solid ${colors.DIVIDER}; - } - - .fixed-bottom { - padding-bottom: 60px; - .sticky-bottom { - position: fixed; - bottom: 0; - background: ${colors.MAIN}; - border-right: 1px solid ${colors.DIVIDER}; - } - } `; -const StickyContainerWrapper = styled.div` - position: relative; - top: 0; +const StickyContainerWrapper = styled.div.attrs({ + style: ({ top, left, paddingBottom }) => ({ + top, + left, + paddingBottom, + }), +})` + position: fixed; + border-right: 1px solid ${colors.DIVIDER}; width: 320px; overflow: hidden; `; -export default class StickyContainer extends React.PureComponent { - // Class variables. - currentScrollY: number = 0; - - lastKnownScrollY: number = 0; - - topOffset: number = 0; - - firstRender: boolean = false; - - framePending: boolean = false; - - stickToBottom: boolean = false; - - top: number = 0; - - aside: ?HTMLElement; - - wrapper: ?HTMLElement; - - subscribers = []; - - // handleResize = (event: Event) => { - - // } - - shouldUpdate = () => { - const { wrapper, aside }: {wrapper: ?HTMLElement, aside: ?HTMLElement} = this; - if (!wrapper || !aside) return; - const bottom: ?HTMLElement = wrapper.querySelector('.sticky-bottom'); - if (!bottom) return; - - const viewportHeight: number = getViewportHeight(); - const bottomBounds = bottom.getBoundingClientRect(); - const asideBounds = aside.getBoundingClientRect(); - const wrapperBounds = wrapper.getBoundingClientRect(); - const scrollDirection = this.currentScrollY >= this.lastKnownScrollY ? 'down' : 'up'; - const distanceScrolled = Math.abs(this.currentScrollY - this.lastKnownScrollY); - - if (asideBounds.top < 0) { - wrapper.classList.add('fixed'); - let maxTop: number = 1; - if (wrapperBounds.height > viewportHeight) { - const bottomOutOfBounds: boolean = (bottomBounds.bottom <= viewportHeight && scrollDirection === 'down'); - const topOutOfBounds: boolean = (wrapperBounds.top > 0 && scrollDirection === 'up'); - if (!bottomOutOfBounds && !topOutOfBounds) { - this.topOffset += scrollDirection === 'down' ? -distanceScrolled : distanceScrolled; - } - maxTop = viewportHeight - wrapperBounds.height; - } - - if (this.topOffset > 0) this.topOffset = 0; - if (maxTop < 0 && this.topOffset < maxTop) this.topOffset = maxTop; - wrapper.style.top = `${this.topOffset}px`; - } else { - wrapper.classList.remove('fixed'); - wrapper.style.top = '0px'; - this.topOffset = 0; - } - - if (wrapperBounds.height > viewportHeight) { - wrapper.classList.remove('fixed-bottom'); - } else if (wrapper.classList.contains('fixed-bottom')) { - if (bottomBounds.top < wrapperBounds.bottom - bottomBounds.height) { - wrapper.classList.remove('fixed-bottom'); - } - } else if (bottomBounds.bottom < viewportHeight) { - wrapper.classList.add('fixed-bottom'); - } - - aside.style.minHeight = `${wrapperBounds.height}px`; - } - - update = () => { - this.currentScrollY = getScrollY(); - this.shouldUpdate(); - this.framePending = false; - this.lastKnownScrollY = this.currentScrollY; +export default class StickyContainer extends React.PureComponent { + constructor() { + super(); + this.state = { + prevScrollY: 0, + asideMinHeight: 0, + wrapperTopOffset: 0, + wrapperLeftOffset: 0, + wrapperBottomPadding: 0, + footerFixed: false, + }; } componentDidMount() { window.addEventListener('scroll', this.handleScroll); window.addEventListener('resize', this.handleScroll); - raf(this.update); + this.update(); } - componentDidUpdate(prevProps: Props) { - if (this.props.location !== prevProps.location && this.aside) { - const asideBounds = this.aside.getBoundingClientRect(); - if (asideBounds.top < 0) { - window.scrollTo(0, getScrollY() + asideBounds.top); - this.topOffset = 0; - } - raf(this.update); - } else if (this.props.deviceSelection !== prevProps.deviceSelection) { - raf(this.update); - } else if (!this.firstRender) { - raf(this.update); - this.firstRender = true; - } + componentDidUpdate(prevProps: Props, newState: State) { + // recalculate view only if props was changed + // ignore when state is changed + if (this.state === newState) raf(this.update); } componentWillUnmount() { @@ -154,26 +76,106 @@ export default class StickyContainer extends React.PureComponent { window.removeEventListener('resize', this.handleScroll); } - handleScroll = (/* event: ?Event */) => { - if (!this.framePending) { - this.framePending = true; - raf(this.update); + update = () => { + this.recalculatePosition(); + } + + handleScroll = () => raf(this.update); + + asideRefCallback = (element: ?HTMLElement) => { + this.aside = element; + } + + wrapperRefCallback = (element: ?HTMLElement) => { + this.wrapper = element; + } + + footerRefCallback = (element: ?HTMLElement) => { + this.footer = element; + } + + aside: ?HTMLElement; + + wrapper: ?HTMLElement; + + footer: ?HTMLElement; + + recalculatePosition() { + const { aside, wrapper, footer } = this; + if (!aside || !wrapper || !footer) return; + + const viewportHeight = getViewportHeight(); + const asideBounds = aside.getBoundingClientRect(); + const wrapperBounds = wrapper.getBoundingClientRect(); + const footerBounds = footer.getBoundingClientRect(); + const isHeaderFixed = asideBounds.top < 0; + const isWrapperBiggerThanViewport = wrapperBounds.height > viewportHeight; + const state = { ...this.state }; + + const scrollX = getScrollX(); + const scrollY = getScrollY(); + + if (isHeaderFixed) { + if (isWrapperBiggerThanViewport) { + const scrollDirection = scrollY >= state.prevScrollY ? 'down' : 'up'; + const topOutOfBounds: boolean = (wrapperBounds.top > 0 && scrollDirection === 'up'); + const bottomOutOfBounds: boolean = (footerBounds.bottom <= viewportHeight && scrollDirection === 'down'); + if (!topOutOfBounds && !bottomOutOfBounds) { + // neither "top" or "bottom" was reached + // scroll whole wrapper + const distanceScrolled = Math.abs(scrollY - state.prevScrollY); + state.wrapperTopOffset += scrollDirection === 'down' ? -distanceScrolled : distanceScrolled; + } + } + // make sure that wrapper will not be over scrolled + if (state.wrapperTopOffset > 0) state.wrapperTopOffset = 0; + } else { + // update wrapper "top" to be same as "aside" element + state.wrapperTopOffset = asideBounds.top; } + + if (isWrapperBiggerThanViewport) { + state.footerFixed = false; + } else if (state.footerFixed) { + if (footerBounds.top < wrapperBounds.bottom - footerBounds.height) { + state.footerFixed = false; + } + } else if (footerBounds.bottom < viewportHeight) { + state.footerFixed = true; + } + + state.prevScrollY = scrollY; + state.asideMinHeight = wrapperBounds.height; + state.wrapperBottomPadding = state.footerFixed ? footerBounds.height : 0; + // update wrapper "left" position + state.wrapperLeftOffset = scrollX > 0 ? -scrollX : asideBounds.left; + + this.setState(state); } render() { return ( { this.aside = node; }} + footerFixed={this.state.footerFixed} + minHeight={this.state.asideMinHeight} + innerRef={this.asideRefCallback} onScroll={this.handleScroll} onTouchStart={this.handleScroll} onTouchMove={this.handleScroll} onTouchEnd={this.handleScroll} > { this.wrapper = node; }} + paddingBottom={this.state.wrapperBottomPadding} + top={this.state.wrapperTopOffset} + left={this.state.wrapperLeftOffset} + innerRef={this.wrapperRefCallback} > - {this.props.children} + {React.Children.map(this.props.children, (child) => { // eslint-disable-line arrow-body-style + return child.key === 'sticky-footer' ? React.cloneElement(child, { + innerRef: this.footerRefCallback, + position: this.state.footerFixed ? 'fixed' : 'relative', + }) : child; + })} ); diff --git a/src/views/Wallet/components/LeftNavigation/index.js b/src/views/Wallet/components/LeftNavigation/index.js index ef3e79f9..b5402d22 100644 --- a/src/views/Wallet/components/LeftNavigation/index.js +++ b/src/views/Wallet/components/LeftNavigation/index.js @@ -1,4 +1,6 @@ -import React, { Component } from 'react'; +/* @flow */ + +import * as React from 'react'; import PropTypes from 'prop-types'; import colors from 'config/colors'; import Icon from 'components/Icon'; @@ -10,6 +12,7 @@ import AccountMenu from './components/AccountMenu'; import CoinMenu from './components/CoinMenu'; import DeviceMenu from './components/DeviceMenu'; import StickyContainer from './components/StickyContainer'; +import type { Props } from './components/common'; const Header = styled(DeviceHeader)``; @@ -23,8 +26,11 @@ const TransitionContentWrapper = styled.div` vertical-align: top; `; -const Footer = styled.div` - position: relative; +const Footer = styled.div.attrs({ + style: ({ position }) => ({ + position, + }), +})` width: 320px; bottom: 0; background: ${colors.MAIN}; @@ -58,6 +64,13 @@ const A = styled.a` } `; +type TransitionMenuProps = { + animationType: ?string; + children?: React.Node; +} + +// TransitionMenu needs to dispatch window.resize even +// in order to StickyContainer be recalculated const TransitionMenu = (props: TransitionMenuProps): React$Element => ( ); -class LeftNavigation extends Component { - constructor(props) { +type State = { + animationType: ?string; + shouldRenderDeviceSelection: boolean; +} + +class LeftNavigation extends React.PureComponent { + constructor(props: Props) { super(props); this.state = { animationType: null, @@ -93,12 +111,11 @@ class LeftNavigation extends Component { }); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { const { deviceDropdownOpened } = nextProps; const { selectedDevice } = nextProps.wallet; - const hasNetwork = nextProps.location.state && nextProps.location.state.network; - const hasFeatures = selectedDevice && selectedDevice.features; - const deviceReady = hasFeatures && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized; + const hasNetwork = nextProps.router.location.state && nextProps.router.location.state.network; + const deviceReady = selectedDevice && selectedDevice.features && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized; if (deviceDropdownOpened) { this.setState({ shouldRenderDeviceSelection: true }); @@ -117,10 +134,11 @@ class LeftNavigation extends Component { shouldRenderAccounts() { const { selectedDevice } = this.props.wallet; + const { location } = this.props.router; return selectedDevice - && this.props.location - && this.props.location.state - && this.props.location.state.network + && location + && location.state + && location.state.network && !this.state.shouldRenderDeviceSelection && this.state.animationType === 'slide-left'; } @@ -130,7 +148,7 @@ class LeftNavigation extends Component { } shouldRenderCoins() { - return !this.state.shouldRenderDeviceSelection && this.state.animationType === 'slide-right'; + return !this.state.shouldRenderDeviceSelection && this.state.animationType !== 'slide-left'; } render() { @@ -152,7 +170,7 @@ class LeftNavigation extends Component { return (
} {menu} -