Skip to main content

maud_ui/primitives/
tooltip.rs

1//! Tooltip component — non-modal hover/focus overlay content
2
3use maud::{html, Markup};
4
5/// Tooltip placement relative to the trigger
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Placement {
8    Top,
9    Bottom,
10    Left,
11    Right,
12}
13
14impl Placement {
15    fn class(&self) -> &'static str {
16        match self {
17            Self::Top => "mui-tooltip__content--top",
18            Self::Bottom => "mui-tooltip__content--bottom",
19            Self::Left => "mui-tooltip__content--left",
20            Self::Right => "mui-tooltip__content--right",
21        }
22    }
23}
24
25/// Tooltip rendering properties
26#[derive(Debug, Clone)]
27pub struct Props {
28    /// The tooltip text displayed on hover/focus
29    pub content: String,
30    /// Position relative to trigger
31    pub placement: Placement,
32    /// Delay in milliseconds before showing (default 500)
33    pub delay_ms: u32,
34    /// The element that triggers the tooltip (button, link, icon, etc.)
35    pub trigger: Markup,
36    /// Unique identifier for aria-describedby linking
37    pub id: String,
38}
39
40impl Default for Props {
41    fn default() -> Self {
42        Self {
43            content: String::new(),
44            placement: Placement::Top,
45            delay_ms: 500,
46            trigger: html! {},
47            id: "tooltip".to_string(),
48        }
49    }
50}
51
52/// Render a single tooltip with the given properties
53pub fn render(props: Props) -> Markup {
54    let content_class = format!("mui-tooltip__content {}", props.placement.class());
55    let tip_id = format!("{}-tip", props.id);
56
57    html! {
58        span.mui-tooltip data-mui="tooltip" data-delay=(props.delay_ms.to_string()) {
59            span.mui-tooltip__trigger aria-describedby=(tip_id.clone()) {
60                (props.trigger)
61            }
62            span class=(content_class)
63                id=(tip_id)
64                role="tooltip"
65                hidden
66                data-visible="false"
67            {
68                (props.content)
69            }
70        }
71    }
72}
73
74/// Showcase all tooltip placements
75pub fn showcase() -> Markup {
76    html! {
77        div.mui-showcase__grid {
78            div {
79                p.mui-showcase__caption { "Icon buttons with contextual tooltips — copy, roster, shortcut, destructive." }
80                div.mui-showcase__row {
81                    // Top — copy-to-clipboard on a copy icon button
82                    (render(Props {
83                        content: "Copy to clipboard".into(),
84                        placement: Placement::Top,
85                        delay_ms: 400,
86                        trigger: html! {
87                            button.mui-btn.mui-btn--ghost.mui-btn--icon type="button" aria-label="Copy" {
88                                span aria-hidden="true" { "\u{2398}" }
89                            }
90                        },
91                        id: "demo-tip-copy".into(),
92                    }))
93                    // Bottom — avatar stack "+3 more"
94                    (render(Props {
95                        content: "View full list (12 members)".into(),
96                        placement: Placement::Bottom,
97                        delay_ms: 400,
98                        trigger: html! {
99                            button.mui-btn.mui-btn--outline.mui-btn--sm type="button" aria-label="+3 more members — view all" {
100                                "+3"
101                            }
102                        },
103                        id: "demo-tip-avatars".into(),
104                    }))
105                    // Left — search with keyboard shortcut hint
106                    (render(Props {
107                        content: "Keyboard shortcut: \u{2318}K".into(),
108                        placement: Placement::Left,
109                        delay_ms: 400,
110                        trigger: html! {
111                            button.mui-btn.mui-btn--outline.mui-btn--md type="button" aria-label="Open search" {
112                                span aria-hidden="true" { "\u{1f50d}" }
113                                " Search"
114                            }
115                        },
116                        id: "demo-tip-search".into(),
117                    }))
118                    // Right — destructive icon with warning
119                    (render(Props {
120                        content: "Delete permanently".into(),
121                        placement: Placement::Right,
122                        delay_ms: 400,
123                        trigger: html! {
124                            button.mui-btn.mui-btn--danger.mui-btn--icon type="button" aria-label="Delete" {
125                                span aria-hidden="true" { "\u{1f5d1}" }
126                            }
127                        },
128                        id: "demo-tip-delete".into(),
129                    }))
130                }
131            }
132        }
133    }
134}