keyboard

Self-contained keyboard system for sortable queue navigation and actions.

create_queue_keyboard_system


def create_queue_keyboard_system(
    config:SortableQueueConfig, # Queue configuration
    ids:SortableQueueHtmlIds, # HTML ID generators
    urls:SortableQueueUrls, # URL endpoints for HTMX actions
    zone_focus_classes:tuple=('ring-0',), # CSS classes when queue zone is active
    item_focus_classes:Optional=None, # CSS classes on focused item (default: bg-base-300)
    data_attributes:tuple=(), # Data attributes to extract (e.g., ("record-id", "provider-id"))
    on_focus_change:Optional=None, # JS callback on item focus change
    hidden_input_prefix:Optional=None, # Prefix for hidden state inputs
    system_id:Optional=None, # Keyboard system ID (auto-generated from ids.system_id if not set)
    manager_label:Optional=None, # Human-readable label for the underlying ZoneManager (used by render_keyboard_hints_modal section header when this system is rendered as a child)
    show_hints:bool=False, # Show keyboard hints UI
)->KeyboardSystem: # Complete rendered keyboard system

Create a self-contained keyboard system for the sortable queue.

Returns a rendered KeyboardSystem with a single FocusZone (LinearVertical navigation) and built-in actions for Delete/Backspace remove and Shift+Arrow reorder. Works standalone or as a child in a hierarchy via coord.setParent(system_id, parent_id).

Pass manager_label (e.g., “Selection Queue”) so that when this system is rendered as a child_managers entry in render_keyboard_hints_modal, the modal’s section header reads as the label instead of falling back to the technical system_id. Access the underlying ZoneManager via the returned KeyboardSystem.manager field for the child_managers handoff.

Tests

from fasthtml.common import to_xml

# Test setup
config = SortableQueueConfig(prefix="tk")
ids = SortableQueueHtmlIds(prefix="tk")
urls = SortableQueueUrls(reorder="/tk/reorder", remove="/tk/remove", clear="/tk/clear")

# --- Basic creation with defaults ---
kb = create_queue_keyboard_system(config, ids, urls)
assert isinstance(kb, KeyboardSystem)
assert kb.script is not None
assert kb.hidden_inputs is not None
assert kb.action_buttons is not None

# --- Default item focus classes (bg-base-300) ---
script_xml = to_xml(kb.script)
assert "bg-base-300" in script_xml  # Default focus styling in JS config

# --- System ID ---
assert "tk-queue-kb" in script_xml  # system_id appears in JS config

# Custom system_id
kb2 = create_queue_keyboard_system(config, ids, urls, system_id="my-custom-kb")
script_xml2 = to_xml(kb2.script)
assert "my-custom-kb" in script_xml2

# --- Action buttons ---
btns_xml = to_xml(kb.action_buttons)
assert ids.remove_btn in btns_xml  # Remove button present
assert ids.reorder_up_btn in btns_xml  # Reorder up button present
assert ids.reorder_down_btn in btns_xml  # Reorder down button present
assert "/tk/remove" in btns_xml  # Remove URL wired
assert "/tk/reorder" in btns_xml  # Reorder URL wired

# --- Direction vals on reorder buttons ---
assert "direction" in btns_xml

# --- Zone config in script ---
assert f"li.{config.item_class}" in script_xml  # Item selector
assert ids.container in script_xml  # Zone container ID

# --- Custom item focus classes override default ---
kb3 = create_queue_keyboard_system(
    config, ids, urls,
    item_focus_classes=("bg-custom",),
    data_attributes=("record-id", "provider-id"),
    on_focus_change="myFocusCallback",
    hidden_input_prefix="my-focused",
    show_hints=True,
)
assert isinstance(kb3, KeyboardSystem)
assert kb3.hints is not None  # Hints rendered when show_hints=True
script_xml3 = to_xml(kb3.script)
assert "bg-custom" in script_xml3
assert "bg-base-300" not in script_xml3  # Default overridden
assert "myFocusCallback" in script_xml3
assert "record-id" in script_xml3

print("All keyboard tests passed")
All keyboard tests passed
# Tests for manager_label + KeyboardSystem.manager handoff
# The L5 contract: create_queue_keyboard_system must expose the underlying
# ZoneManager so consumers can hand it to render_keyboard_hints_modal as a
# child_managers entry. Manager-label must be settable for human-readable
# section headers in the modal.

# Default: manager exists, label is None (falls back to system_id when rendered)
kb_default = create_queue_keyboard_system(config, ids, urls)
assert kb_default.manager is not None, "KeyboardSystem.manager must be populated by render_keyboard_system"
assert kb_default.manager.label is None, "Default manager_label is None — modal falls back to system_id"
assert kb_default.manager.get_display_label() == ids.system_id, \
    "When label is None, get_display_label must fall back to the ZoneManager's system_id"

# With manager_label: ZoneManager carries it and get_display_label returns it
kb_labeled = create_queue_keyboard_system(config, ids, urls, manager_label="Selection Queue")
assert kb_labeled.manager.label == "Selection Queue"
assert kb_labeled.manager.get_display_label() == "Selection Queue", \
    "When label is set, get_display_label must return the label (not system_id)"

# manager_label is render-only — does NOT appear in JS config
import json
script_xml_labeled = to_xml(kb_labeled.script)
# Label should not be serialized into the JS-side cfg
assert '"label":' not in script_xml_labeled, \
    "manager_label is Python-render-only; must not leak into the JS config"

# Sanity check: manager_label + system_id can co-exist
kb_both = create_queue_keyboard_system(
    config, ids, urls,
    system_id="custom-queue-kb",
    manager_label="Sortable Queue",
)
assert kb_both.manager.system_id == "custom-queue-kb"
assert kb_both.manager.label == "Sortable Queue"
assert kb_both.manager.get_display_label() == "Sortable Queue"  # label wins

print("manager_label + KeyboardSystem.manager handoff tests passed")
manager_label + KeyboardSystem.manager handoff tests passed