orbital-base-components 0.1.1

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

use super::types::{SwatchPickerLayout, SwatchPickerShape, SwatchPickerSize};

#[derive(Clone)]
pub struct SwatchItemRegistration {
    pub value: String,
    pub disabled: Signal<bool>,
    pub button_ref: NodeRef<html::Button>,
}

#[derive(Clone)]
pub struct SwatchPickerInjection {
    pub selected_value: RwSignal<Option<String>>,
    pub registered_items: RwSignal<HashMap<String, SwatchItemRegistration>>,
    pub item_order: RwSignal<Vec<String>>,
    pub layout: Signal<SwatchPickerLayout>,
    pub shape: Signal<SwatchPickerShape>,
    pub size: Signal<SwatchPickerSize>,
    pub on_selection_change: Option<Callback<String>>,
}

impl SwatchPickerInjection {
    pub fn expect_context() -> Self {
        expect_context()
    }

    pub fn register_item(&self, registration: SwatchItemRegistration) {
        let value = registration.value.clone();
        self.item_order.update(|order| {
            if !order.contains(&value) {
                order.push(value.clone());
            }
        });
        self.registered_items.update(|items| {
            items.insert(value, registration);
        });
    }

    pub fn unregister_item(&self, value: &str) {
        self.item_order.update(|order| {
            order.retain(|entry| entry != value);
        });
        self.registered_items.update(|items| {
            items.remove(value);
        });
    }

    pub fn is_selected(&self, value: &str) -> bool {
        self.selected_value
            .with(|selected| selected.as_deref() == Some(value))
    }

    pub fn select(&self, value: String) {
        if self.registered_items.with_untracked(|items| {
            items
                .get(&value)
                .is_some_and(|item| item.disabled.get_untracked())
        }) {
            return;
        }

        self.selected_value.set(Some(value.clone()));
        if let Some(callback) = self.on_selection_change {
            callback.run(value);
        }
    }

    pub fn ordered_items(&self) -> Vec<SwatchItemRegistration> {
        self.item_order.with(|order| {
            self.registered_items.with(|items| {
                order
                    .iter()
                    .filter_map(|value| items.get(value).cloned())
                    .collect()
            })
        })
    }

    pub fn focus_item(&self, value: &str) {
        if let Some(item) = self
            .registered_items
            .with_untracked(|items| items.get(value).cloned())
        {
            if let Some(button) = item.button_ref.get_untracked() {
                let _ = button.focus();
            }
        }
    }

    pub fn select_adjacent(&self, direction: i32) {
        let items = self.ordered_items();
        if items.is_empty() {
            return;
        }

        let enabled: Vec<_> = items
            .into_iter()
            .filter(|item| !item.disabled.get_untracked())
            .collect();
        if enabled.is_empty() {
            return;
        }

        let current = self.selected_value.get_untracked();
        let current_index = current
            .as_ref()
            .and_then(|value| enabled.iter().position(|item| &item.value == value))
            .unwrap_or(0);

        let next_index = if direction < 0 {
            if current_index == 0 {
                enabled.len() - 1
            } else {
                current_index - 1
            }
        } else if current_index + 1 >= enabled.len() {
            0
        } else {
            current_index + 1
        };

        let next_value = enabled[next_index].value.clone();
        self.select(next_value.clone());
        self.focus_item(&next_value);
    }

    pub fn select_endpoint(&self, first: bool) {
        let items = self.ordered_items();
        let target = if first {
            items
                .into_iter()
                .find(|item| !item.disabled.get_untracked())
        } else {
            items
                .into_iter()
                .rev()
                .find(|item| !item.disabled.get_untracked())
        };

        if let Some(item) = target {
            self.select(item.value.clone());
            self.focus_item(&item.value);
        }
    }
}