import * as React from 'react' ; import elementClosestPolyfill from 'element-closest' ; export interface InputProviderProps { provideIntent ? : boolean ; container ? : HTMLElement | React . RefObject < HTMLElement > ; } export enum InputType { Initial = 'initial' , Keyboard = 'keyboard' , Mouse = 'mouse' , Pointer = 'pointer' , Touch = 'touch' } export enum InputEventType { KeyDown = 'keydown' , KeyUp = 'keyup' , MouseDown = 'mousedown' , MouseMove = 'mousemove' , Wheel = 'wheel' , MouseWheel = 'mousewheel' , PointerDown = 'pointerdown' , PointerMove = 'pointermove' , TouchStart = 'touchstart' } type InputEvent = Event | React . KeyboardEvent | React . MouseEvent | React . TouchEvent | React . PointerEvent ; const formInputs = [ 'input' , 'select' , 'textarea' ] ; const ignoreKeys = [ 'Shift' , 'Control' , 'Alt' , 'Meta' , 'OS' ] ; export const inputEventMap = { [ InputEventType . KeyDown ] : InputType . Keyboard , [ InputEventType . KeyUp ] : InputType . Keyboard , [ InputEventType . MouseDown ] : InputType . Mouse , [ InputEventType . MouseMove ] : InputType . Mouse , [ InputEventType . Wheel ] : InputType . Mouse , [ InputEventType . MouseWheel ] : InputType . Mouse , [ InputEventType . PointerDown ] : InputType . Pointer , [ InputEventType . PointerMove ] : InputType . Pointer , [ InputEventType . TouchStart ] : InputType . Touch , } ; const pointerMap = { 2 : InputType . Touch , 3 : InputType . Touch , 4 : InputType . Mouse , } ; const getPointerType = ( event ) : InputType => { if ( typeof event . pointerType === 'number' ) { return pointerMap [ event . pointerType ] ; } else if ( event . pointerType === 'mouse' ) { return InputType . Mouse ; } else { return InputType . Touch ; } } ; const detectWheel = ( ) => { let wheelType ; if ( 'onwheel' in document . createElement ( 'div' ) ) { wheelType = InputEventType . Wheel ; } else { wheelType = InputEventType . MouseWheel ; } return wheelType ; } ; const supportsPassive = ( ) => { let supportsPassive ; try { const opts = Object . defineProperty ( { } , 'passive' , { get : ( ) => { supportsPassive = true ; } , } ) ; const stub = ( ) => { return ; } ; window . addEventListener ( 'test' , stub , opts ) ; window . removeEventListener ( 'test' , stub , opts ) ; } catch ( e ) { console . warn ( 'Browser does not support passive event listeners' ) ; } return supportsPassive || false ; } ; /**
 * This component takes heavy inspiration from what-input (https://github.com/ten1seven/what-input)
 */ export class InputProvider extends React . Component < InputProviderProps > { private eventTimer : number | undefined ; private currentInput : InputType ; private currentIntent : InputType ; private supportsPassive ! : boolean ; private isBuffering = false ; private isScrolling = false ; private mousePosX : number | null = null ; private mousePosY : number | null = null ; private deferInputTracking = false ; constructor ( props ) { super ( props ) ; let storedInput , storedIntent ; try { storedInput = window . sessionStorage . getItem ( 'what-input' ) as InputType ; storedIntent = window . sessionStorage . getItem ( 'what-intent' ) as InputType ; } catch ( e ) { console . warn ( 'Failed to retrieve input status from session storage' + e ) ; } this . currentInput = storedInput || InputType . Initial ; this . currentIntent = storedIntent || InputType . Initial ; this . setInput = this . setInput . bind ( this ) ; this . setIntent = this . setIntent . bind ( this ) ; this . eventBuffer = this . eventBuffer . bind ( this ) ; } provideIntent = this . props . provideIntent ; getContainer ( container ? ) { if ( ! container ) { return document . body ; } if ( 'current' in container ) { if ( container . current === null ) { console . warn ( 'Your ref object can not be null, therefore, falling back to document.body' ) ; return document . body ; } else { return container . current ; } } return container ; } componentDidMount ( ) { elementClosestPolyfill ( window ) ; this . supportsPassive = supportsPassive ( ) ; if ( this . getContainer ( this . props . container ) . closest ( '[data-whatinput]' ) ) { this . deferInputTracking = true ; return ; } this . updateAttributes ( ) ; this . enableListeners ( true ) ; } updateAttributes ( ) { const intent = this . provideIntent ? this . currentIntent : null ; this . getContainer ( this . props . container ) . setAttribute ( 'data-whatinput' , this . currentInput ) ; if ( intent ) { this . getContainer ( this . props . container ) . setAttribute ( 'data-whatintent' , intent ) ; } try { window . sessionStorage . setItem ( 'what-input' , this . currentInput ) ; window . sessionStorage . setItem ( 'what-intent' , this . currentIntent ) ; } catch ( e ) { console . warn ( 'Failed to set input status in session storage' + e ) ; } } componentWillUnmount ( ) { if ( this . deferInputTracking ) { return ; } this . getContainer ( this . props . container ) . removeAttribute ( 'data-whatinput' ) ; this . getContainer ( this . props . container ) . removeAttribute ( 'data-whatintent' ) ; window . clearTimeout ( this . eventTimer ) ; this . enableListeners ( false ) ; } enableListeners ( enable ) { if ( typeof window === 'undefined' ) { return ; } const options = this . supportsPassive ? ( { passive : true } as AddEventListenerOptions ) : false ; const fn = enable ? window . addEventListener : window . removeEventListener ; if ( window . PointerEvent ) { fn ( 'pointerdown' , this . setInput ) ; } else { fn ( 'mousedown' , this . setInput ) ; if ( 'ontouchstart' in window ) { fn ( 'touchstart' , this . eventBuffer , options ) ; fn ( 'touchend' , this . setInput ) ; } } if ( this . provideIntent ) { if ( window . PointerEvent ) { fn ( 'pointermove' , this . setIntent ) ; } else { fn ( 'mousemove' , this . setIntent ) ; } fn ( detectWheel ( ) , this . setIntent , options ) ; } fn ( 'keydown' , this . eventBuffer ) ; fn ( 'keyup' , this . eventBuffer ) ; } setInput ( event ) { if ( this . isBuffering ) { return ; } const eventKey = 'key' in event ? event . key : undefined ; const eventType = event . type as keyof typeof inputEventMap ; let value = inputEventMap [ eventType ] ; if ( value === InputType . Pointer ) { value = getPointerType ( event as React . PointerEvent ) ; } const ignoreMatch = eventKey ? ignoreKeys . indexOf ( eventKey ) === - 1 : undefined ; const shouldUpdate = ( value === InputType . Keyboard && eventKey && ignoreMatch ) || value === InputType . Mouse || value === InputType . Touch ; if ( this . currentInput !== value && shouldUpdate ) { this . currentInput = value ; this . updateAttributes ( ) ; } if ( this . currentIntent !== value && shouldUpdate && this . provideIntent ) { const activeElem = document . activeElement ; const notFormInput = activeElem && activeElem . nodeName && formInputs . indexOf ( activeElem . nodeName . toLowerCase ( ) ) === - 1 ; if ( notFormInput ) { this . currentIntent = value ; this . updateAttributes ( ) ; } } } setIntent ( event ) { this . detectScrolling ( event as React . MouseEvent ) ; if ( ! this . isBuffering && ! this . isScrolling ) { const eventType = event . type as keyof typeof inputEventMap ; let value = inputEventMap [ eventType ] ; if ( value === InputType . Pointer ) { value = getPointerType ( event as React . PointerEvent ) ; } this . currentIntent = value ; this . updateAttributes ( ) ; } } eventBuffer ( event ) { this . setInput ( event ) ; window . clearTimeout ( this . eventTimer ) ; this . isBuffering = true ; this . eventTimer = window . setTimeout ( ( ) => { this . isBuffering = false ; } , 100 ) ; } detectScrolling ( event ) { if ( this . mousePosX !== event . screenX || this . mousePosY !== event . screenY ) { this . isScrolling = false ; this . mousePosX = event . screenX ; this . mousePosY = event . screenY ; } else { this . isScrolling = true ; } } render ( ) { return this . props . children || null ; } } export default InputProvider ;