patternfly_yew/components/popover/
mod.rs1use 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 pub fn close(&self) {
18 self.close.emit(());
19 }
20}
21
22#[derive(Clone, Debug, PartialEq, Properties)]
24pub struct PopoverProperties {
25 #[prop_or_default]
27 pub target: Html,
28
29 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#[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 let target_ref = use_node_ref();
60 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#[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 #[prop_or_default]
142 pub onclose: Callback<()>,
143
144 #[prop_or_default]
145 pub r#ref: NodeRef,
146}
147
148#[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}