From 70411dc8a33aeebceb98f48d126693d3360ad4e5 Mon Sep 17 00:00:00 2001 From: Xandor Schiefer Date: Thu, 8 Feb 2024 16:27:12 +0200 Subject: [PATCH] React to ref changes - Use a ref callback - Re-run the effect when the ref changes - Updated docs - Fix: TS doesn't actually infer the type of the ref automatically, this was already the case regardless of whether the ref is a ref object or a ref callback --- .../docs/documentation/typescript.mdx | 5 +-- .../useHotkeys/scoping-hotkeys.mdx | 7 +-- src/useHotkeys.ts | 18 ++++---- tests/useHotkeys.test.tsx | 44 ++++++++++++++++++- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/documentation/docs/documentation/typescript.mdx b/documentation/docs/documentation/typescript.mdx index 43f43aa4..226774f5 100644 --- a/documentation/docs/documentation/typescript.mdx +++ b/documentation/docs/documentation/typescript.mdx @@ -37,7 +37,7 @@ const MyComponent = ({hotKey}: Props) => { *** -Using TypeScript to infer the type of the given ref. +Using TypeScript with refs. ```tsx import { useHotkeys } from "react-hotkeys-hook"; @@ -50,8 +50,7 @@ interface Props { const MyComponent = ({hotKey}: Props) => { const [count, setCount] = useState(0); - // ref will have the type of React.MutableRef - const ref = useHotkeys(hotKey, () => setCount(prevCount => prevCount + 1)); + const ref = useHotkeys(hotKey, () => setCount(prevCount => prevCount + 1)); return (
diff --git a/documentation/docs/documentation/useHotkeys/scoping-hotkeys.mdx b/documentation/docs/documentation/useHotkeys/scoping-hotkeys.mdx index 9367a113..8997b779 100644 --- a/documentation/docs/documentation/useHotkeys/scoping-hotkeys.mdx +++ b/documentation/docs/documentation/useHotkeys/scoping-hotkeys.mdx @@ -40,9 +40,10 @@ render( ``` Everytime we press down the `c` key, both component trigger the callback. But how can we separate those two components -and their assigned hotkeys? The answer is [`Refs`](https://reactjs.org/docs/refs-and-the-dom.html). `useHotkeys` returns -a mutable React reference that we can attach to any component that takes a ref. This way we -can tell the hook which element should receive the users focus before it triggers the callback. +and their assigned hotkeys? The answer is [`Refs`](https://react.dev/learn/manipulating-the-dom-with-refs). `useHotkeys` +returns a [React ref callback function](https://react.dev/reference/react-dom/components/common#ref-callback) that we +can attach to any component that takes a ref. This way we can tell the hook which element should receive the users focus +before it triggers its callback. ```jsx live noInline function ScopedHotkey() { diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index 84197d56..a6d9600c 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -1,5 +1,5 @@ import { HotkeyCallback, Keys, Options, OptionsOrDependencyArray, RefType } from './types' -import { DependencyList, useCallback, useEffect, useLayoutEffect, useRef } from 'react' +import { DependencyList, RefCallback, useCallback, useEffect, useState, useLayoutEffect, useRef } from 'react' import { mapKey, parseHotkey, parseKeysHookInput } from './parseHotkeys' import { isHotkeyEnabled, @@ -28,7 +28,7 @@ export default function useHotkeys( options?: OptionsOrDependencyArray, dependencies?: OptionsOrDependencyArray ) { - const ref = useRef>(null) + const [ref, setRef] = useState>(null) const hasTriggeredRef = useRef(false) const _options: Options | undefined = !(options instanceof Array) @@ -66,12 +66,12 @@ export default function useHotkeys( // TODO: SINCE THE EVENT IS NOW ATTACHED TO THE REF, THE ACTIVE ELEMENT CAN NEVER BE INSIDE THE REF. THE HOTKEY ONLY TRIGGERS IF THE // REF IS THE ACTIVE ELEMENT. THIS IS A PROBLEM SINCE FOCUSED SUB COMPONENTS WON'T TRIGGER THE HOTKEY. - if (ref.current !== null) { - const rootNode = ref.current.getRootNode() + if (ref !== null) { + const rootNode = ref.getRootNode() if ( (rootNode instanceof Document || rootNode instanceof ShadowRoot) && - rootNode.activeElement !== ref.current && - !ref.current.contains(rootNode.activeElement) + rootNode.activeElement !== ref && + !ref.contains(rootNode.activeElement) ) { stopPropagation(e) return @@ -140,7 +140,7 @@ export default function useHotkeys( } } - const domNode = ref.current || _options?.document || document + const domNode = ref || _options?.document || document // @ts-ignore domNode.addEventListener('keyup', handleKeyUp) @@ -165,7 +165,7 @@ export default function useHotkeys( ) } } - }, [_keys, memoisedOptions, enabledScopes]) + }, [ref, _keys, memoisedOptions, enabledScopes]) - return ref + return setRef as RefCallback } diff --git a/tests/useHotkeys.test.tsx b/tests/useHotkeys.test.tsx index 22220106..c6f67894 100644 --- a/tests/useHotkeys.test.tsx +++ b/tests/useHotkeys.test.tsx @@ -372,7 +372,7 @@ test('should reflect set splitKey character', async () => { const user = userEvent.setup() const callback = jest.fn() - const { rerender } = renderHook, HookParameters>( + const { rerender } = renderHook, HookParameters>( ({ keys, options }) => useHotkeys(keys, callback, options), { initialProps: { keys: 'a, b', options: undefined }, @@ -774,6 +774,48 @@ test('should only trigger when the element is focused if a ref is set', async () expect(callback).toHaveBeenCalledTimes(1) }) +test('should trigger when the ref is re-attached to another element', async () => { + const user = userEvent.setup() + const callback = jest.fn() + + const Component = ({ cb }: { cb: HotkeyCallback }) => { + const ref = useHotkeys('a', cb) + const [toggle, setToggle] = useState(false) + + if (toggle) { + return ( + + + + + ) + } + + return ( +
+ + +
+ ) + } + + const { getByTestId } = render() + + await user.keyboard('A') + + expect(callback).not.toHaveBeenCalled() + + await user.click(getByTestId('toggle')) + await user.click(getByTestId('div')) + await user.keyboard('A') + + expect(callback).toHaveBeenCalledTimes(1) +}) + test.skip('should preventDefault and stop propagation when ref is not focused', async () => { const callback = jest.fn()