mirror of
https://github.com/trezor/trezor-wallet
synced 2024-12-25 16:38:08 +00:00
Merge pull request #86 from satoshilabs/fix/LeftNavigation
fixed and refactored LeftNavigation
This commit is contained in:
commit
16c52c05c4
@ -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;
|
||||
|
@ -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,
|
||||
accounts: state.accounts,
|
||||
router: state.router,
|
||||
@ -31,7 +31,6 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
|
||||
});
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
|
||||
//onAccountSelect: bindActionCreators(AccountActions.onAccountSelect, dispatch),
|
||||
toggleDeviceDropdown: bindActionCreators(toggleDeviceDropdown, dispatch),
|
||||
addAccount: bindActionCreators(TrezorConnectActions.addAccount, dispatch),
|
||||
acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch),
|
||||
|
@ -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<Props> {
|
||||
// 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<Props, State> {
|
||||
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<Props> {
|
||||
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 (
|
||||
<AsideWrapper
|
||||
innerRef={(node) => { 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}
|
||||
>
|
||||
<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>
|
||||
</AsideWrapper>
|
||||
);
|
||||
|
@ -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<TransitionGroup> => (
|
||||
<TransitionGroupWrapper component="div" className="transition-container">
|
||||
<CSSTransition
|
||||
@ -77,8 +90,13 @@ const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGro
|
||||
</TransitionGroupWrapper>
|
||||
);
|
||||
|
||||
class LeftNavigation extends Component {
|
||||
constructor(props) {
|
||||
type State = {
|
||||
animationType: ?string;
|
||||
shouldRenderDeviceSelection: boolean;
|
||||
}
|
||||
|
||||
class LeftNavigation extends React.PureComponent<Props, State> {
|
||||
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 (
|
||||
<StickyContainer
|
||||
location={this.props.location.pathname}
|
||||
location={this.props.router.location.pathname}
|
||||
deviceSelection={this.props.deviceDropdownOpened}
|
||||
>
|
||||
<Header
|
||||
@ -167,7 +185,7 @@ class LeftNavigation extends Component {
|
||||
{this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />}
|
||||
{menu}
|
||||
</Body>
|
||||
<Footer className="sticky-bottom">
|
||||
<Footer key="sticky-footer">
|
||||
<Help>
|
||||
<A
|
||||
href="https://trezor.io/support/"
|
||||
@ -184,9 +202,18 @@ class LeftNavigation extends Component {
|
||||
}
|
||||
|
||||
LeftNavigation.propTypes = {
|
||||
selectedDevice: PropTypes.object,
|
||||
wallet: PropTypes.object,
|
||||
connect: PropTypes.object,
|
||||
accounts: PropTypes.array,
|
||||
router: PropTypes.object,
|
||||
deviceDropdownOpened: PropTypes.bool,
|
||||
fiat: PropTypes.array,
|
||||
localStorage: PropTypes.object,
|
||||
discovery: PropTypes.array,
|
||||
wallet: PropTypes.object,
|
||||
devices: PropTypes.array,
|
||||
pending: PropTypes.array,
|
||||
|
||||
toggleDeviceDropdown: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user