In the previous chapter, React Integration Hooks, we learned how to make our components listen for actions like app:save.
However, we glazed over a massive problem.
Imagine your application has a Global shortcut: pressing Enter opens a "Quick Command" bar.
But your application also has a Chat window. When the user types a message and hits Enter, they expect to send the message, not open the Quick Command bar.
If both listeners are active, which one wins?
This is where Context-Aware Resolution comes in. It acts as the "Traffic Controller" of your input system.
We cannot simply list every keybinding in one giant list. Key meanings change depending on where the user is looking.
Consider this hierarchy of needs for the Enter key:
Enter selects the highlighted suggestion.Enter sends the message.Enter opens the command palette.If the Autocomplete dropdown is open, it should "steal" the input. If it closes, the Chat input should get it. If the user clicks away from the chat, the Global app should get it.
Think of your application contexts as sheets of transparent tracing paper stacked on top of each other.
Ctrl+C = Quit).When a key is pressed, we look at the Top Layer first.
How does the system know which layers are currently on the stack?
We use a hook called useRegisterKeybindingContext. When a component mounts (appears on screen), it registers its name. When it unmounts, it removes it.
// Inside ChatComponent.tsx
import { useRegisterKeybindingContext } from './hooks';
function ChatComponent() {
// 1. Tell the system: "The 'Chat' layer is now active!"
useRegisterKeybindingContext('Chat');
return <InputBox />;
}
Explanation: As long as <ChatComponent /> is rendered, the system adds 'Chat' to the list of active contexts. Any keybinding defined in the "Chat" section of our registry now takes priority over "Global".
function Autocomplete() {
// 1. This layer sits on top of Chat
useRegisterKeybindingContext('Autocomplete');
// 2. Define bindings specific to this dropdown
useKeybindings({
'list:select': selectItem, // Bound to 'Enter'
}, { context: 'Autocomplete' });
return <List />;
}
Explanation: If this component appears inside the Chat component, both contexts are active. But typically, the logic checks specific context matches before general ones.
When a key is pressed, the Resolver function takes over. It doesn't just look for a match; it looks for the best match based on what is active.
Let's look under the hood at resolver.ts. The core function is resolveKey.
It filters the entire rulebook down to only the rules that apply to the current situation.
First, we ignore any rules belonging to contexts that aren't currently active (like "Settings" or "Map").
// From resolver.ts (Simplified)
const ctxSet = new Set(activeContexts); // e.g. {'Global', 'Chat'}
// Only keep bindings that belong to active rooms
const relevantBindings = allBindings.filter(b =>
ctxSet.has(b.context)
);
Now we iterate through the relevant bindings. Because of how we load data (User Configs > Defaults), and how we structure context priority, the last matching binding usually wins.
// From resolver.ts (Simplified)
function resolveKey(input, key, activeContexts, bindings) {
let match = undefined;
for (const binding of bindings) {
// 1. Skip if this binding is for an inactive context
if (!activeContexts.includes(binding.context)) continue;
// 2. Check if the physical keys match (e.g. "Enter")
if (matchesBinding(input, key, binding)) {
match = binding; // Found a candidate!
}
}
// Return the last found match (highest priority)
return match ? { type: 'match', action: match.action } : { type: 'none' };
}
Explanation: This function is "stateless". It just takes the current inputs and the list of contexts and returns an answer. This makes it very easy to test.
A powerful side effect of this system is Shadowing.
If you want to disable a Global shortcut while in a specific mode, you can simply bind that key to null (or a no-op action) in the higher-priority context.
Scenario: Ctrl+F searches globally. But in the "Game" context, we want Ctrl+F to do nothing so the user doesn't accidentally open a search bar while playing.
Configuration:
Ctrl+F -> app:searchCtrl+F -> null
When the Resolver sees the "Game" context is active, it finds the null binding. It stops there and tells the system "This key is handled (by doing nothing)." It never reaches the Global app:search.
Context-Aware Resolution transforms a messy list of if/else statements into a clean, layered system.
useRegisterKeybindingContext.This ensures that your application always behaves predictably, even when the same key is used for different things in different places.
So far, we have only talked about pressing one key at a time. But what if we want to support sequences like G then I (Go to Inbox) or Ctrl+K then Ctrl+S?
That requires memory. That requires Chord Sequence Management.
Next: Chord Sequence Management
Generated by Code IQ