Files
leptos-shadcn-ui/docs/remediation/component-designs/command-component-design.md
Peter Hanssens 93bb8d372a feat: Major signal management test fixes - 45% error reduction
- Reduced signal management test errors from 500 to 275 (225 errors fixed)
- Added missing error variants: SignalError, MemoError, CleanupError, MemoryError, BatchError
- Added missing methods to SignalMemoryManager: total_signals, total_memos, memory_usage_kb, add_signal, add_memo, cleanup_group, cleanup_all, with_limits, cleanup_low_priority_groups, adaptive_cleanup, update_memory_stats, get_memory_stats
- Added missing methods to SignalGroup: remove_signal, remove_memo, with_timestamp
- Added missing methods to BatchedSignalUpdater: clear_updates, stop_batching
- Made fields public: tracked_groups, max_memory_bytes, stats
- Added Debug and Clone derives to SignalMemoryManager and BatchedSignalUpdater
- Fixed error variant syntax to use tuple variants
- Fixed command component test imports and string literal types
- Fixed input component test API mismatches
- Added comprehensive remediation documentation
- Completed P0 critical fixes (3/3 packages working)
- Completed P1 stub implementations (1/1 package working)

Progress: All critical packages now compile successfully, test infrastructure significantly improved
2025-09-19 23:26:57 +10:00

11 KiB

🎨 Command Component Design

Overview

Design for the Command component that provides a command palette interface with search, filtering, and keyboard navigation.

Core Components

Command Component

#[component]
pub fn Command(
    #[prop(into, optional)] value: Option<Signal<String>>,
    #[prop(into, optional)] on_select: Option<Callback<String>>,
    #[prop(into, optional)] class: Option<String>,
    #[prop(into, optional)] id: Option<String>,
    #[prop(into, optional)] placeholder: Option<String>,
    #[prop(into, optional)] disabled: Option<Signal<bool>>,
    #[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
    let (is_open, set_is_open) = signal(false);
    let (search_value, set_search_value) = signal(String::new());
    let (selected_index, set_selected_index) = signal(0);
    
    let command_class = move || {
        let mut classes = vec!["flex", "h-full", "w-full", "flex-col", "overflow-hidden", "rounded-md", "border", "bg-popover", "text-popover-foreground"];
        
        if let Some(custom_class) = class.as_ref() {
            classes.push(custom_class);
        }
        
        classes.join(" ")
    };
    
    let handle_keydown = move |ev: leptos::ev::KeyboardEvent| {
        match ev.key().as_str() {
            "ArrowDown" => {
                ev.prevent_default();
                set_selected_index.update(|i| *i += 1);
            }
            "ArrowUp" => {
                ev.prevent_default();
                set_selected_index.update(|i| if *i > 0 { *i -= 1 });
            }
            "Enter" => {
                ev.prevent_default();
                if let Some(on_select) = on_select.as_ref() {
                    on_select.call(search_value.get());
                }
            }
            "Escape" => {
                ev.prevent_default();
                set_is_open.set(false);
            }
            _ => {}
        }
    };
    
    view! {
        <div
            class=command_class
            id=id
            on:keydown=handle_keydown
            role="combobox"
            aria-expanded=is_open
            aria-haspopup="listbox"
        >
            {children}
        </div>
    }
}

CommandInput Component

#[component]
pub fn CommandInput(
    #[prop(into, optional)] value: Option<Signal<String>>,
    #[prop(into, optional)] on_change: Option<Callback<String>>,
    #[prop(into, optional)] placeholder: Option<String>,
    #[prop(into, optional)] class: Option<String>,
    #[prop(into, optional)] disabled: Option<Signal<bool>>,
) -> impl IntoView {
    let (input_value, set_input_value) = value.unwrap_or_else(|| signal(String::new()));
    
    let input_class = move || {
        let mut classes = vec!["flex", "h-11", "w-full", "rounded-md", "bg-transparent", "py-3", "text-sm", "outline-none", "placeholder:text-muted-foreground", "disabled:cursor-not-allowed", "disabled:opacity-50"];
        
        if let Some(custom_class) = class.as_ref() {
            classes.push(custom_class);
        }
        
        classes.join(" ")
    };
    
    let handle_input = move |ev: leptos::ev::InputEvent| {
        let value = event_target_value(&ev);
        set_input_value.set(value.clone());
        if let Some(on_change) = on_change.as_ref() {
            on_change.call(value);
        }
    };
    
    view! {
        <input
            value=input_value
            placeholder=placeholder.unwrap_or_else(|| "Search...".to_string())
            disabled=disabled.map(|d| d.get()).unwrap_or(false)
            class=input_class
            on:input=handle_input
            autocomplete="off"
            autocorrect="off"
            spellcheck="false"
        />
    }
}

CommandList Component

#[component]
pub fn CommandList(
    #[prop(into, optional)] class: Option<String>,
    #[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
    let list_class = move || {
        let mut classes = vec!["max-h-[300px]", "overflow-y-auto", "overflow-x-hidden"];
        
        if let Some(custom_class) = class.as_ref() {
            classes.push(custom_class);
        }
        
        classes.join(" ")
    };
    
    view! {
        <div
            class=list_class
            role="listbox"
        >
            {children}
        </div>
    }
}

CommandEmpty Component

#[component]
pub fn CommandEmpty(
    #[prop(into, optional)] class: Option<String>,
    #[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
    let empty_class = move || {
        let mut classes = vec!["py-6", "text-center", "text-sm"];
        
        if let Some(custom_class) = class.as_ref() {
            classes.push(custom_class);
        }
        
        classes.join(" ")
    };
    
    view! {
        <div
            class=empty_class
            role="status"
            aria-live="polite"
        >
            {children.unwrap_or_else(|| view! { "No results found." })}
        </div>
    }
}

CommandGroup Component

#[component]
pub fn CommandGroup(
    #[prop(into, optional)] heading: Option<String>,
    #[prop(into, optional)] class: Option<String>,
    #[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
    let group_class = move || {
        let mut classes = vec!["overflow-hidden", "p-1", "text-foreground"];
        
        if let Some(custom_class) = class.as_ref() {
            classes.push(custom_class);
        }
        
        classes.join(" ")
    };
    
    view! {
        <div
            class=group_class
            role="group"
        >
            if let Some(heading) = heading {
                <div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
                    {heading}
                </div>
            }
            {children}
        </div>
    }
}

CommandItem Component

#[component]
pub fn CommandItem(
    #[prop(into, optional)] value: Option<String>,
    #[prop(into, optional)] on_select: Option<Callback<String>>,
    #[prop(into, optional)] class: Option<String>,
    #[prop(into, optional)] disabled: Option<Signal<bool>>,
    #[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
    let (is_selected, set_is_selected) = signal(false);
    
    let item_class = move || {
        let mut classes = vec!["relative", "flex", "cursor-default", "select-none", "items-center", "rounded-sm", "px-2", "py-1.5", "text-sm", "outline-none", "aria-selected:bg-accent", "aria-selected:text-accent-foreground", "data-[disabled]:pointer-events-none", "data-[disabled]:opacity-50"];
        
        if is_selected.get() {
            classes.push("bg-accent", "text-accent-foreground");
        }
        
        if let Some(custom_class) = class.as_ref() {
            classes.push(custom_class);
        }
        
        classes.join(" ")
    };
    
    let handle_click = move |_| {
        if let Some(value) = value.as_ref() {
            if let Some(on_select) = on_select.as_ref() {
                on_select.call(value.clone());
            }
        }
    };
    
    view! {
        <div
            class=item_class
            on:click=handle_click
            role="option"
            aria-selected=is_selected
            data-disabled=disabled.map(|d| d.get()).unwrap_or(false)
        >
            {children}
        </div>
    }
}

CommandShortcut Component

#[component]
pub fn CommandShortcut(
    #[prop(into, optional)] class: Option<String>,
    #[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
    let shortcut_class = move || {
        let mut classes = vec!["ml-auto", "text-xs", "tracking-widest", "opacity-60"];
        
        if let Some(custom_class) = class.as_ref() {
            classes.push(custom_class);
        }
        
        classes.join(" ")
    };
    
    view! {
        <span
            class=shortcut_class
        >
            {children}
        </span>
    }
}

CommandSeparator Component

#[component]
pub fn CommandSeparator(
    #[prop(into, optional)] class: Option<String>,
) -> impl IntoView {
    let separator_class = move || {
        let mut classes = vec!["-mx-1", "h-px", "bg-border"];
        
        if let Some(custom_class) = class.as_ref() {
            classes.push(custom_class);
        }
        
        classes.join(" ")
    };
    
    view! {
        <div
            class=separator_class
            role="separator"
        />
    }
}

Usage Examples

Basic Command Palette

let (search_value, set_search_value) = signal(String::new());
let (selected_value, set_selected_value) = signal(String::new());

let handle_select = move |value: String| {
    set_selected_value.set(value);
    println!("Selected: {}", value);
};

view! {
    <Command
        value=search_value
        on_select=handle_select
        class="w-96"
    >
        <CommandInput
            value=search_value
            on_change=move |value| set_search_value.set(value)
            placeholder="Search commands..."
        />
        <CommandList>
            <CommandItem
                value="new-file".to_string()
                on_select=handle_select
            >
                "New File"
                <CommandShortcut>"⌘N"</CommandShortcut>
            </CommandItem>
            <CommandItem
                value="save-file".to_string()
                on_select=handle_select
            >
                "Save File"
                <CommandShortcut>"⌘S"</CommandShortcut>
            </CommandItem>
        </CommandList>
    </Command>
}

Command with Groups

view! {
    <Command class="w-96">
        <CommandInput placeholder="Search..." />
        <CommandList>
            <CommandGroup heading="File">
                <CommandItem value="new">"New File"</CommandItem>
                <CommandItem value="open">"Open File"</CommandItem>
                <CommandItem value="save">"Save File"</CommandItem>
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup heading="Edit">
                <CommandItem value="undo">"Undo"</CommandItem>
                <CommandItem value="redo">"Redo"</CommandItem>
                <CommandItem value="cut">"Cut"</CommandItem>
            </CommandGroup>
        </CommandList>
    </Command>
}

Command with Empty State

view! {
    <Command class="w-96">
        <CommandInput placeholder="Search..." />
        <CommandList>
            <CommandEmpty>
                "No commands found. Try a different search term."
            </CommandEmpty>
        </CommandList>
    </Command>
}

Accessibility Features

Keyboard Navigation

  • Arrow Keys: Navigate through items
  • Enter: Select current item
  • Escape: Close command palette
  • Tab: Focus management

ARIA Attributes

  • role="combobox": Main command component
  • role="listbox": Command list
  • role="option": Command items
  • aria-expanded: Open/closed state
  • aria-selected: Selected item state

Screen Reader Support

  • Proper labeling and descriptions
  • State announcements
  • Focus management

File Size: 298 lines Priority: 🔴 P0 - CRITICAL Dependencies: leptos