Skip to main content

dioxus_ui_system/molecules/
scroll_area.rs

1//! ScrollArea molecule component
2//!
3//! A custom scrollable container with styled scrollbars that match the theme.
4//! Supports vertical, horizontal, and bidirectional scrolling with
5//! customizable scrollbar appearance.
6
7use crate::styles::Style;
8use crate::theme::use_style;
9use dioxus::prelude::*;
10
11/// Scroll orientation options
12#[derive(Default, Clone, PartialEq)]
13pub enum ScrollOrientation {
14    /// Vertical scrolling only (default)
15    #[default]
16    Vertical,
17    /// Horizontal scrolling only
18    Horizontal,
19    /// Both directions
20    Both,
21}
22
23/// ScrollArea properties
24#[derive(Props, Clone, PartialEq)]
25pub struct ScrollAreaProps {
26    /// Content to be scrolled
27    pub children: Element,
28    /// Scroll orientation - Vertical, Horizontal, or Both
29    #[props(default)]
30    pub orientation: ScrollOrientation,
31    /// Custom class name
32    #[props(default)]
33    pub class: Option<String>,
34    /// Custom inline styles
35    #[props(default)]
36    pub style: Option<String>,
37    /// Custom scrollbar size (width for vertical, height for horizontal)
38    #[props(default)]
39    pub scrollbar_size: Option<String>,
40    /// Maximum height for the scroll area
41    #[props(default)]
42    pub max_height: Option<String>,
43    /// Maximum width for the scroll area
44    #[props(default)]
45    pub max_width: Option<String>,
46    /// Whether to auto-hide scrollbar when not scrolling
47    #[props(default)]
48    pub auto_hide: bool,
49}
50
51/// ScrollArea molecule component
52///
53/// A custom scrollable container with theme-matched scrollbars.
54/// Uses CSS custom properties for styling and WebKit scrollbar
55/// pseudo-elements for Chrome/Safari, with Firefox fallback.
56///
57/// # Example
58/// ```rust,ignore
59/// use dioxus_ui_system::molecules::{ScrollArea, ScrollOrientation};
60///
61/// rsx! {
62///     ScrollArea {
63///         max_height: "300px".to_string(),
64///         orientation: ScrollOrientation::Vertical,
65///         // Content that exceeds max_height will be scrollable
66///         for i in 0..50 {
67///             p { "Item {i}" }
68///         }
69///     }
70/// }
71/// ```
72#[component]
73pub fn ScrollArea(props: ScrollAreaProps) -> Element {
74    let orientation = props.orientation.clone();
75    let auto_hide = props.auto_hide;
76    let scrollbar_size_val = props
77        .scrollbar_size
78        .clone()
79        .unwrap_or_else(|| "8px".to_string());
80    let max_height = props.max_height.clone();
81    let max_width = props.max_width.clone();
82
83    // Generate scrollbar colors from theme
84    let scrollbar_colors = use_style(|t| {
85        let thumb_color = t.colors.border.darken(0.2);
86        let thumb_hover_color = t.colors.muted_foreground.clone();
87        let track_color = t.colors.muted.lighten(0.5);
88        let corner_color = t.colors.background.clone();
89
90        (thumb_color, thumb_hover_color, track_color, corner_color)
91    });
92
93    // Build the base container style
94    let container_style = use_style(move |t| {
95        let (thumb_color, _thumb_hover, track_color, _corner_color) = scrollbar_colors().clone();
96
97        // Set overflow based on orientation
98        let (overflow_x, overflow_y) = match orientation {
99            ScrollOrientation::Vertical => ("hidden", "auto"),
100            ScrollOrientation::Horizontal => ("auto", "hidden"),
101            ScrollOrientation::Both => ("auto", "auto"),
102        };
103
104        let mut style = Style::new()
105            .w_full()
106            .h_full()
107            .overflow_hidden() // Will be overridden by inline styles for specific axes
108            .rounded(&t.radius, "md")
109            .bg(&t.colors.background);
110
111        // Add max dimensions if specified
112        if let Some(ref max_h) = max_height {
113            style = Style {
114                max_height: Some(max_h.clone()),
115                ..style
116            };
117        }
118
119        if let Some(ref max_w) = max_width {
120            style = Style {
121                max_width: Some(max_w.clone()),
122                ..style
123            };
124        }
125
126        // Build base style string
127        let base_style = style.build();
128
129        // Add CSS custom properties for scrollbar theming
130        format!(
131            "{} --scrollbar-thumb: {}; --scrollbar-track: {}; --scrollbar-size: {}; overflow-x: {}; overflow-y: {}; scrollbar-width: thin; scrollbar-color: {} {};",
132            base_style,
133            thumb_color.to_rgba(),
134            track_color.to_rgba(),
135            scrollbar_size_val,
136            overflow_x,
137            overflow_y,
138            thumb_color.to_rgba(),
139            track_color.to_rgba()
140        )
141    });
142
143    // Build WebKit scrollbar styles
144    let webkit_styles = use_style(move |t| {
145        let (thumb_color, thumb_hover_color, track_color, corner_color) =
146            scrollbar_colors().clone();
147        let size = props
148            .scrollbar_size
149            .clone()
150            .unwrap_or_else(|| "8px".to_string());
151
152        let hover_opacity = if auto_hide { "0" } else { "1" };
153        let hover_transition = if auto_hide {
154            "transition: opacity 0.2s ease;"
155        } else {
156            ""
157        };
158
159        format!(
160            r#"
161            .scroll-area::-webkit-scrollbar {{
162                width: {size};
163                height: {size};
164                {hover_transition}
165                opacity: {hover_opacity};
166            }}
167            .scroll-area::-webkit-scrollbar-track {{
168                background: {track};
169                border-radius: {radius}px;
170            }}
171            .scroll-area::-webkit-scrollbar-thumb {{
172                background: {thumb};
173                border-radius: {radius}px;
174                border: 2px solid transparent;
175                background-clip: content-box;
176                transition: background-color 0.2s ease;
177            }}
178            .scroll-area::-webkit-scrollbar-thumb:hover {{
179                background: {thumb_hover};
180                border: 2px solid transparent;
181                background-clip: content-box;
182            }}
183            .scroll-area::-webkit-scrollbar-corner {{
184                background: {corner};
185            }}
186            .scroll-area:hover::-webkit-scrollbar {{
187                opacity: 1;
188            }}
189            "#,
190            size = size,
191            track = track_color.to_rgba(),
192            thumb = thumb_color.to_rgba(),
193            thumb_hover = thumb_hover_color.to_rgba(),
194            corner = corner_color.to_rgba(),
195            radius = t.radius.md,
196            hover_transition = hover_transition,
197            hover_opacity = hover_opacity,
198        )
199    });
200
201    let custom_class = props.class.clone().unwrap_or_default();
202    let custom_style = props.style.clone().unwrap_or_default();
203
204    rsx! {
205        // Inject WebKit scrollbar styles
206        style { "{webkit_styles}" }
207
208        div {
209            class: "scroll-area {custom_class}",
210            style: "{container_style} {custom_style}",
211            {props.children}
212        }
213    }
214}
215
216/// ScrollArea viewport component for more complex layouts
217///
218/// Use this when you need a separate viewport with
219/// different styling from the scroll container.
220#[derive(Props, Clone, PartialEq)]
221pub struct ScrollViewportProps {
222    /// Content inside the viewport
223    pub children: Element,
224    /// Custom class name
225    #[props(default)]
226    pub class: Option<String>,
227    /// Custom inline styles
228    #[props(default)]
229    pub style: Option<String>,
230}
231
232/// Scroll viewport component
233///
234/// A viewport container that fills the available scroll area space.
235/// Useful for creating sticky headers/footers within scroll areas.
236#[component]
237pub fn ScrollViewport(props: ScrollViewportProps) -> Element {
238    let viewport_style = use_style(|t| {
239        Style::new()
240            .w_full()
241            .min_h_full()
242            .p(&t.spacing, "md")
243            .build()
244    });
245
246    let custom_class = props.class.clone().unwrap_or_default();
247    let custom_style = props.style.clone().unwrap_or_default();
248
249    rsx! {
250        div {
251            class: "scroll-viewport {custom_class}",
252            style: "{viewport_style} {custom_style}",
253            {props.children}
254        }
255    }
256}
257
258/// Horizontal scroll area component
259///
260/// Convenience component for horizontal scrolling with
261/// common defaults for horizontal scroll layouts.
262#[derive(Props, Clone, PartialEq)]
263pub struct HorizontalScrollProps {
264    /// Content to be scrolled horizontally
265    pub children: Element,
266    /// Custom class name
267    #[props(default)]
268    pub class: Option<String>,
269    /// Custom inline styles
270    #[props(default)]
271    pub style: Option<String>,
272    /// Height of the scroll area
273    #[props(default)]
274    pub height: Option<String>,
275    /// Whether to show scrollbar
276    #[props(default = true)]
277    pub show_scrollbar: bool,
278}
279
280/// Horizontal scroll component
281///
282/// Optimized for horizontal scrolling content like
283/// image galleries, tab lists, etc.
284#[component]
285pub fn HorizontalScroll(props: HorizontalScrollProps) -> Element {
286    rsx! {
287        ScrollArea {
288            orientation: ScrollOrientation::Horizontal,
289            class: props.class.clone(),
290            style: props.style.clone(),
291            scrollbar_size: if props.show_scrollbar { None } else { Some("0px".to_string()) },
292            max_height: props.height.clone(),
293            {props.children}
294        }
295    }
296}
297
298/// Vertical scroll area component
299///
300/// Convenience component for vertical scrolling with
301/// common defaults for vertical scroll layouts.
302#[derive(Props, Clone, PartialEq)]
303pub struct VerticalScrollProps {
304    /// Content to be scrolled vertically
305    pub children: Element,
306    /// Custom class name
307    #[props(default)]
308    pub class: Option<String>,
309    /// Custom inline styles
310    #[props(default)]
311    pub style: Option<String>,
312    /// Maximum height of the scroll area
313    #[props(default)]
314    pub max_height: Option<String>,
315    /// Whether to auto-hide scrollbar
316    #[props(default)]
317    pub auto_hide: bool,
318}
319
320/// Vertical scroll component
321///
322/// Optimized for vertical scrolling content like
323/// lists, content panels, etc.
324#[component]
325pub fn VerticalScroll(props: VerticalScrollProps) -> Element {
326    rsx! {
327        ScrollArea {
328            orientation: ScrollOrientation::Vertical,
329            class: props.class.clone(),
330            style: props.style.clone(),
331            max_height: props.max_height.clone(),
332            auto_hide: props.auto_hide,
333            {props.children}
334        }
335    }
336}