Skip to content

Commit 88264ea

Browse files
Merge pull request #1132 from zeorin/fix/callback-ref
React to ref changes
2 parents e921499 + 70411dc commit 88264ea

File tree

4 files changed

+58
-16
lines changed

4 files changed

+58
-16
lines changed

documentation/docs/documentation/typescript.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const MyComponent = ({hotKey}: Props) => {
3737

3838
***
3939

40-
Using TypeScript to infer the type of the given ref.
40+
Using TypeScript with refs.
4141

4242
```tsx
4343
import { useHotkeys } from "react-hotkeys-hook";
@@ -50,8 +50,7 @@ interface Props {
5050
const MyComponent = ({hotKey}: Props) => {
5151
const [count, setCount] = useState(0);
5252

53-
// ref will have the type of React.MutableRef<HTMLDivElement>
54-
const ref = useHotkeys(hotKey, () => setCount(prevCount => prevCount + 1));
53+
const ref = useHotkeys<HTMLDivElement>(hotKey, () => setCount(prevCount => prevCount + 1));
5554

5655
return (
5756
<div ref={ref}>

documentation/docs/documentation/useHotkeys/scoping-hotkeys.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ render(
4040
```
4141

4242
Everytime we press down the `c` key, both component trigger the callback. But how can we separate those two components
43-
and their assigned hotkeys? The answer is [`Refs`](https://reactjs.org/docs/refs-and-the-dom.html). `useHotkeys` returns
44-
a mutable React reference that we can attach to any component that takes a ref. This way we
45-
can tell the hook which element should receive the users focus before it triggers the callback.
43+
and their assigned hotkeys? The answer is [`Refs`](https://react.dev/learn/manipulating-the-dom-with-refs). `useHotkeys`
44+
returns a [React ref callback function](https://react.dev/reference/react-dom/components/common#ref-callback) that we
45+
can attach to any component that takes a ref. This way we can tell the hook which element should receive the users focus
46+
before it triggers its callback.
4647

4748
```jsx live noInline
4849
function ScopedHotkey() {

src/useHotkeys.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { HotkeyCallback, Keys, Options, OptionsOrDependencyArray, RefType } from './types'
2-
import { DependencyList, useCallback, useEffect, useLayoutEffect, useRef } from 'react'
2+
import { DependencyList, RefCallback, useCallback, useEffect, useState, useLayoutEffect, useRef } from 'react'
33
import { mapKey, parseHotkey, parseKeysHookInput } from './parseHotkeys'
44
import {
55
isHotkeyEnabled,
@@ -28,7 +28,7 @@ export default function useHotkeys<T extends HTMLElement>(
2828
options?: OptionsOrDependencyArray,
2929
dependencies?: OptionsOrDependencyArray
3030
) {
31-
const ref = useRef<RefType<T>>(null)
31+
const [ref, setRef] = useState<RefType<T>>(null)
3232
const hasTriggeredRef = useRef(false)
3333

3434
const _options: Options | undefined = !(options instanceof Array)
@@ -66,12 +66,12 @@ export default function useHotkeys<T extends HTMLElement>(
6666

6767
// 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
6868
// REF IS THE ACTIVE ELEMENT. THIS IS A PROBLEM SINCE FOCUSED SUB COMPONENTS WON'T TRIGGER THE HOTKEY.
69-
if (ref.current !== null) {
70-
const rootNode = ref.current.getRootNode()
69+
if (ref !== null) {
70+
const rootNode = ref.getRootNode()
7171
if (
7272
(rootNode instanceof Document || rootNode instanceof ShadowRoot) &&
73-
rootNode.activeElement !== ref.current &&
74-
!ref.current.contains(rootNode.activeElement)
73+
rootNode.activeElement !== ref &&
74+
!ref.contains(rootNode.activeElement)
7575
) {
7676
stopPropagation(e)
7777
return
@@ -140,7 +140,7 @@ export default function useHotkeys<T extends HTMLElement>(
140140
}
141141
}
142142

143-
const domNode = ref.current || _options?.document || document
143+
const domNode = ref || _options?.document || document
144144

145145
// @ts-ignore
146146
domNode.addEventListener('keyup', handleKeyUp)
@@ -165,7 +165,7 @@ export default function useHotkeys<T extends HTMLElement>(
165165
)
166166
}
167167
}
168-
}, [_keys, memoisedOptions, enabledScopes])
168+
}, [ref, _keys, memoisedOptions, enabledScopes])
169169

170-
return ref
170+
return setRef as RefCallback<T>
171171
}

tests/useHotkeys.test.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ test('should reflect set splitKey character', async () => {
372372
const user = userEvent.setup()
373373
const callback = jest.fn()
374374

375-
const { rerender } = renderHook<MutableRefObject<HTMLElement | null>, HookParameters>(
375+
const { rerender } = renderHook<RefCallback<HTMLElement>, HookParameters>(
376376
({ keys, options }) => useHotkeys(keys, callback, options),
377377
{
378378
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 ()
774774
expect(callback).toHaveBeenCalledTimes(1)
775775
})
776776

777+
test('should trigger when the ref is re-attached to another element', async () => {
778+
const user = userEvent.setup()
779+
const callback = jest.fn()
780+
781+
const Component = ({ cb }: { cb: HotkeyCallback }) => {
782+
const ref = useHotkeys<HTMLDivElement>('a', cb)
783+
const [toggle, setToggle] = useState(false)
784+
785+
if (toggle) {
786+
return (
787+
<span ref={ref} tabIndex={0} data-testid={'div'}>
788+
<button data-testid={'toggle'} onClick={() => setToggle((t) => !t)}>
789+
Toggle
790+
</button>
791+
<input type={'text'} />
792+
</span>
793+
)
794+
}
795+
796+
return (
797+
<div ref={ref} tabIndex={0} data-testid={'div'}>
798+
<button data-testid={'toggle'} onClick={() => setToggle((t) => !t)}>
799+
Toggle
800+
</button>
801+
<input type={'text'} />
802+
</div>
803+
)
804+
}
805+
806+
const { getByTestId } = render(<Component cb={callback} />)
807+
808+
await user.keyboard('A')
809+
810+
expect(callback).not.toHaveBeenCalled()
811+
812+
await user.click(getByTestId('toggle'))
813+
await user.click(getByTestId('div'))
814+
await user.keyboard('A')
815+
816+
expect(callback).toHaveBeenCalledTimes(1)
817+
})
818+
777819
test.skip('should preventDefault and stop propagation when ref is not focused', async () => {
778820
const callback = jest.fn()
779821

0 commit comments

Comments
 (0)