Skip to main content

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-v6-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" style="line-height: 1.5;"></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>
72            <Body
73                date={value.unwrap_or_else(|| Local::now().date_naive())}
74                weekday_start={props.weekday_start}
75                rangestart={props.rangestart}
76                onchange={callback_change_value}
77            />
78        </PopoverBody>
79    );
80
81    // short circuit the text input to the text value
82    let input_change = use_callback(string_value.clone(), |value, string_value| {
83        string_value.set(value);
84    });
85    // when the text value changes, try updating the date value
86    {
87        let onchange = props.onchange.clone();
88        use_effect_with(
89            ((*string_value).clone(), value.clone()),
90            move |(string_value, value)| {
91                // FIXME: should extract an "error" state from this
92                let new = NaiveDate::parse_from_str(string_value, "%Y-%m-%d").ok();
93
94                value.set(new);
95                if let Some(new) = new {
96                    onchange.emit(new);
97                }
98            },
99        );
100    }
101
102    // The text input
103    let input = html! (
104        <TextInput
105            onchange={input_change}
106            disabled={props.disabled}
107            value={(*string_value).clone()}
108            placeholder={props.placeholder.clone()}
109        />
110    );
111
112    html! {
113        <div class="pf-v6-c-date-picker">
114            <div class="pf-v6-c-date-picker__input">
115                <InputGroup>
116                    <InputGroupItem>
117                        {input}
118                    </InputGroupItem>
119                    <InputGroupItem>
120                        <Popover
121                            {target} {body}
122                            no_padding=true
123                            no_close=true
124                            width_auto=true
125                        />
126                    </InputGroupItem>
127                </InputGroup>
128            </div>
129        </div>
130    }
131}
132
133/// the body component, using the popover context
134#[derive(PartialEq, Properties)]
135struct BodyProperties {
136    date: NaiveDate,
137    weekday_start: Weekday,
138    rangestart: Option<NaiveDate>,
139    onchange: Callback<NaiveDate>,
140}
141
142#[function_component(Body)]
143fn body(props: &BodyProperties) -> Html {
144    let context = use_context::<PopoverContext>();
145    let onchange = use_callback(
146        (context, props.onchange.clone()),
147        |value, (context, callback)| {
148            if let Some(context) = context {
149                context.close();
150            }
151            callback.emit(value);
152        },
153    );
154
155    html!(
156        <CalendarView
157            date={props.date}
158            weekday_start={props.weekday_start}
159            rangestart={props.rangestart}
160            {onchange}
161        />
162    )
163}