1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-28 03:08:30 +00:00

Merge pull request #86 from satoshilabs/fix/LeftNavigation

fixed and refactored LeftNavigation
This commit is contained in:
Vladimir Volek 2018-09-25 16:41:47 +02:00 committed by GitHub
commit 16c52c05c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 183 additions and 145 deletions

View File

@ -6,6 +6,16 @@ export const getViewportHeight = (): number => (
window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight // $FlowIssue 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 => { export const getScrollY = (): number => {
if (window.pageYOffset !== undefined) { if (window.pageYOffset !== undefined) {
return window.pageYOffset; return window.pageYOffset;

View File

@ -17,7 +17,7 @@ type OwnProps = {
} }
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State/* , own: OwnProps */): StateProps => ({ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
connect: state.connect, connect: state.connect,
accounts: state.accounts, accounts: state.accounts,
router: state.router, router: state.router,
@ -31,7 +31,6 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
}); });
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({ const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
//onAccountSelect: bindActionCreators(AccountActions.onAccountSelect, dispatch),
toggleDeviceDropdown: bindActionCreators(toggleDeviceDropdown, dispatch), toggleDeviceDropdown: bindActionCreators(toggleDeviceDropdown, dispatch),
addAccount: bindActionCreators(TrezorConnectActions.addAccount, dispatch), addAccount: bindActionCreators(TrezorConnectActions.addAccount, dispatch),
acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch), acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch),

View File

@ -1,152 +1,74 @@
/* @flow */ /* @flow */
// https://github.com/KyleAMathews/react-headroom/blob/master/src/shouldUpdate.js
import * as React from 'react'; import * as React from 'react';
import raf from 'raf'; import raf from 'raf';
import { getViewportHeight, getScrollY } from 'utils/windowUtils'; import { getViewportHeight, getScrollX, getScrollY } from 'utils/windowUtils';
import styled from 'styled-components'; import styled from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
type Props = { type Props = {
location: string,
deviceSelection: boolean,
children?: React.Node, 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; position: relative;
top: 0; top: 0px;
width: 320px; width: 320px;
min-width: 320px; min-width: 320px;
overflow: hidden; overflow: hidden;
background: ${colors.MAIN}; background: ${colors.MAIN};
border-right: 1px solid ${colors.DIVIDER}; 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` const StickyContainerWrapper = styled.div.attrs({
position: relative; style: ({ top, left, paddingBottom }) => ({
top: 0; top,
left,
paddingBottom,
}),
})`
position: fixed;
border-right: 1px solid ${colors.DIVIDER};
width: 320px; width: 320px;
overflow: hidden; overflow: hidden;
`; `;
export default class StickyContainer extends React.PureComponent<Props> { export default class StickyContainer extends React.PureComponent<Props, State> {
// Class variables. constructor() {
currentScrollY: number = 0; super();
this.state = {
lastKnownScrollY: number = 0; prevScrollY: 0,
asideMinHeight: 0,
topOffset: number = 0; wrapperTopOffset: 0,
wrapperLeftOffset: 0,
firstRender: boolean = false; wrapperBottomPadding: 0,
footerFixed: 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;
} }
componentDidMount() { componentDidMount() {
window.addEventListener('scroll', this.handleScroll); window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleScroll); window.addEventListener('resize', this.handleScroll);
raf(this.update); this.update();
} }
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props, newState: State) {
if (this.props.location !== prevProps.location && this.aside) { // recalculate view only if props was changed
const asideBounds = this.aside.getBoundingClientRect(); // ignore when state is changed
if (asideBounds.top < 0) { if (this.state === newState) raf(this.update);
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;
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -154,26 +76,106 @@ export default class StickyContainer extends React.PureComponent<Props> {
window.removeEventListener('resize', this.handleScroll); window.removeEventListener('resize', this.handleScroll);
} }
handleScroll = (/* event: ?Event */) => { update = () => {
if (!this.framePending) { this.recalculatePosition();
this.framePending = true;
raf(this.update);
} }
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() { render() {
return ( return (
<AsideWrapper <AsideWrapper
innerRef={(node) => { this.aside = node; }} footerFixed={this.state.footerFixed}
minHeight={this.state.asideMinHeight}
innerRef={this.asideRefCallback}
onScroll={this.handleScroll} onScroll={this.handleScroll}
onTouchStart={this.handleScroll} onTouchStart={this.handleScroll}
onTouchMove={this.handleScroll} onTouchMove={this.handleScroll}
onTouchEnd={this.handleScroll} onTouchEnd={this.handleScroll}
> >
<StickyContainerWrapper <StickyContainerWrapper
innerRef={(node) => { 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;
})}
</StickyContainerWrapper> </StickyContainerWrapper>
</AsideWrapper> </AsideWrapper>
); );

View File

@ -1,4 +1,6 @@
import React, { Component } from 'react'; /* @flow */
import * as React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
@ -10,6 +12,7 @@ import AccountMenu from './components/AccountMenu';
import CoinMenu from './components/CoinMenu'; import CoinMenu from './components/CoinMenu';
import DeviceMenu from './components/DeviceMenu'; import DeviceMenu from './components/DeviceMenu';
import StickyContainer from './components/StickyContainer'; import StickyContainer from './components/StickyContainer';
import type { Props } from './components/common';
const Header = styled(DeviceHeader)``; const Header = styled(DeviceHeader)``;
@ -23,8 +26,11 @@ const TransitionContentWrapper = styled.div`
vertical-align: top; vertical-align: top;
`; `;
const Footer = styled.div` const Footer = styled.div.attrs({
position: relative; style: ({ position }) => ({
position,
}),
})`
width: 320px; width: 320px;
bottom: 0; bottom: 0;
background: ${colors.MAIN}; 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<TransitionGroup> => ( const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGroup> => (
<TransitionGroupWrapper component="div" className="transition-container"> <TransitionGroupWrapper component="div" className="transition-container">
<CSSTransition <CSSTransition
@ -77,8 +90,13 @@ const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGro
</TransitionGroupWrapper> </TransitionGroupWrapper>
); );
class LeftNavigation extends Component { type State = {
constructor(props) { animationType: ?string;
shouldRenderDeviceSelection: boolean;
}
class LeftNavigation extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
animationType: null, animationType: null,
@ -93,12 +111,11 @@ class LeftNavigation extends Component {
}); });
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
const { deviceDropdownOpened } = nextProps; const { deviceDropdownOpened } = nextProps;
const { selectedDevice } = nextProps.wallet; const { selectedDevice } = nextProps.wallet;
const hasNetwork = nextProps.location.state && nextProps.location.state.network; const hasNetwork = nextProps.router.location.state && nextProps.router.location.state.network;
const hasFeatures = selectedDevice && selectedDevice.features; const deviceReady = selectedDevice && selectedDevice.features && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized;
const deviceReady = hasFeatures && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized;
if (deviceDropdownOpened) { if (deviceDropdownOpened) {
this.setState({ shouldRenderDeviceSelection: true }); this.setState({ shouldRenderDeviceSelection: true });
@ -117,10 +134,11 @@ class LeftNavigation extends Component {
shouldRenderAccounts() { shouldRenderAccounts() {
const { selectedDevice } = this.props.wallet; const { selectedDevice } = this.props.wallet;
const { location } = this.props.router;
return selectedDevice return selectedDevice
&& this.props.location && location
&& this.props.location.state && location.state
&& this.props.location.state.network && location.state.network
&& !this.state.shouldRenderDeviceSelection && !this.state.shouldRenderDeviceSelection
&& this.state.animationType === 'slide-left'; && this.state.animationType === 'slide-left';
} }
@ -130,7 +148,7 @@ class LeftNavigation extends Component {
} }
shouldRenderCoins() { shouldRenderCoins() {
return !this.state.shouldRenderDeviceSelection && this.state.animationType === 'slide-right'; return !this.state.shouldRenderDeviceSelection && this.state.animationType !== 'slide-left';
} }
render() { render() {
@ -152,7 +170,7 @@ class LeftNavigation extends Component {
return ( return (
<StickyContainer <StickyContainer
location={this.props.location.pathname} location={this.props.router.location.pathname}
deviceSelection={this.props.deviceDropdownOpened} deviceSelection={this.props.deviceDropdownOpened}
> >
<Header <Header
@ -167,7 +185,7 @@ class LeftNavigation extends Component {
{this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />} {this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />}
{menu} {menu}
</Body> </Body>
<Footer className="sticky-bottom"> <Footer key="sticky-footer">
<Help> <Help>
<A <A
href="https://trezor.io/support/" href="https://trezor.io/support/"
@ -184,9 +202,18 @@ class LeftNavigation extends Component {
} }
LeftNavigation.propTypes = { LeftNavigation.propTypes = {
selectedDevice: PropTypes.object, connect: PropTypes.object,
wallet: PropTypes.object, accounts: PropTypes.array,
router: PropTypes.object,
deviceDropdownOpened: PropTypes.bool, deviceDropdownOpened: PropTypes.bool,
fiat: PropTypes.array,
localStorage: PropTypes.object,
discovery: PropTypes.array,
wallet: PropTypes.object,
devices: PropTypes.array,
pending: PropTypes.array,
toggleDeviceDropdown: PropTypes.func,
}; };