UNPKG

@gechiui/compose

Version:
121 lines (110 loc) 4.02 kB
/** * External dependencies */ import Mousetrap from 'mousetrap'; import 'mousetrap/plugins/global-bind/mousetrap-global-bind'; import { includes, castArray } from 'lodash'; /** * GeChiUI dependencies */ import { useEffect, useRef } from '@gechiui/element'; /** * A block selection object. * * @typedef {Object} GCKeyboardShortcutConfig * * @property {boolean} [bindGlobal] Handle keyboard events anywhere including inside textarea/input fields. * @property {string} [eventName] Event name used to trigger the handler, defaults to keydown. * @property {boolean} [isDisabled] Disables the keyboard handler if the value is true. * @property {import('react').RefObject<HTMLElement>} [target] React reference to the DOM element used to catch the keyboard event. */ /** * Return true if platform is MacOS. * * @param {Window} [_window] window object by default; used for DI testing. * * @return {boolean} True if MacOS; false otherwise. */ function isAppleOS( _window = window ) { const { platform } = _window.navigator; return ( platform.indexOf( 'Mac' ) !== -1 || includes( [ 'iPad', 'iPhone' ], platform ) ); } /* eslint-disable jsdoc/valid-types */ /** * Attach a keyboard shortcut handler. * * @see https://craig.is/killing/mice#api.bind for information about the `callback` parameter. * * @param {string[]|string} shortcuts Keyboard Shortcuts. * @param {(e: import('mousetrap').ExtendedKeyboardEvent, combo: string) => void} callback Shortcut callback. * @param {GCKeyboardShortcutConfig} options Shortcut options. */ function useKeyboardShortcut( /* eslint-enable jsdoc/valid-types */ shortcuts, callback, { bindGlobal = false, eventName = 'keydown', isDisabled = false, // This is important for performance considerations. target, } = {} ) { const currentCallback = useRef( callback ); useEffect( () => { currentCallback.current = callback; }, [ callback ] ); useEffect( () => { if ( isDisabled ) { return; } const mousetrap = new Mousetrap( target && target.current ? target.current : // We were passing `document` here previously, so to successfully cast it to Element we must cast it first to `unknown`. // Not sure if this is a mistake but it was the behavior previous to the addition of types so we're just doing what's // necessary to maintain the existing behavior /** @type {Element} */ (/** @type {unknown} */ ( document )) ); castArray( shortcuts ).forEach( ( shortcut ) => { const keys = shortcut.split( '+' ); // Determines whether a key is a modifier by the length of the string. // E.g. if I add a pass a shortcut Shift+Cmd+M, it'll determine that // the modifiers are Shift and Cmd because they're not a single character. const modifiers = new Set( keys.filter( ( value ) => value.length > 1 ) ); const hasAlt = modifiers.has( 'alt' ); const hasShift = modifiers.has( 'shift' ); // This should be better moved to the shortcut registration instead. if ( isAppleOS() && ( ( modifiers.size === 1 && hasAlt ) || ( modifiers.size === 2 && hasAlt && hasShift ) ) ) { throw new Error( `Cannot bind ${ shortcut }. Alt and Shift+Alt modifiers are reserved for character input.` ); } const bindFn = bindGlobal ? 'bindGlobal' : 'bind'; // @ts-ignore `bindGlobal` is an undocumented property mousetrap[ bindFn ]( shortcut, ( /* eslint-disable jsdoc/valid-types */ /** @type {[e: import('mousetrap').ExtendedKeyboardEvent, combo: string]} */ ...args ) => /* eslint-enable jsdoc/valid-types */ currentCallback.current( ...args ), eventName ); } ); return () => { mousetrap.reset(); }; }, [ shortcuts, bindGlobal, eventName, target, isDisabled ] ); } export default useKeyboardShortcut;