patternfly_yew/components/
modal.rs

1//! Modal
2use crate::ouia;
3use crate::prelude::use_backdrop;
4use crate::prelude::wrap::wrapper_div_with_attributes;
5use crate::utils::{Ouia, OuiaComponentType, OuiaSafe};
6use yew::prelude::*;
7use yew::virtual_dom::ApplyAttributeAs;
8use yew_hooks::{use_click_away, use_event_with_window};
9
10const OUIA: Ouia = ouia!("ModalContent");
11
12#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
13pub enum ModalVariant {
14    #[default]
15    None,
16    Small,
17    Medium,
18    Large,
19}
20
21impl ModalVariant {
22    pub fn as_classes(&self) -> Classes {
23        match self {
24            ModalVariant::None => classes!(),
25            ModalVariant::Small => classes!("pf-m-sm"),
26            ModalVariant::Medium => classes!("pf-m-md"),
27            ModalVariant::Large => classes!("pf-m-lg"),
28        }
29    }
30}
31
32/// Properties for [`Modal`]
33#[derive(Clone, PartialEq, Properties)]
34pub struct ModalProperties {
35    #[prop_or_default]
36    pub title: String,
37    #[prop_or_default]
38    pub description: String,
39    #[prop_or_default]
40    pub variant: ModalVariant,
41    #[prop_or_default]
42    pub children: Children,
43    #[prop_or_default]
44    pub footer: Option<Html>,
45
46    #[prop_or_default]
47    pub onclose: Option<Callback<()>>,
48
49    /// Disable close button
50    #[prop_or(true)]
51    pub show_close: bool,
52
53    /// Disable closing the modal when the escape key is pressed
54    #[prop_or_default]
55    pub disable_close_escape: bool,
56    /// Disable closing the modal when the user clicks outside the modal
57    #[prop_or_default]
58    pub disable_close_click_outside: bool,
59
60    /// OUIA Component id
61    #[prop_or_default]
62    pub ouia_id: Option<String>,
63    /// OUIA Component Type
64    #[prop_or(OUIA.component_type())]
65    pub ouia_type: OuiaComponentType,
66    /// OUIA Component Safe
67    #[prop_or(OuiaSafe::TRUE)]
68    pub ouia_safe: OuiaSafe,
69}
70
71/// Modal component
72///
73/// > A **modal** displays important information to a user without requiring them to navigate to a new page.
74///
75/// See: <https://www.patternfly.org/components/modal>
76///
77/// ## Properties
78///
79/// Defined by [`ModalProperties`].
80///
81/// ## Contexts
82///
83/// If the modal dialog is wrapped by a [`crate::prelude::BackdropViewer`] component and no
84/// `onclose` callback is set, then it will automatically close the backdrop when the modal dialog
85/// gets closed.
86///
87#[function_component(Modal)]
88pub fn modal(props: &ModalProperties) -> Html {
89    let ouia_id = use_memo(props.ouia_id.clone(), |id| {
90        id.clone().unwrap_or(OUIA.generated_id())
91    });
92    let mut classes = props.variant.as_classes();
93    classes.push("pf-v5-c-modal-box");
94
95    let backdrop = use_backdrop();
96
97    let onclose = use_memo((props.onclose.clone(), backdrop), |(onclose, backdrop)| {
98        let onclose = onclose.clone();
99        let backdrop = backdrop.clone();
100        Callback::from(move |()| {
101            if let Some(onclose) = &onclose {
102                onclose.emit(());
103            } else if let Some(backdrop) = &backdrop {
104                backdrop.close();
105            }
106        })
107    });
108
109    // escape key
110    {
111        let disabled = props.disable_close_escape;
112        let onclose = onclose.clone();
113        use_event_with_window("keydown", move |e: KeyboardEvent| {
114            if !disabled && e.key() == "Escape" {
115                onclose.emit(());
116            }
117        });
118    }
119
120    // outside click
121
122    let node_ref = use_node_ref();
123
124    {
125        let disabled = props.disable_close_click_outside;
126        let onclose = onclose.clone();
127        use_click_away(node_ref.clone(), move |_: Event| {
128            if !disabled {
129                onclose.emit(());
130            }
131        });
132    }
133
134    html! (
135        <div
136            class={classes}
137            role="dialog"
138            aria-modal="true"
139            aria-labelledby="modal-title"
140            aria-describedby="modal-description"
141            ref={node_ref}
142            data-ouia-component-id={(*ouia_id).clone()}
143            data-ouia-component-type={props.ouia_type}
144            data-ouia-safe={props.ouia_safe}
145        >
146            if props.show_close {
147                <div class="pf-v5-c-modal-box__close">
148                    <button
149                        class="pf-v5-c-button pf-m-plain"
150                        type="button"
151                        aria-label="Close dialog"
152                        onclick={onclose.reform(|_|())}
153                    >
154                        <i class="fas fa-times" aria-hidden="true"></i>
155                    </button>
156                </div>
157            }
158
159            <header class="pf-v5-c-modal-box__header">
160                <h1
161                    class="pf-v5-c-modal-box__title"
162                    id="modal-title-modal-with-form"
163                >{ &props.title }</h1>
164            </header>
165
166
167            if !&props.description.is_empty() {
168                <div class="pf-v5-c-modal-box__body">
169                    <p>{ &props.description }</p>
170                </div>
171            }
172
173            { for props.children.iter().map(|c|{
174                wrapper_div_with_attributes(c,
175                    &[
176                        ("class", "pf-v5-c-modal-box__body", ApplyAttributeAs::Attribute),
177                        ("id", "modal-description", ApplyAttributeAs::Attribute)
178                    ])
179            }) }
180
181            if let Some(footer) = &props.footer {
182              <footer class="pf-v5-c-modal-box__footer">
183                  { footer.clone() }
184              </footer>
185            }
186        </div>
187    )
188}