Skip to content

Commit 1a2c890

Browse files
committed
feat(Calendar): add onVisibleMonthsChange callback to calendars and date pickers
1 parent 694c337 commit 1a2c890

File tree

17 files changed

+138
-29
lines changed

17 files changed

+138
-29
lines changed

.changeset/seven-dancers-admire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": minor
3+
---
4+
5+
add onVisibleMonthsChange callback to calendars and date pickers
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

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

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ interface CalendarRootStateOpts
5353
WritableBoxedValues<{
5454
value: DateValue | undefined | DateValue[];
5555
placeholder: DateValue;
56+
months: Month<DateValue>[];
5657
}>,
5758
ReadableBoxedValues<{
5859
preventDeselect: boolean;
@@ -94,7 +95,7 @@ export class CalendarRootState {
9495
}
9596

9697
readonly opts: CalendarRootStateOpts;
97-
readonly visibleMonths = $derived.by(() => this.months.map((month) => month.value));
98+
readonly visibleMonths = $derived.by(() => this.#months.map((month) => month.value));
9899
readonly formatter: Formatter;
99100
readonly accessibleHeadingId = useId();
100101
readonly domContext: DOMContext;
@@ -134,7 +135,7 @@ export class CalendarRootState {
134135
this.announcer = getAnnouncer(this.domContext.getDocument());
135136
});
136137

137-
this.months = createMonths({
138+
this.opts.months.current = createMonths({
138139
dateObj: this.opts.placeholder.current,
139140
weekStartsOn: this.opts.weekStartsOn.current,
140141
locale: this.opts.locale.current,
@@ -156,7 +157,7 @@ export class CalendarRootState {
156157
locale: this.opts.locale,
157158
fixedWeeks: this.opts.fixedWeeks,
158159
numberOfMonths: this.opts.numberOfMonths,
159-
setMonths: (months: Month<DateValue>[]) => (this.months = months),
160+
setMonths: (months: Month<DateValue>[]) => (this.opts.months.current = months),
160161
});
161162

162163
/**
@@ -217,8 +218,24 @@ export class CalendarRootState {
217218
});
218219
}
219220

221+
/**
222+
* Currently displayed months, with default value fallback for SSR,
223+
* as boxes don't update server-side.
224+
*/
225+
get #months() {
226+
return this.opts.months.current.length
227+
? this.opts.months.current
228+
: createMonths({
229+
dateObj: this.opts.placeholder.current,
230+
weekStartsOn: this.opts.weekStartsOn.current,
231+
locale: this.opts.locale.current,
232+
fixedWeeks: this.opts.fixedWeeks.current,
233+
numberOfMonths: this.opts.numberOfMonths.current,
234+
});
235+
}
236+
220237
setMonths(months: Month<DateValue>[]) {
221-
this.months = months;
238+
this.opts.months.current = months;
222239
}
223240

224241
/**
@@ -230,7 +247,7 @@ export class CalendarRootState {
230247
*/
231248
readonly weekdays = $derived.by(() => {
232249
return getWeekdays({
233-
months: this.months,
250+
months: this.#months,
234251
formatter: this.formatter,
235252
weekdayFormat: this.opts.weekdayFormat.current,
236253
});
@@ -293,7 +310,7 @@ export class CalendarRootState {
293310
setMonths: this.setMonths,
294311
setPlaceholder: (date: DateValue) => (this.opts.placeholder.current = date),
295312
weekStartsOn: this.opts.weekStartsOn.current,
296-
months: this.months,
313+
months: this.#months,
297314
});
298315
}
299316

@@ -309,7 +326,7 @@ export class CalendarRootState {
309326
setMonths: this.setMonths,
310327
setPlaceholder: (date: DateValue) => (this.opts.placeholder.current = date),
311328
weekStartsOn: this.opts.weekStartsOn.current,
312-
months: this.months,
329+
months: this.#months,
313330
});
314331
}
315332

@@ -332,15 +349,15 @@ export class CalendarRootState {
332349
isNextButtonDisabled = $derived.by(() => {
333350
return getIsNextButtonDisabled({
334351
maxValue: this.opts.maxValue.current,
335-
months: this.months,
352+
months: this.#months,
336353
disabled: this.opts.disabled.current,
337354
});
338355
});
339356

340357
isPrevButtonDisabled = $derived.by(() => {
341358
return getIsPrevButtonDisabled({
342359
minValue: this.opts.minValue.current,
343-
months: this.months,
360+
months: this.#months,
344361
disabled: this.opts.disabled.current,
345362
});
346363
});
@@ -367,7 +384,7 @@ export class CalendarRootState {
367384
this.opts.monthFormat.current;
368385
this.opts.yearFormat.current;
369386
return getCalendarHeadingValue({
370-
months: this.months,
387+
months: this.#months,
371388
formatter: this.formatter,
372389
locale: this.opts.locale.current,
373390
});
@@ -408,7 +425,7 @@ export class CalendarRootState {
408425
calendarNode: this.opts.ref.current,
409426
isPrevButtonDisabled: this.isPrevButtonDisabled,
410427
isNextButtonDisabled: this.isNextButtonDisabled,
411-
months: this.months,
428+
months: this.#months,
412429
numberOfMonths: this.opts.numberOfMonths.current,
413430
});
414431
}
@@ -509,7 +526,7 @@ export class CalendarRootState {
509526
}
510527

511528
readonly snippetProps = $derived.by(() => ({
512-
months: this.months,
529+
months: this.#months,
513530
weekdays: this.weekdays,
514531
}));
515532

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { noop } from "$lib/internal/noop.js";
99
import { getDefaultDate } from "$lib/internal/date-time/utils.js";
1010
import { resolveLocaleProp } from "$lib/bits/utilities/config/prop-resolvers.js";
11+
import type { Month } from "$lib/shared/index.js";
1112
1213
let {
1314
child,
@@ -38,9 +39,12 @@
3839
maxDays,
3940
monthFormat = "long",
4041
yearFormat = "numeric",
42+
onVisibleMonthsChange = noop,
4143
...restProps
4244
}: CalendarRootProps = $props();
4345
46+
let months = $state<Month<DateValue>[]>([]);
47+
4448
const defaultPlaceholder = getDefaultDate({
4549
defaultValue: value,
4650
});
@@ -117,6 +121,13 @@
117121
monthFormat: boxWith(() => monthFormat),
118122
yearFormat: boxWith(() => yearFormat),
119123
defaultPlaceholder,
124+
months: boxWith(
125+
() => months,
126+
(v) => {
127+
months = v;
128+
onVisibleMonthsChange(v);
129+
}
130+
),
120131
});
121132
122133
const mergedProps = $derived(mergeProps(restProps, rootState.props));

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type CalendarBaseRootPropsWithoutHTML = {
3434
*/
3535
onPlaceholderChange?: OnChangeFn<DateValue>;
3636

37+
/**
38+
* A callback function called when the currently displayed month(s) changes.
39+
*/
40+
onVisibleMonthsChange?: OnChangeFn<Month<DateValue>[]>;
41+
3742
/**
3843
* Whether or not users can deselect a date once selected
3944
* without selecting another date.

packages/bits-ui/src/lib/bits/date-picker/components/date-picker-calendar.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
minValue: datePickerRootState.opts.minValue,
4242
placeholder: datePickerRootState.opts.placeholder,
4343
value: datePickerRootState.opts.value,
44+
months: datePickerRootState.opts.months,
4445
onDateSelect: datePickerRootState.opts.onDateSelect,
4546
initialFocus: datePickerRootState.opts.initialFocus,
4647
defaultPlaceholder: datePickerRootState.opts.defaultPlaceholder,

packages/bits-ui/src/lib/bits/date-picker/components/date-picker.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js";
1212
import { getDefaultDate } from "$lib/internal/date-time/utils.js";
1313
import { resolveLocaleProp } from "$lib/bits/utilities/config/prop-resolvers.js";
14+
import type { Month } from "$lib/shared/index.js";
1415
1516
let {
1617
open = $bindable(false),
@@ -48,8 +49,11 @@
4849
children,
4950
monthFormat = "long",
5051
yearFormat = "numeric",
52+
onVisibleMonthsChange = noop,
5153
}: DatePickerRootProps = $props();
5254
55+
let months = $state<Month<DateValue>[]>([]);
56+
5357
const defaultPlaceholder = getDefaultDate({
5458
granularity,
5559
defaultValue: value,
@@ -125,6 +129,13 @@
125129
numberOfMonths: boxWith(() => numberOfMonths),
126130
initialFocus: boxWith(() => initialFocus),
127131
onDateSelect: boxWith(() => onDateSelect),
132+
months: boxWith(
133+
() => months,
134+
(v) => {
135+
months = v;
136+
onVisibleMonthsChange(v);
137+
}
138+
),
128139
defaultPlaceholder,
129140
monthFormat: boxWith(() => monthFormat),
130141
yearFormat: boxWith(() => yearFormat),

packages/bits-ui/src/lib/bits/date-picker/date-picker.svelte.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { DateValue } from "@internationalized/date";
22
import { Context } from "runed";
33
import { type ReadableBoxedValues, type WritableBoxedValues } from "svelte-toolbelt";
44
import type { DateMatcher, SegmentPart } from "$lib/shared/index.js";
5-
import type { Granularity, HourCycle, WeekStartsOn } from "$lib/shared/date/types.js";
5+
import type { Granularity, HourCycle, Month, WeekStartsOn } from "$lib/shared/date/types.js";
66

77
export const DatePickerRootContext = new Context<DatePickerRootState>("DatePicker.Root");
88

@@ -11,6 +11,7 @@ interface DatePickerRootStateOpts
1111
value: DateValue | undefined;
1212
open: boolean;
1313
placeholder: DateValue;
14+
months: Month<DateValue>[];
1415
}>,
1516
ReadableBoxedValues<{
1617
readonlySegments: SegmentPart[];

packages/bits-ui/src/lib/bits/date-picker/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
DateValidator,
99
EditableSegmentPart,
1010
} from "$lib/shared/index.js";
11-
import type { Granularity, WeekStartsOn } from "$lib/shared/date/types.js";
11+
import type { Granularity, Month, WeekStartsOn } from "$lib/shared/date/types.js";
1212
import type { PortalProps } from "$lib/bits/utilities/portal/index.js";
1313

1414
export type DatePickerRootPropsWithoutHTML = WithChildren<{
@@ -24,6 +24,11 @@ export type DatePickerRootPropsWithoutHTML = WithChildren<{
2424
*/
2525
onValueChange?: OnChangeFn<DateValue | undefined>;
2626

27+
/**
28+
* A callback function called when the currently displayed month(s) changes.
29+
*/
30+
onVisibleMonthsChange?: OnChangeFn<Month<DateValue>[]>;
31+
2732
/**
2833
* The placeholder value of the date field. This determines the format
2934
* and what date the field starts at when it is empty.

0 commit comments

Comments
 (0)