import React, { HTMLAttributes, PureComponent, MouseEvent } from 'react'; import PropTypes from 'prop-types'; import withPlatform from '../../hoc/withPlatform'; import FixedLayout from '../FixedLayout/FixedLayout'; 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 transitionEvents from '../../lib/transitionEvents'; import { ANDROID } from '../../lib/platform'; import { rubber } from '../../lib/touch'; export interface SnackbarProps extends HTMLAttributes, HasPlatform { /** * Название кнопки действия в уведомлении */ 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 = { duration: 4000, }; static contextTypes = { window: PropTypes.any, document: PropTypes.any, }; private innerEl: HTMLDivElement; private readonly bodyElRef: React.RefObject; private closeTimeout: number; private shiftXPercent: number; private shiftXCurrent: number; private touchStartTime: Date; private animationFrame: number; componentDidMount() { this.setCloseTimeout(); } componentWillUnmount() { this.clearCloseTimeout(); } get window() { return this.context.window || window; } setCloseTimeout = () => { if (canUseDOM) { this.closeTimeout = window.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) { const eventName = transitionEvents.transitionEndEventName; element.removeEventListener(eventName, eventHandler); element.addEventListener(eventName, eventHandler); } } 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.window.innerWidth * 100; this.shiftXCurrent = rubber(this.shiftXPercent, 72, 1.2, this.props.platform === ANDROID); this.touchStartTime = startT; this.setBodyTransform(this.shiftXCurrent); }; onTouchEnd = () => { const newState = { touched: false, }; let callback; if (this.state.touched) { let shiftXReal = this.shiftXCurrent; const expectTranslateY = shiftXReal / (Date.now() - this.touchStartTime.getTime()) * 240 * 0.6 * (this.shiftXPercent < 0 ? -1 : 1); shiftXReal = shiftXReal + expectTranslateY; if (shiftXReal >= 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)`; }); } render() { const { children, className, platform, layout, action, before, after, } = this.props; const resolvedLayout = after ? 'vertical' : layout; return (
{before &&
{before}
}
{children}
{action && }
{after &&
{after}
}
); } } export default withPlatform(Snackbar);