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            { for props.children.iter().map(|mut c|{
158                    let props = Rc::make_mut(&mut c.props);
159                    props.component = item_component;
160                    c
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 Display for HelperTextItemComponent {
174    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
175        let value = match self {
176            Self::Div => "div",
177            Self::Li => "li",
178        };
179
180        f.write_str(value)
181    }
182}
183
184#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
185pub enum HelperTextItemIcon {
186    #[default]
187    Default,
188    Hidden,
189    Visible,
190    Custom(Icon),
191}
192
193#[derive(Clone, Debug, PartialEq, Properties)]
194pub struct HelperTextItemProperties {
195    /// Content to be rendered inside the [`HelperTextItem`].
196    #[prop_or_default]
197    pub children: Html,
198    /// Additional classes to be applied to the [`HelperTextItem`].
199    #[prop_or_default]
200    pub class: Classes,
201    /// Set the type of html element to use. When [`HelperTextItem`] is used as a child of
202    /// [`HelperText`] this property is set automatically.
203    #[prop_or_default]
204    pub component: HelperTextItemComponent,
205    /// Flag to modifies a [`HelperTextItem`] to be dynamic. For use when the item changes state as
206    /// the form field the text is associated with is updated.
207    #[prop_or_default]
208    pub dynamic: bool,
209    /// Controls the icon prefixing the helper text. The default is to show an icon when the
210    /// `dynamic` property is `true`
211    #[prop_or_default]
212    pub icon: HelperTextItemIcon,
213    /// id for the [`HelperTextItem`]. The value of this property can be passed into a form
214    /// component's `aria-describedby` property when you intend for only specific helper text items
215    /// to be announced to assistive technologies.
216    #[prop_or_default]
217    pub id: Option<AttrValue>,
218    /// Text that is only accessible to screen readers in order to announce the status of the
219    /// [`HelperTextItem`]. This property is only used when the `dynamic` is `true`.
220    #[prop_or_default]
221    pub screen_reader_text: AttrValue,
222    /// Variant styling of the helper text item
223    #[prop_or_default]
224    pub variant: HelperTextItemVariant,
225}
226
227/// An item in a [`HelperText`] component.
228///
229/// ## Properties
230///
231/// Defined by [`HelperTextItemProperties`].
232///
233/// ## Example
234///
235/// See [`HelperText`] for sample usage
236#[function_component(HelperTextItem)]
237pub fn helper_text_item(props: &HelperTextItemProperties) -> Html {
238    let mut class = classes!("pf-v6-c-helper-text__item", props.class.clone());
239    if props.dynamic {
240        class.push(classes!("pf-m-dynamic"));
241    }
242    class.extend_from(&props.variant);
243    let component = props.component.to_string();
244    let icon = match (props.icon, &props.dynamic) {
245        (HelperTextItemIcon::Default, false) | (HelperTextItemIcon::Hidden, ..) => None,
246        (HelperTextItemIcon::Default, true) | (HelperTextItemIcon::Visible, ..) => {
247            Some(props.variant.icon())
248        }
249        (HelperTextItemIcon::Custom(icon), ..) => Some(icon),
250    };
251
252    let item_icon =
253        icon.map(|icon| html!(<span class="pf-v6-c-helper-text__item-icon">{ icon }</span>));
254
255    let screen_reader = use_memo(
256        (props.screen_reader_text.clone(), props.dynamic),
257        // Use memo so that warn doesn't keep getting called.
258        |(text, _)| {
259            if !text.is_empty() {
260                if props.dynamic {
261                    Some(html!(<span class="pf-v6-u-screen-reader">{ ": " }{ text }{ ";" }</span>))
262                } else {
263                    warn!(
264                        "The screen_reader_text attribute was set but has not been used as the \
265                    dynamic attribute was not set to true."
266                    );
267                    None
268                }
269            } else {
270                None
271            }
272        },
273    );
274
275    html!(
276        <@{component} id={&props.id} {class}>
277            { item_icon }
278            <div class="pf-v6-c-helper-text__item-text">
279                { props.children.clone() }
280                { (*screen_reader).clone() }
281            </div>
282        </@>
283    )
284}