Skip to main content

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 {onclick} {style} ref={target_ref.clone()}>{ props.target.clone() }</span>
87            <PortalPopper
88                visible={*active}
89                content={content_ref.clone()}
90                target={target_ref}
91                {onstatechange}
92                placement={Placement::Right}
93                modifiers={vec![
94                    Modifier::Offset(Offset {
95                        skidding: 0,
96                        distance: 20,
97                    }),
98                    Modifier::PreventOverflow(PreventOverflow { padding: 0 }),
99                ]}
100            >
101                <ContextProvider<PopoverContext> {context}>
102                    <PopoverPopup
103                        width_auto={props.width_auto}
104                        no_padding={props.no_padding}
105                        no_close={props.no_close}
106                        r#ref={content_ref}
107                        style={&state.styles.popper.extend_with("z-index", "1000")}
108                        {orientation}
109                        {onclose}
110                        body={props.body.clone()}
111                    />
112                </ContextProvider<PopoverContext>>
113            </PortalPopper>
114        </>
115    )
116}
117
118// popover popup
119
120/// The popover content component.
121#[derive(Clone, PartialEq, Properties)]
122pub struct PopoverPopupProperties {
123    pub body: VChild<PopoverBody>,
124
125    pub orientation: Orientation,
126
127    #[prop_or_default]
128    pub no_padding: bool,
129    #[prop_or_default]
130    pub no_close: bool,
131
132    #[prop_or_default]
133    pub width_auto: bool,
134
135    #[prop_or_default]
136    pub hidden: bool,
137    #[prop_or_default]
138    pub style: AttrValue,
139
140    /// called when the close button is clicked
141    #[prop_or_default]
142    pub onclose: Callback<()>,
143
144    #[prop_or_default]
145    pub r#ref: NodeRef,
146}
147
148/// The actual popover content component.
149#[function_component(PopoverPopup)]
150pub fn popover_popup(props: &PopoverPopupProperties) -> Html {
151    let mut class = classes!("pf-v6-c-popover");
152
153    class.extend_from(&props.orientation);
154
155    if props.width_auto {
156        class.extend(classes!("pf-m-width-auto"));
157    }
158
159    if props.no_padding {
160        class.extend(classes!("pf-m-no-padding"));
161    }
162
163    let style = if props.hidden {
164        "display: none;".to_string()
165    } else {
166        props.style.to_string()
167    };
168
169    let onclose = {
170        let onclose = props.onclose.clone();
171        Callback::from(move |_| {
172            onclose.emit(());
173        })
174    };
175
176    html! (
177        <div ref={&props.r#ref} {style} {class} role="dialog" aria-model="true">
178            <div class="pf-v6-c-popover__arrow" />
179            <div class="pf-v6-c-popover__content">
180                if !props.no_close {
181                    <div class="pf-v6-c-popover__close">
182                        <Button
183                            variant={ButtonVariant::Plain}
184                            icon={Icon::Times}
185                            aria_label="Close"
186                            onclick={onclose}
187                        />
188                    </div>
189                }
190                { props.body.clone() }
191            </div>
192        </div>
193    )
194}
195
196#[derive(Clone, Debug, PartialEq, Properties)]
197pub struct PopoverBodyProperties {
198    #[prop_or_default]
199    pub children: Html,
200    #[prop_or_default]
201    pub header: Option<Html>,
202    #[prop_or_default]
203    pub footer: Option<Html>,
204}
205
206#[function_component(PopoverBody)]
207pub fn popover_body(props: &PopoverBodyProperties) -> Html {
208    html!(
209        <>
210            if let Some(header) = &props.header {
211                <header class="pf-v6-c-popover__header">
212                    <div class="pf-v6-c-popover__title">
213                        <h1 class="pf-v6-c-title pf-m-md">{ header.clone() }</h1>
214                    </div>
215                </header>
216            }
217            <div
218                class="pf-v6-c-popover__body"
219            >
220                { props.children.clone() }
221            </div>
222            if let Some(footer) = &props.footer {
223                <footer class="pf-v6-c-popover__footer">{ footer.clone() }</footer>
224            }
225        </>
226    )
227}