patternfly_yew/components/popover/
mod.rs

1//! Popover
2use crate::prelude::{Button, ButtonVariant, ExtendClasses, Icon, Orientation};
3use popper_rs::{
4    prelude::{State as PopperState, *},
5    yew::component::PortalPopper,
6};
7use yew::{prelude::*, virtual_dom::VChild};
8use yew_hooks::use_click_away;
9
10#[derive(Clone, PartialEq)]
11pub struct PopoverContext {
12    close: Callback<()>,
13}
14
15impl PopoverContext {
16    /// Close the popover
17    pub fn close(&self) {
18        self.close.emit(());
19    }
20}
21
22/// Properties for [`Popover`]
23#[derive(Clone, Debug, PartialEq, Properties)]
24pub struct PopoverProperties {
25    /// The target, rendered by the component, to which the popover will be aligned to.
26    #[prop_or_default]
27    pub target: Html,
28
29    /// The body content of the popover.
30    pub body: VChild<PopoverBody>,
31
32    #[prop_or_default]
33    pub no_padding: bool,
34
35    #[prop_or_default]
36    pub no_close: bool,
37
38    #[prop_or_default]
39    pub width_auto: bool,
40}
41
42/// Popover component
43///
44/// > A **popover** is in-app messaging that provides more information on specific product areas. Popovers display content in a new window that overlays the current page. Unlike modals, popovers don't block the current page.
45///
46/// See: <https://www.patternfly.org/components/popover>
47///
48/// ## Properties
49///
50/// Defined by [`PopoverProperties`].
51#[function_component(Popover)]
52pub fn popover(props: &PopoverProperties) -> Html {
53    let active = use_state_eq(|| false);
54
55    let state = use_state_eq(PopperState::default);
56    let onstatechange = use_callback(state.clone(), |new_state, state| state.set(new_state));
57
58    // a reference to the target the user clicks on
59    let target_ref = use_node_ref();
60    // a reference to the content
61    let content_ref = use_node_ref();
62
63    let onclick = use_callback(active.clone(), |_, active| active.set(!**active));
64    let onclose = use_callback(active.clone(), |_, active| active.set(false));
65
66    {
67        let active = active.clone();
68        use_click_away(content_ref.clone(), move |_| {
69            active.set(false);
70        });
71    }
72
73    let style = match *active {
74        true => "pointer-events: none;",
75        false => "",
76    };
77
78    let orientation = Orientation::from_popper_data(&state.attributes.popper);
79
80    let context = PopoverContext {
81        close: onclose.clone(),
82    };
83
84    html!(
85        <>
86            <span
87                {onclick}
88                {style}
89                ref={target_ref.clone()}
90            >
91                { props.target.clone() }
92            </span>
93            <PortalPopper
94                visible={*active}
95                content={content_ref.clone()}
96                target={target_ref}
97                {onstatechange}
98                placement={Placement::Right}
99                modifiers={vec![
100                    Modifier::Offset(Offset {
101                        skidding: 0,
102                        distance: 20,
103                    }),
104                    Modifier::PreventOverflow(PreventOverflow { padding: 0 }),
105                ]}
106            >
107                <ContextProvider<PopoverContext> {context}>
108                    <PopoverPopup
109                        width_auto={props.width_auto}
110                        no_padding={props.no_padding}
111                        no_close={props.no_close}
112                        r#ref={content_ref}
113                        style={&state.styles.popper.extend_with("z-index", "1000")}
114                        {orientation}
115                        {onclose}
116                        body={props.body.clone()}
117                    />
118                </ContextProvider<PopoverContext>>
119            </PortalPopper>
120        </>
121    )
122}
123
124// popover popup
125
126/// The popover content component.
127#[derive(Clone, PartialEq, Properties)]
128pub struct PopoverPopupProperties {
129    pub body: VChild<PopoverBody>,
130
131    pub orientation: Orientation,
132
133    #[prop_or_default]
134    pub no_padding: bool,
135    #[prop_or_default]
136    pub no_close: bool,
137
138    #[prop_or_default]
139    pub width_auto: bool,
140
141    #[prop_or_default]
142    pub hidden: bool,
143    #[prop_or_default]
144    pub style: AttrValue,
145
146    /// called when the close button is clicked
147    #[prop_or_default]
148    pub onclose: Callback<()>,
149
150    #[prop_or_default]
151    pub r#ref: NodeRef,
152}
153
154/// The actual popover content component.
155#[function_component(PopoverPopup)]
156pub fn popover_popup(props: &PopoverPopupProperties) -> Html {
157    let mut class = classes!("pf-v5-c-popover");
158
159    class.extend_from(&props.orientation);
160
161    if props.width_auto {
162        class.extend(classes!("pf-m-width-auto"));
163    }
164
165    if props.no_padding {
166        class.extend(classes!("pf-m-no-padding"));
167    }
168
169    let style = if props.hidden {
170        "display: none;".to_string()
171    } else {
172        props.style.to_string()
173    };
174
175    let onclose = {
176        let onclose = props.onclose.clone();
177        Callback::from(move |_| {
178            onclose.emit(());
179        })
180    };
181
182    html! (
183        <div
184            ref={&props.r#ref}
185            {style}
186            {class}
187            role="dialog"
188            aria-model="true"
189        >
190            <div class="pf-v5-c-popover__arrow"></div>
191            <div class="pf-v5-c-popover__content">
192                if !props.no_close {
193                    <div class="pf-v5-c-popover__close">
194                        <Button
195                            variant={ButtonVariant::Plain}
196                            icon={Icon::Times}
197                            aria_label="Close"
198                            onclick={onclose}
199                        />
200                    </div>
201                }
202
203                { props.body.clone() }
204
205            </div>
206        </div>
207    )
208}
209
210#[derive(Clone, Debug, PartialEq, Properties)]
211pub struct PopoverBodyProperties {
212    #[prop_or_default]
213    pub children: Html,
214    #[prop_or_default]
215    pub header: Option<Html>,
216    #[prop_or_default]
217    pub footer: Option<Html>,
218}
219
220#[function_component(PopoverBody)]
221pub fn popover_body(props: &PopoverBodyProperties) -> Html {
222    html!(
223        <>
224            if let Some(header) = &props.header {
225                <header class="pf-v5-c-popover__header">
226                    <div class="pf-v5-c-popover__title">
227                        <h1 class="pf-v5-c-title pf-m-md">
228                            { header.clone() }
229                        </h1>
230                    </div>
231                </header>
232            }
233
234            <div class="pf-v5-c-popover__body">
235                { props.children.clone() }
236            </div>
237
238            if let Some(footer) = &props.footer {
239                <footer class="pf-v5-c-popover__footer">
240                    { footer.clone() }
241                </footer>
242            }
243        </>
244    )
245}