# Test render_keyboard_system
from fasthtml.common import to_xml
from cjm_fasthtml_keyboard_navigation.core.focus_zone import FocusZone
from cjm_fasthtml_keyboard_navigation.core.actions import KeyAction
browser = FocusZone(
id="browser",
item_selector="tr.item",
data_attributes=("job-id",)
)
queue = FocusZone(
id="queue",
item_selector="li.item"
)
manager = ZoneManager(
zones=(browser, queue),
actions=(
KeyAction(key=" ", htmx_trigger="toggle-btn", description="Select"),
KeyAction(key="Delete", htmx_trigger="delete-btn", description="Remove"),
)
)
system = render_keyboard_system(
manager,
url_map={
"toggle-btn": "/toggle",
"delete-btn": "/delete"
},
target_map={
"toggle-btn": "#list",
"delete-btn": "#list"
}
)
assert isinstance(system, KeyboardSystem)
assert system.hints is not None
# Check script
script_html = to_xml(system.script)
assert "handleKeydown" in script_html
# Check hidden inputs
inputs_html = to_xml(system.hidden_inputs)
assert 'id="browser-job-id"' in inputs_html
# Check action buttons - no hx-trigger by default (JS handles triggering)
buttons_html = to_xml(system.action_buttons)
assert 'id="toggle-btn"' in buttons_html
assert 'id="delete-btn"' in buttons_html
assert 'hx-trigger' not in buttons_html # JavaScript handles triggering
# Check hints
hints_html = to_xml(system.hints)
assert "Select" in hints_htmlKeyboard System
KeyboardSystem Result
KeyboardSystem
def KeyboardSystem(
script:Script, hidden_inputs:Div, action_buttons:Div, hints:Optional[Div]=None,
manager:Optional[ZoneManager]=None
)->None:
Container for all keyboard navigation components.
Carries manager so consumers building child systems can hand the underlying ZoneManager to render_keyboard_hints_modal(..., child_managers=[system.manager]) without reconstructing it. Purely additive — pre-existing consumers that unpack the rendered DOM pieces continue to work unchanged.
Render Keyboard System
The main entry point for generating keyboard navigation components.
If include_map is not provided, it will be auto-generated based on the actions: - Actions with zone_ids will include only those zones’ hidden inputs - Actions without zone_ids will include all zones’ hidden inputs
This ensures HTMX requests automatically include the focused item’s data attributes.
render_keyboard_system
def render_keyboard_system(
manager:ZoneManager, # the zone manager configuration
url_map:dict[str, str], # action button ID -> URL
target_map:dict[str, str], # action button ID -> target selector
include_map:dict[str, str] | None=None, # action button ID -> include selector (auto-generated if None)
swap_map:dict[str, str] | None=None, # action button ID -> swap value
vals_map:dict[str, dict] | None=None, # action button ID -> hx-vals dict
show_hints:bool=True, # render keyboard hints UI
hints_badge_style:str='ghost', # badge style for hints
include_state_inputs:bool=False, # include state tracking inputs
)->KeyboardSystem: # complete keyboard system
Render complete keyboard navigation system.
# Test without hints
system_no_hints = render_keyboard_system(
manager,
url_map={"toggle-btn": "/toggle", "delete-btn": "/delete"},
target_map={"toggle-btn": "#list", "delete-btn": "#list"},
show_hints=False
)
assert system_no_hints.hints is None
assert len(system_no_hints.all_components()) == 3 # No hints# Test with vals_map for direction parameters
manager_with_reorder = ZoneManager(
zones=(browser, queue),
actions=(
KeyAction(key="ArrowUp", modifiers=frozenset({"shift"}), htmx_trigger="reorder-up-btn"),
KeyAction(key="ArrowDown", modifiers=frozenset({"shift"}), htmx_trigger="reorder-down-btn"),
)
)
system_with_vals = render_keyboard_system(
manager_with_reorder,
url_map={
"reorder-up-btn": "/reorder",
"reorder-down-btn": "/reorder"
},
target_map={
"reorder-up-btn": "#queue",
"reorder-down-btn": "#queue"
},
vals_map={
"reorder-up-btn": {"direction": "up"},
"reorder-down-btn": {"direction": "down"}
},
show_hints=False
)
buttons_html = to_xml(system_with_vals.action_buttons)
assert '"direction": "up"' in buttons_html
assert '"direction": "down"' in buttons_html# Test all_components unpacking
components = system.all_components()
assert len(components) == 4 # script, inputs, buttons, hints# Test KeyboardSystem.manager field is populated and identity-equal to the input
# This is the L3 hook that lets consumers building child systems hand the
# underlying ZoneManager to render_keyboard_hints_modal(..., child_managers=[s.manager])
# without reconstructing it.
assert system.manager is manager, \
"KeyboardSystem.manager must be identity-equal to the manager passed in (no clone, no copy)"
# manager survives unpacking-style consumer usage (existing all_components consumers don't break)
components_only = system.all_components()
assert len(components_only) == 4, "all_components() must still return only the rendered DOM tuple"
assert manager not in components_only, "all_components() must exclude the manager field (data, not DOM)"
print("KeyboardSystem.manager field tests passed")Quick Setup Function
For simpler use cases with sensible defaults.
quick_keyboard_system
def quick_keyboard_system(
zones:tuple[FocusZone, ...], # focus zones
actions:tuple[KeyAction, ...], # keyboard actions
url_map:dict[str, str], # action URLs
target_map:dict[str, str], # action targets
kwargs:VAR_KEYWORD
)->KeyboardSystem: # complete keyboard system
Quick setup for simple keyboard navigation.
# Test quick setup
system = quick_keyboard_system(
zones=(browser,),
actions=(
KeyAction(key=" ", htmx_trigger="select-btn", description="Select"),
),
url_map={"select-btn": "/select"},
target_map={"select-btn": "#list"},
show_hints=False
)
assert isinstance(system, KeyboardSystem)
assert system.hints is None# quick_keyboard_system also populates KeyboardSystem.manager (delegates to render_keyboard_system)
assert system.manager is not None, "quick_keyboard_system must populate manager via render_keyboard_system"
assert isinstance(system.manager, ZoneManager)
assert system.manager.zones == (browser,)