orbital-base-components 0.1.1

Headless base primitives for Orbital — structure, semantics, and accessibility
Documentation
use leptos::{ev, prelude::*};

use super::item_state::CollectionItemSignals;
use crate::collection::state::{
    collection_keyboard_action, typeahead_match, CollectionKeyboardAction, CollectionSelectionMode,
    CollectionState,
};

pub fn use_item_keyboard(
    state: CollectionState,
    item_id: StoredValue<String>,
    is_branch: bool,
    signals: CollectionItemSignals,
    on_select: impl Fn() + 'static + Clone,
) -> impl Fn(ev::KeyboardEvent) + Clone {
    move |event: ev::KeyboardEvent| {
        if signals.disabled.get_untracked() && !state.disabled_items_focusable.get_untracked() {
            return;
        }

        let action = collection_keyboard_action(&event);
        let id = item_id.get_value();
        match action {
            CollectionKeyboardAction::Select => {
                event.prevent_default();
                on_select();
            }
            CollectionKeyboardAction::Expand => {
                if is_branch {
                    event.prevent_default();
                    state.expansion.set_expansion(id.clone(), true);
                }
            }
            CollectionKeyboardAction::Collapse => {
                if is_branch {
                    event.prevent_default();
                    state.expansion.set_expansion(id.clone(), false);
                }
            }
            CollectionKeyboardAction::Next
            | CollectionKeyboardAction::Previous
            | CollectionKeyboardAction::Home
            | CollectionKeyboardAction::End => {
                event.prevent_default();
                focus_relative_item(&state, &id, action);
            }
            CollectionKeyboardAction::TypeAhead => {
                event.prevent_default();
                typeahead_to_item(&state, event.key());
            }
            CollectionKeyboardAction::None => {}
        }
    }
}

pub fn default_select_action(state: &CollectionState, item_id: &str, is_branch: bool) {
    if state.selection.mode != CollectionSelectionMode::None {
        state.selection.select_item(
            item_id.to_string(),
            false,
            Some(true),
            None,
            Some(&state.registry),
        );
    } else if is_branch {
        state.expansion.toggle(item_id.to_string(), true);
    }
}

fn focus_relative_item(state: &CollectionState, item_id: &str, action: CollectionKeyboardAction) {
    let ordered = state.visible_ordered_ids();
    let Some(index) = ordered.iter().position(|id| id == item_id) else {
        return;
    };
    let next = match action {
        CollectionKeyboardAction::Next => (index + 1).min(ordered.len().saturating_sub(1)),
        CollectionKeyboardAction::Previous => index.saturating_sub(1),
        CollectionKeyboardAction::Home => 0,
        CollectionKeyboardAction::End => ordered.len().saturating_sub(1),
        _ => index,
    };
    if let Some(next_id) = ordered.get(next) {
        state.focus.focus_item(next_id.clone());
        state.dom_registry.focus_dom_element(next_id);
    }
}

fn typeahead_to_item(state: &CollectionState, key: String) {
    let prefix = state.focus.append_typeahead(key);
    let visible = state.visible_ordered_ids();
    let start_after = state.focus.focused_item.get_untracked();
    if let Some(next_id) =
        typeahead_match(&state.registry, &visible, &prefix, start_after.as_deref())
    {
        state.focus.focus_item(next_id.clone());
        state.dom_registry.focus_dom_element(&next_id);
    }
}