Repository: melt-ui/melt-ui
Pull request #786 (2023-12-01 17:09:21)

Chore: Custom ID Tests
This PR adds tests to ensure custom ids are actually applied when provided.

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 648a4a9a71ae351e06cef2c035fee4d43cb9ac53

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name            | Type  |
| --------------- | ----- |
| @melt-ui/svelte | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/huntabyte/melt-ui/new/chore/custom-id-tests?filename=.changeset/cold-singers-behave.md&value=---%0A%22%40melt-ui%2Fsvelte%22%3A%20patch%0A---%0A%0AChore%3A%20Custom%20ID%20Tests%0A)



Patch:
diff --git a/.changeset/heavy-paws-bow.md b/.changeset/heavy-paws-bow.md
new file mode 100644
index 00000000..9602dc37
--- /dev/null
+++ b/.changeset/heavy-paws-bow.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': patch
+---
+
+fix: bug causing custom ids to not always be applied
diff --git a/.changeset/thirty-adults-battle.md b/.changeset/thirty-adults-battle.md
new file mode 100644
index 00000000..4fff926e
--- /dev/null
+++ b/.changeset/thirty-adults-battle.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+Date Picker & Date Range Picker: Fix `weekdayFormat` option reactivity
diff --git a/src/lib/builders/calendar/create.ts b/src/lib/builders/calendar/create.ts
index 2ef18b14..ae21497b 100644
--- a/src/lib/builders/calendar/create.ts
+++ b/src/lib/builders/calendar/create.ts
@@ -63,7 +63,7 @@ type CalendarParts = 'content' | 'nextButton' | 'prevButton' | 'grid' | 'cell' |
 
 const { name } = createElHelpers<CalendarParts>('calendar');
 
-export const calendarIdParts = ['calendar', 'grid', 'accessibleHeading'] as const;
+export const calendarIdParts = ['calendar', 'accessibleHeading'] as const;
 export type CalendarIdParts = typeof calendarIdParts;
 
 export function createCalendar<
@@ -330,11 +330,10 @@ export function createCalendar<
 	 * For more details about the structure of the month object, refer to {@link Month}.
 	 */
 	const grid = builder(name('grid'), {
-		stores: [readonly, disabled, ids.grid],
-		returned: ([$readonly, $disabled, $gridId]) => {
+		stores: [readonly, disabled],
+		returned: ([$readonly, $disabled]) => {
 			return {
 				tabindex: -1,
-				id: $gridId,
 				role: 'grid',
 				'aria-readonly': $readonly ? 'true' : undefined,
 				'aria-disabled': $disabled ? 'true' : undefined,
diff --git a/src/lib/builders/date-field/create.ts b/src/lib/builders/date-field/create.ts
index 49a203aa..f425dced 100644
--- a/src/lib/builders/date-field/create.ts
+++ b/src/lib/builders/date-field/create.ts
@@ -345,8 +345,8 @@ export function createDateField(props?: CreateDateFieldProps) {
 		stores: [value, isInvalid, disabled, readonly, fieldIdDeps],
 		returned: ([$value, $isInvalid, $disabled, $readonly, $ids]) => {
 			const describedBy = $value
-				? `${$ids.description} ${$isInvalid ? $ids.validation : ''}`
-				: undefined;
+				? `${$ids.description}${$isInvalid ? ` ${$ids.validation}` : ''}`
+				: `${$ids.description}`;
 
 			return {
 				role: 'group',
diff --git a/src/lib/builders/date-picker/create.ts b/src/lib/builders/date-picker/create.ts
index 016a9035..28b415d8 100644
--- a/src/lib/builders/date-picker/create.ts
+++ b/src/lib/builders/date-picker/create.ts
@@ -35,14 +35,17 @@ const defaults = {
 	readonly: false,
 	minValue: undefined,
 	maxValue: undefined,
+	weekdayFormat: 'narrow',
 } satisfies CreateDatePickerProps;
 
 export function createDatePicker(props?: CreateDatePickerProps) {
 	const withDefaults = { ...defaults, ...props };
 
 	const options = toWritableStores(omit(withDefaults, 'value', 'placeholder'));
-
-	const dateField = createDateField(withDefaults);
+	const dateField = createDateField({
+		...withDefaults,
+		ids: withDefaults.dateFieldIds,
+	});
 
 	const {
 		states: { value, placeholder: dfPlaceholder },
@@ -68,6 +71,7 @@ export function createDatePicker(props?: CreateDatePickerProps) {
 		portal: withDefaults.portal,
 		forceVisible: withDefaults.forceVisible,
 		openFocus: pickerOpenFocus,
+		ids: withDefaults.popoverIds,
 	});
 
 	const trigger = builder('popover-trigger', {
@@ -101,6 +105,10 @@ export function createDatePicker(props?: CreateDatePickerProps) {
 		formatter.setLocale($locale);
 	});
 
+	effect([options.weekdayFormat], ([$weekdayFormat]) => {
+		calendar.options.weekdayFormat.set($weekdayFormat);
+	});
+
 	effect([options.disabled], ([$disabled]) => {
 		dateField.options.disabled.set($disabled);
 		calendar.options.disabled.set($disabled);
diff --git a/src/lib/builders/date-range-field/create.ts b/src/lib/builders/date-range-field/create.ts
index 57490af2..98df7f8e 100644
--- a/src/lib/builders/date-range-field/create.ts
+++ b/src/lib/builders/date-range-field/create.ts
@@ -86,8 +86,9 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 		value: startValue,
 		name: withDefaults.startName,
 		ids: {
-			...withDefaults.startIds,
 			...generatedIds,
+			...withDefaults.ids,
+			...withDefaults.startIds,
 		},
 	});
 
@@ -96,8 +97,9 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 		value: endValue,
 		name: withDefaults.endName,
 		ids: {
-			...withDefaults.endIds,
 			...generatedIds,
+			...withDefaults.ids,
+			...withDefaults.endIds,
 		},
 	});
 
@@ -180,12 +182,13 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 	});
 
 	const fieldIdDeps = derived(
-		[ids.field, ids.label, ids.description],
-		([$fieldId, $labelId, $descriptionId]) => {
+		[ids.field, ids.label, ids.description, ids.validation],
+		([$fieldId, $labelId, $descriptionId, $validationId]) => {
 			return {
 				field: $fieldId,
 				label: $labelId,
 				description: $descriptionId,
+				validation: $validationId,
 			};
 		}
 	);
@@ -193,11 +196,15 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 	const field = builder(name('field'), {
 		stores: [isCompleted, isInvalid, fieldIdDeps],
 		returned: ([$isCompleted, $isInvalid, $ids]) => {
+			const describedBy = $isCompleted
+				? `${$ids.description}${$isInvalid ? ` ${$ids.validation}` : ''}`
+				: `${$ids.description}`;
+
 			return {
 				role: 'group',
 				id: $ids.field,
 				'aria-labelledby': $ids.label,
-				'aria-describedby': $isCompleted ? $ids.description : undefined,
+				'aria-describedby': describedBy,
 				'data-invalid': $isInvalid ? '' : undefined,
 			};
 		},
diff --git a/src/lib/builders/date-range-picker/create.ts b/src/lib/builders/date-range-picker/create.ts
index a80118d3..8c765089 100644
--- a/src/lib/builders/date-range-picker/create.ts
+++ b/src/lib/builders/date-range-picker/create.ts
@@ -36,6 +36,7 @@ const defaults = {
 	readonly: false,
 	minValue: undefined,
 	maxValue: undefined,
+	weekdayFormat: 'narrow',
 } satisfies CreateDateRangePickerProps;
 
 export function createDateRangePicker(props?: CreateDateRangePickerProps) {
@@ -115,6 +116,10 @@ export function createDateRangePicker(props?: CreateDateRangePickerProps) {
 		formatter.setLocale($locale);
 	});
 
+	effect([options.weekdayFormat], ([$weekdayFormat]) => {
+		calendar.options.weekdayFormat.set($weekdayFormat);
+	});
+
 	effect([options.disabled], ([$disabled]) => {
 		rangeField.options.disabled.set($disabled);
 		calendar.options.disabled.set($disabled);
diff --git a/src/lib/builders/range-calendar/create.ts b/src/lib/builders/range-calendar/create.ts
index d8945dcb..7ef37cac 100644
--- a/src/lib/builders/range-calendar/create.ts
+++ b/src/lib/builders/range-calendar/create.ts
@@ -74,7 +74,7 @@ const defaults = {
 type CalendarParts = 'content' | 'nextButton' | 'prevButton' | 'grid' | 'cell' | 'heading';
 const { name } = createElHelpers<CalendarParts>('calendar');
 
-export const rangeCalendarIdParts = ['calendar', 'grid', 'accessibleHeading'] as const;
+export const rangeCalendarIdParts = ['calendar', 'accessibleHeading'] as const;
 export type RangeCalendarIdParts = typeof rangeCalendarIdParts;
 
 export function createRangeCalendar<T extends DateValue = DateValue>(
@@ -304,10 +304,9 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 	});
 
 	const grid = builder(name('grid'), {
-		stores: [ids.grid, readonly, disabled],
-		returned: ([$gridId, $readonly, $disabled]) => ({
+		stores: [readonly, disabled],
+		returned: ([$readonly, $disabled]) => ({
 			tabindex: -1,
-			id: $gridId,
 			role: 'grid',
 			'aria-readonly': $readonly ? 'true' : undefined,
 			'aria-disabled': $disabled ? 'true' : undefined,
@@ -618,10 +617,10 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 			width: '1px',
 		});
 		const h2 = document.createElement('div');
-		h2.role = 'heading';
-		h2.ariaLevel = '2';
 		h2.textContent = label;
 		h2.id = get(ids.accessibleHeading);
+		h2.role = 'heading';
+		h2.ariaLevel = '2';
 		node.insertBefore(div, node.firstChild);
 		div.appendChild(h2);
 	}
@@ -639,7 +638,6 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 	 * ```svelte
 	 * <script>
 	 * 	import { createCalendar } from '@melt-ui/svelte';
-import { generateIds } from '../../internal/helpers/id'
 	 *  import { prev } from '../../internal/helpers/array'
 	 * 	const { { ... }, helpers: { nextPage } } = createCalendar()
 	 * </script>


Test patch:
diff --git a/src/tests/calendar/Calendar.spec.ts b/src/tests/calendar/Calendar.spec.ts
index e290abd8..a62882c4 100644
--- a/src/tests/calendar/Calendar.spec.ts
+++ b/src/tests/calendar/Calendar.spec.ts
@@ -678,4 +678,19 @@ describe('Calendar', () => {
 			expect(weekdayElement).toHaveTextContent(weekday);
 		}
 	});
+
+	test('custom ids are applied when provided', async () => {
+		const ids = {
+			accessibleHeading: 'id-heading',
+			calendar: 'id-calendar',
+		};
+
+		const { container, calendar } = setup({
+			ids,
+		});
+
+		const heading = container.querySelector(`#${ids.accessibleHeading}`);
+		expect(heading).toBeInTheDocument();
+		expect(calendar.id).toBe(ids.calendar);
+	});
 });
diff --git a/src/tests/calendar/CalendarTest.svelte b/src/tests/calendar/CalendarTest.svelte
index 2592cd5b..bd618f8b 100644
--- a/src/tests/calendar/CalendarTest.svelte
+++ b/src/tests/calendar/CalendarTest.svelte
@@ -25,6 +25,7 @@
 	export let disabled: CreateCalendarProps['disabled'] = undefined;
 	export let readonly: CreateCalendarProps['readonly'] = undefined;
 	export let weekdayFormat: CreateCalendarProps['weekdayFormat'] = undefined;
+	export let ids: CreateCalendarProps['ids'] = undefined;
 
 	const {
 		elements: { calendar, heading, grid, cell, prevButton, nextButton },
@@ -53,6 +54,7 @@
 			disabled,
 			readonly,
 			weekdayFormat,
+			ids,
 		})
 	);
 
diff --git a/src/tests/combobox/Combobox.spec.ts b/src/tests/combobox/Combobox.spec.ts
index f3fe0b3f..d47e538a 100644
--- a/src/tests/combobox/Combobox.spec.ts
+++ b/src/tests/combobox/Combobox.spec.ts
@@ -121,6 +121,25 @@ describe('Combobox', () => {
 		expect(firstItem).toHaveAttribute('data-selected');
 	});
 
+	test('Applies custom ids when provided', async () => {
+		const ids = {
+			label: 'id-label',
+			menu: 'id-menu',
+			trigger: 'id-trigger',
+		};
+
+		const { getByTestId } = render(ComboboxTest, {
+			ids,
+		});
+
+		const trigger = getByTestId('input');
+		const menu = getByTestId('menu');
+		const label = getByTestId('label');
+		expect(trigger.id).toBe(ids.trigger);
+		expect(menu.id).toBe(ids.menu);
+		expect(label.id).toBe(ids.label);
+	});
+
 	test.todo('Selects multiple items when `multiple` is true');
 	test.todo('Manually setting the value updates the label');
 	test.todo('Updating options and setting the value updates the label');
diff --git a/src/tests/combobox/ComboboxTest.svelte b/src/tests/combobox/ComboboxTest.svelte
index b9dc1caa..50b79ce7 100644
--- a/src/tests/combobox/ComboboxTest.svelte
+++ b/src/tests/combobox/ComboboxTest.svelte
@@ -1,5 +1,11 @@
 <script lang="ts">
-	import { createCombobox, melt, type ComboboxOptionProps } from '$lib/index.js';
+	import {
+		createCombobox,
+		melt,
+		type ComboboxOptionProps,
+		type CreateComboboxProps,
+	} from '$lib/index.js';
+	import { removeUndefined } from '../utils';
 
 	export let options: ComboboxOptionProps[] = [
 		{ label: '1234', value: { id: 1234, station: undefined, lastTransmission: '2023-01-01' } },
@@ -10,19 +16,23 @@
 	];
 	export let multiple = false;
 	export let defaultValue: string | undefined = undefined;
+	export let ids: CreateComboboxProps<unknown>['ids'] = undefined;
 
 	const {
 		elements: { menu, input, option, label },
 		states: { open, inputValue, selected },
-	} = createCombobox({
-		multiple,
-		defaultSelected: defaultValue
-			? {
-					value: defaultValue,
-					label: defaultValue,
-			  }
-			: undefined,
-	});
+	} = createCombobox(
+		removeUndefined({
+			multiple,
+			defaultSelected: defaultValue
+				? {
+						value: defaultValue,
+						label: defaultValue,
+				  }
+				: undefined,
+			ids,
+		})
+	);
 
 	$: if (!$open) {
 		$inputValue = $selected?.label || '';
diff --git a/src/tests/context-menu/ContextMenu.spec.ts b/src/tests/context-menu/ContextMenu.spec.ts
index acf24a45..2157e7dd 100644
--- a/src/tests/context-menu/ContextMenu.spec.ts
+++ b/src/tests/context-menu/ContextMenu.spec.ts
@@ -17,7 +17,9 @@ function setup(props: CreateContextMenuProps = {}) {
 	};
 }
 
-async function open(props: CreateContextMenuProps = {}) {
+async function open(
+	props: CreateContextMenuProps & { submenuIds?: CreateContextMenuProps['ids'] } = {}
+) {
 	const returned = setup(props);
 	const { queryByTestId, getByTestId, user, trigger } = returned;
 	expect(queryByTestId('menu')).toBeNull();
@@ -173,4 +175,34 @@ describe('Context Menu', () => {
 		await user.click(outsideClick);
 		expect(menu).toBeVisible();
 	});
+
+	test('Applies custom ids if provided', async () => {
+		const ids = {
+			menu: 'id-menu',
+			trigger: 'id-trigger',
+		};
+
+		const subIds = {
+			menu: 'id-submenu',
+			trigger: 'id-subtrigger',
+		};
+
+		const { getByTestId, user, queryByTestId } = await open({
+			ids,
+			submenuIds: subIds,
+		});
+
+		const trigger = getByTestId('trigger');
+		const menu = getByTestId('menu');
+		const subtrigger = getByTestId('sub-trigger');
+
+		await user.hover(subtrigger);
+		await waitFor(() => expect(queryByTestId('submenu')).not.toBeNull());
+
+		expect(trigger.id).toBe(ids.trigger);
+		expect(menu.id).toBe(ids.menu);
+		expect(subtrigger.id).toBe(subIds.trigger);
+		const submenu = getByTestId('submenu');
+		expect(submenu.id).toBe(subIds.menu);
+	});
 });
diff --git a/src/tests/context-menu/ContextMenuTest.svelte b/src/tests/context-menu/ContextMenuTest.svelte
index ab0c4ba3..64bd0309 100644
--- a/src/tests/context-menu/ContextMenuTest.svelte
+++ b/src/tests/context-menu/ContextMenuTest.svelte
@@ -3,6 +3,7 @@
 	import { writable } from 'svelte/store';
 	import { ChevronRight } from 'lucide-svelte';
 	import { fade } from 'svelte/transition';
+	import { removeUndefined } from '../utils';
 
 	const settingsSync = writable(true);
 	const hideMeltUI = writable(false);
@@ -13,6 +14,7 @@
 	export let closeFocus: CreateContextMenuProps['closeFocus'] = undefined;
 	export let closeOnEscape: CreateContextMenuProps['closeOnEscape'] = true;
 	export let closeOnOutsideClick: CreateContextMenuProps['closeOnOutsideClick'] = true;
+	export let submenuIds: CreateContextMenuProps['ids'] = undefined;
 
 	const {
 		elements: { trigger, menu, item, separator, arrow },
@@ -41,7 +43,11 @@
 	const {
 		elements: { subMenu: subMenuA, subTrigger: subTriggerA },
 		states: { subOpen },
-	} = createSubmenu();
+	} = createSubmenu(
+		removeUndefined({
+			ids: submenuIds,
+		})
+	);
 
 	const {
 		elements: { radioGroup, radioItem },
diff --git a/src/tests/date-field/DateField.spec.ts b/src/tests/date-field/DateField.spec.ts
index da38d0db..1c998349 100644
--- a/src/tests/date-field/DateField.spec.ts
+++ b/src/tests/date-field/DateField.spec.ts
@@ -498,6 +498,57 @@ describe('DateField', () => {
 		await user.click(label);
 		expect(monthSegment).toHaveFocus();
 	});
+
+	test('custom ids are applied when provided', async () => {
+		const ids = {
+			field: 'id-field',
+			day: 'id-day',
+			month: 'id-month',
+			dayPeriod: 'id-dayPeriod',
+			hour: 'id-hour',
+			minute: 'id-minute',
+			year: 'id-year',
+			timeZoneName: 'id-timeZoneName',
+			description: 'id-description',
+			label: 'id-label',
+			second: 'id-second',
+			validation: 'id-validation',
+		};
+		const { getByTestId } = setup({
+			defaultValue: zonedDateTimeOther,
+			ids,
+			granularity: 'second',
+		});
+
+		const field = getByTestId('field');
+		const label = getByTestId('label');
+		const validation = getByTestId('validation');
+		const monthSegment = getByTestId('month');
+		const daySegment = getByTestId('day');
+		const yearSegment = getByTestId('year');
+		const hourSegment = getByTestId('hour');
+		const secondSegment = getByTestId('second');
+		const minuteSegment = getByTestId('minute');
+		const dayPeriodSegment = getByTestId('dayPeriod');
+		const timeZoneSegment = getByTestId('timeZoneName');
+
+		const descriptionEl = document.getElementById(ids.description);
+		expect(descriptionEl).toBeInTheDocument();
+
+		expect(field.id).toBe(ids.field);
+		expect(label.id).toBe(ids.label);
+		expect(validation.id).toBe(ids.validation);
+		expect(monthSegment.id).toBe(ids.month);
+		expect(daySegment.id).toBe(ids.day);
+		expect(yearSegment.id).toBe(ids.year);
+		expect(hourSegment.id).toBe(ids.hour);
+		expect(secondSegment.id).toBe(ids.second);
+		expect(minuteSegment.id).toBe(ids.minute);
+		expect(dayPeriodSegment.id).toBe(ids.dayPeriod);
+		expect(timeZoneSegment.id).toBe(ids.timeZoneName);
+
+		expect(field.getAttribute('aria-describedby')).toBe(ids.description);
+	});
 });
 
 function isDaylightSavingsTime(): boolean {
diff --git a/src/tests/date-field/DateFieldTest.svelte b/src/tests/date-field/DateFieldTest.svelte
index ce378b9c..82db773f 100644
--- a/src/tests/date-field/DateFieldTest.svelte
+++ b/src/tests/date-field/DateFieldTest.svelte
@@ -17,7 +17,7 @@
 	export let granularity: CreateDateFieldProps['granularity'] = undefined;
 
 	const {
-		elements: { field, segment, label },
+		elements: { field, segment, label, validation },
 		states: { value: insideValue, segmentContents },
 	} = createDateField({
 		value,
@@ -58,6 +58,7 @@
 					</div>
 				{/each}
 			</div>
+			<span use:melt={$validation} data-testid="validation">Validation</span>
 		</div>
 	</div>
 </main>
diff --git a/src/tests/date-picker/DatePicker.spec.ts b/src/tests/date-picker/DatePicker.spec.ts
index cb7ac8be..30570601 100644
--- a/src/tests/date-picker/DatePicker.spec.ts
+++ b/src/tests/date-picker/DatePicker.spec.ts
@@ -29,6 +29,17 @@ function setup(props: CreateDatePickerProps = {}) {
 	};
 }
 
+async function open(props: CreateDatePickerProps = {}) {
+	const returned = setup(props);
+	await returned.user.click(returned.trigger);
+	const calendar = returned.getByTestId('calendar');
+	expect(calendar).toBeVisible();
+	return {
+		...returned,
+		calendar,
+	};
+}
+
 describe('DatePicker', () => {
 	describe('Accessibility', () => {
 		test('has no accessibility violations', async () => {
@@ -274,7 +285,12 @@ describe('DatePicker', () => {
 			expect(firstSegment).toHaveTextContent(String(currentMonth));
 
 			await user.keyboard(kbd.ARROW_UP);
-			expect(firstSegment).toHaveTextContent(String(calendarDateToday.month + 1));
+
+			if (calendarDateToday.month === 12) {
+				expect(firstSegment).toHaveTextContent(String(1));
+			} else {
+				expect(firstSegment).toHaveTextContent(String(calendarDateToday.month + 1));
+			}
 		});
 
 		test('increments segment on arrow down', async () => {
@@ -452,6 +468,74 @@ describe('DatePicker', () => {
 				expect(weekdayElement).toHaveTextContent(weekday);
 			}
 		});
+
+		test('custom ids are applied when provided', async () => {
+			const dateFieldIds = {
+				field: 'id-field',
+				day: 'id-day',
+				month: 'id-month',
+				dayPeriod: 'id-dayPeriod',
+				hour: 'id-hour',
+				minute: 'id-minute',
+				year: 'id-year',
+				timeZoneName: 'id-timeZoneName',
+				description: 'id-description',
+				label: 'id-label',
+				second: 'id-second',
+				validation: 'id-validation',
+			};
+
+			const popoverIds = {
+				content: 'id-content',
+				trigger: 'id-trigger',
+			};
+
+			const calendarIds = {
+				accessibleHeading: 'id-heading',
+				calendar: 'id-calendar',
+			};
+
+			const { getByTestId } = await open({
+				defaultValue: zonedDateTime,
+				dateFieldIds,
+				popoverIds,
+				calendarIds,
+				granularity: 'second',
+			});
+
+			const fieldParts = [
+				'month',
+				'day',
+				'year',
+				'hour',
+				'minute',
+				'dayPeriod',
+				'timeZoneName',
+				'field',
+				'label',
+				'validation',
+			] as const;
+			for (const part of fieldParts) {
+				const segmentEl = getByTestId(part);
+				expect(segmentEl.id).toBe(dateFieldIds[part]);
+				if (part === 'field') {
+					expect(segmentEl.getAttribute('aria-describedby')).toBe(dateFieldIds.description);
+				}
+			}
+
+			const descriptionEl = document.getElementById(dateFieldIds.description);
+			expect(descriptionEl).toBeInTheDocument();
+
+			const content = getByTestId('content');
+			const trigger = getByTestId('trigger');
+			expect(content.id).toBe(popoverIds.content);
+			expect(trigger.id).toBe(popoverIds.trigger);
+
+			const calendar = getByTestId('calendar');
+			expect(calendar.id).toBe(calendarIds.calendar);
+			const accessibleHeading = document.getElementById(calendarIds.accessibleHeading);
+			expect(accessibleHeading).toBeInTheDocument();
+		});
 	});
 
 	test('clicking the label focuses the first segment', async () => {
diff --git a/src/tests/date-picker/DatePickerTest.svelte b/src/tests/date-picker/DatePickerTest.svelte
index 88cbffab..4e30dd2d 100644
--- a/src/tests/date-picker/DatePickerTest.svelte
+++ b/src/tests/date-picker/DatePickerTest.svelte
@@ -19,6 +19,7 @@
 	export let hideTimeZone: CreateDatePickerProps['hideTimeZone'] = undefined;
 	export let dateFieldIds: CreateDatePickerProps['dateFieldIds'] = undefined;
 	export let calendarIds: CreateDatePickerProps['calendarIds'] = undefined;
+	export let popoverIds: CreateDatePickerProps['popoverIds'] = undefined;
 	export let granularity: CreateDatePickerProps['granularity'] = undefined;
 	export let calendarLabel: CreateDatePickerProps['calendarLabel'] = undefined;
 	export let preventDeselect: CreateDatePickerProps['preventDeselect'] = undefined;
@@ -41,6 +42,7 @@
 			trigger,
 			content,
 			label,
+			validation,
 		},
 		states: { value: insideValue, months, headingValue, weekdays, segmentContents },
 		options: { locale: insideLocale, weekdayFormat: weekdayFormatOption },
@@ -68,6 +70,7 @@
 			weekStartsOn,
 			isDateDisabled,
 			weekdayFormat,
+			popoverIds,
 		})
 	);
 
@@ -111,11 +114,13 @@
 				</button>
 			</div>
 		</div>
+		<span use:melt={$validation} data-testid="validation">validation</span>
 	</div>
 	<div
 		class="z-10 w-80 rounded-[4px] bg-white p-3 shadow-sm"
 		transition:fade={{ duration: 100 }}
 		use:melt={$content}
+		data-testid="content"
 	>
 		<div class="w-full text-magnum-800" use:melt={$calendar} data-testid="calendar">
 			<header class="flex items-center justify-between pb-4">
diff --git a/src/tests/date-range-field/DateRangeField.spec.ts b/src/tests/date-range-field/DateRangeField.spec.ts
index 6899d8b8..aeab4479 100644
--- a/src/tests/date-range-field/DateRangeField.spec.ts
+++ b/src/tests/date-range-field/DateRangeField.spec.ts
@@ -151,6 +151,81 @@ describe('DateField', () => {
 		}
 	});
 
+	test('custom ids are applied when provided', async () => {
+		const ids = {
+			field: 'id-field',
+			label: 'id-label',
+			description: 'id-description',
+			validation: 'id-validation',
+		};
+
+		const startIds = {
+			day: 'id-day-start',
+			month: 'id-month-start',
+			dayPeriod: 'id-dayPeriod-start',
+			hour: 'id-hour-start',
+			minute: 'id-minute-start',
+			year: 'id-year-start',
+			timeZoneName: 'id-timeZoneName-start',
+			second: 'id-second-start',
+		};
+
+		const endIds = {
+			day: 'id-day-end',
+			month: 'id-month-end',
+			dayPeriod: 'id-dayPeriod-end',
+			hour: 'id-hour-end',
+			minute: 'id-minute-end',
+			year: 'id-year-end',
+			timeZoneName: 'id-timeZoneName-end',
+			second: 'id-second-end',
+		};
+
+		const { getByTestId } = setup({
+			defaultValue: exampleZonedDateTime,
+			ids,
+			startIds,
+			endIds,
+			granularity: 'second',
+		});
+
+		const field = getByTestId('field');
+		const label = getByTestId('label');
+		const validation = getByTestId('validation');
+
+		const segments = [
+			'month',
+			'day',
+			'year',
+			'hour',
+			'second',
+			'minute',
+			'dayPeriod',
+			'timeZoneName',
+		] as const;
+		const fields = ['start', 'end'] as const;
+
+		for (const field of fields) {
+			for (const segment of segments) {
+				const segmentEl = getByTestId(`${field}-${segment}`);
+				if (field === 'start') {
+					expect(segmentEl.id).toBe(startIds[segment]);
+				} else {
+					expect(segmentEl.id).toBe(endIds[segment]);
+				}
+			}
+		}
+
+		const descriptionEl = document.getElementById(ids.description);
+		expect(descriptionEl).toBeInTheDocument();
+
+		expect(field.id).toBe(ids.field);
+		expect(label.id).toBe(ids.label);
+		expect(validation.id).toBe(ids.validation);
+
+		expect(field.getAttribute('aria-describedby')).toBe(ids.description);
+	});
+
 	test('clicking the label focuses the first segment', async () => {
 		const { getByTestId, user } = setup();
 		const label = getByTestId('label');
diff --git a/src/tests/date-range-field/DateRangeFieldTest.svelte b/src/tests/date-range-field/DateRangeFieldTest.svelte
index 5fba0698..8b456da0 100644
--- a/src/tests/date-range-field/DateRangeFieldTest.svelte
+++ b/src/tests/date-range-field/DateRangeFieldTest.svelte
@@ -4,6 +4,7 @@
 		createDateRangeField,
 		type CreateDateRangeFieldProps,
 	} from '$lib/builders/date-range-field';
+	import { removeUndefined } from '../utils';
 
 	export let value: CreateDateRangeFieldProps['value'] = undefined;
 	export let defaultValue: CreateDateRangeFieldProps['defaultValue'] = undefined;
@@ -17,26 +18,32 @@
 	export let locale: CreateDateRangeFieldProps['locale'] = 'en';
 	export let hideTimeZone: CreateDateRangeFieldProps['hideTimeZone'] = undefined;
 	export let ids: CreateDateRangeFieldProps['ids'] = undefined;
+	export let startIds: CreateDateRangeFieldProps['startIds'] = undefined;
+	export let endIds: CreateDateRangeFieldProps['endIds'] = undefined;
 	export let granularity: CreateDateRangeFieldProps['granularity'] = undefined;
 
 	const {
-		elements: { field, startSegment, endSegment, label },
+		elements: { field, startSegment, endSegment, label, validation },
 		states: { value: insideValue, segmentContents, isInvalid },
-	} = createDateRangeField({
-		value,
-		defaultValue,
-		defaultPlaceholder,
-		onValueChange,
-		onPlaceholderChange,
-		isDateUnavailable,
-		disabled,
-		readonly,
-		hourCycle,
-		locale,
-		hideTimeZone,
-		ids,
-		granularity,
-	});
+	} = createDateRangeField(
+		removeUndefined({
+			value,
+			defaultValue,
+			defaultPlaceholder,
+			onValueChange,
+			onPlaceholderChange,
+			isDateUnavailable,
+			disabled,
+			readonly,
+			hourCycle,
+			locale,
+			hideTimeZone,
+			ids,
+			startIds,
+			endIds,
+			granularity,
+		})
+	);
 </script>
 
 <main>
@@ -73,5 +80,6 @@
 				{/each}
 			</div>
 		</div>
+		<span use:melt={$validation} data-testid="validation">Validation</span>
 	</div>
 </main>
diff --git a/src/tests/dialog/Dialog.spec.ts b/src/tests/dialog/Dialog.spec.ts
index 00bea0b4..aedfbb86 100644
--- a/src/tests/dialog/Dialog.spec.ts
+++ b/src/tests/dialog/Dialog.spec.ts
@@ -210,4 +210,22 @@ describe('Dialog', () => {
 		await user.click(closer);
 		await waitFor(() => expect(content).not.toBeVisible());
 	});
+
+	it('Applies custom ids when provided', async () => {
+		const ids = {
+			content: 'id-content',
+			title: 'id-title',
+			description: 'id-description',
+		};
+		const { getByTestId } = setup({
+			ids,
+		});
+
+		const content = getByTestId('content');
+		const title = getByTestId('title');
+		const description = getByTestId('description');
+		expect(content.id).toBe(ids.content);
+		expect(title.id).toBe(ids.title);
+		expect(description.id).toBe(ids.description);
+	});
 });
diff --git a/src/tests/dialog/DialogTest.svelte b/src/tests/dialog/DialogTest.svelte
index e1d6b91e..0c0b213a 100644
--- a/src/tests/dialog/DialogTest.svelte
+++ b/src/tests/dialog/DialogTest.svelte
@@ -17,8 +17,8 @@
 	<div use:melt={$portalled} data-testid="portalled">
 		<div use:melt={$overlay} data-testid="overlay" />
 		<div use:melt={$content} data-testid="content">
-			<h2 use:melt={$title}>Title</h2>
-			<p use:melt={$description}>Description</p>
+			<h2 use:melt={$title} data-testid="title">Title</h2>
+			<p use:melt={$description} data-testid="description">Description</p>
 
 			<button use:melt={$close} data-testid="closer">Close</button>
 			<button use:melt={$close} data-testid="last">Close</button>
diff --git a/src/tests/dropdown-menu/DropdownMenu.spec.ts b/src/tests/dropdown-menu/DropdownMenu.spec.ts
index 24b8e9ba..28842689 100644
--- a/src/tests/dropdown-menu/DropdownMenu.spec.ts
+++ b/src/tests/dropdown-menu/DropdownMenu.spec.ts
@@ -10,7 +10,9 @@ import { sleep } from '$lib/internal/helpers/sleep.js';
 
 const OPEN_KEYS = [kbd.ENTER, kbd.ARROW_DOWN, kbd.SPACE];
 
-function setup(props: CreateDropdownMenuProps = {}) {
+function setup(
+	props: CreateDropdownMenuProps & { submenuIds?: CreateDropdownMenuProps['ids'] } = {}
+) {
 	const user = userEvent.setup();
 	const returned = render(DropdownMenuTest, props);
 	const trigger = returned.getByTestId('trigger');
@@ -212,6 +214,36 @@ describe('Dropdown Menu (Default)', () => {
 		expect(menu).toBeVisible();
 	});
 
+	test('Applies custom ids if provided', async () => {
+		const ids = {
+			menu: 'id-menu',
+			trigger: 'id-trigger',
+		};
+
+		const subIds = {
+			menu: 'id-submenu',
+			trigger: 'id-subtrigger',
+		};
+
+		const { getByTestId, user, queryByTestId } = setup({
+			ids,
+			submenuIds: subIds,
+		});
+
+		const trigger = getByTestId('trigger');
+		const menu = getByTestId('menu');
+		const subtrigger = getByTestId('subtrigger');
+
+		await user.hover(subtrigger);
+		await waitFor(() => expect(queryByTestId('submenu')).not.toBeNull());
+
+		expect(trigger.id).toBe(ids.trigger);
+		expect(menu.id).toBe(ids.menu);
+		expect(subtrigger.id).toBe(subIds.trigger);
+		const submenu = getByTestId('submenu');
+		expect(submenu.id).toBe(subIds.menu);
+	});
+
 	test.todo('Radio items');
 });
 
diff --git a/src/tests/dropdown-menu/DropdownMenuTest.svelte b/src/tests/dropdown-menu/DropdownMenuTest.svelte
index 83fa5771..538ea60f 100644
--- a/src/tests/dropdown-menu/DropdownMenuTest.svelte
+++ b/src/tests/dropdown-menu/DropdownMenuTest.svelte
@@ -2,6 +2,7 @@
 	import { createDropdownMenu, melt, type CreateDropdownMenuProps } from '$lib/index.js';
 	import { writable } from 'svelte/store';
 	import { AlignJustify, ChevronRight } from 'lucide-svelte';
+	import { removeUndefined } from '../utils';
 
 	const settingsSync = writable(true);
 	const hideMeltUI = writable(false);
@@ -12,6 +13,7 @@
 	export let closeFocus: CreateDropdownMenuProps['closeFocus'] = undefined;
 	export let closeOnEscape: CreateDropdownMenuProps['closeOnEscape'] = true;
 	export let closeOnOutsideClick: CreateDropdownMenuProps['closeOnOutsideClick'] = true;
+	export let submenuIds: CreateDropdownMenuProps['ids'] = undefined;
 
 	const {
 		elements: { trigger, menu, item, separator, arrow },
@@ -37,7 +39,11 @@
 
 	const {
 		elements: { subMenu: subMenuA, subTrigger: subTriggerA },
-	} = createSubmenu();
+	} = createSubmenu(
+		removeUndefined({
+			ids: submenuIds,
+		})
+	);
 
 	const {
 		elements: { radioGroup, radioItem },
diff --git a/src/tests/link-preview/LinkPreview.spec.ts b/src/tests/link-preview/LinkPreview.spec.ts
index a397ddf3..6287d1ed 100644
--- a/src/tests/link-preview/LinkPreview.spec.ts
+++ b/src/tests/link-preview/LinkPreview.spec.ts
@@ -73,4 +73,19 @@ describe('LinkPreview (Default)', () => {
 		await user.hover(start);
 		await waitFor(() => expect(content).not.toBeVisible());
 	});
+
+	test('Custom ids are applied when provided', async () => {
+		const ids = {
+			content: 'id-content',
+			trigger: 'id-trigger',
+		};
+		const { getByTestId } = setup({
+			ids,
+		});
+
+		const trigger = getByTestId('trigger');
+		const content = getByTestId('content');
+		expect(trigger.id).toBe(ids.trigger);
+		expect(content.id).toBe(ids.content);
+	});
 });
diff --git a/src/tests/popover/Popover.spec.ts b/src/tests/popover/Popover.spec.ts
index ed5cf928..4acb626c 100644
--- a/src/tests/popover/Popover.spec.ts
+++ b/src/tests/popover/Popover.spec.ts
@@ -96,4 +96,19 @@ describe('Popover (Default)', () => {
 		await sleep(1);
 		expect(closeFocus).toHaveFocus();
 	});
+
+	test('Custom ids are applied when provided', async () => {
+		const ids = {
+			content: 'id-content',
+			trigger: 'id-trigger',
+		};
+		const { getByTestId } = setup({
+			ids,
+		});
+
+		const trigger = getByTestId('trigger');
+		const content = getByTestId('content');
+		expect(trigger.id).toBe(ids.trigger);
+		expect(content.id).toBe(ids.content);
+	});
 });
diff --git a/src/tests/range-calendar/RangeCalendar.spec.ts b/src/tests/range-calendar/RangeCalendar.spec.ts
index 5d24da04..725bbfbd 100644
--- a/src/tests/range-calendar/RangeCalendar.spec.ts
+++ b/src/tests/range-calendar/RangeCalendar.spec.ts
@@ -502,4 +502,19 @@ describe('Range Calendar', () => {
 			expect(weekdayElement).toHaveTextContent(weekday);
 		}
 	});
+
+	test('custom ids are applied when provided', async () => {
+		const ids = {
+			accessibleHeading: 'id-heading',
+			calendar: 'id-calendar',
+		};
+
+		const { container, calendar } = setup({
+			ids,
+		});
+
+		const heading = container.querySelector(`#${ids.accessibleHeading}`);
+		expect(heading).toBeInTheDocument();
+		expect(calendar.id).toBe(ids.calendar);
+	});
 });
diff --git a/src/tests/range-calendar/RangeCalendarTest.svelte b/src/tests/range-calendar/RangeCalendarTest.svelte
index 17db9c4e..05b70b5a 100644
--- a/src/tests/range-calendar/RangeCalendarTest.svelte
+++ b/src/tests/range-calendar/RangeCalendarTest.svelte
@@ -22,6 +22,7 @@
 	export let minValue: CreateRangeCalendarProps['minValue'] = undefined;
 	export let maxValue: CreateRangeCalendarProps['maxValue'] = undefined;
 	export let weekdayFormat: CreateRangeCalendarProps['weekdayFormat'] = undefined;
+	export let ids: CreateRangeCalendarProps['ids'] = undefined;
 
 	const {
 		elements: { calendar, heading, grid, cell, prevButton, nextButton },
@@ -47,6 +48,7 @@
 			minValue,
 			maxValue,
 			weekdayFormat,
+			ids,
 		})
 	);
 
diff --git a/src/tests/select/Select.spec.ts b/src/tests/select/Select.spec.ts
index 6291177b..4155874d 100644
--- a/src/tests/select/Select.spec.ts
+++ b/src/tests/select/Select.spec.ts
@@ -184,6 +184,25 @@ describe('Select', () => {
 		expect(menu).toBeVisible();
 	});
 
+	test('Applies custom ids when provided', async () => {
+		const ids = {
+			label: 'id-label',
+			menu: 'id-menu',
+			trigger: 'id-trigger',
+		};
+
+		const { getByTestId } = render(SelectTest, {
+			ids,
+		});
+
+		const trigger = getByTestId('trigger');
+		const menu = getByTestId('menu');
+		const label = getByTestId('label');
+		expect(trigger.id).toBe(ids.trigger);
+		expect(menu.id).toBe(ids.menu);
+		expect(label.id).toBe(ids.label);
+	});
+
 	test.todo('Disabled select cannot be opened');
 	test.todo('Options loop when loop prop is set');
 });
diff --git a/src/tests/select/SelectTest.svelte b/src/tests/select/SelectTest.svelte
index d26f6a06..984fe574 100644
--- a/src/tests/select/SelectTest.svelte
+++ b/src/tests/select/SelectTest.svelte
@@ -1,27 +1,33 @@
 <script lang="ts">
-	import { createSelect, melt } from '$lib/index.js';
+	import { createSelect, melt, type CreateSelectProps } from '$lib/index.js';
 	import { Check } from 'lucide-svelte';
 	import { tick } from 'svelte';
+	import { removeUndefined } from '../utils';
 
 	export let multiple = false;
 	export let defaultValue: string | undefined = undefined;
 	export let closeOnEscape = true;
 	export let closeOnOutsideClick = true;
+	export let ids: CreateSelectProps['ids'] = undefined;
 	const {
-		elements: { trigger, menu, option, group, groupLabel },
+		elements: { trigger, menu, option, group, groupLabel, label },
 		states: { selected, selectedLabel },
 		helpers: { isSelected },
-	} = createSelect({
-		multiple,
-		defaultSelected: defaultValue
-			? {
-					value: defaultValue,
-					label: defaultValue,
-			  }
-			: undefined,
-		closeOnEscape,
-		closeOnOutsideClick,
-	});
+		ids: { trigger: triggerId },
+	} = createSelect(
+		removeUndefined({
+			multiple,
+			defaultSelected: defaultValue
+				? {
+						value: defaultValue,
+						label: defaultValue,
+				  }
+				: undefined,
+			closeOnEscape,
+			closeOnOutsideClick,
+			ids,
+		})
+	);
 
 	let options = {
 		sweet: ['Caramel', 'Chocolate', 'Strawberry', 'Cookies & Cream'],
@@ -30,6 +36,7 @@
 </script>
 
 <main>
+	<label for={$triggerId} use:melt={$label} data-testid="label">Label</label>
 	<button
 		on:click={() => {
 			selected.set({ value: 'Chocolate', label: 'Chocolate' });
diff --git a/src/tests/tooltip/Tooltip.spec.ts b/src/tests/tooltip/Tooltip.spec.ts
index c4b99025..959f75c7 100644
--- a/src/tests/tooltip/Tooltip.spec.ts
+++ b/src/tests/tooltip/Tooltip.spec.ts
@@ -35,6 +35,21 @@ describe('Tooltip', () => {
 		await waitFor(() => expect(content).not.toBeVisible());
 	});
 
+	test('Applies custom ids when provided', async () => {
+		const ids = {
+			trigger: 'id-trigger',
+			content: 'id-content',
+		};
+		const { getByTestId } = render(Tooltip, {
+			ids,
+		});
+
+		const trigger = getByTestId('trigger');
+		const content = getByTestId('content');
+		expect(trigger.id).toBe(ids.trigger);
+		expect(content.id).toBe(ids.content);
+	});
+
 	test.skip('When the tooltip was opened by focusing, leaving the trigger does not close it', async () => {
 		const { getByTestId } = render(Tooltip);
 		const user = userEvent.setup();
diff --git a/src/tests/tooltip/Tooltip.svelte b/src/tests/tooltip/Tooltip.svelte
index 31e80ed8..d42ff707 100644
--- a/src/tests/tooltip/Tooltip.svelte
+++ b/src/tests/tooltip/Tooltip.svelte
@@ -1,21 +1,26 @@
 <script lang="ts">
 	import { createTooltip, melt, type CreateTooltipProps } from '$lib';
 	import type { Writable } from 'svelte/store';
+	import { removeUndefined } from '../utils';
 
 	export let open: Writable<boolean> | undefined = undefined;
 	export let group: CreateTooltipProps['group'] = undefined;
 	export let closeOnPointerDown: CreateTooltipProps['closeOnPointerDown'] = false;
+	export let ids: CreateTooltipProps['ids'] = undefined;
 
 	const {
 		elements: { content, trigger },
 		options,
-	} = createTooltip({
-		open,
-		group,
-		closeOnPointerDown,
-		openDelay: 0,
-		closeDelay: 0,
-	});
+	} = createTooltip(
+		removeUndefined({
+			open,
+			group,
+			closeOnPointerDown,
+			openDelay: 0,
+			closeDelay: 0,
+			ids,
+		})
+	);
 
 	$: options.group.set(group);
 </script>