Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/vast-jokes-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/hotkeys-devtools': patch
'@tanstack/hotkeys': patch
---

feat: callback variant of conflictBehavior
4 changes: 2 additions & 2 deletions docs/reference/type-aliases/ConflictBehavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ title: ConflictBehavior
# Type Alias: ConflictBehavior

```ts
type ConflictBehavior = "warn" | "error" | "replace" | "allow";
type ConflictBehavior = "warn" | "error" | "replace" | "allow" | CustomConflictHandler;
```

Defined in: [manager.utils.ts:11](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/manager.utils.ts#L11)
Defined in: [manager.utils.ts:16](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/manager.utils.ts#L16)

Behavior when registering a hotkey/sequence that conflicts with an existing registration.

Expand Down
19 changes: 16 additions & 3 deletions packages/hotkeys-devtools/src/components/DetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,16 @@ function getConflictLabel(
if (behavior === 'allow') return 'allowed'
if (behavior === 'error') return 'error'
if (behavior === 'replace') return 'replaced'
return 'warning'
if (behavior === 'warn') return 'warning'
return 'callback handled conflict with'
}

function serializeConflictBehavior(behavior: ConflictBehavior): string {
if (typeof behavior === 'string') {
return behavior
}

return '[Function function]'
}

function HotkeyDetails(props: {
Expand Down Expand Up @@ -285,7 +294,9 @@ function HotkeyDetails(props: {
</div>
<div class={styles().optionRow}>
<span class={styles().optionLabel}>conflictBehavior</span>
<span class={styles().optionValue}>{conflictBehavior()}</span>
<span class={styles().optionValue}>
{serializeConflictBehavior(conflictBehavior())}
</span>
</div>
<div class={styles().optionRow}>
<span class={styles().optionLabel}>hasFired</span>
Expand Down Expand Up @@ -498,7 +509,9 @@ function SequenceDetails(props: {
</div>
<div class={styles().optionRow}>
<span class={styles().optionLabel}>conflictBehavior</span>
<span class={styles().optionValue}>{conflictBehavior()}</span>
<span class={styles().optionValue}>
{serializeConflictBehavior(conflictBehavior())}
</span>
</div>
</div>
</div>
Expand Down
17 changes: 16 additions & 1 deletion packages/hotkeys/src/manager.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ParsedHotkey } from './hotkey'

type CustomConflictHandler = (
keyDisplay: string,
unregisterAnotherConflictingId: () => void,
) => void

/**
* Behavior when registering a hotkey/sequence that conflicts with an existing registration.
*
Expand All @@ -8,7 +13,12 @@ import type { ParsedHotkey } from './hotkey'
* - `'replace'` - Unregister the existing registration and register the new one
* - `'allow'` - Allow multiple registrations without warning
*/
export type ConflictBehavior = 'warn' | 'error' | 'replace' | 'allow'
export type ConflictBehavior =
| 'warn'
| 'error'
| 'replace'
| 'allow'
| CustomConflictHandler

/**
* Default options for hotkey/sequence registration.
Expand Down Expand Up @@ -164,6 +174,11 @@ export function handleConflict(
)
}

if (typeof conflictBehavior === 'function') {
conflictBehavior(keyDisplay, () => unregister(conflictingId))
return
}

// At this point, conflictBehavior must be 'replace'
unregister(conflictingId)
}
15 changes: 15 additions & 0 deletions packages/hotkeys/tests/manager.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,20 @@ describe('manager.utils', () => {

expect(unregister).toHaveBeenCalledWith('id-1')
})

it('should call custom callback if passed as conflictBehaviour', () => {
const unregister = vi.fn()
const handleConflictCallback = vi.fn()

handleConflict('id-1', 'Mod+S', handleConflictCallback, unregister)

expect(unregister).not.toHaveBeenCalledWith()
expect(handleConflictCallback).toHaveBeenCalledWith(
'Mod+S',
expect.any(Function),
)
handleConflictCallback.mock.calls[0]?.[1]()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mimics custom async logic inside the callback, e.g. call unregisterConflicting after displaying a popup and user clicked "Confirm"

expect(unregister).toHaveBeenCalledWith('id-1')
})
})
})