Repository: melt-ui/melt-ui (ts)

Issue:
[Datepicker] options stores not syncronizing with underlying calendar
### Describe the bug

I tought #1069 was all the problem i was facing however there's another bug related to this specifically for the DatePicker (and i would guess for other composed builders too).

Basically here
https://github.com/melt-ui/melt-ui/blob/9dd987f366b32f12bd883e14ffe007cbc304093b/src/lib/builders/date-picker/create.ts#L198-L202
the options for the datepicker override the ones for dateField and calendar. This means that if you pass a default option (in my case was `numberOfMonths`) when setting the store you are actually setting the store in `options.numberOfMonths` and not the underlying `calendarOptions.numberOfMotnhs`.

This is why we have this series of effects
https://github.com/melt-ui/melt-ui/blob/9dd987f366b32f12bd883e14ffe007cbc304093b/src/lib/builders/date-picker/create.ts#L110-L132
This is easily fixable by adding another effects that sync `numberOfMonths` too but i think the same should apply for `fixedWeeks`, `weekStartsOn` and all the other properties.

I do wonder however if there's a way to automate this to make it more reliable in the future (and not forcing contributors to remember to update both places when adding a new prop for example).

I can prepare a PR for the simple case but would love to know if we want to tackle the overall approach (i also kinda need this so maybe i can put up the quick PR and then we can discuss if we want to automate it? 🥺 )

### Reproduction

- go to https://www.sveltelab.dev/ruu6h9eabwthdpr (p.s. i'm using tarball to install melt since it's the version that has the fixes to calendar and range calendar which is still to publish)
- open Calendar.svelte (i'm using the datepicker builder which is the one affected but used only the calendar because that's where the bug is and i already had the component setup)
- comment the option `numberOfMonths`
- try to click on the button to add number of months
- observe that the calendar actually update
- uncomment the option
- try to click on the button to add number of months
- observe that the calendar no longer update

### Logs

_No response_

### System Info

```bash
System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 18.18.0 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.3 - /usr/local/bin/npm
    pnpm: 8.15.3 - /usr/local/bin/pnpm
  npmPackages:
    @melt-ui/svelte: latest => 0.75.3 
    @sveltejs/kit: ^2.0.0 => 2.5.2 
    svelte: ^4.1.2 => 4.2.12
```


### Severity

blocking an upgrade

Pull request description:
fix: datepicker not syncing with calendar
Closes #1074 

I initially added the code to sync the options in the range date picker to but i just now realized while writing the tests that the range date picker doesn't expose the calendar options like `numberOfMonths`, `fixedWeeks` or `weekStartsOn`. I have everything stashed locally so if you prefer that i expose those props in another PR and then sync them i can do it. In the meantime here it is.

EDIT: i've also fixed a couple of test names in my two old PRs

Patch:
diff --git a/.changeset/nervous-mirrors-sit.md b/.changeset/nervous-mirrors-sit.md
new file mode 100644
index 00000000..5adde705
--- /dev/null
+++ b/.changeset/nervous-mirrors-sit.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': patch
+---
+
+Fix incorrect arrow position for Tooltip, Popover, Listbox, Link Preview provided forceVisible: false (closes #1048)
diff --git a/.changeset/perfect-otters-shout.md b/.changeset/perfect-otters-shout.md
new file mode 100644
index 00000000..4b37c769
--- /dev/null
+++ b/.changeset/perfect-otters-shout.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': minor
+---
+
+fix: datepicker not syncing with calendar + add defaults from calendar to datepicker
diff --git a/src/lib/builders/calendar/create.ts b/src/lib/builders/calendar/create.ts
index 0c61962c..1987b1ba 100644
--- a/src/lib/builders/calendar/create.ts
+++ b/src/lib/builders/calendar/create.ts
@@ -44,7 +44,7 @@ import { derived, writable } from 'svelte/store';
 import type { CalendarEvents } from './events.js';
 import type { CalendarValue, CreateCalendarProps } from './types.js';
 
-const defaults = {
+export const defaults = {
 	isDateDisabled: undefined,
 	isDateUnavailable: undefined,
 	value: undefined,
diff --git a/src/lib/builders/date-picker/create.ts b/src/lib/builders/date-picker/create.ts
index 3adbfce7..090a273a 100644
--- a/src/lib/builders/date-picker/create.ts
+++ b/src/lib/builders/date-picker/create.ts
@@ -16,6 +16,7 @@ import { pickerOpenFocus } from '$lib/internal/helpers/date/focus.js';
 import { createFormatter, dateStore, getDefaultDate } from '$lib/internal/helpers/date/index.js';
 import type { MeltActionReturn } from '$lib/internal/types.js';
 import type { DatePickerEvents } from './events.js';
+import { defaults as calendarDefaults } from '../calendar/create.js';
 
 const defaults = {
 	isDateDisabled: undefined,
@@ -36,6 +37,18 @@ const defaults = {
 	minValue: undefined,
 	maxValue: undefined,
 	weekdayFormat: 'narrow',
+	...omit(
+		calendarDefaults,
+		'isDateDisabled',
+		'isDateUnavailable',
+		'value',
+		'locale',
+		'disabled',
+		'readonly',
+		'minValue',
+		'maxValue',
+		'weekdayFormat'
+	),
 } satisfies CreateDatePickerProps;
 
 export function createDatePicker(props?: CreateDatePickerProps) {
@@ -131,6 +144,18 @@ export function createDatePicker(props?: CreateDatePickerProps) {
 		calendar.options.maxValue.set($maxValue);
 	});
 
+	effect([options.numberOfMonths], ([$numberOfMonths]) => {
+		calendar.options.numberOfMonths.set($numberOfMonths);
+	});
+
+	effect([options.fixedWeeks], ([$fixedWeeks]) => {
+		calendar.options.fixedWeeks.set($fixedWeeks);
+	});
+
+	effect([options.weekStartsOn], ([$weekStartsOn]) => {
+		calendar.options.weekStartsOn.set($weekStartsOn);
+	});
+
 	const dateFieldOptions = omit(
 		dateField.options,
 		'locale',
diff --git a/src/lib/builders/link-preview/create.ts b/src/lib/builders/link-preview/create.ts
index bb013909..0830514e 100644
--- a/src/lib/builders/link-preview/create.ts
+++ b/src/lib/builders/link-preview/create.ts
@@ -28,6 +28,7 @@ import { generateIds } from '../../internal/helpers/id.js';
 import { omit } from '../../internal/helpers/object.js';
 import type { LinkPreviewEvents } from './events.js';
 import type { CreateLinkPreviewProps } from './types.js';
+import { tick } from 'svelte';
 
 type LinkPreviewParts = 'trigger' | 'content' | 'arrow';
 const { name } = createElHelpers<LinkPreviewParts>('hover-card');
@@ -189,35 +190,40 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 					unsubPopper();
 					if (!$isVisible || !$activeTrigger) return;
 
-					const popper = usePopper(node, {
-						anchorElement: $activeTrigger,
-						open: open,
-						options: {
-							floating: $positioning,
-							modal: {
-								closeOnInteractOutside: $closeOnOutsideClick,
-								onClose: () => {
-									open.set(false);
-									$activeTrigger.focus();
+					tick().then(() => {
+						const popper = usePopper(node, {
+							anchorElement: $activeTrigger,
+							open: open,
+							options: {
+								floating: $positioning,
+								modal: {
+									closeOnInteractOutside: $closeOnOutsideClick,
+									onClose: () => {
+										open.set(false);
+										$activeTrigger.focus();
+									},
+									shouldCloseOnInteractOutside: (e) => {
+										onOutsideClick.get()?.(e);
+										if (e.defaultPrevented) return false;
+										if (
+											isHTMLElement($activeTrigger) &&
+											$activeTrigger.contains(e.target as Element)
+										)
+											return false;
+										return true;
+									},
+									open: $isVisible,
 								},
-								shouldCloseOnInteractOutside: (e) => {
-									onOutsideClick.get()?.(e);
-									if (e.defaultPrevented) return false;
-									if (isHTMLElement($activeTrigger) && $activeTrigger.contains(e.target as Element))
-										return false;
-									return true;
-								},
-								open: $isVisible,
+								portal: getPortalDestination(node, $portal),
+								focusTrap: null,
+								escapeKeydown: $closeOnEscape ? undefined : null,
 							},
-							portal: getPortalDestination(node, $portal),
-							focusTrap: null,
-							escapeKeydown: $closeOnEscape ? undefined : null,
-						},
-					});
+						});
 
-					if (popper && popper.destroy) {
-						unsubPopper = popper.destroy;
-					}
+						if (popper && popper.destroy) {
+							unsubPopper = popper.destroy;
+						}
+					});
 				}
 			);
 
diff --git a/src/lib/builders/listbox/create.ts b/src/lib/builders/listbox/create.ts
index ac99b27c..f50a8be1 100644
--- a/src/lib/builders/listbox/create.ts
+++ b/src/lib/builders/listbox/create.ts
@@ -479,39 +479,41 @@ export function createListbox<
 
 						if (!$isVisible || !$activeTrigger) return;
 
-						const ignoreHandler = createClickOutsideIgnore(ids.trigger.get());
-
-						const popper = usePopper(node, {
-							anchorElement: $activeTrigger,
-							open,
-							options: {
-								floating: $positioning,
-								focusTrap: null,
-								modal: {
-									closeOnInteractOutside: $closeOnOutsideClick,
-									onClose: closeMenu,
-									open: $isVisible,
-									shouldCloseOnInteractOutside: (e) => {
-										onOutsideClick.get()?.(e);
-										if (e.defaultPrevented) return false;
-										const target = e.target;
-										if (!isElement(target)) return false;
-										if (target === $activeTrigger || $activeTrigger.contains(target)) {
-											return false;
-										}
-										// return opposite of the result of the ignoreHandler
-										if (ignoreHandler(e)) return false;
-										return true;
+						tick().then(() => {
+							const ignoreHandler = createClickOutsideIgnore(ids.trigger.get());
+
+							const popper = usePopper(node, {
+								anchorElement: $activeTrigger,
+								open,
+								options: {
+									floating: $positioning,
+									focusTrap: null,
+									modal: {
+										closeOnInteractOutside: $closeOnOutsideClick,
+										onClose: closeMenu,
+										open: $isVisible,
+										shouldCloseOnInteractOutside: (e) => {
+											onOutsideClick.get()?.(e);
+											if (e.defaultPrevented) return false;
+											const target = e.target;
+											if (!isElement(target)) return false;
+											if (target === $activeTrigger || $activeTrigger.contains(target)) {
+												return false;
+											}
+											// return opposite of the result of the ignoreHandler
+											if (ignoreHandler(e)) return false;
+											return true;
+										},
 									},
-								},
 
-								escapeKeydown: null,
-								portal: getPortalDestination(node, $portal),
-							},
+									escapeKeydown: null,
+									portal: getPortalDestination(node, $portal),
+								},
+							});
+							if (popper && popper.destroy) {
+								unsubPopper = popper.destroy;
+							}
 						});
-						if (popper && popper.destroy) {
-							unsubPopper = popper.destroy;
-						}
 					}
 				)
 			);
diff --git a/src/lib/builders/popover/create.ts b/src/lib/builders/popover/create.ts
index 4eba283b..dec6a850 100644
--- a/src/lib/builders/popover/create.ts
+++ b/src/lib/builders/popover/create.ts
@@ -134,38 +134,40 @@ export function createPopover(args?: CreatePopoverProps) {
 					unsubPopper();
 					if (!$isVisible || !$activeTrigger) return;
 
-					const popper = usePopper(node, {
-						anchorElement: $activeTrigger,
-						open,
-						options: {
-							floating: $positioning,
-							focusTrap: $disableFocusTrap
-								? null
-								: {
-										returnFocusOnDeactivate: false,
-										clickOutsideDeactivates: true,
-										escapeDeactivates: true,
-								  },
-							modal: {
-								shouldCloseOnInteractOutside: shouldCloseOnInteractOutside,
-								onClose: handleClose,
-								open: $isVisible,
-								closeOnInteractOutside: $closeOnOutsideClick,
+					tick().then(() => {
+						const popper = usePopper(node, {
+							anchorElement: $activeTrigger,
+							open,
+							options: {
+								floating: $positioning,
+								focusTrap: $disableFocusTrap
+									? null
+									: {
+											returnFocusOnDeactivate: false,
+											clickOutsideDeactivates: true,
+											escapeDeactivates: true,
+									  },
+								modal: {
+									shouldCloseOnInteractOutside: shouldCloseOnInteractOutside,
+									onClose: handleClose,
+									open: $isVisible,
+									closeOnInteractOutside: $closeOnOutsideClick,
+								},
+								escapeKeydown: $closeOnEscape
+									? {
+											handler: () => {
+												handleClose();
+											},
+									  }
+									: null,
+								portal: getPortalDestination(node, $portal),
 							},
-							escapeKeydown: $closeOnEscape
-								? {
-										handler: () => {
-											handleClose();
-										},
-								  }
-								: null,
-							portal: getPortalDestination(node, $portal),
-						},
-					});
+						});
 
-					if (popper && popper.destroy) {
-						unsubPopper = popper.destroy;
-					}
+						if (popper && popper.destroy) {
+							unsubPopper = popper.destroy;
+						}
+					});
 				}
 			);
 
diff --git a/src/lib/builders/tooltip/create.ts b/src/lib/builders/tooltip/create.ts
index 93f719a7..9d9ffe89 100644
--- a/src/lib/builders/tooltip/create.ts
+++ b/src/lib/builders/tooltip/create.ts
@@ -28,6 +28,7 @@ import { derived, writable, type Writable } from 'svelte/store';
 import { generateIds } from '../../internal/helpers/id.js';
 import type { TooltipEvents } from './events.js';
 import type { CreateTooltipProps } from './types.js';
+import { tick } from 'svelte';
 
 const defaults = {
 	positioning: {
@@ -219,19 +220,22 @@ export function createTooltip(props?: CreateTooltipProps) {
 						return;
 					}
 
-					const floatingReturn = useFloating(triggerEl, node, $positioning);
-					unsubFloating = floatingReturn.destroy;
-					if ($portal === null) {
-						unsubPortal();
-						return;
-					}
-					const portalDest = getPortalDestination(node, $portal);
-					if (portalDest) {
-						const portalReturn = usePortal(node, portalDest);
-						if (portalReturn && portalReturn.destroy) {
-							unsubPortal = portalReturn.destroy;
+					tick().then(() => {
+						if ($portal === null) {
+							unsubPortal();
+						} else {
+							const portalDest = getPortalDestination(node, $portal);
+							if (portalDest) {
+								const portalReturn = usePortal(node, portalDest);
+								if (portalReturn && portalReturn.destroy) {
+									unsubPortal = portalReturn.destroy;
+								}
+							}
 						}
-					}
+
+						const floatingReturn = useFloating(triggerEl, node, $positioning);
+						unsubFloating = floatingReturn.destroy;
+					});
 				}
 			);
 


Test patch:
diff --git a/src/tests/calendar/Calendar.spec.ts b/src/tests/calendar/Calendar.spec.ts
index d47f19e3..b00bc355 100644
--- a/src/tests/calendar/Calendar.spec.ts
+++ b/src/tests/calendar/Calendar.spec.ts
@@ -720,7 +720,7 @@ describe('Calendar', () => {
 		}
 	});
 
-	test('dynamically change weekStartsOn option', async () => {
+	test('dynamically change fixedWeeks option', async () => {
 		const { getByTestId, queryByTestId, user } = setup();
 
 		const nextButton = getByTestId('next-button');
diff --git a/src/tests/date-picker/DatePicker.spec.ts b/src/tests/date-picker/DatePicker.spec.ts
index 2ff9089e..e7f973b4 100644
--- a/src/tests/date-picker/DatePicker.spec.ts
+++ b/src/tests/date-picker/DatePicker.spec.ts
@@ -560,4 +560,66 @@ describe('DatePicker', () => {
 
 		expect(trigger).toBeDisabled();
 	});
+
+	test('dynamically change numberOfMonths option even if numberOfMonths is specified as an option', async () => {
+		const { queryByTestId, getByTestId, user } = setup({
+			numberOfMonths: 1,
+		});
+
+		let grid0 = queryByTestId('grid-0');
+		let grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeNull();
+
+		const numberOfMonthsButton = getByTestId('numberOfMonths');
+		await user.click(numberOfMonthsButton);
+
+		grid0 = queryByTestId('grid-0');
+		grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeInTheDocument();
+	});
+
+	test('dynamically change weekStartsOn option even if weekStartsOn option is set', async () => {
+		const { getByTestId, user } = setup({
+			weekStartsOn: 0,
+		});
+
+		const weekDaysCopy = [...narrowWeekdays];
+
+		let weekdayElement = getByTestId(`weekdays`);
+
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+
+		const weekStartsOnButton = getByTestId('weekStartsOn');
+		await user.click(weekStartsOnButton);
+		const first = weekDaysCopy.shift();
+		if (first) {
+			weekDaysCopy.push(first);
+		}
+
+		weekdayElement = getByTestId(`weekdays`);
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+	});
+
+	test('dynamically change fixedWeeks option even if fixedWeeks option is set', async () => {
+		const { getByTestId, queryByTestId, user } = setup({
+			fixedWeeks: false,
+		});
+
+		const nextButton = getByTestId('next-button');
+
+		while (queryByTestId('week-6') !== null) {
+			await user.click(nextButton);
+		}
+
+		const fixedWeeksButton = getByTestId('fixedWeeks');
+		await user.click(fixedWeeksButton);
+
+		expect(queryByTestId('week-6')).not.toBeNull();
+	});
 });
diff --git a/src/tests/date-picker/DatePickerTest.svelte b/src/tests/date-picker/DatePickerTest.svelte
index 86596a9b..56a2ece7 100644
--- a/src/tests/date-picker/DatePickerTest.svelte
+++ b/src/tests/date-picker/DatePickerTest.svelte
@@ -27,6 +27,7 @@
 	export let placeholder: CreateDatePickerProps['placeholder'] = undefined;
 	export let weekStartsOn: CreateDatePickerProps['weekStartsOn'] = undefined;
 	export let weekdayFormat: CreateDatePickerProps['weekdayFormat'] = undefined;
+	export let fixedWeeks: CreateDatePickerProps['fixedWeeks'] = undefined;
 	export let onOutsideClick: CreateDatePickerProps['onOutsideClick'] = undefined;
 
 	const {
@@ -45,7 +46,13 @@
 			validation,
 		},
 		states: { value: insideValue, months, headingValue, weekdays, segmentContents },
-		options: { locale: insideLocale, weekdayFormat: weekdayFormatOption },
+		options: {
+			locale: insideLocale,
+			weekdayFormat: weekdayFormatOption,
+			numberOfMonths: numberOfMonthsOption,
+			fixedWeeks: fixedWeeksOption,
+			weekStartsOn: weekStartsOnOption,
+		},
 	} = createDatePicker(
 		removeUndefined({
 			value,
@@ -72,9 +79,14 @@
 			weekdayFormat,
 			popoverIds,
 			onOutsideClick,
+			fixedWeeks,
 		})
 	);
 
+	function cycleWeekStart() {
+		$weekStartsOnOption = (($weekStartsOnOption + 1) % 7) as typeof $weekStartsOnOption;
+	}
+
 	function cycleWeekdayFormat() {
 		weekdayFormatOption.update((prev) => {
 			switch (prev) {
@@ -138,7 +150,7 @@
 				{@const { weeks } = month}
 				<table use:melt={$grid} class="w-full" data-testid="grid-{i}">
 					<thead aria-hidden="true">
-						<tr>
+						<tr data-testid="weekdays">
 							{#each $weekdays as day, idx}
 								<th class="text-sm font-semibold text-magnum-800">
 									<div
@@ -152,8 +164,8 @@
 						</tr>
 					</thead>
 					<tbody>
-						{#each weeks as days}
-							<tr>
+						{#each weeks as days, i}
+							<tr data-testid="week-{i + 1}">
 								{#each days as date}
 									<td role="gridcell">
 										<div
@@ -174,6 +186,21 @@
 		<button on:click={cycleWeekdayFormat} data-testid="cycle-weekday-format">
 			Cycle weekdayFormat
 		</button>
+		<button
+			data-testid="numberOfMonths"
+			on:click={() => {
+				$numberOfMonthsOption++;
+			}}>numberOfMonths</button
+		>
+		<br />
+		<button data-testid="weekStartsOn" on:click={cycleWeekStart}>weekStartsOn</button>
+		<br />
+		<button
+			data-testid="fixedWeeks"
+			on:click={() => {
+				$fixedWeeksOption = !$fixedWeeksOption;
+			}}>fixedWeeksOption</button
+		>
 	</div>
 </main>
 
diff --git a/src/tests/range-calendar/RangeCalendar.spec.ts b/src/tests/range-calendar/RangeCalendar.spec.ts
index 98967fa5..3bb62e3e 100644
--- a/src/tests/range-calendar/RangeCalendar.spec.ts
+++ b/src/tests/range-calendar/RangeCalendar.spec.ts
@@ -542,7 +542,7 @@ describe('Range Calendar', () => {
 		}
 	});
 
-	test('dynamically change weekStartsOn option', async () => {
+	test('dynamically change fixedWeeks option', async () => {
 		const { getByTestId, queryByTestId, user } = setup();
 
 		const nextButton = getByTestId('next-button');