Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions app_crates/registry/src/demos/demo_stepper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use leptos::prelude::*;

use crate::ui::stepper::{
Stepper, StepperDescription, StepperIndicator, StepperItem, StepperSeparator, StepperTitle, StepperTrigger,
};

#[component]
pub fn DemoStepper() -> impl IntoView {
view! {
<Stepper total_steps=3 default_step=1 class="w-full max-w-md">
<StepperItem step=0>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Account"</StepperTitle>
<StepperDescription>"Create your account"</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step=1>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Profile"</StepperTitle>
<StepperDescription>"Complete your profile"</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step=2>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Confirmation"</StepperTitle>
<StepperDescription>"Review and confirm"</StepperDescription>
</div>
</StepperTrigger>
</StepperItem>
</Stepper>
}
}
61 changes: 61 additions & 0 deletions app_crates/registry/src/demos/demo_stepper_controlled.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use leptos::prelude::*;

use crate::hooks::use_stepper::StepperContext;
use crate::ui::button::Button;
use crate::ui::stepper::{
Stepper, StepperDescription, StepperIndicator, StepperItem, StepperSeparator, StepperTitle, StepperTrigger,
};

#[component]
fn StepperControls() -> impl IntoView {
let ctx = expect_context::<StepperContext>();

view! {
<div class="flex gap-2 justify-end mt-6">
<Button on:click=move |_| ctx.go_prev.run(()) attr:disabled=move || !ctx.can_go_prev.get()>
"Previous"
</Button>
<Button on:click=move |_| ctx.go_next.run(()) attr:disabled=move || !ctx.can_go_next.get()>
"Next"
</Button>
</div>
}
}

#[component]
pub fn DemoStepperControlled() -> impl IntoView {
view! {
<Stepper total_steps=3 class="w-full max-w-md">
<StepperItem step=0>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Account"</StepperTitle>
<StepperDescription>"Create your account"</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step=1>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Profile"</StepperTitle>
<StepperDescription>"Complete your profile"</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step=2>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Confirmation"</StepperTitle>
<StepperDescription>"Review and confirm"</StepperDescription>
</div>
</StepperTrigger>
</StepperItem>
<StepperControls />
</Stepper>
}
}
43 changes: 43 additions & 0 deletions app_crates/registry/src/demos/demo_stepper_vertical.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use leptos::prelude::*;

use crate::ui::stepper::{
Stepper, StepperDescription, StepperIndicator, StepperItem, StepperOrientation, StepperSeparator, StepperTitle,
StepperTrigger,
};

#[component]
pub fn DemoStepperVertical() -> impl IntoView {
view! {
<Stepper total_steps=3 default_step=1 orientation=StepperOrientation::Vertical class="w-full max-w-xs">
<StepperItem step=0>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Account"</StepperTitle>
<StepperDescription>"Create your account"</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step=1>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Profile"</StepperTitle>
<StepperDescription>"Complete your profile"</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step=2>
<StepperTrigger>
<StepperIndicator />
<div class="flex flex-col gap-0.5">
<StepperTitle>"Confirmation"</StepperTitle>
<StepperDescription>"Review and confirm"</StepperDescription>
</div>
</StepperTrigger>
</StepperItem>
</Stepper>
}
}
3 changes: 3 additions & 0 deletions app_crates/registry/src/demos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ pub mod demo_spinner_button;
pub mod demo_spinner_rtl;
pub mod demo_status;
pub mod demo_status_variants;
pub mod demo_stepper;
pub mod demo_stepper_controlled;
pub mod demo_stepper_vertical;
pub mod demo_switch;
pub mod demo_switch_choice_card;
pub mod demo_switch_rtl;
Expand Down
1 change: 1 addition & 0 deletions app_crates/registry/src/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ pub mod use_pagination;
pub mod use_press_hold;
pub mod use_random;
pub mod use_scroll_lock;
pub mod use_stepper;
pub mod use_theme_mode;
pub mod use_virtual_scroll;
79 changes: 79 additions & 0 deletions app_crates/registry/src/hooks/use_stepper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use leptos::prelude::*;

/// Visual/interactive state of a single step, relative to the current index.
///
/// `Completed`/`Active`/`Pending` are derived automatically from index
/// comparison. `Disabled` is not produced by this hook — it's applied by
/// the caller (e.g. `StepperItem`) on top of the computed state, since it
/// depends on external conditions the hook has no visibility into.
#[derive(Clone, Copy, PartialEq, Eq, strum::Display)]
pub enum StepState {
Completed,
Active,
Pending,
Disabled,
}

/// Shared reactive state for a `Stepper` instance, provided via
/// `provide_context` and consumed by `StepperItem`/`StepperTrigger`.
#[derive(Clone)]
pub struct StepperContext {
pub current_index: RwSignal<usize>,
pub total_steps: usize,
pub can_go_prev: Signal<bool>,
pub can_go_next: Signal<bool>,
pub go_next: Callback<(), ()>,
pub go_prev: Callback<(), ()>,
pub go_to: Callback<usize, ()>,
pub step_state: Callback<usize, StepState>,
}

/// Builds the controlled navigation state for a stepper with `total_steps`
/// steps, starting at `default_index`.
///
/// All navigation methods (`go_next`, `go_prev`, `go_to`) clamp to
/// `[0, total_steps)`, so `current_index` can never be set out of range —
/// callers indexing a step list with it don't need to re-validate.
pub fn use_stepper(total_steps: usize, default_index: usize) -> StepperContext {
// Clamp in case `default_index` is out of range (e.g. caller passes total_steps itself).
let current_index = RwSignal::new(default_index.min(total_steps.saturating_sub(1)));

// Reactive rather than one-shot, so nav buttons can bind `disabled` directly
// and re-evaluate whenever `current_index` changes.
let can_go_prev = Signal::derive(move || current_index.get() > 0);
let can_go_next = Signal::derive(move || current_index.get() + 1 < total_steps);

let go_prev = Callback::new(move |_| {
if current_index.get() > 0 {
current_index.update(|i| *i -= 1);
}
});

let go_next = Callback::new(move |_| {
if current_index.get() + 1 < total_steps {
current_index.update(|i| *i += 1);
}
});

// Backs clickable step triggers — jumps straight to an arbitrary index
// rather than stepping by one, so out-of-range values need their own guard.
let go_to = Callback::new(move |index: usize| {
if index < total_steps {
current_index.set(index);
}
});

// Maps the issue's three-way rule (step < current -> completed, == -> active,
// > -> pending) onto Ordering so it reads as one exhaustive match.
let step_state = Callback::new(move |step: usize| {
let current = current_index.get();

match step.cmp(&current) {
std::cmp::Ordering::Less => StepState::Completed,
std::cmp::Ordering::Equal => StepState::Active,
std::cmp::Ordering::Greater => StepState::Pending,
}
});

StepperContext { current_index, total_steps, can_go_prev, can_go_next, go_next, go_prev, go_to, step_state }
}
1 change: 1 addition & 0 deletions app_crates/registry/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub mod slider;
pub mod sonner;
pub mod spinner;
pub mod status;
pub mod stepper;
pub mod switch;
pub mod table;
pub mod tabs;
Expand Down
Loading