# Test basic ZoneManager
from cjm_fasthtml_keyboard_navigation.core.navigation import LinearVertical
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"),
)
)
assert manager.get_zone("browser") == browser
assert manager.get_zone("queue") == queue
assert manager.get_zone("invalid") is None
assert manager.get_initial_zone_id() == "browser"Zone Manager
Coordinates keyboard navigation across multiple zones, modes, and actions.
ZoneManager
The main coordinator that brings together zones, modes, actions, and key mappings.
ZoneManager
def ZoneManager(
zones:tuple[FocusZone, ...], system_id:Optional[str]=None, prev_zone_key:str='ArrowLeft',
next_zone_key:str='ArrowRight', zone_switch_modifiers:frozenset[str]=<factory>, wrap_zones:bool=True,
key_mapping:KeyMapping=<factory>, initial_zone_id:Optional[str]=None, modes:tuple[KeyboardMode, ...]=(),
default_mode:str='navigation', actions:tuple[KeyAction, ...]=(), on_zone_change:Optional[str]=None,
on_mode_change:Optional[str]=None, on_state_change:Optional[str]=None, skip_when_input_focused:bool=True,
input_selector:str='input, textarea, select, [contenteditable]', htmx_settle_event:str='htmx:afterSettle',
expose_state_globally:bool=False, global_state_name:str='keyboardNavState', state_hidden_inputs:bool=False
)->None:
Coordinates keyboard navigation across zones.
# Test modes
from cjm_fasthtml_keyboard_navigation.core.navigation import LinearHorizontal
split_mode = KeyboardMode(
name="split",
enter_key="Enter",
navigation_override=LinearHorizontal()
)
manager_with_modes = ZoneManager(
zones=(browser,),
modes=(split_mode,)
)
all_modes = manager_with_modes.get_all_modes()
assert len(all_modes) == 2 # navigation + split
assert manager_with_modes.get_mode("navigation") is not None
assert manager_with_modes.get_mode("split") == split_mode# Test action filtering
actions = (
KeyAction(key=" ", htmx_trigger="toggle"), # all zones/modes
KeyAction(key="Delete", htmx_trigger="delete", zone_ids=("queue",)),
KeyAction(key="Enter", htmx_trigger="split", mode_names=("split",)),
)
manager = ZoneManager(zones=(browser, queue), actions=actions)
# Browser in navigation mode
browser_nav_actions = manager.get_actions_for_context("browser", "navigation")
assert len(browser_nav_actions) == 1 # only space toggle
# Queue in navigation mode
queue_nav_actions = manager.get_actions_for_context("queue", "navigation")
assert len(queue_nav_actions) == 2 # space + delete
# Any zone in split mode
split_actions = manager.get_actions_for_context("browser", "split")
assert len(split_actions) == 2 # space + enter# Test validation
import traceback
# Empty zones should fail
try:
ZoneManager(zones=())
assert False, "Should have raised ValueError"
except ValueError as e:
assert "At least one zone" in str(e)
# Duplicate zone IDs should fail
try:
ZoneManager(zones=(
FocusZone(id="same"),
FocusZone(id="same")
))
assert False, "Should have raised ValueError"
except ValueError as e:
assert "Duplicate" in str(e)
# Invalid initial zone should fail
try:
ZoneManager(
zones=(FocusZone(id="zone1"),),
initial_zone_id="nonexistent"
)
assert False, "Should have raised ValueError"
except ValueError as e:
assert "not found" in str(e)# Test JS config generation
config = manager.to_js_config()
assert len(config["zones"]) == 2
assert config["initialZoneId"] == "browser"
assert config["defaultMode"] == "navigation"
assert config["settings"]["skipWhenInputFocused"] == True# Test custom key mapping
from cjm_fasthtml_keyboard_navigation.core.key_mapping import WASD_KEYS
wasd_manager = ZoneManager(
zones=(browser,),
key_mapping=WASD_KEYS
)
config = wasd_manager.to_js_config()
assert config["keyMapping"]["w"] == "up"
assert config["keyMapping"]["s"] == "down"# Test data attributes collection
z1 = FocusZone(id="z1", data_attributes=("a", "b"))
z2 = FocusZone(id="z2", data_attributes=("b", "c"))
m = ZoneManager(zones=(z1, z2))
attrs = m.get_all_data_attributes()
assert attrs == {"a", "b", "c"}# Test system_id auto-generation
z = FocusZone(id="my-zone")
m = ZoneManager(zones=(z,))
assert m.system_id == "my-zone" # defaults to initial zone ID
# Test system_id with explicit initial_zone_id
z1 = FocusZone(id="first")
z2 = FocusZone(id="second")
m = ZoneManager(zones=(z1, z2), initial_zone_id="second")
assert m.system_id == "second" # follows initial_zone_id
# Test explicit system_id
m = ZoneManager(zones=(z1, z2), system_id="custom-id")
assert m.system_id == "custom-id"
# Test system_id in JS config
config = m.to_js_config()
assert config["systemId"] == "custom-id"