radix_leptos_primitives/components/
sheet.rs

1use crate::utils::merge_classes;
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6/// Sheet component - Side panel/drawer component for mobile and desktop
7///
8/// The Sheet component provides accessible side panels and drawers that slide in from
9/// different directions, commonly used for navigation, forms, and secondary content.
10///
11/// # Features
12/// - Accessible modal overlay with proper ARIA attributes
13/// - Multiple positions (left, right, top, bottom)
14/// - Multiple sizes (sm, md, lg, xl, full)
15/// - Focus management and keyboard navigation
16/// - Backdrop click handling
17/// - Escape key handling
18/// - Smooth animations and transitions
19///
20/// # Example
21///
22/// ```rust
23/// use radix_leptos_primitives::*;
24///
25/// #[component]
26/// fn MyComponent() -> impl IntoView {
27///     let (show_sheet, set_show_sheet) = create_signal(false);
28///
29///     view! {
30///         <Button on_click=move |_| set_show_sheet.set(true)>
31///             "Open Sheet"
32///         </Button>
33///         
34///         <Sheet
35///             open=show_sheet
36///             onopen_change=move |open| set_show_sheet.set(open)
37///             position=SheetPosition::Right
38///             size=SheetSize::Medium
39///         >
40///             <SheetContent>
41///                 <SheetHeader>
42///                     <SheetTitle>"Settings"</SheetTitle>
43///                     <SheetDescription>
44///                         "Manage your application settings and preferences."
45///                     </SheetDescription>
46///                 </SheetHeader>
47///                 <SheetBody>
48///                     // Sheet content here
49///                 </SheetBody>
50///                 <SheetFooter>
51///                     <Button on_click=move |_| set_show_sheet.set(false)>
52///                         "Close"
53///                     </Button>
54///                 </SheetFooter>
55///             </SheetContent>
56///         </Sheet>
57///     }
58/// }
59/// ```
60
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub enum SheetVariant {
63    Default,
64    Destructive,
65    Success,
66    Warning,
67    Info,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
71pub enum SheetPosition {
72    Left,
73    Right,
74    Top,
75    Bottom,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq)]
79pub enum SheetSize {
80    Small,
81    Medium,
82    Large,
83    ExtraLarge,
84    Full,
85}
86
87impl SheetVariant {
88    pub fn as_str(&self) -> &'static str {
89        match self {
90            SheetVariant::Default => "default",
91            SheetVariant::Destructive => "destructive",
92            SheetVariant::Success => "success",
93            SheetVariant::Warning => "warning",
94            SheetVariant::Info => "info",
95        }
96    }
97}
98
99impl SheetPosition {
100    pub fn as_str(&self) -> &'static str {
101        match self {
102            SheetPosition::Left => "left",
103            SheetPosition::Right => "right",
104            SheetPosition::Top => "top",
105            SheetPosition::Bottom => "bottom",
106        }
107    }
108}
109
110impl SheetSize {
111    pub fn as_str(&self) -> &'static str {
112        match self {
113            SheetSize::Small => "sm",
114            SheetSize::Medium => "md",
115            SheetSize::Large => "lg",
116            SheetSize::ExtraLarge => "xl",
117            SheetSize::Full => "full",
118        }
119    }
120}
121
122/// Sheet root component
123#[component]
124pub fn Sheet(
125    #[prop(optional)] class: Option<String>,
126    #[prop(optional)] style: Option<String>,
127    #[prop(optional)] children: Option<Children>,
128    #[prop(optional)] open: Option<bool>,
129    #[prop(optional)] position: Option<SheetPosition>,
130    #[prop(optional)] size: Option<SheetSize>,
131    #[prop(optional)] onopen_change: Option<Callback<bool>>,
132) -> impl IntoView {
133    let open = open.unwrap_or(false);
134    let position = position.unwrap_or(SheetPosition::Right);
135    let size = size.unwrap_or(SheetSize::Medium);
136    let onopen_change = onopen_change.unwrap_or_else(|| Callback::new(|_| {}));
137
138    let class = merge_classes(vec!["sheet", position.as_str(), size.as_str()].to_vec());
139}
140
141/// Sheet content component
142#[component]
143pub fn SheetContent(
144    #[prop(optional)] class: Option<String>,
145    #[prop(optional)] style: Option<String>,
146    #[prop(optional)] children: Option<Children>,
147) -> impl IntoView {
148    let class = merge_classes(vec!["sheet-content", class.as_deref().unwrap_or("")].to_vec());
149
150    view! {
151        <div
152            class=class
153            style=style
154        >
155            {children.map(|c| c())}
156        </div>
157    }
158}
159
160/// Sheet header component
161#[component]
162pub fn SheetHeader(
163    #[prop(optional)] class: Option<String>,
164    #[prop(optional)] style: Option<String>,
165    #[prop(optional)] children: Option<Children>,
166) -> impl IntoView {
167    let class = merge_classes(vec!["sheet-header", class.as_deref().unwrap_or("")].to_vec());
168
169    view! {
170        <div
171            class=class
172            style=style
173        >
174            {children.map(|c| c())}
175        </div>
176    }
177}
178
179/// Sheet title component
180#[component]
181pub fn SheetTitle(
182    #[prop(optional)] class: Option<String>,
183    #[prop(optional)] style: Option<String>,
184    #[prop(optional)] children: Option<Children>,
185) -> impl IntoView {
186    let class = merge_classes(vec!["sheet-title", class.as_deref().unwrap_or("")].to_vec());
187
188    view! {
189        <h2
190            class=class
191            style=style
192        >
193            {children.map(|c| c())}
194        </h2>
195    }
196}
197
198/// Sheet description component
199#[component]
200pub fn SheetDescription(
201    #[prop(optional)] class: Option<String>,
202    #[prop(optional)] style: Option<String>,
203    #[prop(optional)] children: Option<Children>,
204) -> impl IntoView {
205    let class = merge_classes(vec!["sheet-description", class.as_deref().unwrap_or("")].to_vec());
206
207    view! {
208        <p
209            class=class
210            style=style
211        >
212            {children.map(|c| c())}
213        </p>
214    }
215}
216
217/// Sheet body component
218#[component]
219pub fn SheetBody(
220    #[prop(optional)] class: Option<String>,
221    #[prop(optional)] style: Option<String>,
222    #[prop(optional)] children: Option<Children>,
223) -> impl IntoView {
224    let class = merge_classes(vec!["sheet-body", class.as_deref().unwrap_or("")].to_vec());
225
226    view! {
227        <div
228            class=class
229            style=style
230        >
231            {children.map(|c| c())}
232        </div>
233    }
234}
235
236/// Sheet footer component
237#[component]
238pub fn SheetFooter(
239    #[prop(optional)] class: Option<String>,
240    #[prop(optional)] style: Option<String>,
241    #[prop(optional)] children: Option<Children>,
242) -> impl IntoView {
243    let class = merge_classes(vec!["sheet-footer", class.as_deref().unwrap_or("")].to_vec());
244
245    view! {
246        <div
247            class=class
248            style=style
249        >
250            {children.map(|c| c())}
251        </div>
252    }
253}
254
255/// Sheet close button component
256#[component]
257pub fn SheetClose(
258    #[prop(optional)] class: Option<String>,
259    #[prop(optional)] style: Option<String>,
260    #[prop(optional)] children: Option<Children>,
261    #[prop(optional)] on_click: Option<Callback<()>>,
262) -> impl IntoView {
263    let on_click = on_click.unwrap_or_else(|| Callback::new(|_| {}));
264
265    let class = merge_classes(vec!["sheet-close", class.as_deref().unwrap_or("")].to_vec());
266
267    view! {
268        <button
269            class=class
270            style=style
271            on:click=move |_| on_click.run(())
272            aria-label="Close sheet"
273        >
274            {children.map(|c| c())}
275        </button>
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use proptest::prelude::*;
282
283    #[test]
284    fn test_sheet_component_creation() {}
285
286    #[test]
287    fn test_sheet_with_position_component_creation() {}
288
289    proptest! {
290        #[test]
291        fn test_sheet_props(___class in ".*", ___style in ".*") {
292
293        }
294
295        #[test]
296        fn test_sheetopen_state(__open: bool, ___position_index in 0..4usize, ___size_index in 0..5usize) {
297
298        }
299
300        #[test]
301        fn test_sheet_positions(___position_index in 0..4usize) {
302
303        }
304
305        #[test]
306        fn test_sheet_sizes(___size_index in 0..5usize) {
307
308        }
309
310        #[test]
311        fn test_sheet_content_props(___class in ".*", ___style in ".*") {
312
313        }
314
315        #[test]
316        fn test_sheet_header_props(___class in ".*", ___style in ".*") {
317
318        }
319
320        #[test]
321        fn test_sheet_title_props(___class in ".*", ___style in ".*") {
322
323        }
324
325        #[test]
326        fn test_sheet_description_props(___class in ".*", ___style in ".*") {
327
328        }
329
330        #[test]
331        fn test_sheet_body_props(___class in ".*", ___style in ".*") {
332
333        }
334
335        #[test]
336        fn test_sheet_footer_props(___class in ".*", ___style in ".*") {
337
338        }
339
340        #[test]
341        fn test_sheet_close_props(___class in ".*", ___style in ".*") {
342
343        }
344    }
345}