import React, { HTMLAttributes, MouseEvent, PureComponent } from 'react'; import { withPlatform } from '../../hoc/withPlatform'; import Touch, { TouchEvent } from '../Touch/Touch'; import { classNames } from '../../lib/classNames'; import { HasPlatform } from '../../types'; import { getClassName } from '../../helpers/getClassName'; import { canUseDOM } from '../../lib/dom'; import { transitionEvent } from '../../lib/supportEvents'; import { ANDROID, VKCOM } from '../../lib/platform'; import { rubber } from '../../lib/touch'; import { withAdaptivity, AdaptivityProps, ViewWidth } from '../../hoc/withAdaptivity'; import Text from '../Typography/Text/Text'; import Button from '../Button/Button'; import { AppRootPortal } from '../AppRoot/AppRootPortal'; export interface SnackbarProps extends HTMLAttributes, HasPlatform, AdaptivityProps { /** * Название кнопки действия в уведомлении */ action?: string | React.ComponentType; /** * Будет вызвано при клике на кнопку действия */ onActionClick?: (e: React.MouseEvent) => void; /** * Цветная иконка 24x24 пикселя */ before?: React.ReactNode; /** * Контент в правой части, может быть `` */ after?: React.ReactNode; /** * Варианты расположения кнопки */ layout?: 'vertical' | 'horizontal'; /** * Время в миллисекундах, через которое плашка скроется */ duration?: number; /** * Обработчик закрытия уведомления */ onClose: () => void; } export interface SnackbarState { closing: boolean; touched: boolean; } class Snackbar extends PureComponent { constructor(props: SnackbarProps) { super(props); this.state = { closing: false, touched: false, }; this.bodyElRef = React.createRef(); this.shiftXPercent = 0; this.shiftXCurrent = 0; } static defaultProps: Partial = { duration: 4000, layout: 'horizontal', }; private innerEl: HTMLDivElement; private readonly bodyElRef: React.RefObject; private shiftXPercent: number; private shiftXCurrent: number; private touchStartTime: Date; private animationFrame: number; private closeTimeout: ReturnType; private transitionFinishTimeout: ReturnType; componentDidMount() { this.setCloseTimeout(); } componentWillUnmount() { this.clearCloseTimeout(); } setCloseTimeout = () => { if (canUseDOM) { this.closeTimeout = setTimeout(() => { this.close(); }, this.props.duration); } }; clearCloseTimeout = () => { clearTimeout(this.closeTimeout); }; private readonly onActionClick = (e: MouseEvent) => { this.close(); if (this.props.action && typeof this.props.onActionClick === 'function') { this.props.onActionClick(e); } }; close() { this.setState({ closing: true }); this.waitTransitionFinish(this.innerEl, () => { this.props.onClose(); }); } waitTransitionFinish(element: HTMLElement, eventHandler: VoidFunction) { if (element) { if (transitionEvent.supported) { element.removeEventListener(transitionEvent.name, eventHandler); element.addEventListener(transitionEvent.name, eventHandler); } else { clearTimeout(this.transitionFinishTimeout); this.transitionFinishTimeout = setTimeout(eventHandler, this.props.platform === ANDROID || this.props.platform === VKCOM ? 400 : 320); } } } getInnerRef = (el: HTMLDivElement) => this.innerEl = el; onTouchStart = () => { this.clearCloseTimeout(); }; onTouchMoveX = (event: TouchEvent) => { const { shiftX, startT, originalEvent } = event; originalEvent.preventDefault(); if (!this.state.touched) { this.setState({ touched: true }); } this.shiftXPercent = shiftX / this.bodyElRef.current.offsetWidth * 100; this.shiftXCurrent = rubber(this.shiftXPercent, 72, 1.2, this.props.platform === ANDROID || this.props.platform === VKCOM); this.touchStartTime = startT; this.setBodyTransform(this.shiftXCurrent); }; onTouchEnd = () => { const newState = { touched: false, }; let callback; if (this.state.touched) { let shiftXCurrent = this.shiftXCurrent; const expectTranslateY = shiftXCurrent / (Date.now() - this.touchStartTime.getTime()) * 240 * 0.6; shiftXCurrent = shiftXCurrent + expectTranslateY; if (this.isDesktop && shiftXCurrent <= -50) { this.clearCloseTimeout(); this.waitTransitionFinish(this.bodyElRef.current, () => { this.props.onClose(); }); this.setBodyTransform(-120); } else if (!this.isDesktop && shiftXCurrent >= 50) { this.clearCloseTimeout(); this.waitTransitionFinish(this.bodyElRef.current, () => { this.props.onClose(); }); this.setBodyTransform(120); } else { callback = () => { this.setCloseTimeout(); this.setBodyTransform(0); }; } } else { this.setCloseTimeout(); } this.setState(newState, callback); }; setBodyTransform(percent: number) { cancelAnimationFrame(this.animationFrame); this.animationFrame = requestAnimationFrame(() => { this.bodyElRef.current.style.transform = `translate3d(${percent}%, 0, 0)`; }); } get isDesktop() { return this.props.viewWidth >= ViewWidth.SMALL_TABLET; } render() { const { children, className, platform, layout, action, before, after, viewWidth, duration, onActionClick, onClose, ...restProps } = this.props; const resolvedLayout = after || this.isDesktop ? 'vertical' : layout; return ( {before && {before} } {children} {action && {action} } {after && {after} } ); } } export default withPlatform(withAdaptivity(Snackbar, { viewWidth: true, }));