Skip to content

Commit 182dd42

Browse files
authored
fix(Calendar): ensure time set outside persists when new day is set (#1359)
1 parent 6384cfe commit 182dd42

File tree

6 files changed

+91
-23
lines changed

6 files changed

+91
-23
lines changed

.changeset/early-maps-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": patch
3+
---
4+
5+
fix(Calendar): ensure time set outside persists when new day is selected

packages/bits-ui/src/lib/bits/calendar/calendar.svelte.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
createMonths,
3333
getCalendarElementProps,
3434
getCalendarHeadingValue,
35+
getDateWithPreviousTime,
3536
getIsNextButtonDisabled,
3637
getIsPrevButtonDisabled,
3738
getWeekdays,
@@ -343,9 +344,8 @@ export class CalendarRootState {
343344
return value.some((d) => isSameDay(d, date));
344345
} else if (!value) {
345346
return false;
346-
} else {
347-
return isSameDay(value, date);
348347
}
348+
return isSameDay(value, date);
349349
}
350350

351351
shiftFocus(node: HTMLElement, add: number) {
@@ -362,33 +362,33 @@ export class CalendarRootState {
362362
}
363363

364364
handleCellClick(_: Event, date: DateValue) {
365-
const readonly = this.opts.readonly.current;
366-
if (readonly) return;
367-
const isDateDisabled = this.opts.isDateDisabled.current;
368-
const isDateUnavailable = this.opts.isDateUnavailable.current;
369-
if (isDateDisabled?.(date) || isDateUnavailable?.(date)) return;
365+
if (this.opts.readonly.current) return;
366+
if (
367+
this.opts.isDateDisabled.current?.(date) ||
368+
this.opts.isDateUnavailable.current?.(date)
369+
) {
370+
return;
371+
}
370372

371373
const prev = this.opts.value.current;
372374
const multiple = this.opts.type.current === "multiple";
373375
if (multiple) {
374376
if (Array.isArray(prev) || prev === undefined) {
375377
this.opts.value.current = this.handleMultipleUpdate(prev, date);
376378
}
377-
} else {
378-
if (!Array.isArray(prev)) {
379-
const next = this.handleSingleUpdate(prev, date);
380-
if (!next) {
381-
this.announcer.announce("Selected date is now empty.", "polite", 5000);
382-
} else {
383-
this.announcer.announce(
384-
`Selected Date: ${this.formatter.selectedDate(next, false)}`,
385-
"polite"
386-
);
387-
}
388-
this.opts.value.current = next;
389-
if (next !== undefined) {
390-
this.opts.onDateSelect?.current?.();
391-
}
379+
} else if (!Array.isArray(prev)) {
380+
const next = this.handleSingleUpdate(prev, date);
381+
if (!next) {
382+
this.announcer.announce("Selected date is now empty.", "polite", 5000);
383+
} else {
384+
this.announcer.announce(
385+
`Selected Date: ${this.formatter.selectedDate(next, false)}`,
386+
"polite"
387+
);
388+
}
389+
this.opts.value.current = getDateWithPreviousTime(next, prev);
390+
if (next !== undefined) {
391+
this.opts.onDateSelect?.current?.();
392392
}
393393
}
394394
}

packages/bits-ui/src/lib/internal/date-time/calendar-helpers.svelte.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getDaysInMonth,
1212
getLastFirstDayOfWeek,
1313
getNextLastDayOfWeek,
14+
hasTime,
1415
isAfter,
1516
isBefore,
1617
parseAnyDateValue,
@@ -797,3 +798,18 @@ export function useEnsureNonDisabledPlaceholder({
797798
}
798799
);
799800
}
801+
802+
export function getDateWithPreviousTime(date: DateValue | undefined, prev: DateValue | undefined) {
803+
if (!date || !prev) return date;
804+
805+
if (hasTime(date) && hasTime(prev)) {
806+
return date.set({
807+
hour: prev.hour,
808+
minute: prev.minute,
809+
millisecond: prev.millisecond,
810+
second: prev.second,
811+
});
812+
}
813+
814+
return date;
815+
}

packages/bits-ui/src/lib/internal/date-time/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export function isZonedDateTime(dateValue: DateValue): dateValue is ZonedDateTim
121121
return dateValue instanceof ZonedDateTime;
122122
}
123123

124-
export function hasTime(dateValue: DateValue) {
124+
export function hasTime(dateValue: DateValue): dateValue is CalendarDateTime | ZonedDateTime {
125125
return isCalendarDateTime(dateValue) || isZonedDateTime(dateValue);
126126
}
127127

tests/src/tests/calendar/calendar-test.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts" module>
2+
import { CalendarDateTime, ZonedDateTime } from "@internationalized/date";
23
import { Calendar, type CalendarSingleRootProps } from "bits-ui";
34
export type CalendarSingleTestProps = CalendarSingleRootProps;
45
</script>
@@ -70,4 +71,14 @@
7071
<button onclick={() => changeValue("day")} data-testid="add-day"> Add Day </button>
7172
<button onclick={() => changeValue("month")} data-testid="add-month">Add Month</button>
7273
<button onclick={() => changeValue("year")} data-testid="add-year">Add Year</button>
74+
<button
75+
data-testid="set-time"
76+
onclick={() => {
77+
if (value instanceof CalendarDateTime || value instanceof ZonedDateTime) {
78+
value = value.set({ hour: 15, minute: 15, second: 15, millisecond: 15 });
79+
}
80+
}}
81+
>
82+
Set time
83+
</button>
7384
</main>

tests/src/tests/calendar/calendar.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,42 @@ describe("type='single'", () => {
118118
expect(getSelectedDay(t.calendar)).toHaveTextContent("21");
119119
expect(getSelectedDays(t.calendar).length).toBe(1);
120120
});
121+
122+
it("should persist time when selecting a date (CalendarDateTime)", async () => {
123+
const t = setup({ value: calendarDateTime });
124+
const value = t.getByTestId("value");
125+
expect(value).toHaveTextContent(calendarDateTime.toString());
126+
await t.user.click(t.getByTestId("set-time"));
127+
expect(value).toHaveTextContent(
128+
calendarDateTime
129+
.set({ hour: 15, minute: 15, second: 15, millisecond: 15 })
130+
.toString()
131+
);
132+
const firstDayInMonth = t.getByTestId("date-1-1");
133+
await t.user.click(firstDayInMonth);
134+
expect(value).toHaveTextContent(
135+
calendarDateTime
136+
.set({ day: 1, hour: 15, minute: 15, second: 15, millisecond: 15 })
137+
.toString()
138+
);
139+
});
140+
141+
it("should persist time when selecting a date (ZonedDateTime)", async () => {
142+
const t = setup({ value: zonedDateTime });
143+
const value = t.getByTestId("value");
144+
expect(value).toHaveTextContent(zonedDateTime.toString());
145+
await t.user.click(t.getByTestId("set-time"));
146+
expect(value).toHaveTextContent(
147+
zonedDateTime.set({ hour: 15, minute: 15, second: 15, millisecond: 15 }).toString()
148+
);
149+
const firstDayInMonth = t.getByTestId("date-1-1");
150+
await t.user.click(firstDayInMonth);
151+
expect(value).toHaveTextContent(
152+
zonedDateTime
153+
.set({ day: 1, hour: 15, minute: 15, second: 15, millisecond: 15 })
154+
.toString()
155+
);
156+
});
121157
});
122158

123159
describe("Navigation", () => {

0 commit comments

Comments
 (0)