Keyboard System

High-level API for rendering complete keyboard navigation systems.

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 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_html
# 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,)