# Keyboard Support - Test Instructions ## ⚠️ Breaking Change **Version 2.0** uses HTMX configuration objects instead of simple URL strings. The old format is **not supported**. **Old format (no longer supported)**: ```javascript {"A": "/url"} ``` **New format (required)**: ```javascript {"A": {"hx-post": "/url"}} ``` ## Files - `keyboard_support.js` - Main keyboard support library with smart timeout logic - `test_keyboard_support.html` - Test page to verify functionality ## Key Features ### Scope Control with `require_inside` Each combination can declare whether it should only trigger when the focus is **inside** the registered element, or fire **globally** regardless of focus. | `require_inside` | Behavior | |-----------------|----------| | `true` (default) | Triggers only if focus is inside the element or one of its children | | `false` | Triggers regardless of where the focus is (global shortcut) | ```javascript // Only fires when focus is inside #tree-panel add_keyboard_support('tree-panel', '{"esc": {"hx-post": "/cancel", "require_inside": true}}'); // Fires anywhere on the page add_keyboard_support('app', '{"ctrl+n": {"hx-post": "/new", "require_inside": false}}'); ``` **Python usage (`Keyboard` component):** ```python # Default: require_inside=True — fires only when inside the element Keyboard(self, _id="-kb").add("esc", self.commands.cancel()) # Explicit global shortcut Keyboard(self, _id="-kb").add("ctrl+n", self.commands.new_item(), require_inside=False) ``` ### Multiple Simultaneous Triggers **IMPORTANT**: If multiple elements listen to the same combination, all of them whose `require_inside` condition is satisfied will be triggered simultaneously: ```javascript add_keyboard_support('modal', '{"esc": {"hx-post": "/close-modal", "require_inside": true}}'); add_keyboard_support('editor', '{"esc": {"hx-post": "/cancel-edit", "require_inside": true}}'); add_keyboard_support('sidebar', '{"esc": {"hx-post": "/hide-sidebar", "require_inside": false}}'); // Pressing ESC while focus is inside 'editor': // - 'modal' → skipped (require_inside: true, focus not inside) // - 'editor' → triggered ✓ // - 'sidebar' → triggered ✓ (require_inside: false) ``` ### Smart Timeout Logic (Longest Match) The library uses **a single global timeout** based on the sequence state, not on individual elements: **Key principle**: If **any element** has a longer sequence possible, **all matching elements wait**. Examples: **Example 1**: Three elements, same combination ```javascript add_keyboard_support('elem1', '{"esc": "/url1"}'); add_keyboard_support('elem2', '{"esc": "/url2"}'); add_keyboard_support('elem3', '{"esc": "/url3"}'); // Press ESC → ALL 3 trigger immediately (no longer sequences exist) ``` **Example 2**: Mixed - one has longer sequence ```javascript add_keyboard_support('elem1', '{"A": "/url1"}'); add_keyboard_support('elem2', '{"A": "/url2"}'); add_keyboard_support('elem3', '{"A": "/url3", "A B": "/url3b"}'); // Press A: // - elem3 has "A B" possible → EVERYONE WAITS 500ms // - If B arrives: only elem3 triggers with "A B" // - If timeout expires: elem1, elem2, elem3 ALL trigger with "A" ``` **Example 3**: Different combinations ```javascript add_keyboard_support('elem1', '{"A B": "/url1"}'); add_keyboard_support('elem2', '{"C D": "/url2"}'); // Press A: elem1 waits for B, elem2 not affected // Press C: elem2 waits for D, elem1 not affected ``` The timeout is tied to the **sequence being typed**, not to individual elements. ### Enabling and Disabling Combinations Each combination can be enabled or disabled independently. A disabled combination is registered and tracked, but its action is never triggered. | State | Behavior | |-------|----------| | `enabled=True` (default) | Combination triggers normally | | `enabled=False` | Combination is silently ignored when pressed | **Setting the initial state at registration:** ```python # Enabled by default keyboard.add("ctrl+s", self.commands.save()) # Disabled at startup keyboard.add("ctrl+d", self.commands.delete(), enabled=False) ``` **Toggling dynamically at runtime:** Use `mk_enable()` and `mk_disable()` to change the state from a server response. Both methods return an out-of-band HTMX element (`hx-swap-oob`) that updates the DOM without a full page reload. ```python # Enable a combination (e.g., once an item is selected) def handle_select(self): item = ... return item.render(), self.keyboard.mk_enable("ctrl+d") # Disable a combination (e.g., when nothing is selected) def handle_deselect(self): return self.keyboard.mk_disable("ctrl+d") ``` The enabled state is stored in a hidden control `