UNPKG

@gechiui/block-editor

Version:
173 lines (144 loc) 3.88 kB
/** * External dependencies */ import { noop } from 'lodash'; /** * GeChiUI dependencies */ import { useState, useRef, useEffect } from '@gechiui/element'; const { clearTimeout, setTimeout } = window; const DEBOUNCE_TIMEOUT = 200; /** * Hook that creates a showMover state, as well as debounced show/hide callbacks. * * @param {Object} props Component props. * @param {Object} props.ref Element reference. * @param {boolean} props.isFocused Whether the component has current focus. * @param {number} [props.debounceTimeout=250] Debounce timeout in milliseconds. * @param {Function} [props.onChange=noop] Callback function. */ export function useDebouncedShowMovers( { ref, isFocused, debounceTimeout = DEBOUNCE_TIMEOUT, onChange = noop, } ) { const [ showMovers, setShowMovers ] = useState( false ); const timeoutRef = useRef(); const handleOnChange = ( nextIsFocused ) => { if ( ref?.current ) { setShowMovers( nextIsFocused ); } onChange( nextIsFocused ); }; const getIsHovered = () => { return ref?.current && ref.current.matches( ':hover' ); }; const shouldHideMovers = () => { const isHovered = getIsHovered(); return ! isFocused && ! isHovered; }; const clearTimeoutRef = () => { const timeout = timeoutRef.current; if ( timeout && clearTimeout ) { clearTimeout( timeout ); } }; const debouncedShowMovers = ( event ) => { if ( event ) { event.stopPropagation(); } clearTimeoutRef(); if ( ! showMovers ) { handleOnChange( true ); } }; const debouncedHideMovers = ( event ) => { if ( event ) { event.stopPropagation(); } clearTimeoutRef(); timeoutRef.current = setTimeout( () => { if ( shouldHideMovers() ) { handleOnChange( false ); } }, debounceTimeout ); }; useEffect( () => () => clearTimeoutRef(), [] ); return { showMovers, debouncedShowMovers, debouncedHideMovers, }; } /** * Hook that provides a showMovers state and gesture events for DOM elements * that interact with the showMovers state. * * @param {Object} props Component props. * @param {Object} props.ref Element reference. * @param {number} [props.debounceTimeout=250] Debounce timeout in milliseconds. * @param {Function} [props.onChange=noop] Callback function. */ export function useShowMoversGestures( { ref, debounceTimeout = DEBOUNCE_TIMEOUT, onChange = noop, } ) { const [ isFocused, setIsFocused ] = useState( false ); const { showMovers, debouncedShowMovers, debouncedHideMovers, } = useDebouncedShowMovers( { ref, debounceTimeout, isFocused, onChange } ); const registerRef = useRef( false ); const isFocusedWithin = () => { return ( ref?.current && ref.current.contains( ref.current.ownerDocument.activeElement ) ); }; useEffect( () => { const node = ref.current; const handleOnFocus = () => { if ( isFocusedWithin() ) { setIsFocused( true ); debouncedShowMovers(); } }; const handleOnBlur = () => { if ( ! isFocusedWithin() ) { setIsFocused( false ); debouncedHideMovers(); } }; /** * Events are added via DOM events (vs. React synthetic events), * as the child React components swallow mouse events. */ if ( node && ! registerRef.current ) { node.addEventListener( 'focus', handleOnFocus, true ); node.addEventListener( 'blur', handleOnBlur, true ); registerRef.current = true; } return () => { if ( node ) { node.removeEventListener( 'focus', handleOnFocus ); node.removeEventListener( 'blur', handleOnBlur ); } }; }, [ ref, registerRef, setIsFocused, debouncedShowMovers, debouncedHideMovers, ] ); return { showMovers, gestures: { onMouseMove: debouncedShowMovers, onMouseLeave: debouncedHideMovers, }, }; }