Skip to main content

stratum_components/utility/
focus_scope.rs

1//! FocusScope utility — a pass-through container that traps focus within its children.
2
3use crate::common::merge_classes;
4use stratum_core::render::RenderOutput;
5
6/// Properties for FocusScope.
7#[derive(Debug, Clone, PartialEq, Default)]
8pub struct FocusScopeProps {
9    /// Whether focus should be trapped within this scope.
10    pub trapped: bool,
11    /// Whether to auto-focus the first focusable element on mount.
12    pub auto_focus: bool,
13    pub class: Option<String>,
14}
15
16pub struct FocusScope;
17
18impl FocusScope {
19    pub fn classes(props: &FocusScopeProps) -> String {
20        merge_classes("", &props.class)
21    }
22
23    pub fn render(props: &FocusScopeProps) -> RenderOutput {
24        let mut output = RenderOutput::new()
25            .with_tag("div")
26            .with_class(Self::classes(props));
27
28        if props.trapped {
29            output = output.with_data("focus-trap", "true");
30        }
31        if props.auto_focus {
32            output = output.with_data("auto-focus", "true");
33        }
34
35        output
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn focus_scope_default() {
45        let props = FocusScopeProps::default();
46        let output = FocusScope::render(&props);
47        assert_eq!(output.effective_tag(), "div");
48        assert!(output.data_attrs.is_empty());
49    }
50
51    #[test]
52    fn focus_scope_trapped() {
53        let props = FocusScopeProps {
54            trapped: true,
55            ..Default::default()
56        };
57        let output = FocusScope::render(&props);
58        assert!(
59            output
60                .data_attrs
61                .iter()
62                .any(|(k, v)| k == "focus-trap" && v == "true")
63        );
64    }
65
66    #[test]
67    fn focus_scope_auto_focus() {
68        let props = FocusScopeProps {
69            auto_focus: true,
70            ..Default::default()
71        };
72        let output = FocusScope::render(&props);
73        assert!(
74            output
75                .data_attrs
76                .iter()
77                .any(|(k, v)| k == "auto-focus" && v == "true")
78        );
79    }
80}