patternfly_yew/components/
date.rs

1use crate::prelude::{
2    CalendarView, InputGroup, InputGroupItem, Popover, PopoverBody, PopoverContext, TextInput,
3};
4use chrono::{Local, NaiveDate, Weekday};
5use yew::prelude::*;
6
7/// Properties for [`DatePicker`].
8#[derive(Clone, PartialEq, Properties)]
9pub struct DatePickerProperties {
10    /// Disable the component
11    #[prop_or_default]
12    pub disabled: bool,
13    /// The change callback
14    #[prop_or_default]
15    pub onchange: Callback<NaiveDate>,
16    /// The placeholder string
17    #[prop_or(String::from("YYYY-MM-DD"))]
18    pub placeholder: String,
19    #[prop_or_default]
20    pub rangestart: Option<NaiveDate>,
21    /// The currently selected value
22    #[prop_or_default]
23    pub value: Option<NaiveDate>,
24    /// The day to start the week with
25    #[prop_or(Weekday::Mon)]
26    pub weekday_start: Weekday,
27}
28
29/// Date picker component
30///
31/// > A *date picker* helps users enter or select a specific date from a calendar.
32///
33/// See: <https://www.patternfly.org/components/date-and-time/date-picker>
34///
35/// ## Properties
36///
37/// Defined by [`DatePickerProperties`].
38#[function_component(DatePicker)]
39pub fn date_picker(props: &DatePickerProperties) -> Html {
40    let value = use_state_eq(|| props.value);
41    let string_value =
42        use_state_eq(|| props.value.map(|date| date.to_string()).unwrap_or_default());
43
44    let callback_change_value = {
45        let onchange = props.onchange.clone();
46        use_callback(
47            (value.clone(), string_value.clone()),
48            move |new_date: NaiveDate, (value, string_value)| {
49                value.set(Some(new_date));
50                string_value.set(new_date.to_string());
51                onchange.emit(new_date);
52            },
53        )
54    };
55
56    let target = html! {
57        <button
58            class="pf-v5-c-button pf-m-control"
59            type="button"
60            aria-label="Toggle date picker"
61            disabled={props.disabled}
62        >
63        <i class="fas fa-calendar-alt" aria-hidden="true"></i>
64        </button>
65    };
66
67    let body = html_nested! (
68        // We need to extract the body component, as we need the PopoverContext using use_context.
69        // However, that only works if the call of use_context comes from a component wrapped by
70        // Popover.
71        <PopoverBody> <Body
72                date={value.unwrap_or_else(|| Local::now().date_naive())}
73                weekday_start={props.weekday_start}
74                rangestart={props.rangestart}
75                onchange={callback_change_value}
76        /> </PopoverBody>
77    );
78
79    // short circuit the text input to the text value
80    let input_change = use_callback(string_value.clone(), |value, string_value| {
81        string_value.set(value);
82    });
83    // when the text value changes, try updating the date value
84    {
85        let onchange = props.onchange.clone();
86        use_effect_with(
87            ((*string_value).clone(), value.clone()),
88            move |(string_value, value)| {
89                let new = match NaiveDate::parse_from_str(string_value, "%Y-%m-%d") {
90                    Ok(v) => Some(v),
91                    // FIXME: should extract an "error" state from this
92                    Err(_err) => None,
93                };
94
95                value.set(new);
96                if let Some(new) = new {
97                    onchange.emit(new);
98                }
99            },
100        );
101    }
102
103    // The text input
104    let input = html! (
105        <TextInput
106            onchange={input_change}
107            disabled={props.disabled}
108            value={(*string_value).clone()}
109            placeholder={props.placeholder.clone()}
110        />
111    );
112
113    html! {
114        <div class="pf-v5-c-date-picker">
115            <div class="pf-v5-c-date-picker__input">
116                <InputGroup>
117                    <InputGroupItem>
118                        {input}
119                    </InputGroupItem>
120                    <InputGroupItem>
121                        <Popover
122                            {target} {body}
123                            no_padding=true
124                            no_close=true
125                            width_auto=true
126                        />
127                    </InputGroupItem>
128                </InputGroup>
129            </div>
130        </div>
131    }
132}
133
134/// the body component, using the popover context
135#[derive(PartialEq, Properties)]
136struct BodyProperties {
137    date: NaiveDate,
138    weekday_start: Weekday,
139    rangestart: Option<NaiveDate>,
140    onchange: Callback<NaiveDate>,
141}
142
143#[function_component(Body)]
144fn body(props: &BodyProperties) -> Html {
145    let context = use_context::<PopoverContext>();
146    let onchange = use_callback(
147        (context, props.onchange.clone()),
148        |value, (context, callback)| {
149            if let Some(context) = context {
150                context.close();
151            }
152            callback.emit(value);
153        },
154    );
155
156    html!(
157        <CalendarView
158            date={props.date}
159            weekday_start={props.weekday_start}
160            rangestart={props.rangestart}
161            {onchange}
162        />
163    )
164}