Skip to main content

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