import React, { Component, HTMLAttributes, ReactElement, ReactNode } from 'react'; import PropTypes, { Requireable, Validator } from 'prop-types'; import classNames from '../../lib/classNames'; import getClassName from '../../helpers/getClassName'; import transitionEvents from '../../lib/transitionEvents'; import { ANDROID } from '../../lib/platform'; import withPlatform from '../../hoc/withPlatform'; import { HasPlatform } from '../../types'; export interface RootProps extends HTMLAttributes, HasPlatform { activeView: string; onTransition?(params: { isBack: boolean; from: string; to: string }): void; popout?: ReactNode; modal?: ReactNode; } export type AnimationEndCallback = (e?: AnimationEvent) => void; export interface RootState { activeView: string; prevView: string; nextView: string; visibleViews: [string] | [string, string]; isBack: boolean; scrolls: { [index: string]: number; }; transition: boolean; } export interface RootContext { document: Requireable; window: Requireable; transitionMotionEnabled: Validator; } class Root extends Component { constructor(props: RootProps) { super(props); this.state = { activeView: props.activeView, prevView: null, nextView: null, visibleViews: [props.activeView], isBack: undefined, scrolls: {}, transition: false, }; } static defaultProps: Partial = { popout: null, }; static contextTypes: RootContext = { window: PropTypes.any, document: PropTypes.any, transitionMotionEnabled: PropTypes.bool, }; get document() { return this.context.document || document; } get window() { return this.context.window || window; } get arrayChildren() { return [].concat(this.props.children); } componentDidUpdate(prevProps: RootProps, prevState: RootState) { if (this.props.popout && !prevProps.popout) { this.blurActiveElement(); } // Нужен переход if (this.props.activeView !== prevProps.activeView) { let pageYOffset = this.window.pageYOffset; const firstLayerId = [].concat(prevProps.children).find((view: ReactElement) => { return view.props.id === prevProps.activeView || view.props.id === this.props.activeView; }).props.id; const isBack = firstLayerId === this.props.activeView; this.blurActiveElement(); const nextView = this.props.activeView; const prevView = prevProps.activeView; this.setState({ scrolls: { ...this.state.scrolls, [prevProps.activeView]: pageYOffset, }, transition: true, activeView: null, nextView, prevView, visibleViews: [nextView, prevView], isBack, }); } // Начался переход if (!prevState.transition && this.state.transition) { const prevViewElement = this.document.getElementById(`view-${this.state.prevView}`); const nextViewElement = this.document.getElementById(`view-${this.state.nextView}`); prevViewElement.querySelector('.View__panel').scrollTop = this.state.scrolls[this.state.prevView]; if (this.state.isBack) { nextViewElement.querySelector('.View__panel').scrollTop = this.state.scrolls[this.state.nextView]; } this.waitAnimationFinish(this.state.isBack ? prevViewElement : nextViewElement, this.onAnimationEnd); } } waitAnimationFinish(elem: HTMLElement, eventHandler: AnimationEndCallback) { if (transitionEvents.supported) { const eventName = transitionEvents.prefix ? transitionEvents.prefix + 'AnimationEnd' : 'animationend'; elem.removeEventListener(eventName, eventHandler); elem.addEventListener(eventName, eventHandler); } else { setTimeout(eventHandler.bind(this), this.props.platform === ANDROID ? 300 : 600); } } onAnimationEnd: AnimationEndCallback = (e?: AnimationEvent) => { if (!e || [ 'root-android-animation-hide-back', 'root-android-animation-show-forward', 'root-ios-animation-hide-back', 'root-ios-animation-show-forward', ].includes(e.animationName)) { const isBack = this.state.isBack; const prevView = this.state.prevView; const nextView = this.state.nextView; this.setState({ activeView: nextView, prevView: null, nextView: null, visibleViews: [nextView], transition: false, isBack: undefined, }, () => { this.window.scrollTo(0, isBack ? this.state.scrolls[this.state.activeView] : 0); this.props.onTransition && this.props.onTransition({ isBack, from: prevView, to: nextView }); }); } }; blurActiveElement() { if (typeof this.window !== 'undefined' && this.document.activeElement) { this.document.activeElement.blur(); } } render() { const { popout, modal, platform } = this.props; const { transition, isBack, prevView, activeView, nextView } = this.state; const Views = this.arrayChildren.filter((view: ReactElement) => { return this.state.visibleViews.includes(view.props.id); }); const baseClassName = getClassName('Root', platform); return ( {Views.map((view: ReactElement) => { return ( {view} ); })} {!!popout && {popout}} {!!modal && {modal}} ); } } export default withPlatform(Root);