Sheet
Rust UI component that displays a sheet.
navigation
Sheet Title
This is the content inside the sheet.
use leptos::html::Div; use leptos::prelude::*; use leptos_use::on_click_outside; use crate::components::hooks::use_lock_body_scroll::use_lock_body_scroll; use crate::components::ui::sheet::{ SheetCancel, SheetContent, SheetDescription, SheetTitle, SheetTrigger, SheetVariant, }; #[component] pub fn DemoSheet() -> impl IntoView { let is_open = RwSignal::new(false); let scroll_locked = use_lock_body_scroll(false); let _sheet_ref = NodeRef::<Div>::new(); Effect::new(move |_| { scroll_locked.set(is_open.get()); }); let toggle_sheet = move |_| { is_open.update(|v| { *v = !*v; }); }; Effect::new(move |_| { if is_open.get() { let handle_click_outside = move || { is_open.set(false); }; let _ = on_click_outside(_sheet_ref.get(), move |_| handle_click_outside()); } }); view! { <> <SheetTrigger on:click=toggle_sheet>"Open Sheet"</SheetTrigger> <div node_ref=_sheet_ref> <SheetContent is_open=is_open class="w-[400px]"> <SheetTitle>{"Sheet Title"}</SheetTitle> <SheetDescription>{"This is the content inside the sheet."}</SheetDescription> <SheetCancel on:click=toggle_sheet variant=SheetVariant::Destructive> {"Cancel"} </SheetCancel> </SheetContent> </div> </> } }
Examples
Directions
Sheet Title
This is the content inside the sheet.
Sheet Title
This is the content inside the sheet.
Sheet Title
This is the content inside the sheet.
Sheet Title
This is the content inside the sheet.
use leptos::html::Div; use leptos::prelude::*; use leptos_use::on_click_outside; use crate::components::hooks::use_lock_body_scroll::use_lock_body_scroll; use crate::components::ui::sheet::{ SheetCancel, SheetContent, SheetDescription, SheetDirection, SheetTitle, SheetTrigger, SheetVariant, }; // TODO later. Refactor this to use a single component. #[component] pub fn DemoSheetDirections() -> impl IntoView { view! { <div class="flex flex-col gap-4 items-center"> <DemoSheetTop /> <div class="flex gap-4"> <DemoSheetLeft /> <DemoSheetRight /> </div> <DemoSheetBottom /> </div> } } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ✨ FUNCTIONS ✨ */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ #[component] pub fn DemoSheetTop() -> impl IntoView { let is_open = RwSignal::new(false); let scroll_locked = use_lock_body_scroll(false); let _sheet_ref = NodeRef::<Div>::new(); Effect::new(move |_| { scroll_locked.set(is_open.get()); }); let toggle_sheet = move |_| { is_open.update(|v| { *v = !*v; }); }; Effect::new(move |_| { if is_open.get() { let handle_click_outside = move || { is_open.set(false); }; let _ = on_click_outside(_sheet_ref.get(), move |_| handle_click_outside()); } }); view! { <> <SheetTrigger on:click=toggle_sheet>"Top"</SheetTrigger> <div node_ref=_sheet_ref> <SheetContent direction=SheetDirection::Top is_open=is_open class="w-full h-[200px]" > <SheetTitle>{"Sheet Title"}</SheetTitle> <SheetDescription>{"This is the content inside the sheet."}</SheetDescription> <SheetCancel on:click=toggle_sheet variant=SheetVariant::Destructive> {"Cancel"} </SheetCancel> </SheetContent> </div> </> } } #[component] pub fn DemoSheetLeft() -> impl IntoView { let is_open = RwSignal::new(false); let scroll_locked = use_lock_body_scroll(false); let _sheet_ref = NodeRef::<Div>::new(); Effect::new(move |_| { scroll_locked.set(is_open.get()); }); let toggle_sheet = move |_| { is_open.update(|v| { *v = !*v; }); }; Effect::new(move |_| { if is_open.get() { let handle_click_outside = move || { is_open.set(false); }; let _ = on_click_outside(_sheet_ref.get(), move |_| handle_click_outside()); } }); view! { <> <SheetTrigger on:click=toggle_sheet>"Left"</SheetTrigger> <div node_ref=_sheet_ref> <SheetContent direction=SheetDirection::Left is_open=is_open class="w-[400px]"> <SheetTitle>{"Sheet Title"}</SheetTitle> <SheetDescription>{"This is the content inside the sheet."}</SheetDescription> <SheetCancel on:click=toggle_sheet variant=SheetVariant::Destructive> {"Cancel"} </SheetCancel> </SheetContent> </div> </> } } #[component] pub fn DemoSheetRight() -> impl IntoView { let is_open = RwSignal::new(false); let scroll_locked = use_lock_body_scroll(false); let _sheet_ref = NodeRef::<Div>::new(); Effect::new(move |_| { scroll_locked.set(is_open.get()); }); let toggle_sheet = move |_| { is_open.update(|v| { *v = !*v; }); }; Effect::new(move |_| { if is_open.get() { let handle_click_outside = move || { is_open.set(false); }; let _ = on_click_outside(_sheet_ref.get(), move |_| handle_click_outside()); } }); view! { <> <SheetTrigger on:click=toggle_sheet>"Right"</SheetTrigger> <div node_ref=_sheet_ref> <SheetContent is_open=is_open class="w-[400px]"> <SheetTitle>{"Sheet Title"}</SheetTitle> <SheetDescription>{"This is the content inside the sheet."}</SheetDescription> <SheetCancel on:click=toggle_sheet variant=SheetVariant::Destructive> {"Cancel"} </SheetCancel> </SheetContent> </div> </> } } #[component] pub fn DemoSheetBottom() -> impl IntoView { let is_open = RwSignal::new(false); let scroll_locked = use_lock_body_scroll(false); let _sheet_ref = NodeRef::<Div>::new(); Effect::new(move |_| { scroll_locked.set(is_open.get()); }); let toggle_sheet = move |_| { is_open.update(|v| { *v = !*v; }); }; Effect::new(move |_| { if is_open.get() { let handle_click_outside = move || { is_open.set(false); }; let _ = on_click_outside(_sheet_ref.get(), move |_| handle_click_outside()); } }); view! { <> <SheetTrigger on:click=toggle_sheet>"Bottom"</SheetTrigger> <div node_ref=_sheet_ref> <SheetContent direction=SheetDirection::Bottom is_open=is_open class="w-full"> <SheetTitle>{"Sheet Title"}</SheetTitle> <SheetDescription>{"This is the content inside the sheet."}</SheetDescription> <SheetCancel on:click=toggle_sheet variant=SheetVariant::Destructive> {"Cancel"} </SheetCancel> </SheetContent> </div> </> } }
Experimental
use leptos::prelude::*; use leptos_ui::clx; #[component] pub fn DemoSheetExperimental() -> impl IntoView { clx! {SheetTrigger, button, "bg-none border-none cursor-pointer text-2xl flex items-center"} // TODO. `anchor` attribute is still an experimental feature: // TODO. See: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/anchor const TARGET_ID: &str = "sheet__target"; const ANCHOR_ID: &str = "menu__anchor"; view! { <style> {"nav.my__sheet[popover] { transform: translateX(-100%); } nav.my__sheet:popover-open { transform: translateX(0); } "} </style> <div> <button class="flex items-center text-2xl border-none cursor-pointer bg-none" id=ANCHOR_ID popovertarget=TARGET_ID popovertargetaction="toggle" aria-label="Open settings my_sheet" > "☰" </button> <nav id="sheet__target" // id=TARGET_ID anchor="menu__anchor" // anchor=ANCHOR_ID class="my__sheet w-[320px] z-50 h-[100dvh] flex flex-col justify-between p-4 bg-neutral-200 border-r border-r-gray-200 fixed inset-y-0 left-0 transition-transform duration-300 ease-in-out" popover > <div class="relative"> <button class="absolute p-2 border rounded-lg cursor-pointer top-2 right-2 text-neutral-500 border-neutral-300" popovertarget="sheet__target" // popovertarget=TARGET_ID. popovertargetaction="hide" > "X" </button> <h2>Workspace Settings</h2> <ul> <li> <a href="#">Team</a> </li> <li> <a href="#">Billing</a> </li> <li> <a href="#">Integrations</a> </li> <li> <a href="#">Keyboard Shortcuts</a> </li> </ul> </div> <footer class="text-right"> <button class="px-4 py-2 text-white rounded-md bg-sky-500" popovertarget="sheet__target" // popovertarget=TARGET_ID. popovertargetaction="hide" > Close </button> </footer> </nav> </div> } }
Installation
You can run either of the following commands:
# cargo install ui-cli --forceui add demo_sheetui add sheet
Update the imports to match your project setup.
Copy and paste the following code into your project:
components/ui/sheet.rs
use icons::X; use leptos::prelude::*; use leptos_ui::clx; use tw_merge::*; use super::button::ButtonSize; use crate::components::ui::button::{Button, ButtonVariant}; // TODO. Improve the use of Button in SheetTrigger // TODO. USe Heading variants from Headings mod components { use super::*; clx! {SheetTitle, h2, "font-bold text-2xl"} clx! {SheetDescription, p, ""} } pub use components::*; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ✨ FUNCTIONS ✨ */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ pub type SheetVariant = ButtonVariant; pub type SheetSize = ButtonSize; #[component] pub fn SheetTrigger( #[prop(into, optional)] class: Signal<String>, #[prop(into, optional)] variant: Signal<SheetVariant>, #[prop(into, optional)] size: Signal<SheetSize>, children: Children, ) -> impl IntoView { let class = Memo::new(move |_| tw_merge!("", class())); view! { <Button class=class variant=variant size=size> {children()} </Button> } } #[component] pub fn SheetCancel( #[prop(into, optional)] class: Signal<String>, #[prop(into, optional)] variant: Signal<ButtonVariant>, children: Children, ) -> impl IntoView { let class = Memo::new(move |_| tw_merge!("", class())); view! { <Button class=class variant=variant> {children()} </Button> } } // // Update the SheetContent component #[component] pub fn SheetContent( #[prop(into, optional)] class: Signal<String>, #[prop(into)] is_open: RwSignal<bool>, #[prop(into, default = SheetDirection::Right.into())] direction: Signal<SheetDirection>, children: Children, ) -> impl IntoView { let outer_class = Memo::new(move |_| { let direction = direction.get(); tw_merge!( "fixed z-200 shadow-lg transform transition-transform duration-300", direction.initial_position(), direction.to_class(is_open.get()), class() ) }); let inner_class = Memo::new(move |_| { let base_class = "p-4 h-screen bg-card transition-opacity duration-300 overflow-y-auto shortfix__sidenav_todo_properly"; let opacity_class = if is_open.get() { "opacity-100" } else { "opacity-0 pointer-events-none" }; tw_merge!(base_class, opacity_class) }); let close = move |_| { is_open.set(false); }; view! { <div class=outer_class> <div class=inner_class> <button class="absolute top-0 right-0 m-2" on:click=close> <X class="size-6" /> </button> {children()} </div> </div> } } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ✨ FUNCTIONS ✨ */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ #[derive(Clone, Copy)] pub enum SheetDirection { Right, Left, Top, Bottom, } impl SheetDirection { fn to_class(self, is_open: bool) -> &'static str { match self { SheetDirection::Right => match is_open { true => "translate-x-0", _ => "translate-x-full", }, SheetDirection::Left => match is_open { true => "translate-x-0", _ => "-translate-x-full", }, SheetDirection::Top => match is_open { true => "translate-y-0", _ => "-translate-y-full", }, SheetDirection::Bottom => match is_open { true => "translate-y-0", _ => "translate-y-full", }, } } fn initial_position(self) -> &'static str { match self { SheetDirection::Right => "top-0 right-0 h-full w-64", SheetDirection::Left => "top-0 left-0 h-full w-64", SheetDirection::Top => "top-0 left-0 w-full h-64", SheetDirection::Bottom => "bottom-0 left-0 w-full h-64", } } }
Update the imports to match your project setup.
Usage
// Coming soon 🦀