Skip to content

Commit 545aa7f

Browse files
ddudeninealmloff
andauthored
Date range in DatePicker (#148)
* [*] DatePicker: date range selection - Add DatePicker variants - Draft implementation DateRange for DatePicker - Extend DateRange struct - Use min and max dates in props as DateRange - Spell check fix for Calendar * [*] DatePicker: date range selection - Fix date range selection - Support disabled ranges * [*] DatePicker: draft implementation for calendar multi-view * [*] DatePicker: implement multi-month (Calendar/DatePicker) * [*] DatePicker: refact * [*] DatePicker: updates - Fix docs - Add alignment between the days of the week and the Calendar grid - Add simple title variant in Calendar demo - Add css for Calendar title - Update time crate version (support PartialOrd for Month) - Fix view day on next/previous button click - Fix DateRangePickerContext (sync data) - Fix arrow navigation * fix after merge * try longer timeout * clean up some of the calendar logic by centralizing the view_date and add a default calendar * fix multi-calendar edge title centering * make the calendar div style-able * fix clippy * fix lints * more calendar tests * reformat tests * [*] DatePicker: fix navigation --------- Co-authored-by: Evan Almloff <[email protected]>
1 parent 3564270 commit 545aa7f

File tree

23 files changed

+1363
-358
lines changed

23 files changed

+1363
-358
lines changed

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

playwright/calendar.spec.ts

Lines changed: 207 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test("test", async ({ page }) => {
1212
const nextButton = calendar.locator(".calendar-nav-next");
1313

1414
// Assert the calendar is displayed
15-
await expect(calendar).toBeVisible();
15+
await expect(calendar).toBeVisible({ timeout: 30000 });
1616
// Assert the current month is displayed
1717
const currentMonth = calendar.locator(".calendar-month-select");
1818
let currentMonthText = await currentMonth.inputValue();
@@ -31,7 +31,7 @@ test("test", async ({ page }) => {
3131
// Move focus to the calendar with tab
3232
await page.keyboard.press("Tab");
3333
const focusedDay = calendar.locator(
34-
'.calendar-grid-cell[data-month="current"]:focus',
34+
'.calendar-grid-cell[data-month="current"]:focus'
3535
);
3636
// Assert a day is focused
3737
const firstDay = focusedDay.first();
@@ -53,13 +53,13 @@ test("test", async ({ page }) => {
5353
// Assert the next week day is focused
5454
const nextWeekDayNumber = parseInt(
5555
(await nextWeekDay.textContent()) || "",
56-
10,
56+
10
5757
);
5858
let current_date = new Date();
5959
let daysInMonth = new Date(
6060
current_date.getFullYear(),
6161
current_date.getMonth() + 1,
62-
0,
62+
0
6363
).getDate();
6464
if (dayNumber + 7 > daysInMonth) {
6565
// If the next week day is in the next month, it should wrap around
@@ -71,3 +71,206 @@ test("test", async ({ page }) => {
7171
await page.keyboard.press("ArrowUp");
7272
await expect(firstDay).toContainText(day || "failure");
7373
});
74+
75+
test("year navigation by moving 52 weeks with arrow keys", async ({ page }) => {
76+
await page.goto("http://127.0.0.1:8080/component/?name=calendar&", {
77+
timeout: 20 * 60 * 1000,
78+
});
79+
80+
// Find the calendar element
81+
const calendar = page.locator(".calendar").nth(0);
82+
const monthSelect = calendar.locator(".calendar-month-select");
83+
const yearSelect = calendar.locator(".calendar-year-select");
84+
85+
// Assert the calendar is displayed
86+
await expect(calendar).toBeVisible({ timeout: 30000 });
87+
88+
// Get the initial month and year
89+
const initialMonth = await monthSelect.inputValue();
90+
const initialYear = await yearSelect.inputValue();
91+
const initialYearNumber = parseInt(initialYear, 10);
92+
const initialMonthNumber = parseInt(initialMonth, 10);
93+
94+
// Calculate the exact number of weeks needed to move to the next year
95+
// Start from the first day of the current month
96+
const startDate = new Date(initialYearNumber, initialMonthNumber - 1, 1);
97+
// Calculate the same date next year
98+
const targetDate = new Date(initialYearNumber + 1, initialMonthNumber - 1, 1);
99+
// Calculate the difference in days
100+
const daysDifference = Math.floor(
101+
(targetDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)
102+
);
103+
// Calculate the number of weeks (round to nearest week)
104+
const weeksToMove = Math.ceil(daysDifference / 7);
105+
106+
// Move focus to the calendar manually
107+
const firstDay = calendar
108+
.locator('.calendar-grid-cell[data-month="current"]')
109+
.first();
110+
await firstDay.focus();
111+
112+
// Press ArrowDown the calculated number of times to move forward by one year
113+
for (let i = 0; i < weeksToMove; i++) {
114+
await page.keyboard.press("ArrowDown");
115+
}
116+
117+
// Assert the year has changed to the next year
118+
const nextYear = await yearSelect.inputValue();
119+
const nextYearNumber = parseInt(nextYear, 10);
120+
expect(nextYearNumber).toBe(initialYearNumber + 1);
121+
122+
// The month should be exactly the same
123+
const nextMonth = await monthSelect.inputValue();
124+
expect(nextMonth).toBe(initialMonth);
125+
126+
// Press ArrowUp the same number of times to move back by one year
127+
for (let i = 0; i < weeksToMove; i++) {
128+
await page.keyboard.press("ArrowUp");
129+
}
130+
131+
// Assert the year has changed back to the original year
132+
const currentYear = await yearSelect.inputValue();
133+
expect(currentYear).toBe(initialYear);
134+
135+
// The month should be exactly the same
136+
const currentMonth = await monthSelect.inputValue();
137+
expect(currentMonth).toBe(initialMonth);
138+
});
139+
140+
test("shift + arrow keys navigation", async ({ page }) => {
141+
await page.goto("http://127.0.0.1:8080/component/?name=calendar&", {
142+
timeout: 20 * 60 * 1000,
143+
});
144+
145+
// Find the calendar element
146+
const calendar = page.locator(".calendar").nth(0);
147+
const monthSelect = calendar.locator(".calendar-month-select");
148+
const yearSelect = calendar.locator(".calendar-year-select");
149+
150+
// Assert the calendar is displayed
151+
await expect(calendar).toBeVisible({ timeout: 30000 });
152+
153+
// Get the initial month and year
154+
const initialMonth = await monthSelect.inputValue();
155+
const initialYear = await yearSelect.inputValue();
156+
const initialYearNumber = parseInt(initialYear, 10);
157+
const initialMonthNumber = parseInt(initialMonth, 10);
158+
159+
// Move focus to the calendar
160+
const firstDay = calendar
161+
.locator('.calendar-grid-cell[data-month="current"]')
162+
.first();
163+
await firstDay.focus();
164+
165+
// Test Shift + ArrowDown - should move forward by one month
166+
await page.keyboard.press("Shift+ArrowDown");
167+
168+
let currentMonth = await monthSelect.inputValue();
169+
let currentYear = await yearSelect.inputValue();
170+
let expectedMonth = initialMonthNumber === 12 ? 1 : initialMonthNumber + 1;
171+
let expectedYear =
172+
initialMonthNumber === 12 ? initialYearNumber + 1 : initialYearNumber;
173+
174+
expect(parseInt(currentMonth, 10)).toBe(expectedMonth);
175+
expect(parseInt(currentYear, 10)).toBe(expectedYear);
176+
177+
// Test Shift + ArrowUp - should move back to the initial month
178+
await page.keyboard.press("Shift+ArrowUp");
179+
180+
currentMonth = await monthSelect.inputValue();
181+
currentYear = await yearSelect.inputValue();
182+
expect(currentMonth).toBe(initialMonth);
183+
expect(currentYear).toBe(initialYear);
184+
});
185+
186+
async function testArrowKeyNavigation(
187+
page: any,
188+
arrowKey: "ArrowRight" | "ArrowLeft",
189+
startPosition: "first" | "last",
190+
expectedOrder: "ascending" | "descending"
191+
) {
192+
await page.goto("http://127.0.0.1:8080/component/?name=calendar&", {
193+
timeout: 20 * 60 * 1000,
194+
});
195+
196+
// Find the calendar element
197+
const calendar = page.locator(".calendar").nth(0);
198+
const monthSelect = calendar.locator(".calendar-month-select");
199+
const yearSelect = calendar.locator(".calendar-year-select");
200+
201+
// Assert the calendar is displayed
202+
await expect(calendar).toBeVisible({ timeout: 30000 });
203+
204+
// Get the current month and year to calculate days in month
205+
const currentMonthValue = await monthSelect.inputValue();
206+
const currentYearValue = await yearSelect.inputValue();
207+
const monthNumber = parseInt(currentMonthValue, 10);
208+
const yearNumber = parseInt(currentYearValue, 10);
209+
210+
// Calculate the number of days in the current month
211+
const daysInMonth = new Date(yearNumber, monthNumber, 0).getDate();
212+
213+
// Move focus to the starting day of the current month
214+
const startDay = calendar
215+
.locator('.calendar-grid-cell[data-month="current"]')
216+
[startPosition]();
217+
await startDay.focus();
218+
219+
// Get the focused day selector
220+
const focusedDay = calendar.locator(
221+
'.calendar-grid-cell[data-month="current"]:focus'
222+
);
223+
224+
// Array to track all days visited
225+
const daysVisited = [];
226+
227+
// Get the starting day number
228+
let dayText = await focusedDay.first().textContent();
229+
let dayNumber = parseInt(dayText || "", 10);
230+
daysVisited.push(dayNumber);
231+
232+
// Press arrow key to navigate through all remaining days of the month
233+
for (let i = 1; i < daysInMonth; i++) {
234+
await page.keyboard.press(arrowKey);
235+
236+
// Get the new focused day
237+
dayText = await focusedDay.first().textContent();
238+
dayNumber = parseInt(dayText || "", 10);
239+
daysVisited.push(dayNumber);
240+
}
241+
242+
// Assert that we visited the correct number of days
243+
expect(daysVisited.length).toBe(daysInMonth);
244+
245+
// Sort the days visited to check we got all days from 1 to daysInMonth
246+
const sortedDays = [...daysVisited].sort((a, b) => a - b);
247+
248+
// Create the expected array [1, 2, 3, ..., daysInMonth]
249+
const expectedDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
250+
251+
// Assert that we visited every day exactly once
252+
expect(sortedDays).toEqual(expectedDays);
253+
254+
// Verify we traversed in the expected order
255+
if (expectedOrder === "ascending") {
256+
expect(daysVisited).toEqual(expectedDays);
257+
} else {
258+
const expectedReverseDays = Array.from(
259+
{ length: daysInMonth },
260+
(_, i) => daysInMonth - i
261+
);
262+
expect(daysVisited).toEqual(expectedReverseDays);
263+
}
264+
}
265+
266+
test("right arrow key navigates through all days of the month", async ({
267+
page,
268+
}) => {
269+
await testArrowKeyNavigation(page, "ArrowRight", "first", "ascending");
270+
});
271+
272+
test("left arrow key navigates through all days of the month in reverse", async ({
273+
page,
274+
}) => {
275+
await testArrowKeyNavigation(page, "ArrowLeft", "last", "descending");
276+
});

preview/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dioxus-i18n = { git = "https://github.com/ealmloff/dioxus-i18n", branch = "bump-
1111
unic-langid = { version = "0.9", features = ["macros"] }
1212
strum = { version = "0.27.2", features = ["derive"] }
1313
tracing.workspace = true
14-
time = { version = "0.3.41", features = ["std", "macros"] }
14+
time = { version = "0.3.44", features = ["std", "macros"] }
1515

1616
[build-dependencies]
1717
syntect = "5.2"

preview/src/components/calendar/component.rs

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@ use dioxus_primitives::calendar::{
99
pub fn Calendar(props: CalendarProps) -> Element {
1010
rsx! {
1111
document::Link { rel: "stylesheet", href: asset!("./style.css") }
12-
div { class: "calendar",
13-
calendar::Calendar {
14-
selected_date: props.selected_date,
15-
on_date_change: props.on_date_change,
16-
on_format_weekday: props.on_format_weekday,
17-
on_format_month: props.on_format_month,
18-
view_date: props.view_date,
19-
today: props.today,
20-
on_view_change: props.on_view_change,
21-
disabled: props.disabled,
22-
first_day_of_week: props.first_day_of_week,
23-
min_date: props.min_date,
24-
max_date: props.max_date,
25-
disabled_ranges: props.disabled_ranges,
26-
attributes: props.attributes,
27-
{props.children}
28-
}
12+
calendar::Calendar {
13+
class: "calendar",
14+
selected_date: props.selected_date,
15+
on_date_change: props.on_date_change,
16+
on_format_weekday: props.on_format_weekday,
17+
on_format_month: props.on_format_month,
18+
view_date: props.view_date,
19+
today: props.today,
20+
on_view_change: props.on_view_change,
21+
disabled: props.disabled,
22+
first_day_of_week: props.first_day_of_week,
23+
min_date: props.min_date,
24+
max_date: props.max_date,
25+
month_count: props.month_count,
26+
disabled_ranges: props.disabled_ranges,
27+
attributes: props.attributes,
28+
{props.children}
2929
}
3030
}
3131
}
@@ -34,23 +34,37 @@ pub fn Calendar(props: CalendarProps) -> Element {
3434
pub fn RangeCalendar(props: RangeCalendarProps) -> Element {
3535
rsx! {
3636
document::Link { rel: "stylesheet", href: asset!("./style.css") }
37-
div { class: "calendar",
38-
calendar::RangeCalendar {
39-
selected_range: props.selected_range,
40-
on_range_change: props.on_range_change,
41-
on_format_weekday: props.on_format_weekday,
42-
on_format_month: props.on_format_month,
43-
view_date: props.view_date,
44-
today: props.today,
45-
on_view_change: props.on_view_change,
46-
disabled: props.disabled,
47-
first_day_of_week: props.first_day_of_week,
48-
min_date: props.min_date,
49-
max_date: props.max_date,
50-
disabled_ranges: props.disabled_ranges,
51-
attributes: props.attributes,
52-
{props.children}
53-
}
37+
calendar::RangeCalendar {
38+
class: "calendar",
39+
selected_range: props.selected_range,
40+
on_range_change: props.on_range_change,
41+
on_format_weekday: props.on_format_weekday,
42+
on_format_month: props.on_format_month,
43+
view_date: props.view_date,
44+
today: props.today,
45+
on_view_change: props.on_view_change,
46+
disabled: props.disabled,
47+
first_day_of_week: props.first_day_of_week,
48+
min_date: props.min_date,
49+
max_date: props.max_date,
50+
month_count: props.month_count,
51+
disabled_ranges: props.disabled_ranges,
52+
attributes: props.attributes,
53+
{props.children}
54+
}
55+
}
56+
}
57+
58+
#[component]
59+
pub fn CalendarView(
60+
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
61+
children: Element,
62+
) -> Element {
63+
rsx! {
64+
div {
65+
class: "calendar-view",
66+
..attributes,
67+
{children}
5468
}
5569
}
5670
}

0 commit comments

Comments
 (0)