import React , { useRef , useMemo , useEffect } from "react" ; import { useEventCallback } from "../../hooks/useEventCallback" ; import { clamp } from "../../utils/clamp" ; export interface Interaction { left : number ; top : number ; } const isTouch = ( event ) : event is TouchEvent => "touches" in event ; const getTouchPoint = ( touches , touchId ) : Touch => { for ( let i = 0 ; i < touches . length ; i ++ ) { if ( touches [ i ] . identifier === touchId ) return touches [ i ] ; } return touches [ 0 ] ; } ; const getParentWindow = ( node ? ) : Window => { return ( node && node . ownerDocument . defaultView ) || self ; } ; const getRelativePosition = ( node , event , touchId ) : Interaction => { const rect = node . getBoundingClientRect ( ) ; const pointer = isTouch ( event ) ? getTouchPoint ( event . touches , touchId ) : ( event as MouseEvent ) ; return { left : clamp ( ( pointer . pageX - ( rect . left + getParentWindow ( node ) . pageXOffset ) ) / rect . width ) , top : clamp ( ( pointer . pageY - ( rect . top + getParentWindow ( node ) . pageYOffset ) ) / rect . height ) , } ; } ; const preventDefaultMove = ( event ) : void => { ! isTouch ( event ) && event . preventDefault ( ) ; } ; const isInvalid = ( event , hasTouch ) : boolean => { return hasTouch && ! isTouch ( event ) ; } ; interface Props { onMove : ( interaction : Interaction ) => void ; onKey : ( offset : Interaction ) => void ; children : React . ReactNode ; } const InteractiveBase = ( { onMove , onKey , ... rest } ) => { const container = useRef < HTMLDivElement > ( null ) ; const onMoveCallback = useEventCallback < Interaction > ( onMove ) ; const onKeyCallback = useEventCallback < Interaction > ( onKey ) ; const touchId = useRef < null | number > ( null ) ; const hasTouch = useRef ( false ) ; const [ handleMoveStart , handleKeyDown , toggleDocumentEvents ] = useMemo ( ( ) => { const handleMoveStart = ( { nativeEvent } ) => { const el = container . current ; if ( ! el ) return ; preventDefaultMove ( nativeEvent ) ; if ( isInvalid ( nativeEvent , hasTouch . current ) || ! el ) return ; if ( isTouch ( nativeEvent ) ) { hasTouch . current = true ; const changedTouches = nativeEvent . changedTouches || [ ] ; if ( changedTouches . length ) touchId . current = changedTouches [ 0 ] . identifier ; } el . focus ( ) ; onMoveCallback ( getRelativePosition ( el , nativeEvent , touchId . current ) ) ; toggleDocumentEvents ( true ) ; } ; const handleMove = ( event ) => { preventDefaultMove ( event ) ; const isDown = isTouch ( event ) ? event . touches . length > 0 : event . buttons > 0 ; if ( isDown && container . current ) { onMoveCallback ( getRelativePosition ( container . current , event , touchId . current ) ) ; } else { toggleDocumentEvents ( false ) ; } } ; const handleMoveEnd = ( ) => toggleDocumentEvents ( false ) ; const handleKeyDown = ( event ) => { const keyCode = event . which || event . keyCode ; if ( keyCode < 37 || keyCode > 40 ) return ; event . preventDefault ( ) ; onKeyCallback ( { left : keyCode === 39 ? 0.05 : keyCode === 37 ? - 0.05 : 0 , top : keyCode === 40 ? 0.05 : keyCode === 38 ? - 0.05 : 0 , } ) ; } ; function toggleDocumentEvents ( state ? ) { const touch = hasTouch . current ; const el = container . current ; const parentWindow = getParentWindow ( el ) ; const toggleEvent = state ? parentWindow . addEventListener : parentWindow . removeEventListener ; toggleEvent ( touch ? "touchmove" : "mousemove" , handleMove ) ; toggleEvent ( touch ? "touchend" : "mouseup" , handleMoveEnd ) ; } return [ handleMoveStart , handleKeyDown , toggleDocumentEvents ] ; } , [ onKeyCallback , onMoveCallback ] ) ; useEffect ( ( ) => toggleDocumentEvents , [ toggleDocumentEvents ] ) ; return ( < div { ... rest } onTouchStart = { handleMoveStart } onMouseDown = { handleMoveStart } className = "react-colorful__interactive" ref = { container } onKeyDown = { handleKeyDown } tabIndex = { 0 } role = "slider" / > ) ; } ; export const Interactive = React . memo ( InteractiveBase ) ;