import * as React from 'react' ; import { eventFrom , setEventFrom , EventFromInput } from 'event-from' ; import { stateChanged , enterKeyTrigger , spaceKeyTrigger , cursorPointer , elementSupportsDisabled , classNameToString , setUserSelectNone , resetUserSelect , } from './utils' ; import { PolymorphicForwardRefExoticComponent , PolymorphicPropsWithoutRef , PolymorphicPropsWithRef , PolymorphicMemoExoticComponent , } from 'react-polymorphic-types' ; export { eventFrom , setEventFrom , EventFromInput } ; export type ActiveState = 'mouseActive' | 'touchActive' | 'keyActive' | false ; export type FocusState = 'focusFromMouse' | 'focusFromTouch' | 'focusFromKey' | false ; /**
 * State object used by React Interactive to determine how the `<Interactive>` component is rendered.
 * The InteractiveState object is also passed to the `onStateChange` callback and `children` (when `children` is a function).
 */ export interface InteractiveState { hover : boolean ; active : ActiveState ; focus : FocusState ; } export type InteractiveStateKey = 'hover' | 'active' | 'focus' ; /**
 * InteractiveStateChange is the type for the argument passed to the `onStateChange` callback.
 */ export interface InteractiveStateChange { state : InteractiveState ; prevState : InteractiveState ; } const initialState = { hover : false , active : false , focus : false , } ; const eventMap = { mouseover : 'onMouseOver' , mouseleave : 'onMouseLeave' , mousedown : 'onMouseDown' , mouseup : 'onMouseUp' , pointerover : 'onPointerOver' , pointerleave : 'onPointerLeave' , pointerdown : 'onPointerDown' , pointerup : 'onPointerUp' , pointercancel : 'onPointerCancel' , touchstart : 'onTouchStart' , touchend : 'onTouchEnd' , touchcancel : 'onTouchCancel' , keydown : 'onKeyDown' , keyup : 'onKeyUp' , focus : 'onFocus' , blur : 'onBlur' , dragstart : 'onDragStart' , dragend : 'onDragEnd' , } ; const eventListenerPropNames = Object . values ( eventMap ) ; const defaultAs = 'button' ; export interface InteractiveOwnProps { children ? : React . ReactNode | ( ( state : InteractiveState ) => React . ReactNode ) ; onStateChange ? : ( { state , prevState } : InteractiveStateChange ) => void ; disabled ? : boolean ; useExtendedTouchActive ? : boolean ; hoverClassName ? : string ; activeClassName ? : string ; mouseActiveClassName ? : string ; touchActiveClassName ? : string ; keyActiveClassName ? : string ; focusClassName ? : string ; focusFromKeyClassName ? : string ; focusFromMouseClassName ? : string ; focusFromTouchClassName ? : string ; disabledClassName ? : string ; hoverStyle ? : React . CSSProperties ; activeStyle ? : React . CSSProperties ; mouseActiveStyle ? : React . CSSProperties ; touchActiveStyle ? : React . CSSProperties ; keyActiveStyle ? : React . CSSProperties ; focusStyle ? : React . CSSProperties ; focusFromKeyStyle ? : React . CSSProperties ; focusFromMouseStyle ? : React . CSSProperties ; focusFromTouchStyle ? : React . CSSProperties ; disabledStyle ? : React . CSSProperties ; } /**
 * Usage: `InteractiveProps<'button'>`, or `InteractiveProps<typeof Component>`
 *
 * Only use the `InteractiveProps` type when typing props that are directly passed to an `<Interactive>` component.
 * `InteractiveProps` includes the `as` prop and `ref` prop and should not be used for
 * typing components that wrap an `<Interactive>` component.
 *
 * For typing components that wrap an `<Interactive>` component use the type `InteractiveExtendableProps`
 *
 * For more see: https://github.com/rafgraph/react-interactive#using-with-typescript
 */ export type InteractiveProps < T extends React . ElementType = typeof defaultAs > = PolymorphicPropsWithRef < InteractiveOwnProps , T > ; /**
 * Usage: `InteractiveExtendableProps<'button'>`, or `InteractiveExtendableProps<typeof Component>`
 *
 * Use the `InteractiveExtendableProps` type when typing components that wrap an `<Interactive>` component
 * where the props are passed through to the `<Interactive>` component.
 *
 * For more see: https://github.com/rafgraph/react-interactive#using-with-typescript
 */ export type InteractiveExtendableProps < T extends React . ElementType = typeof defaultAs > = Omit < InteractiveProps < T > , 'as' | 'ref' > ; const InteractiveNotMemoized = React . forwardRef ( function < T extends React . ElementType = typeof defaultAs > ( { as , children , onStateChange , disabled = false , useExtendedTouchActive = false , hoverClassName = 'hover' , activeClassName = 'active' , mouseActiveClassName = 'mouseActive' , touchActiveClassName = 'touchActive' , keyActiveClassName = 'keyActive' , focusClassName = 'focus' , focusFromKeyClassName = 'focusFromKey' , focusFromMouseClassName = 'focusFromMouse' , focusFromTouchClassName = 'focusFromTouch' , disabledClassName = 'disabled' , hoverStyle , activeStyle , mouseActiveStyle , touchActiveStyle , keyActiveStyle , focusStyle , focusFromKeyStyle , focusFromMouseStyle , focusFromTouchStyle , disabledStyle , ... restProps } , ref ) { const As = as || defaultAs ; const [ iState , setIState ] = React . useState < InteractiveStateChange > ( { state : initialState , prevState : initialState , } ) ; const inTouchActiveState = iState . state . active === 'touchActive' ; React . useEffect ( ( ) => { if ( onStateChange ) { onStateChange ( iState ) ; } } , [ iState . state . hover , iState . state . active , iState . state . focus ] ) ; const disabledAndAsComponent = React . useRef < boolean > ( false ) ; disabledAndAsComponent . current = disabled && typeof As !== 'string' ; const [ , forceUpdateDisabledComponent ] = React . useState ( false ) ; const localRef = React . useRef < Element | null > ( null ) ; const callbackRef = React . useCallback ( ( node ) => { localRef . current = node ; if ( typeof ref === 'function' ) { ref ( node ) ; } else if ( ref ) { ref . current = node ; } if ( disabledAndAsComponent . current ) { forceUpdateDisabledComponent ( ( s ) => ! s ) ; } } , [ ref ] ) ; const keyTracking = React . useRef < { enterKeyDown : boolean ; spaceKeyDown : boolean ; } > ( { enterKeyDown : false , spaceKeyDown : false } ) ; const dragTracking = React . useRef < { isDragging : boolean ; } > ( { isDragging : false } ) ; interface HoverStateChange { iStateKey : 'hover' ; state : boolean ; action : 'enter' | 'exit' ; } interface ActiveStateChange { iStateKey : 'active' ; state : ActiveState ; action : 'enter' | 'exit' ; } interface FocusStateChange { iStateKey : 'focus' ; state : FocusState ; action : 'enter' | 'exit' ; } type StateChangeFunction = ( ... changes : ( HoverStateChange | ActiveStateChange | FocusStateChange ) [ ] ) => void ; const stateChange = React . useCallback ( ( ... changes ) => { setIState ( ( previous ) => { const newState = { ... previous . state } ; changes . forEach ( ( { iStateKey , state , action } ) => { if ( action === 'enter' ) { newState [ iStateKey ] = state as any ; } else if ( action === 'exit' && previous . state [ iStateKey ] === state && ( ! dragTracking . current . isDragging || iStateKey !== 'active' ) ) { newState [ iStateKey ] = false ; } } ) ; const newInteractiveStateChange = { state : newState , prevState : previous . state , } ; return stateChanged ( newInteractiveStateChange ) ? newInteractiveStateChange : previous ; } ) ; } , [ ] ) ; const blurInteractive = React . useCallback ( ( ) => { keyTracking . current . enterKeyDown = false ; keyTracking . current . spaceKeyDown = false ; stateChange ( { iStateKey : 'focus' , state : false , action : 'enter' , } , { iStateKey : 'active' , state : 'keyActive' , action : 'exit' , } ) ; } , [ stateChange ] ) ; React . useEffect ( ( ) => { if ( disabled && iState . state . focus && ( elementSupportsDisabled ( localRef . current || { } ) || typeof As === 'string' ) ) { if ( localRef . current && typeof ( localRef . current as HTMLElement ) . blur === 'function' ) { ( localRef . current as HTMLElement ) . blur ( ) ; } blurInteractive ( ) ; } } , [ disabled , iState . state . focus , blurInteractive , As ] ) ; const handleEvent = React . useCallback ( ( e ) => { switch ( e . type ) { case 'focus' : if ( e . target === localRef . current ) { stateChange ( { iStateKey : 'focus' , state : `focusFrom${ eventFrom ( e ) . replace ( /^\w/ , ( c ) => c . toUpperCase ( ) ) }` as FocusState , action : 'enter' , } ) ; } break ; case 'blur' : blurInteractive ( ) ; break ; case 'keydown' : case 'keyup' : if ( ( e as React . KeyboardEvent ) . key === ' ' ) { keyTracking . current . spaceKeyDown = e . type === 'keydown' ; } else if ( ( e as React . KeyboardEvent ) . key === 'Enter' ) { keyTracking . current . enterKeyDown = e . type === 'keydown' ; } else { break ; } stateChange ( { iStateKey : 'active' , state : 'keyActive' , action : ( keyTracking . current . enterKeyDown && enterKeyTrigger ( localRef . current || { } ) ) || ( keyTracking . current . spaceKeyDown && spaceKeyTrigger ( localRef . current || { } ) ) ? 'enter' : 'exit' , } ) ; break ; case 'dragstart' : dragTracking . current . isDragging = true ; stateChange ( { iStateKey : 'active' , state : `${ eventFrom ( e ) }Active` as ActiveState , action : 'enter' , } ) ; break ; case 'dragend' : dragTracking . current . isDragging = false ; stateChange ( { iStateKey : 'active' , state : false , action : 'enter' , } ) ; break ; default : switch ( eventFrom ( e ) ) { case 'mouse' : switch ( e . type ) { case 'mouseover' : case 'pointerover' : stateChange ( { iStateKey : 'hover' , state : true , action : 'enter' , } ) ; break ; case 'mouseleave' : case 'pointerleave' : case 'pointercancel' : stateChange ( { iStateKey : 'hover' , state : false , action : 'enter' , } , { iStateKey : 'active' , state : 'mouseActive' , action : 'exit' , } ) ; break ; case 'mousedown' : case 'pointerdown' : stateChange ( { iStateKey : 'active' , state : 'mouseActive' , action : 'enter' , } ) ; break ; case 'mouseup' : case 'pointerup' : stateChange ( { iStateKey : 'active' , state : 'mouseActive' , action : 'exit' , } ) ; break ; } break ; case 'touch' : switch ( e . type ) { case 'touchstart' : case 'pointerdown' : stateChange ( { iStateKey : 'active' , state : 'touchActive' , action : 'enter' , } ) ; break ; case 'touchend' : case 'touchcancel' : case 'pointerup' : case 'pointercancel' : case 'mouseover' : if ( ! useExtendedTouchActive || [ 'touchend' , 'touchcancel' ] . includes ( e . type ) ) { stateChange ( { iStateKey : 'active' , state : 'touchActive' , action : 'exit' , } ) ; } break ; case 'mouseleave' : stateChange ( { iStateKey : 'hover' , state : false , action : 'enter' , } , { iStateKey : 'active' , state : 'mouseActive' , action : 'exit' , } ) ; break ; } break ; } break ; } if ( restProps [ eventMap [ e . type ] as any ] ) { restProps [ eventMap [ e . type ] as any ] ( e ) ; } } , [ ... eventListenerPropNames . map ( ( listenerPropName ) => restProps [ listenerPropName as any ] ) , useExtendedTouchActive , stateChange , blurInteractive , ] ) ; const eventListeners = { } ; eventListenerPropNames . forEach ( ( listenerPropName ) => { eventListeners [ listenerPropName ] = handleEvent ; } ) ; const touchActiveTimeoutId = React . useRef < number | undefined > ( undefined ) ; React . useEffect ( ( ) => { if ( inTouchActiveState && ! useExtendedTouchActive ) { touchActiveTimeoutId . current = window . setTimeout ( ( ) => { stateChange ( { iStateKey : 'active' , state : 'touchActive' , action : 'exit' , } ) ; } , 750 ) ; return ( ) => window . clearTimeout ( touchActiveTimeoutId . current ) ; } return ; } , [ inTouchActiveState , useExtendedTouchActive , stateChange ] ) ; React . useEffect ( ( ) => { if ( inTouchActiveState && useExtendedTouchActive ) { setUserSelectNone ( ) ; return resetUserSelect ; } return ; } , [ inTouchActiveState , useExtendedTouchActive ] ) ; let className = classNameToString ( restProps . className ) ; const style = { } ; if ( ! disabled && cursorPointer ( localRef . current || { } , Boolean ( restProps . onClick || restProps . onClickCapture ) ) ) { style . cursor = 'pointer' ; } Object . assign ( style , restProps . style ) ; const addToClassAndStyleProps = ( addClassName , addStyle ? ) => { className = [ className , classNameToString ( addClassName ) ] . filter ( ( cN ) => cN ) . join ( ' ' ) ; Object . assign ( style , addStyle ) ; } ; if ( disabled ) { addToClassAndStyleProps ( disabledClassName , disabledStyle ) ; } else { if ( iState . state . hover ) { addToClassAndStyleProps ( hoverClassName , hoverStyle ) ; } if ( iState . state . active ) { addToClassAndStyleProps ( activeClassName , activeStyle ) ; switch ( iState . state . active ) { case 'mouseActive' : addToClassAndStyleProps ( mouseActiveClassName , mouseActiveStyle ) ; break ; case 'touchActive' : addToClassAndStyleProps ( touchActiveClassName , touchActiveStyle ) ; break ; case 'keyActive' : addToClassAndStyleProps ( keyActiveClassName , keyActiveStyle ) ; break ; } } if ( iState . state . focus ) { addToClassAndStyleProps ( focusClassName , focusStyle ) ; switch ( iState . state . focus ) { case 'focusFromMouse' : addToClassAndStyleProps ( focusFromMouseClassName , focusFromMouseStyle ) ; break ; case 'focusFromTouch' : addToClassAndStyleProps ( focusFromTouchClassName , focusFromTouchStyle ) ; break ; case 'focusFromKey' : addToClassAndStyleProps ( focusFromKeyClassName , focusFromKeyStyle ) ; break ; } } } const memoizedStyle = React . useMemo ( ( ) => style , [ Object . entries ( style ) . join ( ) ] ) ; let disabledProps = null ; if ( disabled ) { disabledProps = { onClick : undefined , onClickCapture : undefined , onDoubleClick : undefined , onDoubleClickCapture : undefined , tabIndex : undefined , href : undefined , } ; if ( elementSupportsDisabled ( typeof As === 'string' ? { tagName : ( As as string ) . toUpperCase ( ) } : localRef . current || { } ) ) { disabledProps . disabled = true ; } } return ( < As { ... restProps } { ... eventListeners } { ... disabledProps } className = { className === '' ? undefined : className } style = { memoizedStyle } ref = { callbackRef } > { typeof children === 'function' ? children ( iState . state ) : children } < /As>); } ) ; type InteractivePropsWithoutAs < T extends React . ElementType = typeof defaultAs > = Omit < InteractiveProps < T > , 'as' > ; interface PolymorphicInteractive extends PolymorphicMemoExoticComponent < InteractiveOwnProps , typeof defaultAs > { Button : React . ForwardRefExoticComponent < InteractivePropsWithoutAs < 'button' > > ; A : React . ForwardRefExoticComponent < InteractivePropsWithoutAs < 'a' > > ; Input : React . ForwardRefExoticComponent < InteractivePropsWithoutAs < 'input' > > ; Select : React . ForwardRefExoticComponent < InteractivePropsWithoutAs < 'select' > > ; Div : React . ForwardRefExoticComponent < InteractivePropsWithoutAs < 'div' > > ; Span : React . ForwardRefExoticComponent < InteractivePropsWithoutAs < 'span' > > ; } export const Interactive = Object . assign ( React . memo ( InteractiveNotMemoized ) , { Button : React . memo ( InteractiveNotMemoized ) , A : createInteractive ( 'a' ) , Input : createInteractive ( 'input' ) , Select : createInteractive ( 'select' ) , Div : createInteractive ( 'div' ) , Span : createInteractive ( 'span' ) , } ) ; export function createInteractive < T extends React . ElementType = typeof defaultAs > ( as ) { const WrappedInteractive = React . forwardRef ( function < T extends React . ElementType = typeof as > ( props , ref ) { return < Interactive { ... props } as = { as } ref = { ref } / > ; } ) as React . ForwardRefExoticComponent < InteractivePropsWithoutAs < T > > ; if ( process . env . NODE_ENV !== 'production' ) { WrappedInteractive . displayName = `createInteractive.${ typeof as === 'string' ? as : Object ( as ) . displayName }` ; } return WrappedInteractive ; } if ( process . env . NODE_ENV !== 'production' ) { Interactive . displayName = 'Interactive' ; }