Skip to content

Commit ca3655d

Browse files
committed
feat(Calendar): add onVisibleMonthsChange callback to calendars and date pickers
1 parent 484b2d4 commit ca3655d

File tree

19 files changed

+337
-191
lines changed

19 files changed

+337
-191
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

docs/src/routes/api/demos.json/demos.json

Lines changed: 21 additions & 15 deletions
Large diffs are not rendered by default.

docs/src/routes/api/demos.json/stackblitz-files.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

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

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type CalendarRootStateProps = WithRefProps<
5050
WritableBoxedValues<{
5151
value: DateValue | undefined | DateValue[];
5252
placeholder: DateValue;
53+
months: Month<DateValue>[];
5354
}> &
5455
ReadableBoxedValues<{
5556
preventDeselect: boolean;
@@ -80,8 +81,7 @@ type CalendarRootStateProps = WithRefProps<
8081
>;
8182

8283
export class CalendarRootState {
83-
months: Month<DateValue>[] = $state([]);
84-
visibleMonths = $derived.by(() => this.months.map((month) => month.value));
84+
visibleMonths = $derived.by(() => this.#months.map((month) => month.value));
8585
announcer: Announcer;
8686
formatter: Formatter;
8787
accessibleHeadingId = useId();
@@ -109,7 +109,7 @@ export class CalendarRootState {
109109

110110
useRefById(opts);
111111

112-
this.months = createMonths({
112+
this.opts.months.current = createMonths({
113113
dateObj: this.opts.placeholder.current,
114114
weekStartsOn: this.opts.weekStartsOn.current,
115115
locale: this.opts.locale.current,
@@ -154,7 +154,7 @@ export class CalendarRootState {
154154
locale: this.opts.locale,
155155
fixedWeeks: this.opts.fixedWeeks,
156156
numberOfMonths: this.opts.numberOfMonths,
157-
setMonths: (months: Month<DateValue>[]) => (this.months = months),
157+
setMonths: (months: Month<DateValue>[]) => (this.opts.months.current = months),
158158
});
159159

160160
/**
@@ -212,8 +212,24 @@ export class CalendarRootState {
212212
});
213213
}
214214

215+
/**
216+
* Currently displayed months, with default value fallback for SSR,
217+
* as boxes don't update server-side.
218+
*/
219+
get #months() {
220+
return this.opts.months.current.length
221+
? this.opts.months.current
222+
: createMonths({
223+
dateObj: this.opts.placeholder.current,
224+
weekStartsOn: this.opts.weekStartsOn.current,
225+
locale: this.opts.locale.current,
226+
fixedWeeks: this.opts.fixedWeeks.current,
227+
numberOfMonths: this.opts.numberOfMonths.current,
228+
});
229+
}
230+
215231
setMonths(months: Month<DateValue>[]) {
216-
this.months = months;
232+
this.opts.months.current = months;
217233
}
218234

219235
/**
@@ -225,7 +241,7 @@ export class CalendarRootState {
225241
*/
226242
weekdays = $derived.by(() => {
227243
return getWeekdays({
228-
months: this.months,
244+
months: this.#months,
229245
formatter: this.formatter,
230246
weekdayFormat: this.opts.weekdayFormat.current,
231247
});
@@ -243,7 +259,7 @@ export class CalendarRootState {
243259
setMonths: this.setMonths,
244260
setPlaceholder: (date: DateValue) => (this.opts.placeholder.current = date),
245261
weekStartsOn: this.opts.weekStartsOn.current,
246-
months: this.months,
262+
months: this.#months,
247263
});
248264
}
249265

@@ -259,7 +275,7 @@ export class CalendarRootState {
259275
setMonths: this.setMonths,
260276
setPlaceholder: (date: DateValue) => (this.opts.placeholder.current = date),
261277
weekStartsOn: this.opts.weekStartsOn.current,
262-
months: this.months,
278+
months: this.#months,
263279
});
264280
}
265281

@@ -282,15 +298,15 @@ export class CalendarRootState {
282298
isNextButtonDisabled = $derived.by(() => {
283299
return getIsNextButtonDisabled({
284300
maxValue: this.opts.maxValue.current,
285-
months: this.months,
301+
months: this.#months,
286302
disabled: this.opts.disabled.current,
287303
});
288304
});
289305

290306
isPrevButtonDisabled = $derived.by(() => {
291307
return getIsPrevButtonDisabled({
292308
minValue: this.opts.minValue.current,
293-
months: this.months,
309+
months: this.#months,
294310
disabled: this.opts.disabled.current,
295311
});
296312
});
@@ -315,7 +331,7 @@ export class CalendarRootState {
315331

316332
headingValue = $derived.by(() => {
317333
return getCalendarHeadingValue({
318-
months: this.months,
334+
months: this.#months,
319335
formatter: this.formatter,
320336
locale: this.opts.locale.current,
321337
});
@@ -356,7 +372,7 @@ export class CalendarRootState {
356372
calendarNode: this.opts.ref.current,
357373
isPrevButtonDisabled: this.isPrevButtonDisabled,
358374
isNextButtonDisabled: this.isNextButtonDisabled,
359-
months: this.months,
375+
months: this.#months,
360376
numberOfMonths: this.opts.numberOfMonths.current,
361377
});
362378
}
@@ -437,10 +453,12 @@ export class CalendarRootState {
437453
});
438454
}
439455

440-
snippetProps = $derived.by(() => ({
441-
months: this.months,
442-
weekdays: this.weekdays,
443-
}));
456+
snippetProps = $derived.by(() => {
457+
return {
458+
months: this.#months,
459+
weekdays: this.weekdays,
460+
};
461+
});
444462

445463
getBitsAttr(part: CalendarParts) {
446464
return `data-bits-calendar-${part}`;

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { useId } from "$lib/internal/use-id.js";
77
import { noop } from "$lib/internal/noop.js";
88
import { getDefaultDate } from "$lib/internal/date-time/utils.js";
9+
import type { Month } from "$lib/shared/index.js";
910
1011
let {
1112
child,
@@ -33,9 +34,12 @@
3334
type,
3435
disableDaysOutsideMonth = true,
3536
initialFocus = false,
37+
onVisibleMonthsChange = noop,
3638
...restProps
3739
}: CalendarRootProps = $props();
3840
41+
let months = $state<Month<DateValue>[]>([]);
42+
3943
const defaultPlaceholder = getDefaultDate({
4044
defaultValue: value,
4145
});
@@ -89,6 +93,13 @@
8993
),
9094
type: box.with(() => type),
9195
defaultPlaceholder,
96+
months: box.with(
97+
() => months,
98+
(v) => {
99+
months = v;
100+
onVisibleMonthsChange(v);
101+
}
102+
),
92103
});
93104
94105
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
@@ -33,6 +33,11 @@ type CalendarBaseRootPropsWithoutHTML = {
3333
*/
3434
onPlaceholderChange?: OnChangeFn<DateValue>;
3535

36+
/**
37+
* A callback function called when the currently displayed month(s) changes.
38+
*/
39+
onVisibleMonthsChange?: OnChangeFn<Month<DateValue>[]>;
40+
3641
/**
3742
* Whether or not users can deselect a date once selected
3843
* 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
@@ -39,6 +39,7 @@
3939
minValue: datePickerRootState.opts.minValue,
4040
placeholder: datePickerRootState.opts.placeholder,
4141
value: datePickerRootState.opts.value,
42+
months: datePickerRootState.opts.months,
4243
onDateSelect: datePickerRootState.opts.onDateSelect,
4344
initialFocus: datePickerRootState.opts.initialFocus,
4445
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
@@ -9,6 +9,7 @@
99
import { useDateFieldRoot } from "$lib/bits/date-field/date-field.svelte.js";
1010
import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js";
1111
import { getDefaultDate } from "$lib/internal/date-time/utils.js";
12+
import type { Month } from "$lib/shared/index.js";
1213
1314
let {
1415
open = $bindable(false),
@@ -43,8 +44,11 @@
4344
initialFocus = false,
4445
errorMessageId,
4546
children,
47+
onVisibleMonthsChange = noop,
4648
}: DatePickerRootProps = $props();
4749
50+
let months = $state<Month<DateValue>[]>([]);
51+
4852
const defaultPlaceholder = getDefaultDate({
4953
granularity,
5054
defaultValue: value,
@@ -104,6 +108,13 @@
104108
numberOfMonths: box.with(() => numberOfMonths),
105109
initialFocus: box.with(() => initialFocus),
106110
onDateSelect: box.with(() => onDateSelect),
111+
months: box.with(
112+
() => months,
113+
(v) => {
114+
months = v;
115+
onVisibleMonthsChange(v);
116+
}
117+
),
107118
defaultPlaceholder,
108119
});
109120

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,12 +2,13 @@ import type { DateValue } from "@internationalized/date";
22
import { Context } from "runed";
33
import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js";
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
type DatePickerRootStateProps = WritableBoxedValues<{
88
value: DateValue | undefined;
99
open: boolean;
1010
placeholder: DateValue;
11+
months: Month<DateValue>[];
1112
}> &
1213
ReadableBoxedValues<{
1314
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

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

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
placeholder: dateRangePickerRootState.opts.placeholder,
4040
value: dateRangePickerRootState.opts.value,
4141
onRangeSelect: dateRangePickerRootState.opts.onRangeSelect,
42+
months: dateRangePickerRootState.opts.months,
4243
startValue: dateRangePickerRootState.opts.startValue,
4344
endValue: dateRangePickerRootState.opts.endValue,
4445
defaultPlaceholder: dateRangePickerRootState.opts.defaultPlaceholder,

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { useDateRangeFieldRoot } from "$lib/bits/date-range-field/date-range-field.svelte.js";
99
import FloatingLayer from "$lib/bits/utilities/floating-layer/components/floating-layer.svelte";
1010
import { useId } from "$lib/internal/use-id.js";
11-
import type { DateRange } from "$lib/shared/index.js";
11+
import type { DateRange, Month } from "$lib/shared/index.js";
1212
import { getDefaultDate } from "$lib/internal/date-time/utils.js";
1313
1414
let {
@@ -44,6 +44,7 @@
4444
closeOnRangeSelect = true,
4545
onStartValueChange = noop,
4646
onEndValueChange = noop,
47+
onVisibleMonthsChange = noop,
4748
validate = noop,
4849
errorMessageId,
4950
child,
@@ -53,6 +54,7 @@
5354
5455
let startValue = $state<DateValue | undefined>(value?.start);
5556
let endValue = $state<DateValue | undefined>(value?.end);
57+
let months = $state.raw<Month<DateValue>[]>([]);
5658
5759
if (value === undefined) {
5860
value = { start: undefined, end: undefined };
@@ -115,6 +117,13 @@
115117
fixedWeeks: box.with(() => fixedWeeks),
116118
numberOfMonths: box.with(() => numberOfMonths),
117119
onRangeSelect: box.with(() => onRangeSelect),
120+
months: box.with(
121+
() => months,
122+
(v) => {
123+
months = v;
124+
onVisibleMonthsChange(v);
125+
}
126+
),
118127
startValue: box.with(
119128
() => startValue,
120129
(v) => {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import type { DateValue } from "@internationalized/date";
22
import { Context } from "runed";
33
import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js";
44
import type { DateMatcher, DateRange, 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
type DateRangePickerRootStateProps = WritableBoxedValues<{
88
value: DateRange;
99
startValue: DateValue | undefined;
1010
endValue: DateValue | undefined;
11+
months: Month<DateValue>[];
1112
open: boolean;
1213
placeholder: DateValue;
1314
}> &

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
EditableSegmentPart,
1010
} from "$lib/shared/index.js";
1111
import type { CalendarRootSnippetProps } from "$lib/types.js";
12-
import type { Granularity, WeekStartsOn } from "$lib/shared/date/types.js";
12+
import type { Granularity, Month, WeekStartsOn } from "$lib/shared/date/types.js";
1313

1414
export type DateRangePickerRootPropsWithoutHTML = WithChild<{
1515
/**
@@ -266,6 +266,11 @@ export type DateRangePickerRootPropsWithoutHTML = WithChild<{
266266
*/
267267
onEndValueChange?: OnChangeFn<DateValue | undefined>;
268268

269+
/**
270+
* A callback function called when the currently displayed month(s) changes.
271+
*/
272+
onVisibleMonthsChange?: OnChangeFn<Month<DateValue>[]>;
273+
269274
/**
270275
* The `id` of the element which contains the error messages for the date field when the
271276
* date is invalid.

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { noop } from "$lib/internal/noop.js";
77
import { useId } from "$lib/internal/use-id.js";
88
import { getDefaultDate } from "$lib/internal/date-time/utils.js";
9+
import type { Month } from "$lib/shared/index.js";
910
1011
let {
1112
children,
@@ -33,11 +34,13 @@
3334
disableDaysOutsideMonth = true,
3435
onStartValueChange = noop,
3536
onEndValueChange = noop,
37+
onVisibleMonthsChange = noop,
3638
...restProps
3739
}: RangeCalendarRootProps = $props();
3840
3941
let startValue = $state<DateValue | undefined>(value?.start);
4042
let endValue = $state<DateValue | undefined>(value?.end);
43+
let months = $state.raw<Month<DateValue>[]>([]);
4144
4245
const defaultPlaceholder = getDefaultDate({
4346
defaultValue: value?.start,
@@ -87,6 +90,13 @@
8790
calendarLabel: box.with(() => calendarLabel),
8891
fixedWeeks: box.with(() => fixedWeeks),
8992
disableDaysOutsideMonth: box.with(() => disableDaysOutsideMonth),
93+
months: box.with(
94+
() => months,
95+
(v) => {
96+
months = v;
97+
onVisibleMonthsChange(v);
98+
}
99+
),
90100
startValue: box.with(
91101
() => startValue,
92102
(v) => {

0 commit comments

Comments
 (0)