import React from 'react' ; import classnames from 'classnames' ; import { Subtract } from 'utility-types' ; import { MDCRippleFoundation } from '@material/ripple/foundation' ; import { MDCRippleAdapter } from '@material/ripple/adapter' ; import { supportsCssVariables , applyPassive } from '@material/ripple/util' ; import { EventType , SpecificEventListener } from '@material/base/types' ; import { matches } from '@material/dom/ponyfill' ; export interface RippledComponentProps < T > { unbounded ? : boolean ; disabled ? : boolean ; style ? : React . CSSProperties ; className ? : string ; onMouseDown ? : React . MouseEventHandler < T > ; onMouseUp ? : React . MouseEventHandler < T > ; onTouchStart ? : React . TouchEventHandler < T > ; onTouchEnd ? : React . TouchEventHandler < T > ; onKeyDown ? : React . KeyboardEventHandler < T > ; onKeyUp ? : React . KeyboardEventHandler < T > ; onFocus ? : React . FocusEventHandler < T > ; onBlur ? : React . FocusEventHandler < T > ; computeBoundingRect ? : ( surface : T ) => ClientRect ; } export interface RippledComponentState { classList : Set < string > ; style : React . CSSProperties ; } export interface InjectedProps < S , A = Element > extends RippledComponentProps < S > { initRipple ? : React . Ref < S > | ( ( surface : S | null , activator ? : A | null ) => void ) ; } type ActivateEventTypes < S > = React . MouseEvent < S > | React . TouchEvent < S > | React . KeyboardEvent < S > | React . FocusEvent < S > ; export interface RippledComponentInterface < Surface , Activator = Element > { foundation ? : MDCRippleFoundation ; isComponentMounted : boolean ; isTouched : boolean ; initializeFoundation : ( surface : Surface , activator ? : Activator ) => void ; handleFocus : React . FocusEventHandler < Surface > ; handleBlur : React . FocusEventHandler < Surface > ; handleMouseDown : React . MouseEventHandler < Surface > ; handleMouseUp : React . MouseEventHandler < Surface > ; handleTouchStart : React . TouchEventHandler < Surface > ; handleTouchEnd : React . TouchEventHandler < Surface > ; handleKeyDown : React . KeyboardEventHandler < Surface > ; handleKeyUp : React . KeyboardEventHandler < Surface > ; activateRipple : ( e : ActivateEventTypes < Surface > ) => void ; deactivateRipple : ( ) => void ; classes : string ; style : React . CSSProperties ; displayName : string ; createAdapter : ( surface : Surface , activator ? : Activator ) => MDCRippleAdapter ; updateCssVariable : ( varName : string , value : string | null ) => void ; componentDidMount : ( ) => void ; componentWillUnmount : ( ) => void ; render : ( ) => JSX . Element ; } export function withRipple < P extends InjectedProps < Surface , Activator > , Surface extends Element = Element , Activator extends Element = Element > ( WrappedComponent ) { return class RippledComponent extends React . Component < Subtract < P , InjectedProps < Surface , Activator > > & RippledComponentProps < Surface > , RippledComponentState > implements RippledComponentInterface < Surface , Activator > { foundation ! : MDCRippleFoundation ; isComponentMounted : boolean = true ; isTouched : boolean = false ; displayName = `WithRipple(${ getDisplayName < P > ( WrappedComponent ) })` ; state : RippledComponentState = { classList : new Set ( ) , style : { } , } ; static defaultProps : Partial < RippledComponentProps < HTMLElement > > = { unbounded : false , disabled : false , style : { } , className : '' , onMouseDown : ( ) => { } , onMouseUp : ( ) => { } , onTouchStart : ( ) => { } , onTouchEnd : ( ) => { } , onKeyDown : ( ) => { } , onKeyUp : ( ) => { } , onFocus : ( ) => { } , onBlur : ( ) => { } , ... WrappedComponent . defaultProps , } ; componentDidMount ( ) { if ( ! this . foundation ) { throw new Error ( "You must call initRipple from the element's " + 'ref prop to initialize the adapter for withRipple' ) ; } } componentDidUpdate ( prevProps ) { if ( this . props . disabled !== prevProps . disabled && this . props . disabled ) { this . foundation . handleBlur ( ) ; } } componentWillUnmount ( ) { if ( this . foundation ) { this . isComponentMounted = false ; this . foundation . destroy ( ) ; } } initializeFoundation = ( surface , activator ? ) => { const adapter = this . createAdapter ( surface , activator ) ; this . foundation = new MDCRippleFoundation ( adapter ) ; this . foundation . init ( ) ; } ; createAdapter = ( surface , activator ? ) : MDCRippleAdapter => { return { browserSupportsCssVars : ( ) => supportsCssVariables ( window ) , isUnbounded : ( ) => this . props . unbounded ! as boolean , isSurfaceActive : ( ) => { if ( activator ) { return matches ( activator , ':active' ) ; } return matches ( surface , ':active' ) ; } , isSurfaceDisabled : ( ) => this . props . disabled as boolean , addClass : ( className ) => { if ( ! this . isComponentMounted ) { return ; } this . setState ( { classList : this . state . classList . add ( className ) } ) ; } , removeClass : ( className ) => { if ( ! this . isComponentMounted ) { return ; } const { classList } = this . state ; classList . delete ( className ) ; this . setState ( { classList } ) ; } , registerDocumentInteractionHandler : < K extends EventType > ( evtType , handler ) => document . documentElement . addEventListener ( evtType , handler , applyPassive ( ) ) , deregisterDocumentInteractionHandler : < K extends EventType > ( evtType , handler ) => document . documentElement . removeEventListener ( evtType , handler , applyPassive ( ) ) , registerResizeHandler : ( handler ) => window . addEventListener ( 'resize' , handler ) , deregisterResizeHandler : ( handler ) => window . removeEventListener ( 'resize' , handler ) , updateCssVariable : this . updateCssVariable , computeBoundingRect : ( ) => { if ( ! this . isComponentMounted ) { const defaultDOMRect = { bottom : 0 , height : 0 , left : 0 , right : 0 , top : 0 , width : 0 , x : 0 , y : 0 , } ; return defaultDOMRect ; } if ( this . props . computeBoundingRect ) { return this . props . computeBoundingRect ( surface ) ; } return surface . getBoundingClientRect ( ) ; } , containsEventTarget : ( target ) => { if ( activator && target !== null ) { return activator . contains ( target as Node ) ; } return false ; } , registerInteractionHandler : ( ) => null , deregisterInteractionHandler : ( ) => null , getWindowPageOffset : ( ) => ( { x : window . pageXOffset , y : window . pageYOffset , } ) , } ; } ; handleFocus = ( e ) => { this . props . onFocus && this . props . onFocus ( e ) ; this . foundation . handleFocus ( ) ; } ; handleBlur = ( e ) => { this . props . onBlur && this . props . onBlur ( e ) ; this . foundation . handleBlur ( ) ; } ; handleMouseDown = ( e ) => { this . props . onMouseDown && this . props . onMouseDown ( e ) ; if ( ! this . isTouched ) { this . activateRipple ( e ) ; } else { this . isTouched = false ; } } ; handleMouseUp = ( e ) => { this . props . onMouseUp && this . props . onMouseUp ( e ) ; this . deactivateRipple ( ) ; } ; handleTouchStart = ( e ) => { this . isTouched = true ; this . props . onTouchStart && this . props . onTouchStart ( e ) ; this . activateRipple ( e ) ; } ; handleTouchEnd = ( e ) => { this . props . onTouchEnd && this . props . onTouchEnd ( e ) ; this . deactivateRipple ( ) ; } ; handleKeyDown = ( e ) => { this . props . onKeyDown && this . props . onKeyDown ( e ) ; this . activateRipple ( e ) ; } ; handleKeyUp = ( e ) => { this . props . onKeyUp && this . props . onKeyUp ( e ) ; this . deactivateRipple ( ) ; } ; activateRipple = ( e ) => { e . persist ( ) ; this . foundation . activate ( e . nativeEvent ) ; } ; deactivateRipple = ( ) => { this . foundation . deactivate ( ) ; } ; updateCssVariable = ( varName , value ) => { if ( ! this . isComponentMounted ) { return ; } this . setState ( ( prevState ) => { const updatedStyle = Object . assign ( { } , this . state . style , prevState . style ) as React . CSSProperties ; if ( value === null ) { delete updatedStyle [ varName as keyof React . CSSProperties ] ; } else { updatedStyle [ varName as keyof React . CSSProperties ] = value ; } return Object . assign ( prevState , { style : updatedStyle , } ) ; } ) ; } ; get classes ( ) { const { className : wrappedComponentClasses } = this . props ; const { classList } = this . state ; return classnames ( Array . from ( classList ) , wrappedComponentClasses ) ; } get style ( ) { const { style : wrappedStyle } = this . props ; const { style } = this . state ; return Object . assign ( { } , style , wrappedStyle ) ; } render ( ) { const { unbounded , style , className , onMouseDown , onMouseUp , onTouchStart , onTouchEnd , onKeyDown , onKeyUp , onFocus , onBlur , ... otherProps } = this . props as P ; const updatedProps = { ... otherProps , onMouseDown : this . handleMouseDown , onMouseUp : this . handleMouseUp , onTouchStart : this . handleTouchStart , onTouchEnd : this . handleTouchEnd , onKeyDown : this . handleKeyDown , onKeyUp : this . handleKeyUp , onFocus : this . handleFocus , onBlur : this . handleBlur , initRipple : this . initializeFoundation , className : this . classes , style : this . style , } ; return ( < WrappedComponent { ... updatedProps } / > ) ; } } as any ; } function getDisplayName < P extends { } > ( WrappedComponent ) { return WrappedComponent . displayName || WrappedComponent . name || 'Component' ; }