Skip to main content

A declarative keyboard navigation framework for FastHTML applications with multi-zone focus management, mode switching, and HTMX integration.

Project description

cjm-fasthtml-keyboard-navigation

Install

pip install cjm_fasthtml_keyboard_navigation

Project Structure

nbs/
├── components/ (3)
│   ├── hints.ipynb        # Components for displaying keyboard shortcut hints to users.
│   ├── hints_modal.ipynb  # Modal-based keyboard shortcut reference with scannable grouped layout and `?` key trigger.
│   └── system.ipynb       # High-level API for rendering complete keyboard navigation systems.
├── core/ (6)
│   ├── actions.ipynb      # Declarative keyboard action bindings supporting HTMX triggers and JS callbacks.
│   ├── focus_zone.ipynb   # Configuration for focusable containers with navigable items.
│   ├── key_mapping.ipynb  # Configurable key-to-direction mappings for customizable navigation keys.
│   ├── manager.ipynb      # Coordinates keyboard navigation across multiple zones, modes, and actions.
│   ├── modes.ipynb        # Configuration for keyboard modes that change navigation and action behavior.
│   └── navigation.ipynb   # Protocols and implementations for keyboard navigation within focus zones.
├── htmx/ (2)
│   ├── buttons.ipynb  # Generate hidden HTMX action buttons triggered by keyboard events.
│   └── inputs.ipynb   # Generate hidden inputs for HTMX integration with keyboard navigation.
└── js/ (3)
    ├── coordinator.ipynb  # Global coordinator for hierarchical keyboard system management with single-listener event dispatch.
    ├── generators.ipynb   # Generate complete keyboard navigation JavaScript from configuration.
    └── utils.ipynb        # Core JavaScript utility generators for keyboard navigation.

Total: 14 notebooks across 4 directories

Module Dependencies

graph LR
    components_hints[components.hints<br/>Keyboard Hints]
    components_hints_modal[components.hints_modal<br/>Keyboard Hints Modal]
    components_system[components.system<br/>Keyboard System]
    core_actions[core.actions<br/>Key Actions]
    core_focus_zone[core.focus_zone<br/>Focus Zone]
    core_key_mapping[core.key_mapping<br/>Key Mapping]
    core_manager[core.manager<br/>Zone Manager]
    core_modes[core.modes<br/>Keyboard Modes]
    core_navigation[core.navigation<br/>Navigation Patterns]
    htmx_buttons[htmx.buttons<br/>Action Buttons]
    htmx_inputs[htmx.inputs<br/>Hidden Inputs]
    js_coordinator[js.coordinator<br/>Keyboard Coordinator]
    js_generators[js.generators<br/>Script Generators]
    js_utils[js.utils<br/>JavaScript Utilities]

    components_hints --> core_focus_zone
    components_hints --> core_manager
    components_hints --> core_actions
    components_hints_modal --> core_focus_zone
    components_hints_modal --> core_manager
    components_hints_modal --> components_hints
    components_hints_modal --> core_actions
    components_system --> components_hints
    components_system --> htmx_inputs
    components_system --> core_actions
    components_system --> js_generators
    components_system --> core_focus_zone
    components_system --> core_manager
    components_system --> htmx_buttons
    core_actions --> core_key_mapping
    core_focus_zone --> core_navigation
    core_manager --> core_navigation
    core_manager --> core_key_mapping
    core_manager --> core_modes
    core_manager --> core_actions
    core_manager --> core_focus_zone
    core_modes --> core_navigation
    htmx_buttons --> core_actions
    htmx_buttons --> core_focus_zone
    htmx_buttons --> core_manager
    htmx_inputs --> core_focus_zone
    htmx_inputs --> core_manager
    js_generators --> js_coordinator
    js_generators --> core_actions
    js_generators --> core_focus_zone
    js_generators --> core_manager
    js_generators --> js_utils

32 cross-module dependencies detected

CLI Reference

No CLI commands found in this project.

Module Overview

Detailed documentation for each module in the project:

Key Actions (actions.ipynb)

Declarative keyboard action bindings supporting HTMX triggers and JS callbacks.

Import

from cjm_fasthtml_keyboard_navigation.core.actions import (
    KeyAction
)

Classes

@dataclass
class KeyAction:
    "A keyboard shortcut binding."
    
    key: str  # JavaScript key name (e.g., "Enter", " ", "ArrowUp")
    modifiers: frozenset[str] = field(...)
    htmx_trigger: Optional[str]  # ID of hidden button to click
    js_callback: Optional[str]  # JS function name to call
    mode_enter: Optional[str]  # mode name to enter
    mode_exit: bool = False  # exit current mode (return to default)
    prevent_default: bool = True  # call e.preventDefault()
    stop_propagation: bool = False  # call e.stopPropagation()
    zone_ids: Optional[tuple[str, ...]]  # only in these zones (None = all)
    mode_names: Optional[tuple[str, ...]]  # only in these modes (None = all)
    not_modes: Optional[tuple[str, ...]]  # not in these modes
    custom_condition: Optional[str]  # raw JS expression for additional conditions
    description: str = ''  # human-readable description for hints
    hint_group: str = 'General'  # grouping for keyboard hints display
    show_in_hints: bool = True  # whether to show in keyboard hints
    
    def matches_context(
            self,
            zone_id: str,  # current active zone
            mode_name: str  # current mode
        ) -> bool:         # True if action is valid in this context
        "Check if action is valid for given zone and mode."
    
    def get_display_key(self) -> str: # formatted key combo for display
            """Get formatted key combination for display."""
            return format_key_combo(self.key, self.modifiers)
    
        def to_js_config(self) -> dict: # JavaScript-compatible configuration
        "Get formatted key combination for display."
    
    def to_js_config(self) -> dict: # JavaScript-compatible configuration
            """Convert to JavaScript configuration object."""
            return {
                "key": self.key,
        "Convert to JavaScript configuration object."

Action Buttons (buttons.ipynb)

Generate hidden HTMX action buttons triggered by keyboard events.

Import

from cjm_fasthtml_keyboard_navigation.htmx.buttons import (
    build_htmx_trigger,
    render_action_button,
    render_action_buttons
)

Functions

def build_htmx_trigger(
    key: str,                           # JavaScript key name
    modifiers: frozenset[str] = frozenset(),  # modifier keys
    input_selector: str = "input, textarea, select, [contenteditable]"  # input elements to exclude
) -> str:                               # HTMX trigger expression
    "Build HTMX trigger expression for keyboard event."
def render_action_button(
    action: KeyAction,           # the action configuration
    url: str,                    # POST URL for the action
    target: str,                 # HTMX target selector
    include: str = "",           # hx-include selector
    swap: str = "outerHTML",     # hx-swap value
    vals: dict | None = None,    # hx-vals dictionary (JSON values to include in request)
    use_htmx_trigger: bool = False,  # use hx-trigger (False = JS triggerClick only)
    input_selector: str = "input, textarea, select, [contenteditable]"  # inputs to exclude from trigger
) -> Button | None:              # hidden button or None if not HTMX action
    "Render a hidden HTMX button for a keyboard action."
def render_action_buttons(
    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
    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
    use_htmx_triggers: bool = False,              # use hx-trigger (False = JS triggerClick only)
    container_id: str = "kb-action-buttons"       # container element ID
) -> Div:                                         # container with all action buttons
    "Render all hidden HTMX action buttons for keyboard navigation."

Keyboard Coordinator (coordinator.ipynb)

Global coordinator for hierarchical keyboard system management with single-listener event dispatch.

Import

from cjm_fasthtml_keyboard_navigation.js.coordinator import (
    js_coordinator_setup
)

Functions

def js_coordinator_setup() -> str: # JavaScript coordinator singleton code
    "Generate the global keyboard coordinator singleton."

Focus Zone (focus_zone.ipynb)

Configuration for focusable containers with navigable items.

Import

from cjm_fasthtml_keyboard_navigation.core.focus_zone import (
    FocusZone
)

Classes

@dataclass
class FocusZone:
    "A focusable container with navigable items."
    
    id: str  # HTML element ID of the container
    item_selector: Optional[str]  # CSS selector for items (None = scroll only)
    navigation: Union[NavigationPattern, LinearVertical] = field(...)
    navigation_throttle_ms: int = 0  # minimum ms between navigation events (0 = no throttle)
    item_focus_classes: tuple[str, ...] = (str(ring(2)), str(ring_dui.primary))  # CSS classes for focused item
    item_focus_attribute: str = 'data-focused'  # attribute set to "true" on focused item
    zone_focus_classes: tuple[str, ...] = (str(ring(2)), str(ring_dui.primary), str(inset_ring(2)))
    data_attributes: tuple[str, ...] = ()  # data attributes to extract from focused item
    on_focus_change: Optional[str]  # called when focused item changes
    on_navigate: Optional[str]  # called on any navigation (for side effects like audition)
    on_zone_enter: Optional[str]  # called when zone becomes active
    on_zone_leave: Optional[str]  # called when zone loses focus
    scroll_behavior: str = 'smooth'  # "smooth" or "auto"
    scroll_block: str = 'nearest'  # "start", "center", "end", "nearest"
    hidden_input_prefix: str = ''  # prefix for auto-generated hidden input IDs
    initial_index: int = 0  # initial focused item index
    
    def has_items(self) -> bool: # True if zone has selectable items
            """Check if zone has selectable items."""
            return self.item_selector is not None
    
        def get_hidden_input_id(
            self,
            attr: str  # the data attribute name
        ) -> str:      # the hidden input element ID
        "Check if zone has selectable items."
    
    def get_hidden_input_id(
            self,
            attr: str  # the data attribute name
        ) -> str:      # the hidden input element ID
        "Get the hidden input ID for a data attribute."
    
    def to_js_config(self) -> dict: # JavaScript-compatible configuration
            """Convert to JavaScript configuration object."""
            return {
                "id": self.id,
        "Convert to JavaScript configuration object."

Script Generators (generators.ipynb)

Generate complete keyboard navigation JavaScript from configuration.

Import

from cjm_fasthtml_keyboard_navigation.js.generators import (
    js_zone_state,
    js_focus_management,
    js_zone_switching,
    js_navigation,
    js_mode_management,
    js_action_dispatch,
    js_keyboard_handler,
    js_state_notification,
    js_initialization,
    js_global_api,
    generate_keyboard_script
)

Functions

def js_zone_state() -> str: # JavaScript state and getter/setter code
    "Generate JavaScript code for zone state management."
def js_focus_management() -> str: # JavaScript focus management code
    "Generate JavaScript code for focus management."
def js_zone_switching() -> str: # JavaScript zone switching code
    "Generate JavaScript code for zone switching."
def js_navigation() -> str: # JavaScript navigation code
    """Generate JavaScript code for item navigation."""
    return '''
// === Navigation ===
// Track last navigation time per zone for throttling
let lastNavigationTime = {};

function getNavigationPattern(zoneId) {
    // Check if mode overrides navigation
    const modeConfig = getModeConfig(currentMode);
    if (modeConfig && modeConfig.navigationOverride) {
        return modeConfig.navigationOverride;
    }
    // Use zone's pattern
    const zone = getZoneConfig(zoneId);
    return zone ? zone.navigationPattern : 'linear_vertical';
    "Generate JavaScript code for item navigation."
def js_mode_management() -> str: # JavaScript mode management code
    "Generate JavaScript code for mode management."
def js_action_dispatch() -> str: # JavaScript action dispatch code
    "Generate JavaScript code for action dispatch."
def js_keyboard_handler() -> str: # JavaScript keyboard handler code
    "Generate JavaScript code for keyboard event handling."
def js_state_notification() -> str: # JavaScript state notification code
    "Generate JavaScript code for state change notification."
def js_initialization() -> str: # JavaScript initialization code
    "Generate JavaScript code for initialization with focus recovery."
def js_global_api() -> str:  # JavaScript global API exposure code
    "Generate JavaScript code to expose control functions globally."
def generate_keyboard_script(
    manager: ZoneManager  # the zone manager configuration
) -> str:                 # complete JavaScript code wrapped in IIFE
    "Generate complete keyboard navigation JavaScript from ZoneManager."

Keyboard Hints (hints.ipynb)

Components for displaying keyboard shortcut hints to users.

Import

from cjm_fasthtml_keyboard_navigation.components.hints import (
    NAV_ICON_MAP,
    KEY_ICON_MAP,
    get_key_icon,
    render_hint_badge,
    create_nav_icon_hint,
    create_modifier_key_hint,
    render_hint_group,
    group_actions_by_hint_group,
    render_hints_from_actions,
    render_keyboard_hints
)

Functions

def get_key_icon(
    key_name: str,    # key name to look up (case-insensitive)
    size: int = 3     # icon size
) -> FT | None:       # icon component or None if no icon mapping
    "Get a lucide icon for a key name, if one exists."
def render_hint_badge(
    key_display: Union[str, FT],  # formatted key string or icon component
    description: str,              # action description
    style: str = "ghost",          # badge style (ghost, outline, soft, dash)
    auto_icon: bool = False        # auto-convert known keys to icons
) -> Div:                          # hint badge component
    "Render a single keyboard hint as a badge."
def create_nav_icon_hint(
    icon_name: str,      # lucide icon name (e.g., "arrow-down-up")
    description: str,    # action description
    style: str = "ghost" # badge style
) -> Div:                # hint badge with icon
    "Create a hint badge with a lucide icon."
def create_modifier_key_hint(
    modifier: str,       # modifier key name (e.g., "shift", "ctrl")
    key_icon_or_text: Union[str, FT],  # the main key icon or text
    description: str,    # action description
    style: str = "ghost" # badge style
) -> Div:                # hint badge with modifier + key
    "Create a hint badge with a modifier key and main key."
def render_hint_group(
    group_name: str,         # group header text
    hints: list[tuple[str, str]],  # list of (key_display, description) tuples
    badge_style: str = "ghost"     # badge style for this group
) -> Div:                    # group container with header and hints
    "Render a group of related keyboard hints."
def group_actions_by_hint_group(
    actions: tuple[KeyAction, ...]  # actions to group
) -> dict[str, list[KeyAction]]:    # grouped actions
    "Group actions by their hint_group attribute."
def render_hints_from_actions(
    actions: tuple[KeyAction, ...],  # actions to display hints for
    badge_style: str = "ghost"       # badge style
) -> Div:                            # container with all hint groups
    "Render keyboard hints from action configurations."
def render_keyboard_hints(
    manager: ZoneManager,                      # the zone manager
    include_navigation: bool = True,           # include navigation hints
    include_zone_switch: bool = True,          # include zone switching hints
    badge_style: str = "ghost",                # badge style
    container_id: str = "kb-hints",            # container element ID
    use_icons: bool = True                     # use lucide icons for nav hints
) -> Div:                                      # complete hints component
    "Render complete keyboard hints for a zone manager."

Variables

NAV_ICON_MAP = {2 items}
KEY_ICON_MAP = {9 items}

Keyboard Hints Modal (hints_modal.ipynb)

Modal-based keyboard shortcut reference with scannable grouped layout and ? key trigger.

Import

from cjm_fasthtml_keyboard_navigation.components.hints_modal import (
    render_keyboard_hints_trigger,
    render_keyboard_hints_modal
)

Functions

def _render_key_combo(
    display_key: str,  # formatted key combo string (e.g., "Ctrl+Shift+\u2191")
) -> Div:              # container with kbd elements for each key part
    "Render a key combination as a sequence of kbd elements joined by `+`."
def _render_hint_row(
    display_key: str,  # formatted key combo string
    description: str,  # action description
) -> Div:              # single shortcut row with key and description
    "Render a single shortcut row: key combo on left, description on right."
def _render_modal_group(
    group_name: str,                        # group header text
    actions: list[tuple[str, str]],          # list of (display_key, description)
) -> Div:                                    # group container with header and rows
    "Render a group of related shortcuts with a header."
def _render_modal_body(
    manager: ZoneManager,          # keyboard zone manager
    include_navigation: bool = True,  # include \u2191/\u2193 navigation hint
    include_zone_switch: bool = True, # include \u2190/\u2192 zone switch hint
) -> Div:                             # modal body with grouped shortcuts
    "Render the modal body with grouped keyboard shortcuts."
def render_keyboard_hints_trigger(
    modal_id: str = "kb-hints-modal",  # ID of the modal dialog to open
    icon_size: int = 4,                 # lucide icon size
) -> Button:                            # ghost button with keyboard icon
    "Render a keyboard icon button that opens the hints modal."
def _render_question_mark_listener(
    modal_id: str,  # ID of the modal dialog to toggle
) -> Script:        # script element with global `?` key listener
    """
    Render a global `?` key listener that toggles the hints modal.
    
    Uses a named function stored on `window` so that HTMX re-renders
    replace the previous listener instead of accumulating duplicates.
    """
def render_keyboard_hints_modal(
    manager: ZoneManager,               # keyboard zone manager with actions configured
    modal_id: str = "kb-hints-modal",    # HTML ID for the modal dialog
    include_navigation: bool = True,     # include \u2191/\u2193 navigation hint
    include_zone_switch: bool = True,    # include zone switch hint (auto-hidden for single zone)
    enable_question_mark_key: bool = True,  # add global `?` key listener
    title: str = "Keyboard Shortcuts",   # modal title text
) -> tuple[FT, FT, FT]:                 # (modal_dialog, trigger_button, question_mark_script)
    """
    Render a modal-based keyboard shortcut reference.
    
    Returns three components:
    - `modal_dialog`: The Dialog element (place anywhere in page)
    - `trigger_button`: Small keyboard icon button (place in step header)
    - `question_mark_script`: Global `?` key listener Script (place in page)
    
    If `enable_question_mark_key` is False, `question_mark_script` is an empty Div.
    """

Hidden Inputs (inputs.ipynb)

Generate hidden inputs for HTMX integration with keyboard navigation.

Import

from cjm_fasthtml_keyboard_navigation.htmx.inputs import (
    render_zone_hidden_inputs,
    render_hidden_inputs,
    build_include_selector,
    build_all_zones_include_selector
)

Functions

def render_zone_hidden_inputs(
    zone: FocusZone  # the focus zone configuration
) -> list:           # list of Hidden input components
    "Render hidden inputs for a single zone's data attributes."
def render_hidden_inputs(
    manager: ZoneManager,            # the zone manager configuration
    include_state: bool = False,     # include state tracking inputs
    container_id: str = "kb-hidden-inputs"  # container element ID
) -> Div:                            # container with all hidden inputs
    """
    Render all hidden inputs for keyboard navigation.
    
    Deduplicates inputs by ID - zones with the same hidden_input_prefix
    will share inputs rather than creating duplicates.
    """
def build_include_selector(
    zone: FocusZone,              # the zone to include inputs from
    include_state: bool = False   # include state inputs
) -> str:                         # CSS selector for hx-include
    "Build hx-include selector for zone's hidden inputs."
def build_all_zones_include_selector(
    manager: ZoneManager,         # the zone manager
    include_state: bool = False   # include state inputs
) -> str:                         # CSS selector for all zones
    """
    Build hx-include selector for all zones' hidden inputs.
    
    Deduplicates selectors - zones with the same hidden_input_prefix
    will only include each input once.
    """

Key Mapping (key_mapping.ipynb)

Configurable key-to-direction mappings for customizable navigation keys.

Import

from cjm_fasthtml_keyboard_navigation.core.key_mapping import (
    ARROW_KEYS,
    WASD_KEYS,
    VIM_KEYS,
    NUMPAD_KEYS,
    ARROWS_AND_WASD,
    ARROWS_AND_VIM,
    KEY_DISPLAY_MAP,
    KeyMapping,
    format_key_for_display,
    format_key_combo
)

Functions

def format_key_for_display(
    key: str  # the JavaScript key name
) -> str:     # human-readable display string
    "Format a key name for user display."
def format_key_combo(
    key: str,                        # the main key
    modifiers: frozenset[str] = frozenset() # modifier keys (shift, ctrl, alt, meta)
) -> str:                            # formatted string like "Ctrl+Shift+A"
    "Format a key combination for display."

Classes

class KeyMapping:
    "Maps physical keys to navigation directions."
    
    def get_direction(
            self,
            key: str  # the pressed key (e.g., "ArrowUp", "w")
        ) -> str | None: # the direction ("up", "down", "left", "right") or None
        "Get direction for a given key press."
    
    def all_keys(self) -> tuple[str, ...]: # all mapped keys
            """Return all mapped keys."""
            return self.up + self.down + self.left + self.right
    
        def to_js_map(self) -> dict[str, str]: # {key: direction} mapping
        "Return all mapped keys."
    
    def to_js_map(self) -> dict[str, str]: # {key: direction} mapping
            """Convert to JavaScript-compatible key-to-direction map."""
            result = {}
            for key in self.up
        "Convert to JavaScript-compatible key-to-direction map."

Variables

ARROW_KEYS
WASD_KEYS
VIM_KEYS
NUMPAD_KEYS
ARROWS_AND_WASD
ARROWS_AND_VIM
KEY_DISPLAY_MAP: dict[str, str]

Zone Manager (manager.ipynb)

Coordinates keyboard navigation across multiple zones, modes, and actions.

Import

from cjm_fasthtml_keyboard_navigation.core.manager import (
    ZoneManager
)

Classes

@dataclass
class ZoneManager:
    "Coordinates keyboard navigation across zones."
    
    zones: tuple[FocusZone, ...]  # all focus zones
    system_id: Optional[str]  # unique ID for coordinator registration (defaults to initial zone ID)
    prev_zone_key: str = 'ArrowLeft'  # key to switch to previous zone
    next_zone_key: str = 'ArrowRight'  # key to switch to next zone
    zone_switch_modifiers: frozenset[str] = field(...)
    wrap_zones: bool = True  # wrap from last zone to first
    key_mapping: KeyMapping = field(...)
    initial_zone_id: Optional[str]  # defaults to first zone
    modes: tuple[KeyboardMode, ...] = ()  # custom modes (navigation mode is implicit)
    default_mode: str = 'navigation'  # mode to return to after exiting others
    actions: tuple[KeyAction, ...] = ()  # keyboard action bindings
    on_zone_change: Optional[str]  # called when active zone changes
    on_mode_change: Optional[str]  # called when mode changes
    on_state_change: Optional[str]  # called on any state change (for persistence)
    skip_when_input_focused: bool = True  # ignore keys in input/textarea
    input_selector: str = 'input, textarea, select, [contenteditable]'  # elements to skip
    htmx_settle_event: str = 'htmx:afterSettle'  # event to reinitialize on
    expose_state_globally: bool = False  # expose state on window object
    global_state_name: str = 'keyboardNavState'  # name for global state
    state_hidden_inputs: bool = False  # write state to hidden inputs
    
    def get_zone(
            self,
            zone_id: str  # zone ID to find
        ) -> Optional[FocusZone]: # the zone or None
        "Get zone by ID."
    
    def get_initial_zone_id(self) -> str: # the initial zone ID
            """Get initial zone ID."""
            return self.initial_zone_id or self.zones[0].id
    
        def get_all_modes(self) -> tuple[KeyboardMode, ...]: # all modes including default
        "Get initial zone ID."
    
    def get_all_modes(self) -> tuple[KeyboardMode, ...]: # all modes including default
            """Get all modes including the default navigation mode."""
            return (NAVIGATION_MODE,) + self.modes
    
        def get_mode(
            self,
            mode_name: str  # mode name to find
        ) -> Optional[KeyboardMode]: # the mode or None
        "Get all modes including the default navigation mode."
    
    def get_mode(
            self,
            mode_name: str  # mode name to find
        ) -> Optional[KeyboardMode]: # the mode or None
        "Get mode by name."
    
    def get_actions_for_context(
            self,
            zone_id: str,  # current zone
            mode_name: str  # current mode
        ) -> list[KeyAction]: # actions valid in this context
        "Get actions valid for given zone and mode."
    
    def get_all_data_attributes(self) -> set[str]: # unique data attributes across all zones
            """Get all unique data attributes from all zones."""
            attrs = set()
            for zone in self.zones
        "Get all unique data attributes from all zones."
    
    def to_js_config(self) -> dict: # JavaScript-compatible configuration
            """Convert to JavaScript configuration object."""
            return {
                "systemId": self.system_id,
        "Convert to JavaScript configuration object."

Keyboard Modes (modes.ipynb)

Configuration for keyboard modes that change navigation and action behavior.

Import

from cjm_fasthtml_keyboard_navigation.core.modes import (
    NAVIGATION_MODE,
    KeyboardMode
)

Classes

@dataclass
class KeyboardMode:
    "A named mode that changes keyboard behavior."
    
    name: str  # unique mode name (e.g., "navigation", "split", "audition")
    enter_key: Optional[str]  # key to enter mode (None = programmatic only)
    enter_modifiers: frozenset[str] = field(...)
    exit_key: str = 'Escape'  # key to exit mode
    exit_modifiers: frozenset[str] = field(...)
    zone_ids: Optional[tuple[str, ...]]  # only available in these zones (None = all)
    navigation_override: Optional[NavigationPattern]  # override zone's navigation pattern
    on_enter: Optional[str]  # called when entering mode
    on_exit: Optional[str]  # called when exiting mode
    indicator_text: Optional[str]  # text shown in UI when mode is active
    exit_on_zone_change: bool = True  # exit mode when switching zones
    
    def is_available_in_zone(
            self,
            zone_id: str  # the zone to check
        ) -> bool:        # True if mode is available in zone
        "Check if mode is available in given zone."
    
    def to_js_config(self) -> dict: # JavaScript-compatible configuration
            """Convert to JavaScript configuration object."""
            return {
                "name": self.name,
        "Convert to JavaScript configuration object."

Variables

NAVIGATION_MODE

Navigation Patterns (navigation.ipynb)

Protocols and implementations for keyboard navigation within focus zones.

Import

from cjm_fasthtml_keyboard_navigation.core.navigation import (
    Direction,
    NavigationPattern,
    LinearVertical,
    LinearHorizontal,
    ScrollOnly,
    Grid
)

Classes

@runtime_checkable
class NavigationPattern(Protocol):
    "Protocol for navigation within a focus zone."
    
    def name(self) -> str: # unique identifier for this pattern
            """Return the pattern name."""
            ...
    
        def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # navigation direction
            total: int,        # total number of items
            columns: int = 1   # number of columns (for grid navigation)
        ) -> int:              # the new index after navigation
        "Return the pattern name."
    
    def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # navigation direction
            total: int,        # total number of items
            columns: int = 1   # number of columns (for grid navigation)
        ) -> int:              # the new index after navigation
        "Calculate next index given current position and direction."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # directions this pattern responds to
        "Return which arrow key directions this pattern handles."
@dataclass
class LinearVertical:
    "Up/Down navigation through a vertical list."
    
    wrap: bool = False  # wrap from last item to first (and vice versa)
    
    def name(self) -> str:
            """Return the pattern name."""
            return "linear_vertical"
    
        def get_supported_directions(self) -> tuple[Direction, ...]: # ("up", "down")
        "Return the pattern name."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # ("up", "down")
            """Return supported directions."""
            return ("up", "down")
    
        def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # "up" or "down"
            total: int,        # total number of items
            columns: int = 1   # unused for linear navigation
        ) -> int:              # the new index
        "Return supported directions."
    
    def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # "up" or "down"
            total: int,        # total number of items
            columns: int = 1   # unused for linear navigation
        ) -> int:              # the new index
        "Calculate next index for vertical navigation."
@dataclass
class LinearHorizontal:
    "Left/Right navigation through a horizontal list."
    
    wrap: bool = False  # wrap from last item to first (and vice versa)
    
    def name(self) -> str:
            """Return the pattern name."""
            return "linear_horizontal"
    
        def get_supported_directions(self) -> tuple[Direction, ...]: # ("left", "right")
        "Return the pattern name."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # ("left", "right")
            """Return supported directions."""
            return ("left", "right")
    
        def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # "left" or "right"
            total: int,        # total number of items
            columns: int = 1   # unused for linear navigation
        ) -> int:              # the new index
        "Return supported directions."
    
    def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # "left" or "right"
            total: int,        # total number of items
            columns: int = 1   # unused for linear navigation
        ) -> int:              # the new index
        "Calculate next index for horizontal navigation."
@dataclass
class ScrollOnly:
    "No item navigation, zone is scrollable content only."
    
    def name(self) -> str:
            """Return the pattern name."""
            return "scroll_only"
    
        def get_supported_directions(self) -> tuple[Direction, ...]: # empty tuple
        "Return the pattern name."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # empty tuple
            """Return no supported directions."""
            return ()
    
        def get_next_index(
            self,
            current: int,      # current index (unused)
            direction: Direction, # direction (unused)
            total: int,        # total items (unused)
            columns: int = 1   # columns (unused)
        ) -> int:              # always returns current
        "Return no supported directions."
    
    def get_next_index(
            self,
            current: int,      # current index (unused)
            direction: Direction, # direction (unused)
            total: int,        # total items (unused)
            columns: int = 1   # columns (unused)
        ) -> int:              # always returns current
        "Return current index unchanged."
@dataclass
class Grid:
    "2D grid navigation (placeholder for future implementation)."
    
    columns: int = 4  # number of columns in the grid
    wrap_horizontal: bool = True  # wrap at row edges
    wrap_vertical: bool = False  # wrap at grid top/bottom
    
    def name(self) -> str:
            """Return the pattern name."""
            return "grid"
    
        def get_supported_directions(self) -> tuple[Direction, ...]: # all four directions
        "Return the pattern name."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # all four directions
            """Return all four directions."""
            return ("up", "down", "left", "right")
    
        def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # navigation direction
            total: int,        # total number of items
            columns: int = 0   # override columns (0 = use self.columns)
        ) -> int:              # the new index
        "Return all four directions."
    
    def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # navigation direction
            total: int,        # total number of items
            columns: int = 0   # override columns (0 = use self.columns)
        ) -> int:              # the new index
        "Calculate next index for 2D grid navigation."

Keyboard System (system.ipynb)

High-level API for rendering complete keyboard navigation systems.

Import

from cjm_fasthtml_keyboard_navigation.components.system import (
    KeyboardSystem,
    render_keyboard_system,
    quick_keyboard_system
)

Functions

def _build_auto_include_map(
    manager: ZoneManager,        # the zone manager configuration
    include_state: bool = False  # include state inputs in selector
) -> dict[str, str]:             # action button ID -> include selector
    "Auto-generate include_map based on actions and their zone constraints."
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."
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                                     # additional ZoneManager/render options
) -> KeyboardSystem:                             # complete keyboard system
    "Quick setup for simple keyboard navigation."

Classes

@dataclass
class KeyboardSystem:
    "Container for all keyboard navigation components."
    
    script: Script  # the keyboard navigation JavaScript
    hidden_inputs: Div  # hidden inputs for HTMX
    action_buttons: Div  # hidden action buttons for HTMX
    hints: Optional[Div]  # optional keyboard hints UI
    
    def all_components(self) -> tuple:  # all components as tuple
            """Return all components for easy unpacking into render."""
            components = [self.script, self.hidden_inputs, self.action_buttons]
            if self.hints
        "Return all components for easy unpacking into render."

JavaScript Utilities (utils.ipynb)

Core JavaScript utility generators for keyboard navigation.

Import

from cjm_fasthtml_keyboard_navigation.js.utils import (
    js_config_from_dict,
    js_input_detection,
    js_focus_ring_helpers,
    js_scroll_into_view,
    js_hidden_input_update,
    js_trigger_click,
    js_get_data_attributes,
    js_get_modifiers,
    js_all_utils
)

Functions

def js_config_from_dict(
    config: dict[str, Any],  # Python dict to convert
    var_name: str = "cfg"    # JavaScript variable name
) -> str:                    # JavaScript const declaration
    "Generate JavaScript const declaration from Python dict."
def js_input_detection(
    selector: str = "input, textarea, select, [contenteditable='true']"  # CSS selector for input elements
) -> str:  # JavaScript function definition
    "Generate JavaScript function to detect if input element is focused."
def js_focus_ring_helpers(
    default_classes: tuple[str, ...] = (str(ring(2)), str(ring_dui.primary))  # default focus ring CSS classes
) -> str:  # JavaScript function definitions
    "Generate JavaScript functions for adding/removing focus ring classes."
def js_scroll_into_view(
    behavior: str = "smooth",  # "smooth" or "auto"
    block: str = "nearest"     # "start", "center", "end", "nearest"
) -> str:  # JavaScript function definition
    "Generate JavaScript function to scroll element into view."
def js_hidden_input_update() -> str: # JavaScript function definition
    "Generate JavaScript function to update hidden input values."
def js_trigger_click() -> str: # JavaScript function definition
    "Generate JavaScript function to programmatically click a button."
def js_get_data_attributes() -> str: # JavaScript function definition
    "Generate JavaScript function to extract data attributes from element."
def js_get_modifiers() -> str: # JavaScript function definition
    "Generate JavaScript function to extract modifier keys from event."
def js_all_utils(
    input_selector: str = "input, textarea, select, [contenteditable='true']",  # input element selector
    default_focus_classes: tuple[str, ...] = (str(ring(2)), str(ring_dui.primary)),  # focus ring classes
    scroll_behavior: str = "smooth",  # scroll behavior
    scroll_block: str = "nearest"     # scroll block alignment
) -> str:  # all utility functions combined
    "Generate all JavaScript utility functions."

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cjm_fasthtml_keyboard_navigation-0.0.21.tar.gz (66.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

File details

Details for the file cjm_fasthtml_keyboard_navigation-0.0.21.tar.gz.

File metadata

File hashes

Hashes for cjm_fasthtml_keyboard_navigation-0.0.21.tar.gz
Algorithm Hash digest
SHA256 ac9eff9891393420da37a5256e0241842b16815f7d9bb75ee097a45425975b01
MD5 7e5e63dccd8b338c7225e7e398206977
BLAKE2b-256 3280e94ee2847787c301a8034eb653c2ac3d09a0ffbe01180a966f38927ef06d

See more details on using hashes here.

File details

Details for the file cjm_fasthtml_keyboard_navigation-0.0.21-py3-none-any.whl.

File metadata

File hashes

Hashes for cjm_fasthtml_keyboard_navigation-0.0.21-py3-none-any.whl
Algorithm Hash digest
SHA256 f2b8e583d2eab4c2ec3cde8c76c53289e9efdfd6bd682f7ea9669f05f087c971
MD5 28b1ab0b0462d165790d9db333779fb4
BLAKE2b-256 0f4da2676330aa9ade4391fdb0a106207182ad377344990eb395f979f49c8c33

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page