radix_leptos_primitives/components/
sheet.rs

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