import * as React from 'react'; import { assoc, dissoc, keys } from 'ramda'; import { Component, ComponentClass, MouseEvent, ReactNode } from 'react'; import { Component as GenericComponent } from '../../types'; import getPassThrough, { PassTroughFunction } from '../../utils/getPassThrough'; import getMousePosition from '../../utils/getMousePosition'; import getTouchPosition from '../../utils/getTouchPosition'; import measureElement from '../../utils/measureElement'; export type RippleOptions = { centered: boolean; className: string; enabled: boolean; multiple: boolean; passthrough: boolean; spread: number; }; export interface RippleWrapperProps { className: string; innerRef(instance: HTMLElement): void; } export interface RippleNodeProps { active: boolean; idx: string; innerRef(instance: HTMLElement): void; isTouch: boolean; onDeactivate(): void; onFinish(idx: string): void; spreadSize: number; startX: number; startY: number; } export interface WithRippleFactoryArgs { RippleNode: GenericComponent; RippleWrapper: GenericComponent; passthrough: PassTroughFunction; } export interface RippledProps { children: ReactNode; disabled: boolean; onMouseDown(event: MouseEvent): void; onMouseUp(event: MouseEvent): void; onTouchEnd(event: MouseEvent): void; onTouchStart(event: MouseEvent): void; ripple: boolean; rippleCentered: boolean; rippleClassName: string; rippleMultiple: boolean; rippleSpread: number; } export type RippleWrapperDescriptor = { width: number; x: number; y: number; }; export type RippleDescriptor = { active: boolean; isTouch: boolean; width: number; x: number; y: number; }; export interface RippledState { ripples: { [key: string]: RippleDescriptor; }; } const defaults: RippleOptions = { centered: false, className: '', enabled: true, multiple: true, passthrough: true, spread: 2, }; export interface DecoratedProps { onMouseDown(event: MouseEvent): void; onMouseUp(event: MouseEvent): void; onTouchEnd(event: MouseEvent): void; onTouchStart(event: MouseEvent): void; } const withRippleFactory = ({ RippleNode, RippleWrapper, passthrough, }: WithRippleFactoryArgs) => (options: Partial = {}) => { const passProps = getPassThrough(passthrough); const { centered: defaultCentered, className: defaultClassName, enabled: defaultEnabled, multiple: defaultMultiple, passthrough: defaultPassthrough, spread: defaultSpread, }: RippleOptions = { ...defaults, ...options }; return function withRipple

( ComposedComponent: ComponentClass

, ): ComponentClass { return class RippledComponent extends Component< RippledProps, RippledState > { public static defaultProps = { disabled: false, ripple: defaultEnabled, rippleCentered: defaultCentered, rippleClassName: defaultClassName, rippleMultiple: defaultMultiple, rippleSpread: defaultSpread, }; public state = { ripples: {}, }; private currentCount = 0; private deactivateTimeout: number | null = null; private ripples: { [key: string]: HTMLElement } = {}; // due to https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825 private rootNode: HTMLElement | any = null; private touchCache = false; public componentWillUnmount() { if (this.deactivateTimeout) { clearTimeout(this.deactivateTimeout); } } private getDescriptor = (x: number, y: number, isTouch: boolean) => new Promise(resolve => { if (this.rootNode) { measureElement( this.rootNode, ).then(({ left, top, height, width }) => { resolve({ active: true, isTouch, width: Math.max(width, height) * this.props.rippleSpread, x: this.props.rippleCentered ? left + width / 2 : x - left, y: this.props.rippleCentered ? top + height / 2 : y - top, }); }); } else { resolve({ active: true, isTouch, width: 0, x: 0, y: 0, }); } }); private getNextKey = () => { this.currentCount += 1; return `ripple${this.currentCount}`; }; private rippleShouldTrigger(isTouch) { const shouldStart = isTouch ? true : !this.touchCache; this.touchCache = isTouch; return shouldStart; } private createRipple(interactionX, interactionY, isTouch) { if (this.rippleShouldTrigger(isTouch)) { this.getDescriptor( interactionX, interactionY, isTouch, ).then(descriptor => { const ripples = { [this.getNextKey()]: descriptor }; this.setState({ ripples: this.props.rippleMultiple ? { ...this.state.ripples, ...ripples } : ripples, }); }); } } private handleDeactivate = () => { this.setState({ ripples: keys(this.state.ripples).reduce( (result: { [key: string]: RippleDescriptor }, rippleKey: string) => assoc( rippleKey, { ...this.state.ripples[rippleKey], active: false }, result, ), {}, ), }); }; private handleRippleFinish = (key: string) => { const ripples = dissoc(key, this.state.ripples) as { [key: string]: RippleDescriptor; }; this.setState({ ripples }); }; private handleInnerRef = node => { this.rootNode = node; }; private handleMouseDown = event => { if (!this.props.disabled && this.props.ripple) { const { x, y } = getMousePosition(event); this.createRipple(x, y, false); } if (this.props.onMouseDown) { this.props.onMouseDown(event); } }; private handleTouchStart = event => { if (!this.props.disabled) { const { x, y } = getTouchPosition(event); this.createRipple(x, y, true); } if (this.props.onTouchStart) { this.props.onTouchStart(event); } }; private handleMouseUp = event => { this.deactivateTimeout = window.setTimeout(this.handleDeactivate, 100); if (this.props.onMouseUp) { this.props.onMouseUp(event); } }; private handleTouchEnd = event => { this.deactivateTimeout = window.setTimeout(this.handleDeactivate, 100); if (this.props.onMouseUp) { this.props.onTouchEnd(event); } }; private renderRipple = ( key, className, { x, y, width, active, isTouch }, ) => ( { this.ripples[key] = node; }} isTouch={isTouch} key={key} onDeactivate={this.handleDeactivate} onFinish={this.handleRippleFinish} spreadSize={width} startX={x} startY={y} /> ); public render() { const { children, disabled, ripple, rippleCentered, rippleClassName, rippleMultiple, rippleSpread, ...other, } = this.props; const { ripples } = this.state; const childRipples = keys(ripples).map(key => this.renderRipple(key, rippleClassName, ripples[key]), ); const childProps = { ...other, onMouseDown: this.handleMouseDown, onTouchStart: this.handleTouchStart, onTouchEnd: this.handleTouchEnd, onMouseUp: this.handleMouseUp, }; const finalProps = (defaultPassthrough ? { ...childProps, disabled } : childProps) as any; if (!ripple) { return React.createElement(ComposedComponent, finalProps, children); } return React.createElement( ComposedComponent, finalProps, children, {childRipples} , ); } }; }; }; export default withRippleFactory;