hikari-components 0.1.5

Core UI components (40+) for the Hikari design system
// hi-components/src/data/cell.rs
// Cell component for table cells

use std::rc::Rc;

use hikari_palette::classes::CellClass;
use tairitsu_style::ClassesBuilder;

use super::column::ColumnDef;
use crate::prelude::*;

#[derive(Clone, Props, Default)]
pub struct CellProps {
    #[props(default)]
    pub value: String,

    pub column: ColumnDef,

    #[props(default)]
    pub row_index: usize,

    #[props(default)]
    pub col_index: usize,

    #[props(default)]
    pub class: String,

    #[props(default)]
    pub render: Option<CellRenderer>,

    #[props(default)]
    pub editable: bool,
}

impl PartialEq for CellProps {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value
            && self.column == other.column
            && self.row_index == other.row_index
            && self.col_index == other.col_index
            && self.class == other.class
            && self.editable == other.editable
    }
}

impl std::fmt::Debug for CellProps {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CellProps")
            .field("value", &self.value)
            .field("column", &self.column)
            .field("row_index", &self.row_index)
            .field("col_index", &self.col_index)
            .field("class", &self.class)
            .field("render", &self.render.is_some())
            .field("editable", &self.editable)
            .finish()
    }
}

/// Table cell component
///
/// Renders a single table cell with support for custom rendering and alignment.
#[component]
pub fn Cell(props: CellProps) -> Element {
    let align_class = match props.column.align {
        super::column::ColumnAlign::Left => CellClass::AlignLeft,
        super::column::ColumnAlign::Center => CellClass::AlignCenter,
        super::column::ColumnAlign::Right => CellClass::AlignRight,
    };

    let classes = ClassesBuilder::new()
        .add_typed(CellClass::Cell)
        .add_typed(align_class)
        .add_typed(CellClass::CellHover)
        .add_typed_if(CellClass::CellEditable, props.editable)
        .add(&props.class)
        .build();

    // Use custom render callback if provided
    if let Some(render_fn) = &props.render {
        let element = render_fn(&props.value, props.row_index, props.col_index);
        return rsx! {
            td {
                class: classes,
                "data-row-index": "{props.row_index}",
                "data-col-index": "{props.col_index}",
                "data-key": props.column.column_key,
                {element}
            }
        };
    }

    // Default rendering
    let value = props.value.clone();
    rsx! {
        td {
            class: classes,
            "data-row-index": "{props.row_index}",
            "data-col-index": "{props.col_index}",
            "data-key": props.column.column_key,
            "data-editable": "{props.editable}",

            "{value}"
        }
    }
}

pub type CellRenderer = Rc<dyn Fn(&str, usize, usize) -> Element>;

/// Creates a cell renderer from a closure
///
/// This function wraps a closure in an Rc for use as a cell renderer.
pub fn create_cell_renderer<F>(f: F) -> CellRenderer
where
    F: Fn(&str, usize, usize) -> Element + 'static,
{
    Rc::new(f)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cell_props_default() {
        let props = CellProps::default();
        assert_eq!(props.value, String::new());
        assert_eq!(props.row_index, 0);
        assert_eq!(props.col_index, 0);
        assert!(props.class.is_empty());
        assert!(props.render.is_none());
        assert!(!props.editable);
    }

    #[test]
    fn test_cell_props_value() {
        let props = CellProps {
            value: "test value".to_string(),
            ..Default::default()
        };
        assert_eq!(props.value, "test value");
    }

    #[test]
    fn test_cell_props_indices() {
        let props = CellProps {
            row_index: 5,
            col_index: 3,
            ..Default::default()
        };
        assert_eq!(props.row_index, 5);
        assert_eq!(props.col_index, 3);
    }

    #[test]
    fn test_cell_props_editable() {
        let props1 = CellProps {
            editable: false,
            ..Default::default()
        };

        let props2 = CellProps {
            editable: true,
            ..Default::default()
        };

        assert!(!props1.editable);
        assert!(props2.editable);
    }

    #[test]
    fn test_cell_props_class() {
        let props = CellProps {
            class: "custom-cell".to_string(),
            ..Default::default()
        };
        assert_eq!(props.class, "custom-cell");
    }

    #[test]
    fn test_cell_props_clone() {
        let render = create_cell_renderer(|_, _, _| {
            rsx! {
                span { "rendered" }
            }
        });
        let props = CellProps {
            value: "test".to_string(),
            render: Some(render),
            ..Default::default()
        };

        let cloned = props.clone();
        assert_eq!(cloned.value, "test");
        assert!(cloned.render.is_some());
    }

    #[test]
    fn test_cell_props_partial_eq() {
        let render = create_cell_renderer(|_, _, _| {
            rsx! {
                span { "rendered" }
            }
        });

        let props1 = CellProps {
            value: "test".to_string(),
            render: Some(render.clone()),
            ..Default::default()
        };

        let props2 = CellProps {
            value: "test".to_string(),
            render: Some(render),
            ..Default::default()
        };

        assert_eq!(props1, props2);
    }

    #[test]
    fn test_cell_props_not_equal() {
        let props1 = CellProps {
            value: "value1".to_string(),
            ..Default::default()
        };

        let props2 = CellProps {
            value: "value2".to_string(),
            ..Default::default()
        };

        assert_ne!(props1, props2);
    }

    #[test]
    fn test_cell_props_inequality_with_value() {
        let props1 = CellProps {
            value: "test".to_string(),
            ..Default::default()
        };

        let props2 = CellProps {
            value: "different".to_string(),
            ..Default::default()
        };

        assert_ne!(props1, props2);
    }

    #[test]
    fn test_cell_props_inequality_with_column() {
        let column1 = ColumnDef {
            column_key: "name1".to_string(),
            ..Default::default()
        };

        let column2 = ColumnDef {
            column_key: "name2".to_string(),
            ..Default::default()
        };

        let props1 = CellProps {
            column: column1.clone(),
            ..Default::default()
        };

        let props2 = CellProps {
            column: column2,
            ..Default::default()
        };

        assert_ne!(props1, props2);
    }

    #[test]
    fn test_cell_renderer_creation() {
        let renderer = create_cell_renderer(|_, _, _| {
            rsx! {
                span { "test" }
            }
        });
        assert!(Rc::strong_count(&renderer) > 0);
    }
}