patternfly_yew/components/
calendar.rs1use crate::prelude::{
2 Button, ButtonType, ButtonVariant, Icon, InputGroup, InputGroupItem, SimpleSelect, TextInput,
3 TextInputType,
4};
5use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday};
6use num_traits::cast::FromPrimitive;
7use std::str::FromStr;
8
9use yew::{
10 Callback, Html, Properties, classes, function_component, html, use_callback, use_state_eq,
11};
12
13use super::select::SelectItemRenderer;
14
15#[derive(Clone, PartialEq, Properties)]
16pub struct CalendarMonthProperties {
17 #[prop_or(Local::now().date_naive())]
18 pub date: NaiveDate,
19 #[prop_or_default]
20 pub onchange: Callback<NaiveDate>,
21 #[prop_or_default]
22 pub rangestart: Option<NaiveDate>,
23 #[prop_or(Weekday::Mon)]
24 pub weekday_start: Weekday,
25}
26
27fn build_calendar(date: NaiveDate, weekday_start: Weekday) -> Vec<Vec<NaiveDate>> {
32 const ONE_DAY: Days = Days::new(1);
33 let mut ret: Vec<Vec<NaiveDate>> = Vec::new();
34 let mut first_day = date.with_day(1).unwrap();
36 let mut day = first_day.week(weekday_start).first_day();
37 let mut week: Vec<NaiveDate>;
38
39 while first_day.month() == date.month() {
40 week = Vec::new();
41 while first_day.week(weekday_start).days().contains(&day) {
42 week.push(day);
43 day = day + ONE_DAY;
44 }
45
46 first_day = first_day.week(weekday_start).last_day() + ONE_DAY;
47 ret.push(week);
48 }
49
50 ret
51}
52
53#[function_component(CalendarView)]
54pub fn calendar(props: &CalendarMonthProperties) -> Html {
55 let date = use_state_eq(|| props.date);
57 let show_date = use_state_eq(|| props.date);
59 let weeks = build_calendar(*show_date, props.weekday_start);
61 let month = use_state_eq(|| Month::from_u32(props.date.month()).unwrap());
63
64 let callback_month_select = use_callback(
65 (show_date.clone(), month.clone()),
66 move |new_month: MonthLocal, (show_date, month)| {
67 if let Some(d) = NaiveDate::from_ymd_opt(
68 show_date.year(),
69 new_month.0.number_from_month(),
70 show_date.day(),
71 ) {
72 show_date.set(d);
73 month.set(new_month.0);
74 }
75 },
76 );
77
78 let callback_years = use_callback(show_date.clone(), move |new_year: String, show_date| {
79 if let Ok(y) = i32::from_str(&new_year)
80 && let Some(d) = NaiveDate::from_ymd_opt(y, show_date.month(), show_date.day())
81 {
82 show_date.set(d)
83 }
84 });
85
86 let callback_prev = use_callback(
87 (show_date.clone(), month.clone()),
88 move |_, (show_date, month)| {
89 if let Some(d) = show_date.checked_sub_months(Months::new(1)) {
90 show_date.set(d);
91 month.set(month.pred());
92 }
93 },
94 );
95
96 let callback_next = use_callback(
97 (show_date.clone(), month.clone()),
98 move |_, (show_date, month)| {
99 if let Some(d) = show_date.checked_add_months(Months::new(1)) {
100 show_date.set(d);
101 month.set(month.succ());
102 }
103 },
104 );
105
106 html! {
107 <div class="pf-v6-c-calendar-month">
108 <div class="pf-v6-c-calendar-month__header">
109 <div class="pf-v6-c-calendar-month__header-nav-control pf-m-prev-month">
110 <Button
111 variant={ButtonVariant::Plain}
112 aria_label="Previous month"
113 onclick={callback_prev}
114 >
115 { Icon::AngleLeft.as_html() }
116 </Button>
117 </div>
118 <InputGroup>
119 <InputGroupItem>
120 <div class="pf-v6-c-calendar-month__header-month">
121 <SimpleSelect<MonthLocal>
122 entries={vec![
123 MonthLocal(Month::January),
124 MonthLocal(Month::February),
125 MonthLocal(Month::March),
126 MonthLocal(Month::April),
127 MonthLocal(Month::May),
128 MonthLocal(Month::June),
129 MonthLocal(Month::July),
130 MonthLocal(Month::August),
131 MonthLocal(Month::September),
132 MonthLocal(Month::October),
133 MonthLocal(Month::November),
134 MonthLocal(Month::December)
135 ]}
136 selected={MonthLocal(*month)}
137 onselect={callback_month_select}
138 />
139 </div>
140 </InputGroupItem>
141 <InputGroupItem>
142 <div class="pf-v6-c-calendar-month__header-year">
143 <TextInput
144 value={show_date.year().to_string()}
145 r#type={TextInputType::Number}
146 onchange={callback_years}
147 />
148 </div>
149 </InputGroupItem>
150 </InputGroup>
151 <div class="pf-v6-c-calendar-month__header-nav-control pf-m-next-month">
152 <Button
153 variant={ButtonVariant::Plain}
154 aria_label="Next month"
155 onclick={callback_next}
156 >
157 { Icon::AngleRight.as_html() }
158 </Button>
159 </div>
160 </div>
161 <table class="pf-v6-c-calendar-month__calendar">
162 <thead class="pf-v6-c-calendar-month__days">
163 <tr class="pf-v6-c-calendar-month__days-row">
164 { weeks[0].clone().into_iter().map(|day| {
165 html!{
166 <th class="pf-v6-c-calendar-month__day">
167 <span class="pf-v6-screen-reader">{weekday_name(day.weekday())}</span>
168 <span aria-hidden="true">{weekday_name(day.weekday())}</span>
169 </th>
170 }
171 }).collect::<Html>() }
172 </tr>
173 </thead>
174 <tbody class="pf-v6-c-calendar-month__dates">
175 { weeks.into_iter().map(|week| {
176 html!{
177 <>
178 <tr class="pf-v6-c-calendar-month__dates-row">
179 {
180 week.into_iter().map(|day| {
181 let callback_date = {
182 let date = date.clone();
183 let month = month.clone();
184 let show_date = show_date.clone();
185 let onchange = props.onchange.clone();
186 move |day: NaiveDate| {
187 Callback::from(move |_| {
188 let new = NaiveDate::from_ymd_opt(day.year(), day.month(), day.day()).unwrap();
189 date.set(new);
190 show_date.set(new);
191 month.set(Month::from_u32(day.month()).unwrap());
192 onchange.emit(new);
193 })
194 }
195 };
196
197 let mut classes = classes!("pf-v6-c-calendar-month__dates-cell");
198
199 if day == *date {
200 classes.extend(classes!("pf-m-selected"));
201 }
202
203 if day.month() != show_date.month() {
204 classes.extend(classes!("pf-m-adjacent-month"));
205 }
206
207 let before_range = if let Some(range_start) = props.rangestart {
208 if day < range_start {
209 classes.extend(classes!("pf-m-disabled"));
210 }
211
212 if day == range_start {
213 classes.extend(classes!("pf-m-start-range"));
214 classes.extend(classes!("pf-m-selected"));
215 }
216
217 if day >= range_start && day <= *date {
218 classes.extend(classes!("pf-m-in-range"));
219 }
220
221 if day == *date {
222 classes.extend(classes!("pf-m-end-range"));
223 }
224
225 day < range_start
226 } else { false };
227
228 html!{
229 <>
230 <td class={classes}>
231 <Button
232 class="pf-v6-c-calendar-month__date"
233 r#type={ButtonType::Button}
234 variant={if before_range {
235 ButtonVariant::Plain
236 } else {
237 ButtonVariant::None
238 }}
239 onclick={callback_date(day)}
240 disabled={before_range}
241 >
242 {day.day()}
243 </Button>
244 </td>
245 </>
246 }
247 }).collect::<Html>()
248 }
249 </tr>
250 </>
251 }
252 }).collect::<Html>() }
253 </tbody>
254 </table>
255 </div>
256 }
257}
258
259#[derive(Clone, PartialEq, Eq)]
261struct MonthLocal(Month);
262
263impl SelectItemRenderer for MonthLocal {
264 type Item = String;
265
266 #[cfg(feature = "localization")]
267 fn label(&self) -> Self::Item {
268 self.0.localized_name()
269 }
270
271 #[cfg(not(feature = "localization"))]
272 fn label(&self) -> Self::Item {
273 self.0.name().to_string()
274 }
275}
276
277#[cfg(feature = "localization")]
278trait Localized {
279 fn localized_name(&self) -> String;
280}
281
282#[cfg(feature = "localization")]
283impl Localized for Month {
284 fn localized_name(&self) -> String {
286 let date = NaiveDate::from_ymd_opt(2024, self.number_from_month(), 1).unwrap();
288
289 date.format_localized("%B", current_locale()).to_string()
291 }
292}
293
294#[cfg(feature = "localization")]
295fn weekday_name(weekday: Weekday) -> String {
296 localized_weekday_name(weekday)
297}
298
299#[cfg(not(feature = "localization"))]
300fn weekday_name(weekday: Weekday) -> String {
301 weekday.to_string()
302}
303
304#[cfg(feature = "localization")]
305fn localized_weekday_name(weekday: Weekday) -> String {
306 let today = chrono::Local::now().naive_local();
308
309 let days_until_weekday = (7 + weekday.num_days_from_monday() as i64
311 - today.weekday().num_days_from_monday() as i64)
312 % 7;
313
314 let one_day = today + chrono::Duration::days(days_until_weekday);
316
317 one_day
319 .date()
320 .format_localized("%a", current_locale())
321 .to_string()
322}
323
324#[cfg(feature = "localization")]
325static CURRENT_LOCALE_CELL: std::sync::OnceLock<chrono::Locale> = std::sync::OnceLock::new();
326
327#[cfg(feature = "localization")]
328fn current_locale() -> chrono::Locale {
329 CURRENT_LOCALE_CELL
330 .get_or_init(|| {
331 let current_locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
333
334 let current_locale_snake_case = current_locale
336 .as_str()
337 .split('.')
338 .next()
339 .map(|s| s.replace('-', "_"))
340 .unwrap_or("en_US".to_string());
341
342 chrono::Locale::try_from(current_locale_snake_case.as_str())
344 .unwrap_or(chrono::Locale::POSIX)
345 })
346 .to_owned()
347}