diff --git a/Cargo.toml b/Cargo.toml index ce06992c..d37ce921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,34 +1,57 @@ [workspace] -# Temporarily disabled to upgrade individual packages to Leptos 0.7. -# members = [ -# "book-examples/*/*", -# "packages/colors", -# "packages/icons/*", -# "packages/primitives/*/*", -# "packages/themes/*", -# "scripts", -# "stories/*", -# ] +# Temporarily disabled subcrates to be upgraded individually to Leptos 0.7. +# Once a crate is ready, uncomment it here. members = [ "book-examples/*/*", "packages/colors", - "packages/icons/*", - "packages/primitives/leptos/accessible-icon", - "packages/primitives/leptos/arrow", - "packages/primitives/leptos/aspect-ratio", - "packages/primitives/leptos/direction", - "packages/primitives/leptos/id", - "packages/primitives/leptos/label", - "packages/primitives/leptos/use-controllable-state", - "packages/primitives/leptos/use-escape-keydown", - "packages/primitives/leptos/use-previous", - "packages/primitives/leptos/use-size", - "packages/primitives/leptos/visually-hidden", + "packages/icons/dioxus", + "packages/icons/yew", + + # -- Leptos Primitives (commented until they're upgraded) -- + # "packages/primitives/leptos/accessible-icon", + # "packages/primitives/leptos/arrow", + # "packages/primitives/leptos/aspect-ratio", + # "packages/primitives/leptos/avatar", + # "packages/primitives/leptos/checkbox", + "packages/primitives/leptos/context", + # "packages/primitives/leptos/collection", + # "packages/primitives/leptos/compose-refs", + # "packages/primitives/leptos/direction", + # "packages/primitives/leptos/dismissable-layer", + # "packages/primitives/leptos/dropdown-menu", + # "packages/primitives/leptos/focus-guards", + # "packages/primitives/leptos/focus-scope", + # "packages/primitives/leptos/id", + # "packages/primitives/leptos/label", + # "packages/primitives/leptos/menu", + # "packages/primitives/leptos/popover", + # "packages/primitives/leptos/popper", + # "packages/primitives/leptos/portal", + # "packages/primitives/leptos/presence", + "packages/primitives/leptos/primitive", + "packages/primitives/leptos/progress", + # "packages/primitives/leptos/roving-focus", + # "packages/primitives/leptos/select", + # "packages/primitives/leptos/separator", + # "packages/primitives/leptos/slot", + # "packages/primitives/leptos/switch", + # "packages/primitives/leptos/tabs", + # "packages/primitives/leptos/toggle", + # "packages/primitives/leptos/use-controllable-state", + # "packages/primitives/leptos/use-escape-keydown", + # "packages/primitives/leptos/use-previous", + # "packages/primitives/leptos/use-size", + # "packages/primitives/leptos/visually-hidden", + + # -- Yew Primitives -- "packages/primitives/yew/*", + + # -- Themes, Scripts, and Stories -- "packages/themes/yew", "scripts", "stories/*", ] + resolver = "2" [workspace.package] @@ -39,14 +62,17 @@ repository = "https://github.com/RustForWeb/radix" version = "0.0.2" [workspace.dependencies] -console_log = "1.0.0" console_error_panic_hook = "0.1.7" +console_log = "1.0.0" dioxus = "0.6.1" leptos = "0.7.2" leptos_dom = "0.7.2" leptos_router = "0.7.2" leptos-node-ref = "0.0.3" +leptos-maybe-callback = "0.0.3" leptos-style = "0.0.3" +leptos-typed-fallback-show = "0.0.3" +leptos-use = "0.15.2" log = "0.4.22" send_wrapper = "0.6.0" serde = "1.0.198" @@ -58,6 +84,44 @@ yew-router = "0.18.0" yew-struct-component = "0.1.4" yew-style = "0.1.4" +# Subcrate packages (paths remain the same; you can comment out any subcrate not yet upgraded). +# We centralize shared dependencies in [workspace.dependencies] for a single source of truth, +# reducing duplication, preventing version drift, and keeping the Cargo.lock consistent. +#radix-leptos-arrow.path = "./packages/primitives/leptos/arrow" +#radix-leptos-aspect-ratio.path = "./packages/primitives/leptos/aspect-ratio" +#radix-leptos-accessible-icon.path = "./packages/primitives/leptos/accessible-icon" +#radix-leptos-avatar.path = "./packages/primitives/leptos/avatar" +#radix-leptos-checkbox.path = "./packages/primitives/leptos/checkbox" +radix-leptos-context.path = "./packages/primitives/leptos/context" +#radix-leptos-collection.path = "./packages/primitives/leptos/collection" +#radix-leptos-compose-refs.path = "./packages/primitives/leptos/compose-refs" +#radix-leptos-direction.path = "./packages/primitives/leptos/direction" +#radix-leptos-dismissable-layer.path = "./packages/primitives/leptos/dismissable-layer" +#radix-leptos-dropdown-menu.path = "./packages/primitives/leptos/dropdown-menu" +#radix-leptos-focus-guards.path = "./packages/primitives/leptos/focus-guards" +#radix-leptos-focus-scope.path = "./packages/primitives/leptos/focus-scope" +#radix-leptos-id.path = "./packages/primitives/leptos/id" +#radix-leptos-label.path = "./packages/primitives/leptos/label" +#radix-leptos-menu.path = "./packages/primitives/leptos/menu" +#radix-leptos-popper.path = "./packages/primitives/leptos/popper" +#radix-leptos-portal.path = "./packages/primitives/leptos/portal" +#radix-leptos-presence.path = "./packages/primitives/leptos/presence" +radix-leptos-primitive.path = "./packages/primitives/leptos/primitive" +radix-leptos-progress.path = "./packages/primitives/leptos/progress" +#radix-leptos-roving-focus.path = "./packages/primitives/leptos/roving-focus" +#radix-leptos-select.path = "./packages/primitives/leptos/select" +#radix-leptos-separator.path = "./packages/primitives/leptos/separator" +#radix-leptos-slot.path = "./packages/primitives/leptos/slot" +#radix-leptos-switch.path = "./packages/primitives/leptos/switch" +#radix-leptos-tabs.path = "./packages/primitives/leptos/tabs" +#radix-leptos-toggle.path = "./packages/primitives/leptos/toggle" +#radix-leptos-use-controllable-state.path = "./packages/primitives/leptos/use-controllable-state" +#radix-leptos-use-escape-keydown.path = "./packages/primitives/leptos/use-escape-keydown" +#radix-leptos-use-previous.path = "./packages/primitives/leptos/use-previous" +#radix-leptos-use-size.path = "./packages/primitives/leptos/use-size" +#radix-leptos-visually-hidden.path = "./packages/primitives/leptos/visually-hidden" + [patch.crates-io] yew = { git = "https://github.com/RustForWeb/yew.git", branch = "feature/use-composed-ref" } yew-router = { git = "https://github.com/RustForWeb/yew.git", branch = "feature/use-composed-ref" } +leptos-node-ref = { git = "https://github.com/geoffreygarrett/leptos-utils", branch = "feature/any-node-ref" } diff --git a/book/src/primitives/components/progress.md b/book/src/primitives/components/progress.md index a3b0d1d2..a6ed6d45 100644 --- a/book/src/primitives/components/progress.md +++ b/book/src/primitives/components/progress.md @@ -16,7 +16,7 @@ files = ["src/progress.rs"] ## Features -- Provides context for assistive technology to read the progress of a task. +- Provides context for assistive technology to read the progress of a task. ## Installation @@ -29,9 +29,9 @@ Install the component from your command line. cargo add radix-leptos-progress ``` -- [View on crates.io](https://crates.io/crates/radix-leptos-progress) -- [View on docs.rs](https://docs.rs/radix-leptos-progress/latest/radix_leptos_progress/) -- [View source](https://github.com/RustForWeb/radix/tree/main/packages/primitives/leptos/progress) +- [View on crates.io](https://crates.io/crates/radix-leptos-progress) +- [View on docs.rs](https://docs.rs/radix-leptos-progress/latest/radix_leptos_progress/) +- [View source](https://github.com/RustForWeb/radix/tree/main/packages/primitives/leptos/progress) {{#endtab }} {{#endtabs }} @@ -45,14 +45,14 @@ Import all parts and piece them together. ```rust,ignore use leptos::*; -use radix_leptos_progress::*; +use radix_leptos_progress::primitive as Progress; #[component] fn Anatomy() -> impl IntoView { view! { - - - + + + } } ``` @@ -70,11 +70,11 @@ Contains all of the progress parts. {{#tab name="Leptos" }} | Prop | Type | Default | -| ----------------- | -------------------------------------- | ------- | +|-------------------|----------------------------------------|---------| | `as_child` | `MaybeProp` | `false` | -| `value` | `MaybeProp` | - | +| `value` | `MaybeProp` | – | | `max` | `MaybeProp` | `100.0` | -| `get_value_label` | `Option>` | - | +| `get_value_label` | `Option>` | – | {{#endtab }} {{#endtabs }} @@ -82,7 +82,7 @@ Contains all of the progress parts.
| Data attribute | Values | -| -------------- | -------------------------------------------- | +|----------------|----------------------------------------------| | `[data-state]` | `"complete" \| "indeterminate" \| "loading"` | | `[data-value]` | The current value | | `[data-max]` | The max value | @@ -95,7 +95,7 @@ Used to show the progress visually. It also makes progress accessible to assisti {{#tab name="Leptos" }} | Prop | Type | Default | -| ---------- | ----------------- | ------- | +|------------|-------------------|---------| | `as_child` | `MaybeProp` | `false` | {{#endtab }} @@ -104,15 +104,15 @@ Used to show the progress visually. It also makes progress accessible to assisti
| Data attribute | Values | -| -------------- | -------------------------------------------- | +|----------------|----------------------------------------------| | `[data-state]` | `"complete" \| "indeterminate" \| "loading"` | | `[data-value]` | The current value | | `[data-max]` | The max value | ## Accessibility -Adheres to the [`progressbar` role requirements](https://www.w3.org/WAI/ARIA/apg/patterns/meter/). +Adheres to the [`progressbar` role requirements](https://www.w3.org/WAI/ARIA/apg/patterns/meter). ## See Also -- [Radix documentation](https://www.radix-ui.com/primitives/docs/components/progress) +- [Radix documentation](https://www.radix-ui.com/primitives/docs/components/progress) diff --git a/packages/primitives/leptos/context/Cargo.toml b/packages/primitives/leptos/context/Cargo.toml new file mode 100644 index 00000000..411e26ce --- /dev/null +++ b/packages/primitives/leptos/context/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "radix-leptos-context" +description = "Leptos port of Radix Context." + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +leptos.workspace = true diff --git a/packages/primitives/leptos/context/README.md b/packages/primitives/leptos/context/README.md new file mode 100644 index 00000000..7f54f041 --- /dev/null +++ b/packages/primitives/leptos/context/README.md @@ -0,0 +1,21 @@ +

+ + Rust Radix Logo + +

+ +

radix-leptos-context

+ +This is an internal utility, not intended for public usage. + +[Rust Radix](https://github.com/RustForWeb/radix) is a Rust port of [Radix](https://www.radix-ui.com/primitives). + +## Documentation + +See [the Rust Radix book](https://radix.rustforweb.org/) for documentation. + +## Rust For Web + +The Rust Radix project is part of the [Rust For Web](https://github.com/RustForWeb). + +[Rust For Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source. diff --git a/packages/primitives/leptos/context/src/create_context.rs b/packages/primitives/leptos/context/src/create_context.rs new file mode 100644 index 00000000..92d05f46 --- /dev/null +++ b/packages/primitives/leptos/context/src/create_context.rs @@ -0,0 +1,84 @@ +/// Macro to create a context provider and a hook to consume the context. +/// +/// # Example +/// ```rust +/// use leptos::prelude::*; +/// use radix_leptos_context::create_context; +/// +/// #[derive(Clone)] +/// struct CountContext(i32); +/// +/// create_context!( +/// context_type: CountContext, +/// provider: CountProvider, +/// hook: use_count, +/// root: "Count" +/// ); +/// +/// #[component] +/// fn Counter() -> impl IntoView { +/// let count = use_count("Counter"); +/// view! {
"Count: "{count.0}
} +/// } +/// +/// #[component] +/// fn App() -> impl IntoView { +/// view! { +/// +/// +/// +/// } +/// } +/// ``` +/// +/// # Panics +/// +/// The hook will panic if used in a component that is not wrapped in its provider: +/// ```should_panic +/// use leptos::prelude::*; +/// use radix_leptos_context::create_context; +/// +/// #[derive(Clone)] +/// struct CountContext(i32); +/// +/// create_context!( +/// context_type: CountContext, +/// provider: CountProvider, +/// hook: use_count, +/// root: "Count" +/// ); +/// +/// #[component] +/// fn BadApp() -> impl IntoView { +/// let count = use_count("BadApp"); // Panics: "`BadApp` must be used within `Count`" +/// view! {
{count.0}
} +/// } +/// ``` +#[macro_export] +macro_rules! create_context { + ( + context_type: $context_ty:ty, + provider: $provider:ident, + hook: $hook:ident, + root: $root_component_name:expr + ) => { + use leptos::prelude::*; + + #[component] + #[allow(non_snake_case)] + pub fn $provider( + value: $context_ty, + children: TypedChildren, + ) -> impl IntoView { + view! { } + } + + pub fn $hook(component_name: &'static str) -> $context_ty { + use_context::<$context_ty>() + .expect(&format!("`{}` must be used within `{}`", component_name, $root_component_name)) + .clone() + } + }; +} +// TODO: Default context support + diff --git a/packages/primitives/leptos/context/src/lib.rs b/packages/primitives/leptos/context/src/lib.rs new file mode 100644 index 00000000..01d9501f --- /dev/null +++ b/packages/primitives/leptos/context/src/lib.rs @@ -0,0 +1,3 @@ +mod create_context; + +// pub use create_context::*; diff --git a/packages/primitives/leptos/primitive/Cargo.toml b/packages/primitives/leptos/primitive/Cargo.toml new file mode 100644 index 00000000..fecb0b9d --- /dev/null +++ b/packages/primitives/leptos/primitive/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "radix-leptos-primitive" +description = "Leptos port of Radix Primitive." + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +leptos.workspace = true +leptos-node-ref.workspace = true +leptos-typed-fallback-show.workspace = true \ No newline at end of file diff --git a/packages/primitives/leptos/primitive/README.md b/packages/primitives/leptos/primitive/README.md new file mode 100644 index 00000000..296e16dc --- /dev/null +++ b/packages/primitives/leptos/primitive/README.md @@ -0,0 +1,98 @@ + +

+ + Rust Radix Logo + +

+ +

radix-leptos-primitive

+ +This is an internal utility, not intended for public usage. + +[Rust Radix](https://github.com/RustForWeb/radix) is a Rust port of [Radix](https://www.radix-ui.com/primitives). + +## Overview + +```rust +use leptos::*; +use leptos_node_ref::AnyNodeRef; +use leptos_typed_fallback_show::TypedFallbackShow; + +/// A generic Primitive component. Renders `element()` by default, or its +/// children directly if `as_child` is `true`. We rely on `TypedChildrenFn` +/// so that attributes can pass through at runtime—critical in Leptos v0.7 +/// because `Children`-based types block such passthrough. +#[component] +#[allow(non_snake_case)] +pub fn Primitive( + element: fn() -> HtmlElement, + children: TypedChildrenFn, + #[prop(optional, into)] as_child: MaybeProp, + #[prop(optional, into)] node_ref: AnyNodeRef, +) -> impl IntoView +where + E: ElementType + 'static, + C: IntoView + 'static, +{ + let children = StoredValue::new(children.into_inner()); + view! { + + {children.with_value(|c| c()) + .add_any_attr(leptos_node_ref::any_node_ref(node_ref))} + + } +} + +/// Same idea, but for elements that do not take children (e.g. `img`, `input`). +#[component] +#[allow(non_snake_case)] +pub fn VoidPrimitive( + element: fn() -> HtmlElement, + children: TypedChildrenFn, + #[prop(optional, into)] as_child: MaybeProp, + #[prop(optional, into)] node_ref: AnyNodeRef, +) -> impl IntoView +where + E: ElementType + 'static, +{ + let children = StoredValue::new(children.into_inner()); + view! { + + {children.with_value(|c| c()) + .add_any_attr(leptos_node_ref::any_node_ref(node_ref))} + + } +} + +// (Compose callbacks is an internal piece from Radix Core; omitted for brevity.) +``` + +## Notes + +- **Why `TypedChildrenFn`?**: Leptos attribute passthrough only works if a component doesn't rely on `AnyView` or `Children`. Using typed children ensures classes, events, etc. from the parent can flow to the rendered DOM node. +- **`as_child`**: Mimics `asChild` in Radix’s React version, but we skip an explicit ``: Leptos’s approach to typed fallback rendering covers “slot-like” logic. +- **Class Handling**: Static classes from a parent can overwrite child-defined classes. No built-in merging exists. +- **Attribute System Limitations**: Leptos limits you to 26 dynamic attributes. Past that, nest components or try a custom approach. +- **Parity with React**: In React, `...props` merges everything automatically. In Leptos, we rely on typed props/attributes and can intercept unknown ones with `AttributeInterceptor`. + +## Documentation + +See [the Rust Radix book](https://radix.rustforweb.org/) for documentation. + +## Rust For Web + +The Rust Radix project is part of the [Rust For Web](https://github.com/RustForWeb). + +[Rust For Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source. diff --git a/packages/primitives/leptos/primitive/src/lib.rs b/packages/primitives/leptos/primitive/src/lib.rs new file mode 100644 index 00000000..304fec6d --- /dev/null +++ b/packages/primitives/leptos/primitive/src/lib.rs @@ -0,0 +1,9 @@ +//! Leptos port of [Radix Primitive](https://www.radix-ui.com/primitives). +//! +//! This is an internal utility, not intended for public usage. + +//! See [`@radix-ui/react-primitive`](https://www.npmjs.com/package/@radix-ui/react-primitive) for the original package. + +mod primitive; + +pub use primitive::*; diff --git a/packages/primitives/leptos/primitive/src/primitive.rs b/packages/primitives/leptos/primitive/src/primitive.rs new file mode 100644 index 00000000..fd39d291 --- /dev/null +++ b/packages/primitives/leptos/primitive/src/primitive.rs @@ -0,0 +1,99 @@ +use leptos::{ + ev::Event, + html::{ElementType, HtmlElement}, + prelude::*, + wasm_bindgen::JsCast, + tachys::html::{node_ref::NodeRefContainer}, +}; +use leptos_node_ref::{any_node_ref, AnyNodeRef}; +use leptos_typed_fallback_show::TypedFallbackShow; + +/* ------------------------------------------------------------------------------------------------- + * Primitive + * -----------------------------------------------------------------------------------------------*/ + +#[component] +#[allow(non_snake_case)] +pub fn Primitive( + element: fn() -> HtmlElement, + children: TypedChildrenFn, + #[prop(optional, into)] as_child: MaybeProp, + #[prop(optional, into)] node_ref: AnyNodeRef, +) -> impl IntoView +where + E: ElementType + 'static, + C: IntoView + 'static, + View: RenderHtml, + HtmlElement: ElementChild>, + as ElementChild>>::Output: IntoView, + ::Output: JsCast, + AnyNodeRef: NodeRefContainer, +{ + let children = StoredValue::new(children.into_inner()); + + view! { + + {children.with_value(|children| children()).add_any_attr(any_node_ref(node_ref))} + + } +} + +#[component] +#[allow(non_snake_case)] +pub fn VoidPrimitive( + element: fn() -> HtmlElement, + children: TypedChildrenFn, + #[prop(into, optional)] as_child: MaybeProp, + #[prop(into, optional)] node_ref: AnyNodeRef, +) -> impl IntoView +where + E: ElementType + 'static, + C: IntoView + 'static, + View: RenderHtml, + ::Output: JsCast, + AnyNodeRef: NodeRefContainer, +{ + let children = StoredValue::new(children.into_inner()); + view! { + + {children.with_value(|children| children()).add_any_attr(any_node_ref(node_ref))} + + } +} + +/* ------------------------------------------------------------------------------------------------- + * Utils + * -----------------------------------------------------------------------------------------------*/ + +pub fn compose_callbacks( + original_handler: Option>, + our_handler: Option>, + check_default_prevented: Option, +) -> impl Fn(E) +where + E: Clone + Into + 'static, +{ + let check_default_prevented = check_default_prevented.unwrap_or(true); + + move |event: E| { + // Run original handler first, matching TypeScript behavior + if let Some(original) = &original_handler { + original.run(event.clone()); + } + + // Only run our handler if default wasn't prevented (when checking is enabled) + if !check_default_prevented || !event.clone().into().default_prevented() { + if let Some(our) = &our_handler { + our.run(event); + } + } + } +} diff --git a/packages/primitives/leptos/progress/Cargo.toml b/packages/primitives/leptos/progress/Cargo.toml index 811222b9..7066143d 100644 --- a/packages/primitives/leptos/progress/Cargo.toml +++ b/packages/primitives/leptos/progress/Cargo.toml @@ -11,3 +11,6 @@ version.workspace = true [dependencies] leptos.workspace = true +leptos-node-ref.workspace = true +radix-leptos-primitive.workspace = true +radix-leptos-context.workspace = true \ No newline at end of file diff --git a/packages/primitives/leptos/progress/src/lib.rs b/packages/primitives/leptos/progress/src/lib.rs index f1419cc9..cffd1630 100644 --- a/packages/primitives/leptos/progress/src/lib.rs +++ b/packages/primitives/leptos/progress/src/lib.rs @@ -6,6 +6,8 @@ //! //! See [`@radix-ui/react-progress`](https://www.npmjs.com/package/@radix-ui/react-progress) for the original package. +extern crate core; + mod progress; pub use progress::*; diff --git a/packages/primitives/leptos/progress/src/progress.rs b/packages/primitives/leptos/progress/src/progress.rs index 850e29b5..f390cab9 100644 --- a/packages/primitives/leptos/progress/src/progress.rs +++ b/packages/primitives/leptos/progress/src/progress.rs @@ -1,6 +1,13 @@ -use std::fmt::{Display, Formatter}; +use leptos::context::Provider; +use leptos::{html, logging}; +use leptos::prelude::*; +use leptos_node_ref::AnyNodeRef; +use radix_leptos_primitive::Primitive; +use radix_leptos_context::create_context; -use leptos::{html::AnyElement, *}; +/* ------------------------------------------------------------------------------------------------- + * Progress Types & Constants + * -----------------------------------------------------------------------------------------------*/ const DEFAULT_MAX: f64 = 100.0; @@ -11,8 +18,8 @@ pub enum ProgressState { Loading, } -impl Display for ProgressState { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl std::fmt::Display for ProgressState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", @@ -25,121 +32,201 @@ impl Display for ProgressState { } } -impl IntoAttribute for ProgressState { - fn into_attribute(self) -> Attribute { - Attribute::String(self.to_string().into()) - } +/* ------------------------------------------------------------------------------------------------- + * Progress Context + * -----------------------------------------------------------------------------------------------*/ - fn into_attribute_boxed(self: Box) -> Attribute { - self.into_attribute() - } -} +const PROGRESS_NAME: &'static str = "Progress"; #[derive(Clone, Debug)] -struct ProgressContextValue { - value: Signal>, - max: Signal, +pub struct ProgressContextValue { + /// Current progress value, or None for indeterminate. + pub value: Signal>, + /// Maximum progress value. + pub max: Signal, } +create_context!( + context_type: ProgressContextValue, + provider: ProgressProvider, + hook: use_progress_context, + root: PROGRESS_NAME // for debug labels +); + +/* ------------------------------------------------------------------------------------------------- + * Progress (Root) + * -----------------------------------------------------------------------------------------------*/ + #[component] +#[allow(non_snake_case)] pub fn Progress( - #[prop(into, optional)] value: MaybeProp, - #[prop(into, optional)] max: MaybeProp, - #[prop(into, optional)] get_value_label: Option String>>, - #[prop(into, optional)] as_child: MaybeProp, - #[prop(optional)] node_ref: NodeRef, - #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>, - children: ChildrenFn, + /// The current progress value. + #[prop(into, optional)] + value: MaybeProp, + /// The maximum allowed progress value (defaults to 100). + #[prop(into, optional, default=100.0.into())] + max: MaybeProp, + /// A callback `(f64, f64) -> String`. Defaults to a "percentage" label if not provided. + #[prop(into, optional)] + get_value_label: Option>, + /// If `true`, renders the `
` as a child (no extra wrapper). + #[prop(into, optional)] + as_child: MaybeProp, + /// A reference to the `
` node, if needed. + #[prop(optional)] + node_ref: AnyNodeRef, + /// Child elements, usually `ProgressIndicator`. + children: TypedChildrenFn, ) -> impl IntoView { - let get_value_label = get_value_label.unwrap_or(Box::new(default_get_value_label)); - let max = Signal::derive(move || { - max.get() - .and_then(|max| match max == 0.0 { - true => None, - false => Some(max), - }) - .unwrap_or(DEFAULT_MAX) + // Provide a default callback if none is given. + let get_value_label = get_value_label.unwrap_or_else(|| { + Callback::new(|(value, max): (f64, f64)| { + let pct = ((value / max) * f64::from(100.0)).round(); + format!("{}%", pct) + }) + }); + + // Derive signals for max and clamped value. + let max_signal = Signal::derive({ + let max = max.clone(); + move || max.get().filter(|m| *m > 0.0).unwrap_or(DEFAULT_MAX) + }); + + let value_signal = Signal::derive({ + let value = value.clone(); + move || value.get().map(|v| v.clamp(0.0, max_signal.get())) + }); + + // Derive signals for data/aria attributes. + let progress_state = Signal::derive({ + let val = value_signal.clone(); + let mx = max_signal.clone(); + move || get_progress_state(val.get(), mx.get()) + }); + + let value_label = Signal::derive({ + let val = value_signal.clone(); + let mx = max_signal.clone(); + let label_cb = get_value_label; // Callback is Copy + move || val.get().map(|v| label_cb.run((v, mx.get()))) }); - let value = Signal::derive(move || value.get().map(|value| value.min(max.get()).max(0.0))); - - let value_label = - Signal::derive(move || value.get().map(|value| get_value_label(value, max.get()))); - - let context_value = ProgressContextValue { value, max }; - - let mut attrs = attrs.clone(); - attrs.extend([ - ("aria-valuemax", max.into_attribute()), - ("aria-valuemin", "0".into_attribute()), - ("aria-valuenow", value.into_attribute()), - ("aria-valuetext", value_label.into_attribute()), - ("role", "progressbar".into_attribute()), - ( - "data-state", - (move || get_progress_state(value.get(), max.get())).into_attribute(), - ), - ("data-value", value.into_attribute()), - ("data-max", max.into_attribute()), - ]); + + // Add a reactive effect for warning checks + #[debug_assertions] + Effect::new(move |_| { + let current_max = max_signal.get(); + if current_max <= 0.0 { + logging::warn!( + "Invalid prop `max` of value `{}` supplied to `Progress`. Defaulting to `{}`.", + current_max, + DEFAULT_MAX + ); + } + + if let Some(v) = value_signal.get() { + if v < 0.0 || v > current_max { + logging::warn!( + "Invalid prop `value` of `{}` supplied to `Progress` (must be between 0 and {}). \ + Defaulting to `None` (indeterminate).", + v, + current_max + ); + } + } + }); + + // Provide context to children. + let ctx_value = ProgressContextValue { + value: value_signal, + max: max_signal, + }; view! { - - + - {children()} - - + children=children + + // ARIA + attr:role="progressbar" + attr:aria-valuemax=move || max_signal.get().to_string() + attr:aria-valuemin="0" + attr:aria-valuenow=move || value_signal.get().map(|v| v.to_string()) + attr:aria-valuetext=move || value_label.get() + + // Data attributes + attr:data-state=move || progress_state.get().to_string() + attr:data-value=move || value_signal.get().map(|v| v.to_string()) + attr:data-max=move || max_signal.get().to_string() + /> + } } +/* ------------------------------------------------------------------------------------------------- + * ProgressIndicator + * -----------------------------------------------------------------------------------------------*/ + +const INDICATOR_NAME: &'static str = "ProgressIndicator"; + #[component] +#[allow(non_snake_case)] pub fn ProgressIndicator( - #[prop(into, optional)] as_child: MaybeProp, - #[prop(optional)] node_ref: NodeRef, - #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>, - #[prop(optional)] children: Option, + /// If `true`, renders as a child without an extra `
`. + #[prop(into, optional)] + as_child: MaybeProp, + /// Reference to the `
`. + #[prop(into, optional)] + node_ref: AnyNodeRef, + /// Child elements, e.g. a styled bar or animated element. + #[prop(optional)] + children: Option, ) -> impl IntoView { + let context = use_progress_context(INDICATOR_NAME); let children = StoredValue::new(children); - let context = expect_context::(); - - let mut attrs = attrs.clone(); - attrs.extend([ - ( - "data-state", - (move || get_progress_state(context.value.get(), context.max.get())).into_attribute(), - ), - ("data-value", context.value.into_attribute()), - ("data-max", context.max.into_attribute()), - ]); - let attrs = StoredValue::new(attrs); + let progress_state = Signal::derive({ + let val = context.value; + let mx = context.max; + move || get_progress_state(val.get(), mx.get()) + }); view! { - {children.with_value(|children| children.as_ref().map(|children| children()))} + {children.with_value(|c| c.as_ref().map(|inner| inner()))} } } -fn default_get_value_label(value: f64, max: f64) -> String { - format!("{}%", (value / max).round() * 100.0) -} +/* ------------------------------------------------------------------------------------------------- + * Helpers + * -----------------------------------------------------------------------------------------------*/ fn get_progress_state(value: Option, max_value: f64) -> ProgressState { match value { - Some(value) => match value == max_value { - true => ProgressState::Complete, - false => ProgressState::Loading, - }, + Some(v) if v == max_value => ProgressState::Complete, + Some(_) => ProgressState::Loading, None => ProgressState::Indeterminate, } } + +/* ------------------------------------------------------------------------------------------------- + * Re-exports + * -----------------------------------------------------------------------------------------------*/ + +pub mod primitive { + pub use super::*; + pub use Progress as Root; + pub use ProgressIndicator as Indicator; +} diff --git a/stories/leptos/Cargo.toml b/stories/leptos/Cargo.toml index b56a152e..edeb1c1c 100644 --- a/stories/leptos/Cargo.toml +++ b/stories/leptos/Cargo.toml @@ -29,7 +29,7 @@ radix-leptos-label = { path = "../../packages/primitives/leptos/label" } # "csr", # ] } # radix-leptos-presence = { path = "../../packages/primitives/leptos/presence" } -# radix-leptos-progress = { path = "../../packages/primitives/leptos/progress" } +radix-leptos-progress = { path = "../../packages/primitives/leptos/progress" } # radix-leptos-separator = { path = "../../packages/primitives/leptos/separator" } # radix-leptos-switch = { path = "../../packages/primitives/leptos/switch" } # radix-leptos-toggle = { path = "../../packages/primitives/leptos/toggle" } diff --git a/stories/leptos/src/app.rs b/stories/leptos/src/app.rs index 6f066343..d1f5e22b 100644 --- a/stories/leptos/src/app.rs +++ b/stories/leptos/src/app.rs @@ -4,7 +4,7 @@ use leptos_router::{ path, }; -use crate::primitives::{accessible_icon, arrow, aspect_ratio, label, visually_hidden}; +use crate::primitives::{accessible_icon, arrow, aspect_ratio, label, progress, visually_hidden}; #[component] fn NavLink(href: H, children: Children) -> impl IntoView @@ -152,14 +152,14 @@ pub fn App() -> impl IntoView { //
  • With Deferred Mount Animation
  • // // - //
  • - // Progress +
  • + Progress - //
      - //
    • Styled
    • - //
    • Chromatic
    • - //
    - //
  • +
      +
    • Styled
    • +
    • Chromatic
    • +
    + //
  • // Separator @@ -264,8 +264,8 @@ pub fn App() -> impl IntoView { // // - // - // + + // diff --git a/stories/leptos/src/primitives.rs b/stories/leptos/src/primitives.rs index 4af00626..33a052d6 100644 --- a/stories/leptos/src/primitives.rs +++ b/stories/leptos/src/primitives.rs @@ -11,7 +11,7 @@ pub mod label; // pub mod popper; // pub mod portal; // pub mod presence; -// pub mod progress; +pub mod progress; // pub mod separator; // pub mod slot; // pub mod switch; diff --git a/stories/leptos/src/primitives/progress.rs b/stories/leptos/src/primitives/progress.rs index 37f20259..06ca1169 100644 --- a/stories/leptos/src/primitives/progress.rs +++ b/stories/leptos/src/primitives/progress.rs @@ -1,5 +1,5 @@ -use leptos::*; -use radix_leptos_progress::*; +use leptos::prelude::*; +use radix_leptos_progress::primitive as Progress; use tailwind_fuse::*; #[component] @@ -7,18 +7,23 @@ pub fn Styled() -> impl IntoView { let root_class = Memo::new(move |_| RootClass::default().to_class()); let indicator_class = Memo::new(move |_| IndicatorClass::default().to_class()); - let (max, _) = create_signal(150.0); + let (max, _) = signal(150.0); let (value, percentage, set_value) = use_progress_value_state(Some(0.0), max); let toggle_indeterminate = use_indeterminate_toggle(value, set_value); view! {
    - - - + + +
    - - + +
    } } @@ -33,40 +38,40 @@ pub fn Chromatic() -> impl IntoView { view! {

    Loading (not started)

    - - / - + + / +

    Loading (started)

    - - / - + + / +

    Indeterminate

    - - / - + + / +

    Complete

    - - / - + + / +

    State attributes

    Loading (started)

    - - / - + + / +

    Indeterminate

    - - / - + + / +

    Complete

    - - / - + + / + } } @@ -81,13 +86,13 @@ pub fn ProgressRange( view! { () { - set_value.call(val); + set_value.run(val); } } /> @@ -132,7 +137,7 @@ fn use_progress_value_state( initial_state: Option, max: ReadSignal, ) -> UsePreviousValueReturn { - let (value, set_value) = create_signal(initial_state); + let (value, set_value) = signal(initial_state); let percentage = Signal::derive(move || { value .get() @@ -158,7 +163,7 @@ fn use_indeterminate_toggle( } fn use_previous_value(value: ReadSignal>) -> ReadSignal { - let (previous, set_previous) = create_signal(value.get_untracked().unwrap_or(0.0)); + let (previous, set_previous) = signal(value.get_untracked().unwrap_or(0.0)); Effect::new(move |_| { if let Some(value) = value.get() {