Skip to content

Commit 90742c9

Browse files
authored
fix(date-picker): match keyboard range selection to pointer behavior (#3205)
2 parents 519478e + d0baf58 commit 90742c9

5 files changed

Lines changed: 85 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zag-js/date-picker": patch
3+
---
4+
5+
Fix keyboard range selection to match the pointer behavior.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zag-js/date-picker": patch
3+
---
4+
5+
Resume range selection when reopening the calendar with an incomplete range.

e2e/date-picker.e2e.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,62 @@ test.describe("datepicker [range]", () => {
307307
await I.seeInputHasValue("06/10/2024", 0)
308308
await I.seeInputHasValue("06/20/2024", 1)
309309
})
310+
311+
test("Enter on a completed range starts a new range instead of keeping the old end", async () => {
312+
// create initial range
313+
await I.focusInput(0)
314+
await I.type("06/10/2024", 0)
315+
await I.clickOutsideToBlur()
316+
await I.focusInput(1)
317+
await I.type("06/20/2024", 1)
318+
await I.clickOutsideToBlur()
319+
await I.seeInputHasValue("06/10/2024", 0)
320+
await I.seeInputHasValue("06/20/2024", 1)
321+
322+
// reopen and pick a new date with the keyboard
323+
await I.clickTrigger()
324+
await I.seeContent()
325+
await I.seeDayCellIsFocused("2024-06-10")
326+
await I.pressKey("ArrowRight")
327+
await I.seeDayCellIsFocused("2024-06-11")
328+
await I.pressKey("Enter")
329+
330+
// should reset to a single new start, stay open and move to the next day
331+
await I.seeInputHasValue("06/11/2024", 0)
332+
await I.seeInputHasValue("", 1)
333+
await I.seeContent()
334+
await I.seeDayCellIsFocused("2024-06-12")
335+
})
336+
337+
test("selecting a range start with Enter previews the range band", async () => {
338+
await I.clickTrigger()
339+
await I.seeContent()
340+
await I.seeTodayCellIsFocused()
341+
await I.pressKey("Enter")
342+
// the start is selected and the band previews to the next focused day, without moving first
343+
await expect(I.todayCell).toHaveAttribute("data-range-start", "")
344+
await expect(I.getNextDayCell()).toHaveAttribute("data-in-hover-range", "")
345+
})
346+
347+
test("reopening with only a start date resumes range selection", async () => {
348+
const start = I.getDate({ day: 10 })
349+
const end = I.getDate({ day: 20 })
350+
351+
// commit only a start date, then click outside
352+
await I.clickTrigger()
353+
await I.seeContent()
354+
await I.clickDayCell(10)
355+
await I.clickOutsideToBlur()
356+
await I.seeInputHasValue(start.formatted, 0)
357+
await I.seeInputHasValue("", 1)
358+
359+
// reopening and picking another date should complete the range
360+
await I.clickTrigger()
361+
await I.seeContent()
362+
await I.clickDayCell(20)
363+
await I.seeInputHasValue(start.formatted, 0)
364+
await I.seeInputHasValue(end.formatted, 1)
365+
})
310366
})
311367

312368
test.describe("datepicker [locale numerals]", () => {

e2e/models/datepicker.model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,12 @@ export class DatePickerModel extends Model {
253253
seeFocusedValue(value: string) {
254254
return expect(this.page.locator(".date-output")).toContainText(`Focused: ${value}`)
255255
}
256+
257+
getDayCellByValue(value: string) {
258+
return this.page.locator(`${part("table-cell-trigger")}[data-view=day][data-value="${value}"]`)
259+
}
260+
261+
seeDayCellIsFocused(value: string) {
262+
return expect(this.getDayCellByValue(value)).toBeFocused()
263+
}
256264
}

packages/machines/date-picker/src/date-picker.machine.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ export const machine = createMachine<DatePickerSchema>({
419419

420420
open: {
421421
tags: ["open"],
422+
entry: ["resumeRangeSelection"],
422423
effects: ["trackDismissableElement", "trackPositioning"],
423424
exit: ["clearHoveredDate"],
424425
on: {
@@ -539,7 +540,7 @@ export const machine = createMachine<DatePickerSchema>({
539540
},
540541
{
541542
guard: and("isRangePicker", "hasSelectedRange"),
542-
actions: ["setActiveIndexToStart", "clearDateValue", "setSelectedDate", "setActiveIndexToEnd"],
543+
actions: ["setActiveIndexToStart", "resetSelection", "setActiveIndexToEnd", "focusNextDay"],
543544
},
544545
// === Grouped transitions (based on `closeOnSelect` and `isOpenControlled`) ===
545546
{
@@ -1094,6 +1095,11 @@ export const machine = createMachine<DatePickerSchema>({
10941095
setActiveIndexToStart({ context }) {
10951096
context.set("activeIndex", 0)
10961097
},
1098+
resumeRangeSelection({ context, prop }) {
1099+
if (prop("selectionMode") === "range" && context.get("value").length === 1) {
1100+
context.set("activeIndex", 1)
1101+
}
1102+
},
10971103
focusActiveCell({ scope, context, event }) {
10981104
if (event.src === "input.click") return
10991105
raf(() => {
@@ -1109,12 +1115,10 @@ export const machine = createMachine<DatePickerSchema>({
11091115
})
11101116
},
11111117
setHoveredValueIfKeyboard({ context, event, prop }) {
1112-
if (
1113-
!event.type.startsWith("TABLE.ARROW") ||
1114-
prop("selectionMode") !== "range" ||
1115-
context.get("activeIndex") === 0
1116-
)
1117-
return
1118+
const isKeyboardNavigation =
1119+
event.type.startsWith("TABLE.ARROW") ||
1120+
["TABLE.ENTER", "TABLE.HOME", "TABLE.END", "TABLE.PAGE_UP", "TABLE.PAGE_DOWN"].includes(event.type)
1121+
if (!isKeyboardNavigation || prop("selectionMode") !== "range" || context.get("activeIndex") === 0) return
11181122
context.set("hoveredValue", context.get("focusedValue").copy())
11191123
},
11201124
focusTriggerElement({ scope }) {

0 commit comments

Comments
 (0)