import React, { Component, HTMLAttributes, ReactElement, ReactNode } from 'react'; import { classNames } from '../../lib/classNames'; import { getClassName } from '../../helpers/getClassName'; import { animationEvent } from '../../lib/supportEvents'; import { ANDROID, VKCOM } from '../../lib/platform'; import { withPlatform } from '../../hoc/withPlatform'; import { withContext } from '../../hoc/withContext'; import { HasPlatform } from '../../types'; import { ConfigProviderContext, ConfigProviderContextInterface } from '../ConfigProvider/ConfigProviderContext'; import { SplitColContextProps, SplitColContext } from '../SplitCol/SplitCol'; import { AppRootPortal } from '../AppRoot/AppRootPortal'; import { DOMProps, withDOM } from '../../lib/dom'; export interface RootProps extends HTMLAttributes, HasPlatform { activeView: string; onTransition?(params: { isBack: boolean; from: string; to: string }): void; popout?: ReactNode; modal?: ReactNode; /** * @ignore */ splitCol?: SplitColContextProps; /** * @ignore */ configProvider?: ConfigProviderContextInterface; } 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; } 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, }; private animationFinishTimeout: ReturnType; get document() { return this.props.document; } get window() { return this.props.window; } 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); } } shouldDisableTransitionMotion(): boolean { return this.props.configProvider.transitionMotionEnabled === false || !this.props.splitCol.animate; } waitAnimationFinish(elem: HTMLElement, eventHandler: AnimationEndCallback) { if (this.shouldDisableTransitionMotion()) { eventHandler(); return; } if (animationEvent.supported) { elem.removeEventListener(animationEvent.name, eventHandler); elem.addEventListener(animationEvent.name, eventHandler); } else { clearTimeout(this.animationFinishTimeout); this.animationFinishTimeout = setTimeout(eventHandler.bind(this), this.props.platform === ANDROID || this.props.platform === VKCOM ? 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 as HTMLElement).blur(); } } render() { const { popout, modal, platform, splitCol, configProvider, activeView: _1, onTransition, window, document, ...restProps } = this.props; const { transition, isBack, prevView, activeView, nextView } = this.state; const Views = React.Children.toArray(this.props.children).filter((view: ReactElement) => { return this.state.visibleViews.includes(view.props.id); }); const baseClassName = getClassName('Root', platform); const disableAnimation = this.shouldDisableTransitionMotion(); return (
{Views.map((view: ReactElement) => { return (
{view}
); })} {!!popout &&
{popout}
} {!!modal &&
{modal}
}
); } } export default withContext(withContext( withPlatform(withDOM(Root)), SplitColContext, 'splitCol', ), ConfigProviderContext, 'configProvider');