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" />
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>{ input }</InputGroupItem>
117                    <InputGroupItem>
118                        <Popover {target} {body} no_padding=true no_close=true width_auto=true />
119                    </InputGroupItem>
120                </InputGroup>
121            </div>
122        </div>
123    }
124}
125
126/// the body component, using the popover context
127#[derive(PartialEq, Properties)]
128struct BodyProperties {
129    date: NaiveDate,
130    weekday_start: Weekday,
131    rangestart: Option<NaiveDate>,
132    onchange: Callback<NaiveDate>,
133}
134
135#[function_component(Body)]
136fn body(props: &BodyProperties) -> Html {
137    let context = use_context::<PopoverContext>();
138    let onchange = use_callback(
139        (context, props.onchange.clone()),
140        |value, (context, callback)| {
141            if let Some(context) = context {
142                context.close();
143            }
144            callback.emit(value);
145        },
146    );
147
148    html!(
149        <CalendarView
150            date={props.date}
151            weekday_start={props.weekday_start}
152            rangestart={props.rangestart}
153            {onchange}
154        />
155    )
156}