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
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#[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 #[prop_or_default]
148 pub onclose: Callback<()>,
149
150 #[prop_or_default]
151 pub r#ref: NodeRef,
152}
153
154#[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}