patternfly_yew/components/
helper_text.rs

1//! Helper text
2//!
3//! **NOTE:** While it looks similar to the [`Form`](crate::prelude::Form)'s helper text, it is
4//! a different type.
5
6use crate::prelude::{AsClasses, ExtendClasses, Icon};
7use log::warn;
8use std::rc::Rc;
9use yew::prelude::*;
10
11#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
12pub enum HelperTextComponent {
13    #[default]
14    Div,
15    Ul,
16}
17
18impl ToString for HelperTextComponent {
19    fn to_string(&self) -> String {
20        match self {
21            Self::Div => "div",
22            Self::Ul => "ul",
23        }
24        .to_string()
25    }
26}
27
28#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
29pub enum HelperTextItemVariant {
30    #[default]
31    Default,
32    Intermediate,
33    Warning,
34    Success,
35    Error,
36}
37
38impl AsClasses for HelperTextItemVariant {
39    fn extend_classes(&self, classes: &mut Classes) {
40        match self {
41            Self::Default => {}
42            Self::Intermediate => classes.push(classes!("pf-m-indeterminate")),
43            Self::Warning => classes.push(classes!("pf-m-warning")),
44            Self::Success => classes.push(classes!("pf-m-success")),
45            Self::Error => classes.push(classes!("pf-m-error")),
46        }
47    }
48}
49
50impl HelperTextItemVariant {
51    pub fn icon(&self) -> Icon {
52        match self {
53            Self::Default => Icon::Minus,
54            Self::Intermediate => Icon::Minus,
55            Self::Warning => Icon::ExclamationTriangle,
56            Self::Success => Icon::CheckCircle,
57            Self::Error => Icon::ExclamationCircle,
58        }
59    }
60}
61
62#[derive(Clone, Debug, PartialEq, Properties)]
63pub struct HelperTextProperties {
64    /// Adds an accessible label to the helper text when `component` is [`HelperTextComponent::Ul`].
65    #[prop_or_default]
66    pub aria_label: Option<AttrValue>,
67    /// Content to be rendered inside the [`HelperText`] container. This must be a [`HelperTextItem`] component.
68    #[prop_or_default]
69    pub children: ChildrenWithProps<HelperTextItem>,
70    /// Additional classes to be applied to the [`HelperText`] container.
71    #[prop_or_default]
72    pub class: Classes,
73    /// Specify the html element of the [`HelperText`] container. Defaults to using a `div`.
74    #[prop_or_default]
75    pub component: HelperTextComponent,
76    /// id for the helper text container. The value of this prop can be passed into a form
77    /// component's `aria-describedby` property when you intend for all helper text items to be
78    /// announced to assistive technologies.
79    #[prop_or_default]
80    pub id: Option<AttrValue>,
81    /// Flag for indicating whether the helper text container is a live region. Use this prop when
82    /// you expect or intend for any [`HelperTextItem`] within the container to be dynamically updated.
83    #[prop_or_default]
84    pub live_region: bool,
85
86    // Not included in PF React, but is in the html spec.
87    /// Hides the [`HelperText`]
88    #[prop_or_default]
89    pub hidden: bool,
90}
91
92/// HelperText component
93///
94/// > **HelperText** is an on-screen field guideline that helps provide context regarding field inputs.
95///
96/// See: <https://www.patternfly.org/components/helper-text>
97///
98/// ## Properties
99///
100/// Defined by [`HelperTextProperties`].
101///
102/// ## Children
103///
104/// This component may contain one or more [`HelperTextItem`] components.
105///
106/// ## Example
107///
108/// ```
109/// use yew::prelude::*;
110/// use patternfly_yew::prelude::*;
111///
112/// #[function_component(Example)]
113/// fn example() -> Html {
114///     html!(
115///         <HelperText>
116///             <HelperTextItem>{"This is default helper text"}</HelperTextItem>
117///         </HelperText>
118///     )
119/// }
120/// ```
121#[function_component(HelperText)]
122pub fn helper_text(props: &HelperTextProperties) -> Html {
123    let mut class = classes!("pf-v5-c-helper-text", props.class.clone());
124    if props.hidden {
125        class.push("pf-m-hidden")
126    }
127    let aria_live = props.live_region.then_some("polite");
128    let component = props.component.to_string();
129    let item_component = match props.component {
130        HelperTextComponent::Div => HelperTextItemComponent::Div,
131        HelperTextComponent::Ul => HelperTextItemComponent::Li,
132    };
133    let role = (props.component == HelperTextComponent::Ul).then_some("list");
134    let _ = use_memo(
135        (props.component, props.aria_label.clone()),
136        // Use memo so that warn doesnt keep getting called.
137        |(component, label)| {
138            if component == &HelperTextComponent::Ul && label.is_none() {
139                warn!(
140                    "The aria_label property should be set on the HelperText component when the \
141                    component attribute is set to HelperTextComponent::Ul"
142                );
143            }
144        },
145    );
146
147    html!(
148        <@{component}
149            id={ &props.id }
150            { class }
151            aria-label={ &props.aria_label }
152            aria-live={ aria_live }
153            { role }
154        >
155            {
156                for props.children.iter().map(|mut c|{
157                    let props = Rc::make_mut(&mut c.props);
158                    props.component = item_component;
159                    c
160                })
161            }
162        </@>
163    )
164}
165
166#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
167pub enum HelperTextItemComponent {
168    #[default]
169    Div,
170    Li,
171}
172
173impl ToString for HelperTextItemComponent {
174    fn to_string(&self) -> String {
175        match self {
176            Self::Div => "div",
177            Self::Li => "li",
178        }
179        .to_string()
180    }
181}
182
183#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
184pub enum HelperTextItemIcon {
185    #[default]
186    Default,
187    Hidden,
188    Visible,
189    Custom(Icon),
190}
191
192#[derive(Clone, Debug, PartialEq, Properties)]
193pub struct HelperTextItemProperties {
194    /// Content to be rendered inside the [`HelperTextItem`].
195    #[prop_or_default]
196    pub children: Html,
197    /// Additional classes to be applied to the [`HelperTextItem`].
198    #[prop_or_default]
199    pub class: Classes,
200    /// Set the type of html element to use. When [`HelperTextItem`] is used as a child of
201    /// [`HelperText`] this property is set automatically.
202    #[prop_or_default]
203    pub component: HelperTextItemComponent,
204    /// Flag to modifies a [`HelperTextItem`] to be dynamic. For use when the item changes state as
205    /// the form field the text is associated with is updated.
206    #[prop_or_default]
207    pub dynamic: bool,
208    /// Controls the icon prefixing the helper text. The default is to show an icon when the
209    /// `dynamic` property is `true`
210    #[prop_or_default]
211    pub icon: HelperTextItemIcon,
212    /// id for the [`HelperTextItem`]. The value of this property can be passed into a form
213    /// component's `aria-describedby` property when you intend for only specific helper text items
214    /// to be announced to assistive technologies.
215    #[prop_or_default]
216    pub id: Option<AttrValue>,
217    /// Text that is only accessible to screen readers in order to announce the status of the
218    /// [`HelperTextItem`]. This property is only used when the `dynamic` is `true`.
219    #[prop_or_default]
220    pub screen_reader_text: AttrValue,
221    /// Variant styling of the helper text item
222    #[prop_or_default]
223    pub variant: HelperTextItemVariant,
224}
225
226/// An item in a [`HelperText`] component.
227///
228/// ## Properties
229///
230/// Defined by [`HelperTextItemProperties`].
231///
232/// ## Example
233///
234/// See [`HelperText`] for sample usage
235#[function_component(HelperTextItem)]
236pub fn helper_text_item(props: &HelperTextItemProperties) -> Html {
237    let mut class = classes!("pf-v5-c-helper-text__item", props.class.clone());
238    if props.dynamic {
239        class.push(classes!("pf-m-dynamic"));
240    }
241    class.extend_from(&props.variant);
242    let component = props.component.to_string();
243    let icon = match (props.icon, &props.dynamic) {
244        (HelperTextItemIcon::Default, false) | (HelperTextItemIcon::Hidden, ..) => None,
245        (HelperTextItemIcon::Default, true) | (HelperTextItemIcon::Visible, ..) => {
246            Some(props.variant.icon())
247        }
248        (HelperTextItemIcon::Custom(icon), ..) => Some(icon),
249    };
250
251    let item_icon = icon.map(|icon| {
252        html!(
253            <span class="pf-v5-c-helper-text__item-icon">
254                { icon }
255            </span>
256        )
257    });
258
259    let screen_reader = use_memo(
260        (props.screen_reader_text.clone(), props.dynamic),
261        // Use memo so that warn doesn't keep getting called.
262        |(text, _)| {
263            if !text.is_empty() {
264                if props.dynamic {
265                    Some(html!(
266                        <span class="pf-v5-u-screen-reader">
267                            { ": " }{ text }{ ";" }
268                        </span>
269                    ))
270                } else {
271                    warn!(
272                        "The screen_reader_text attribute was set but has not been used as the \
273                    dynamic attribute was not set to true."
274                    );
275                    None
276                }
277            } else {
278                None
279            }
280        },
281    );
282
283    html!(
284        <@{component} id={ &props.id } { class }>
285            { item_icon }
286            <div class="pf-v5-c-helper-text__item-text">
287                { props.children.clone() }
288                { (*screen_reader).clone() }
289            </div>
290        </@>
291    )
292}