Repository: melt-ui/melt-ui
Pull request #877 (2024-01-04 03:30:23)

chore: update issue template
Improve the issue template. Follows a similar format as https://github.com/sveltejs/svelte which gives us a good amount of information to debug (versions, reproduction, etc.) so we aren't wasting energy running down non-issue issues 🫡

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 00df0f77e24f3d711dc69f7e1881ebbc030d8d36

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/huntabyte/melt-ui/new/chore/issue-temp?filename=.changeset/five-squids-smell.md&value=---%0A%22%40melt-ui%2Fsvelte%22%3A%20patch%0A---%0A%0Achore%3A%20update%20issue%20template%0A)



Patch:
diff --git a/.changeset/big-walls-yell.md b/.changeset/big-walls-yell.md
new file mode 100644
index 00000000..6fa6a5f3
--- /dev/null
+++ b/.changeset/big-walls-yell.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+Radio Group: fix keyboard navigation when disabled (closes #820)
diff --git a/.changeset/giant-buckets-bake.md b/.changeset/giant-buckets-bake.md
new file mode 100644
index 00000000..29cdea95
--- /dev/null
+++ b/.changeset/giant-buckets-bake.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+fix: missing tooltip data-attrs
diff --git a/.changeset/green-berries-fly.md b/.changeset/green-berries-fly.md
new file mode 100644
index 00000000..a2e35780
--- /dev/null
+++ b/.changeset/green-berries-fly.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': patch
+---
+
+DateField & DateRangeField: add `readonlySegments` prop
diff --git a/.changeset/lemon-shrimps-tan.md b/.changeset/lemon-shrimps-tan.md
new file mode 100644
index 00000000..7bb5e40f
--- /dev/null
+++ b/.changeset/lemon-shrimps-tan.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": minor
+---
+
+Menubar: Add `preventScroll` prop. Removes it from the returned `createMenu` builders (closes #821)
diff --git a/.changeset/seven-pillows-turn.md b/.changeset/seven-pillows-turn.md
new file mode 100644
index 00000000..7843dc8f
--- /dev/null
+++ b/.changeset/seven-pillows-turn.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+fix(menu): focus on the trigger when rootActiveTrigger is defined
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index f764cafc..00000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: "\U0001F41BBug:"
-labels: bug
-assignees: ''
-
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Desktop (please complete the following information):**
- - OS: [e.g. iOS]
- - Browser [e.g. chrome, safari]
- - Version [e.g. 22]
-
-**Smartphone (please complete the following information):**
- - Device: [e.g. iPhone6]
- - OS: [e.g. iOS8.1]
- - Browser [e.g. stock browser, safari]
- - Version [e.g. 22]
-
-**Additional context**
-Add any other context about the problem here.
-
-To reproduce using SvelteLab, use the [Melt UI template](https://www.sveltelab.dev/?t=melt_ui) and modify the `src/routes/+page.svelte` file.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..420ef513
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,50 @@
+name: "🐛 Bug report"
+description: Report an issue with Melt UI
+labels: ["bug"]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out this bug report!
+  - type: textarea
+    id: bug-description
+    attributes:
+      label: Describe the bug
+      description: A clear and concise description of what the bug is. Feel free to include screenshots or video recordings here as well! If you intend to submit a PR for this issue, tell us how in the description. Thanks!
+      placeholder: Bug description
+    validations:
+      required: true
+  - type: textarea
+    id: reproduction
+    attributes:
+      label: Reproduction
+      description: Please provide a link to a repo or SvelteLab (https://www.sveltelab.dev/?t=melt_ui) that can reproduce the problem you ran into. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "need reproduction" label. If no reproduction is provided within a reasonable time-frame, the issue will be closed.
+      placeholder: Reproduction
+    validations:
+      required: true
+  - type: textarea
+    id: logs
+    attributes:
+      label: Logs
+      description: "Please include browser console and server logs around the time this bug occurred. Optional if provided reproduction. Please try not to insert an image but copy paste the log text."
+      render: bash
+  - type: textarea
+    id: system-info
+    attributes:
+      label: System Info
+      description: Output of `npx envinfo --system --npmPackages svelte,@sveltejs/kit,@melt-ui/svelte --binaries --browsers`
+      render: bash
+      placeholder: System, Binaries, Browsers
+    validations:
+      required: true
+  - type: dropdown
+    id: severity
+    attributes:
+      label: Severity
+      description: Select the severity of this issue
+      options:
+        - annoyance
+        - blocking an upgrade
+        - blocking all usage of Melt UI
+    validations:
+      required: true
diff --git a/package.json b/package.json
index 162ae406..89f28f3e 100644
--- a/package.json
+++ b/package.json
@@ -93,7 +93,6 @@
 		"@sveltejs/adapter-vercel": "^3.0.2",
 		"@sveltejs/kit": "^1.22.3",
 		"@sveltejs/package": "^2.2.0",
-		"@sveltejs/vite-plugin-svelte": "^3.0.1",
 		"@svitejs/changesets-changelog-github-compact": "^1.1.0",
 		"@tailwindcss/typography": "^0.5.9",
 		"@testing-library/dom": "^9.3.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 29bba330..51033baa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -94,9 +94,6 @@ devDependencies:
   '@sveltejs/package':
     specifier: ^2.2.0
     version: 2.2.0(svelte@4.0.0)(typescript@5.1.6)
-  '@sveltejs/vite-plugin-svelte':
-    specifier: ^3.0.1
-    version: 3.0.1(svelte@4.0.0)(vite@4.4.7)
   '@svitejs/changesets-changelog-github-compact':
     specifier: ^1.1.0
     version: 1.1.0
@@ -4569,22 +4566,6 @@ packages:
       - supports-color
     dev: true
 
-  /@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.0.0)(vite@4.4.7):
-    resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==}
-    engines: {node: ^18.0.0 || >=20}
-    peerDependencies:
-      '@sveltejs/vite-plugin-svelte': ^3.0.0
-      svelte: ^4.0.0 || ^5.0.0-next.0
-      vite: ^5.0.0
-    dependencies:
-      '@sveltejs/vite-plugin-svelte': 3.0.1(svelte@4.0.0)(vite@4.4.7)
-      debug: 4.3.4
-      svelte: 4.0.0
-      vite: 4.4.7(@types/node@20.3.1)(less@4.2.0)
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@sveltejs/vite-plugin-svelte@2.4.2(svelte@4.0.0)(vite@4.4.7):
     resolution: {integrity: sha512-ePfcC48ftMKhkT0OFGdOyycYKnnkT6i/buzey+vHRTR/JpQvuPzzhf1PtKqCDQfJRgoPSN2vscXs6gLigx/zGw==}
     engines: {node: ^14.18.0 || >= 16}
@@ -4605,26 +4586,6 @@ packages:
       - supports-color
     dev: true
 
-  /@sveltejs/vite-plugin-svelte@3.0.1(svelte@4.0.0)(vite@4.4.7):
-    resolution: {integrity: sha512-CGURX6Ps+TkOovK6xV+Y2rn8JKa8ZPUHPZ/NKgCxAmgBrXReavzFl8aOSCj3kQ1xqT7yGJj53hjcV/gqwDAaWA==}
-    engines: {node: ^18.0.0 || >=20}
-    peerDependencies:
-      svelte: ^4.0.0 || ^5.0.0-next.0
-      vite: ^5.0.0
-    dependencies:
-      '@sveltejs/vite-plugin-svelte-inspector': 2.0.0(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.0.0)(vite@4.4.7)
-      debug: 4.3.4
-      deepmerge: 4.3.1
-      kleur: 4.1.5
-      magic-string: 0.30.5
-      svelte: 4.0.0
-      svelte-hmr: 0.15.3(svelte@4.0.0)
-      vite: 4.4.7(@types/node@20.3.1)(less@4.2.0)
-      vitefu: 0.2.5(vite@4.4.7)
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@svitejs/changesets-changelog-github-compact@1.1.0:
     resolution: {integrity: sha512-qhUGGDHcpbY2zpjW3SwqchuW8J/5EzlPFud7xNntHKA7f3a/mx5+g+ruJKFHSAiVZYo30PALt+AyhmPUNKH/Og==}
     engines: {node: ^14.13.1 || ^16.0.0 || >=18}
@@ -12152,15 +12113,6 @@ packages:
       svelte: 4.0.0
     dev: true
 
-  /svelte-hmr@0.15.3(svelte@4.0.0):
-    resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
-    engines: {node: ^12.20 || ^14.13.1 || >= 16}
-    peerDependencies:
-      svelte: ^3.19.0 || ^4.0.0
-    dependencies:
-      svelte: 4.0.0
-    dev: true
-
   /svelte-htm@1.2.0(svelte@4.0.0):
     resolution: {integrity: sha512-6YFNncbyXbCa3PSoMQc/JR6O/Yg1OgNioH5rMgE88RVPzU8714y2Urfenlqs+ryRS+Inv+m6TJ9jH3W7wDCS1A==}
     engines: {node: '>=10'}
@@ -13164,17 +13116,6 @@ packages:
       vite: 4.4.7(@types/node@20.3.1)(less@4.2.0)
     dev: true
 
-  /vitefu@0.2.5(vite@4.4.7):
-    resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==}
-    peerDependencies:
-      vite: ^3.0.0 || ^4.0.0 || ^5.0.0
-    peerDependenciesMeta:
-      vite:
-        optional: true
-    dependencies:
-      vite: 4.4.7(@types/node@20.3.1)(less@4.2.0)
-    dev: true
-
   /vitest@0.33.0(@vitest/ui@0.34.6)(happy-dom@10.5.2)(jsdom@22.1.0)(less@4.2.0):
     resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==}
     engines: {node: '>=v14.18.0'}
diff --git a/src/docs/content/builders/date-field.md b/src/docs/content/builders/date-field.md
index 8349ca91..c3ba250d 100644
--- a/src/docs/content/builders/date-field.md
+++ b/src/docs/content/builders/date-field.md
@@ -543,6 +543,36 @@ Here's an example showcasing a few different locales:
 Notice that they all have the same `defaultPlaceholder`, yet the segments are formatted differently
 depending on the locale, and all it took was changing the `locale` prop. Pretty cool, right?
 
+### Readonly Segments
+
+The entire Date Field builder can be set as readonly using the `readonly` prop. But in some
+situations, you may want a user to be able to edit the time but not the date, or the month but not
+the year.
+
+The `readonlySegments` prop can be used for this.
+
+```svelte showLineNumbers {9-10}
+<script lang="ts">
+	import { createDateField, melt } from '$lib'
+	import { CalendarDate } from '@internationalized/date'
+
+	const {
+		elements: { field, segment, label },
+		states: { segmentContents }
+	} = createDateField({
+		defaultValue: new CalendarDate(2023, 10, 11),
+		readonlySegments: ['year']
+	})
+</script>
+```
+
+In this example, the `year` segment is readonly, but the `month` and `day` segments are focusable
+and editable.
+
+<Preview code={snippets.readonlySegments} variant="dark" size="sm">
+	<svelte:component this={previews.readonlySegments} />
+</Preview>
+
 ## API Reference
 
 <APIReference {schemas} />
diff --git a/src/docs/content/builders/date-range-field.md b/src/docs/content/builders/date-range-field.md
index 6a73ca94..e34492ab 100644
--- a/src/docs/content/builders/date-range-field.md
+++ b/src/docs/content/builders/date-range-field.md
@@ -563,6 +563,44 @@ Here's an example showcasing a few different locales:
 Notice that they all have the same `defaultPlaceholder`, yet the segments are formatted differently
 depending on the locale, and all it took was changing the `locale` prop. Pretty cool, right?
 
+### Readonly Segments
+
+Like the Date Field builder, Date Range Fields can be set as readonly using the `readonly` prop. But
+in some situations, you may want a user to be able to edit the time but not the date, or the month
+but not the year. You can configure which segments are readonly for the start and end fields
+separately using the `readonlySegments` prop.
+
+```svelte showLineNumbers {13-16}
+<script lang="ts">
+	import { createDateRangeField, melt } from '$lib'
+	import { CalendarDate } from '@internationalized/date'
+
+	const {
+		elements: { field, startSegment, endSegment, label },
+		states: { segmentContents }
+	} = createDateRangeField({
+		defaultValue: {
+			start: new CalendarDate(2023, 10, 11),
+			end: new CalendarDate(2023, 10, 12)
+		},
+		readonlySegments: {
+			start: ['month', 'year'],
+			end: ['year']
+		}
+	})
+</script>
+```
+
+In this example, the `year` segment is readonly on both start and end fields, `month` is also
+readonly on start, but the `day` segment is focusable and editable on both.
+
+<Preview code={snippets.readonlySegments} variant="dark" size="sm">
+	<svelte:component this={previews.readonlySegments} />
+</Preview>
+
+This could be used to ensure the end year can not be set different from the start year, using a
+controlled value prop, for example.
+
 ## API Reference
 
 <APIReference {schemas} />
diff --git a/src/docs/data/builders/date-field.ts b/src/docs/data/builders/date-field.ts
index 88674d36..fe56888a 100644
--- a/src/docs/data/builders/date-field.ts
+++ b/src/docs/data/builders/date-field.ts
@@ -71,6 +71,11 @@ const dateFieldProps = [
 		default: 'false',
 		description: 'Whether the date field is readonly.',
 	},
+	{
+		name: 'readonlySegments',
+		type: 'EditableSegmentPart[]',
+		description: 'The set of segments that are readonly.',
+	},
 	{
 		name: 'hourCycle',
 		type: 'HourCycle',
diff --git a/src/docs/data/builders/date-range-field.ts b/src/docs/data/builders/date-range-field.ts
index 0f20b6a6..9d770555 100644
--- a/src/docs/data/builders/date-range-field.ts
+++ b/src/docs/data/builders/date-range-field.ts
@@ -70,6 +70,11 @@ const dateRangeFieldProps = [
 		default: 'false',
 		description: 'Whether the date field is readonly.',
 	},
+	{
+		name: 'readonlySegments',
+		type: '{ start: EditableSegmentPart[], end: EditableSegmentPart[] }',
+		description: 'The sets of segments that are readonly on the start and end fields.',
+	},
 	{
 		name: 'hourCycle',
 		type: 'HourCycle',
diff --git a/src/docs/data/builders/menubar.ts b/src/docs/data/builders/menubar.ts
index 868122a1..01863b5b 100644
--- a/src/docs/data/builders/menubar.ts
+++ b/src/docs/data/builders/menubar.ts
@@ -7,7 +7,7 @@ import { getMenuSchemas, getMenuTriggerDataAttrs } from './menu.js';
 import { menubarEvents } from '$lib/builders/menubar/events.js';
 import { menubarIdParts } from '$lib';
 
-const OPTION_PROPS = [PROPS.CLOSE_ON_ESCAPE, PROPS.LOOP];
+const OPTION_PROPS = [PROPS.CLOSE_ON_ESCAPE, PROPS.LOOP, PROPS.PREVENT_SCROLL];
 const BUILDER_NAME = 'menubar';
 
 const builder = builderSchema(BUILDER_NAME, {
@@ -23,9 +23,16 @@ const builder = builderSchema(BUILDER_NAME, {
 	options: OPTION_PROPS,
 });
 
+const menubarMenuBuilderProps = dropdownBuilder.props?.filter((p) => p.name !== 'preventScroll');
+const menubarMenuBuilderOptions = dropdownBuilder.options?.filter(
+	(opt) => opt.name !== 'preventScroll'
+);
+
 const menuBuilder = {
 	...dropdownBuilder,
 	title: 'createMenu',
+	props: menubarMenuBuilderProps,
+	options: menubarMenuBuilderOptions,
 };
 const {
 	menu,
diff --git a/src/docs/data/builders/tooltip.ts b/src/docs/data/builders/tooltip.ts
index cdda15c0..6afc60fb 100644
--- a/src/docs/data/builders/tooltip.ts
+++ b/src/docs/data/builders/tooltip.ts
@@ -95,6 +95,10 @@ const trigger = elementSchema('trigger', {
 const content = elementSchema('content', {
 	description: 'The tooltip content element.',
 	dataAttributes: [
+		{
+			name: 'data-state',
+			value: ATTRS.OPEN_CLOSED,
+		},
 		{
 			name: 'data-melt-tooltip-content',
 			value: ATTRS.MELT('tooltip content'),
diff --git a/src/docs/previews/date-field/readonlySegments/css/index.svelte b/src/docs/previews/date-field/readonlySegments/css/index.svelte
new file mode 100644
index 00000000..5093640f
--- /dev/null
+++ b/src/docs/previews/date-field/readonlySegments/css/index.svelte
@@ -0,0 +1,69 @@
+<script lang="ts">
+	import { createDateField, melt } from '$lib';
+	import { CalendarDate } from '@internationalized/date';
+
+	const {
+		elements: { field, segment, label },
+		states: { segmentContents },
+	} = createDateField({
+		defaultValue: new CalendarDate(2023, 10, 11),
+		readonlySegments: ['year'],
+	});
+</script>
+
+<div>
+	<div>
+		<span use:melt={$label}>Appointment Date</span>
+		<div use:melt={$field}>
+			{#each $segmentContents as seg, i (i)}
+				<div use:melt={$segment(seg.part)}>
+					{seg.value}
+				</div>
+			{/each}
+		</div>
+	</div>
+</div>
+
+<style lang="postcss">
+	[data-melt-datefield-label] {
+		user-select: none;
+		font-weight: 500;
+		color: rgb(var(--color-white) / 1);
+	}
+
+	[data-melt-datefield-label][data-invalid] {
+		color: rgb(var(--color-magnum-500) / 1);
+	}
+
+	[data-melt-datefield-field] {
+		width: 100%;
+		min-width: 200px;
+		margin-top: 0.375rem;
+		padding: 0.375rem;
+
+		display: flex;
+		align-items: center;
+
+		background-color: rgb(var(--color-neutral-800) / 0.8);
+		color: rgb(var(--color-magnum-400) / 1);
+		border: 1px solid rgb(var(--color-magnum-400) / 0.6);
+		border-radius: 0.5rem;
+	}
+
+	[data-melt-datefield-field][data-invalid] {
+		border: 1px solid rgb(var(--color-magnum-400) / 0.6);
+	}
+
+	[data-melt-datefield-segment][data-invalid] {
+		color: rgb(var(--color-magnum-500) / 1);
+	}
+
+	[data-melt-datefield-segment]:not([data-segment='literal']) {
+		padding: 0 0.125rem;
+	}
+
+	[data-melt-datefield-validation] {
+		align-self: self-start;
+		color: rgb(var(--color-magnum-500) / 1);
+	}
+</style>
diff --git a/src/docs/previews/date-field/readonlySegments/tailwind/index.svelte b/src/docs/previews/date-field/readonlySegments/tailwind/index.svelte
new file mode 100644
index 00000000..294b66d8
--- /dev/null
+++ b/src/docs/previews/date-field/readonlySegments/tailwind/index.svelte
@@ -0,0 +1,55 @@
+<script lang="ts">
+	import { createDateField, melt } from '$lib';
+	import { CalendarDate } from '@internationalized/date';
+
+	const {
+		elements: { field, segment, label },
+		states: { segmentContents },
+	} = createDateField({
+		defaultValue: new CalendarDate(2023, 10, 11),
+		readonlySegments: ['year'],
+	});
+</script>
+
+<div>
+	<div>
+		<span use:melt={$label}>Appointment Date</span>
+		<div use:melt={$field}>
+			{#each $segmentContents as seg, i (i)}
+				<div use:melt={$segment(seg.part)}>
+					{seg.value}
+				</div>
+			{/each}
+		</div>
+	</div>
+</div>
+
+<style lang="postcss">
+	[data-melt-datefield-label] {
+		@apply select-none font-medium text-white;
+	}
+
+	[data-melt-datefield-label][data-invalid] {
+		@apply text-red-500;
+	}
+
+	[data-melt-datefield-field] {
+		@apply mt-1.5 flex w-full min-w-[200px] items-center rounded-lg border border-magnum-400/60 bg-neutral-800/80 p-1.5 text-magnum-400;
+	}
+
+	[data-melt-datefield-field][data-invalid] {
+		@apply border-red-400;
+	}
+
+	[data-melt-datefield-segment][data-invalid] {
+		@apply text-red-500;
+	}
+
+	[data-melt-datefield-segment]:not([data-segment='literal']) {
+		@apply px-0.5;
+	}
+
+	[data-melt-datefield-validation] {
+		@apply self-start text-red-500;
+	}
+</style>
diff --git a/src/docs/previews/date-range-field/readonlySegments/css/index.svelte b/src/docs/previews/date-range-field/readonlySegments/css/index.svelte
new file mode 100644
index 00000000..9d76e4ac
--- /dev/null
+++ b/src/docs/previews/date-range-field/readonlySegments/css/index.svelte
@@ -0,0 +1,88 @@
+<script lang="ts">
+	import { createDateRangeField, melt } from '$lib';
+	import { CalendarDate } from '@internationalized/date';
+
+	const {
+		elements: { field, startSegment, endSegment, label },
+		states: { segmentContents },
+	} = createDateRangeField({
+		defaultValue: {
+			start: new CalendarDate(2023, 10, 11),
+			end: new CalendarDate(2023, 10, 12),
+		},
+		readonlySegments: {
+			start: ['month', 'year'],
+			end: ['year'],
+		},
+	});
+</script>
+
+<span use:melt={$label}>Trip Dates</span>
+<div use:melt={$field}>
+	<div>
+		{#each $segmentContents.start as seg, i (i)}
+			<div use:melt={$startSegment(seg.part)}>
+				{seg.value}
+			</div>
+		{/each}
+	</div>
+	<div aria-hidden="true">-</div>
+	<div>
+		{#each $segmentContents.end as seg, i (i)}
+			<div use:melt={$endSegment(seg.part)}>
+				{seg.value}
+			</div>
+		{/each}
+	</div>
+</div>
+
+<style lang="postcss">
+	[data-melt-datefield-label] {
+		user-select: none;
+		font-weight: 500;
+		color: rgb(var(--color-white) / 1);
+	}
+
+	[data-melt-datefield-label][data-invalid] {
+		color: rgb(var(--color-magnum-500) / 1);
+	}
+
+	[data-melt-datefield-field] {
+		width: 100%;
+		min-width: 300px;
+		margin-top: 0.375rem;
+		padding: 0.375rem;
+
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		gap: 0.5rem;
+
+		background-color: rgb(var(--color-neutral-800) / 0.8);
+		color: rgb(var(--color-magnum-400) / 1);
+		border: 1px solid rgb(var(--color-magnum-400) / 0.6);
+		border-radius: 0.5rem;
+	}
+
+	[data-melt-datefield-field] > div {
+		display: flex;
+		align-items: center;
+	}
+
+	[data-melt-datefield-field][data-invalid] {
+		border: 1px solid rgb(var(--color-magnum-400) / 0.6);
+	}
+
+	[data-melt-datefield-segment][data-invalid] {
+		color: rgb(var(--color-magnum-500) / 1);
+	}
+
+	[data-melt-datefield-segment]:not([data-segment='literal']) {
+		padding: 0 0.125rem;
+	}
+
+	[data-melt-datefield-validation] {
+		align-self: self-start;
+		color: rgb(var(--color-magnum-500) / 1);
+	}
+</style>
diff --git a/src/docs/previews/date-range-field/readonlySegments/tailwind/index.svelte b/src/docs/previews/date-range-field/readonlySegments/tailwind/index.svelte
new file mode 100644
index 00000000..862fb01c
--- /dev/null
+++ b/src/docs/previews/date-range-field/readonlySegments/tailwind/index.svelte
@@ -0,0 +1,71 @@
+<script lang="ts">
+	import { createDateRangeField, melt } from '$lib';
+	import { CalendarDate } from '@internationalized/date';
+
+	const {
+		elements: { field, startSegment, endSegment, label },
+		states: { segmentContents },
+	} = createDateRangeField({
+		defaultValue: {
+			start: new CalendarDate(2023, 10, 11),
+			end: new CalendarDate(2023, 10, 12),
+		},
+		readonlySegments: {
+			start: ['month', 'year'],
+			end: ['year'],
+		},
+	});
+</script>
+
+<span use:melt={$label}>Trip Dates</span>
+<div use:melt={$field}>
+	<div>
+		{#each $segmentContents.start as seg, i (i)}
+			<div use:melt={$startSegment(seg.part)}>
+				{seg.value}
+			</div>
+		{/each}
+	</div>
+	<div aria-hidden="true">-</div>
+	<div>
+		{#each $segmentContents.end as seg, i (i)}
+			<div use:melt={$endSegment(seg.part)}>
+				{seg.value}
+			</div>
+		{/each}
+	</div>
+</div>
+
+<style lang="postcss">
+	[data-melt-datefield-label] {
+		@apply select-none font-medium text-white;
+	}
+
+	[data-melt-datefield-label][data-invalid] {
+		@apply text-red-500;
+	}
+
+	[data-melt-datefield-field] {
+		@apply mt-1.5 flex w-full min-w-[300px] items-center justify-center gap-2 rounded-lg border border-magnum-400/60 bg-neutral-800/80 p-1.5 text-magnum-400;
+
+		& > div {
+			@apply flex items-center;
+		}
+	}
+
+	[data-melt-datefield-field][data-invalid] {
+		@apply border-red-400;
+	}
+
+	[data-melt-datefield-segment][data-invalid] {
+		@apply text-red-500;
+	}
+
+	[data-melt-datefield-segment]:not([data-segment='literal']) {
+		@apply px-0.5;
+	}
+
+	[data-melt-datefield-validation] {
+		@apply self-start text-red-500;
+	}
+</style>
diff --git a/src/lib/builders/date-field/create.ts b/src/lib/builders/date-field/create.ts
index 2b9f03bb..5e5b0a22 100644
--- a/src/lib/builders/date-field/create.ts
+++ b/src/lib/builders/date-field/create.ts
@@ -73,6 +73,7 @@ const defaults = {
 	hideTimeZone: false,
 	disabled: false,
 	readonly: false,
+	readonlySegments: undefined,
 	name: undefined,
 	required: false,
 	minValue: undefined,
@@ -111,6 +112,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 		isDateUnavailable,
 		disabled,
 		readonly,
+		readonlySegments,
 		name: nameStore,
 		required,
 		minValue,
@@ -168,6 +170,11 @@ export function createDateField(props?: CreateDateFieldProps) {
 	 */
 	const updatingDayPeriod = writable<DayPeriod>(null);
 
+	const readonlySegmentsSet = derived(
+		readonlySegments,
+		($readonlySegments) => new Set<SegmentPart>($readonlySegments)
+	);
+
 	const ids = toWritableStores({ ...generateIds(dateFieldIdParts), ...withDefaults.ids });
 
 	const idValues = derived(
@@ -424,6 +431,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 			isInvalid,
 			disabled,
 			readonly,
+			readonlySegmentsSet,
 			idValues,
 			locale,
 		],
@@ -435,6 +443,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 			$isInvalid,
 			$disabled,
 			$readonly,
+			$readonlySegmentsSet,
 			$idValues,
 			_,
 		]) => {
@@ -445,11 +454,12 @@ export function createDateField(props?: CreateDateFieldProps) {
 				ids: $idValues,
 			};
 			return (part: SegmentPart) => {
+				const inReadonlySegments = $readonlySegmentsSet.has(part);
 				const defaultAttrs = {
 					...getSegmentAttrs(part, props),
 					'aria-invalid': $isInvalid ? ('true' as const) : undefined,
 					'aria-disabled': $disabled ? ('true' as const) : undefined,
-					'aria-readonly': $readonly ? ('true' as const) : undefined,
+					'aria-readonly': $readonly || inReadonlySegments ? ('true' as const) : undefined,
 					'data-invalid': $isInvalid ? '' : undefined,
 					'data-disabled': $disabled ? '' : undefined,
 					'data-segment': `${part}`,
@@ -467,7 +477,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 					...defaultAttrs,
 					id: $idValues[part],
 					'aria-labelledby': getLabelledBy(part),
-					contentEditable: $readonly || $disabled ? false : true,
+					contenteditable: $readonly || inReadonlySegments || $disabled ? false : true,
 					'aria-describedby': describedBy,
 					tabindex: $disabled ? undefined : 0,
 				};
@@ -485,7 +495,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 			? Updater<TimeSegmentObj[T]>
 			: Updater<DateAndTimeSegmentObj[T]>
 	) {
-		if (get(disabled) || get(readonly)) return;
+		if (get(disabled) || get(readonly) || get(readonlySegmentsSet).has(part)) return;
 		segmentValues.update((prev) => {
 			const dateRef = get(placeholder);
 			if (isDateAndTimeSegmentObj(prev)) {
diff --git a/src/lib/builders/date-field/types.ts b/src/lib/builders/date-field/types.ts
index 6d94a0c1..161a0b9a 100644
--- a/src/lib/builders/date-field/types.ts
+++ b/src/lib/builders/date-field/types.ts
@@ -3,6 +3,7 @@ import type { ChangeFn, IdObj } from '$lib/internal/helpers/index.js';
 import type { DateFieldIdParts, createDateField } from './create.js';
 import type { DateValue } from '@internationalized/date';
 import type { Granularity, Matcher } from '$lib/index.js';
+import type { EditableSegmentPart } from '$lib/shared/index.js';
 
 export type DateFieldProps = {
 	/**
@@ -116,6 +117,14 @@ export type DateFieldProps = {
 	 */
 	readonly?: boolean;
 
+	/**
+	 *
+	 * List of segment names that will be readonly.
+	 *
+	 * @default undefined
+	 */
+	readonlySegments?: EditableSegmentPart[];
+
 	/**
 	 * The format to use for displaying the time in the input.
 	 * If using a 12 hour clock, ensure you also include the
diff --git a/src/lib/builders/date-picker/types.ts b/src/lib/builders/date-picker/types.ts
index 02a20402..c69771a2 100644
--- a/src/lib/builders/date-picker/types.ts
+++ b/src/lib/builders/date-picker/types.ts
@@ -125,7 +125,11 @@ export type DatePickerProps = {
 	locale?: string;
 };
 
-type ModifiedDateFieldProps = RenameProperties<CreateDateFieldProps, { ids: 'dateFieldIds' }>;
+type ModifiedDateFieldProps = Omit<
+	RenameProperties<CreateDateFieldProps, { ids: 'dateFieldIds' }>,
+	'readonlySegments'
+>;
+
 type ModifiedCalendarProps = RenameProperties<CreateCalendarProps, { ids: 'calendarIds' }>;
 type ModifiedPopoverProps = Omit<
 	RenameProperties<CreatePopoverProps, { ids: 'popoverIds' }>,
diff --git a/src/lib/builders/date-range-field/create.ts b/src/lib/builders/date-range-field/create.ts
index 174bd634..d0f21f76 100644
--- a/src/lib/builders/date-range-field/create.ts
+++ b/src/lib/builders/date-range-field/create.ts
@@ -40,6 +40,7 @@ const defaults = {
 	endName: undefined,
 	disabled: false,
 	readonly: false,
+	readonlySegments: undefined,
 	minValue: undefined,
 	maxValue: undefined,
 } satisfies CreateDateRangeFieldProps;
@@ -86,9 +87,17 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 	);
 
 	const startField = createDateField({
-		...omit(withDefaults, 'defaultValue', 'onValueChange', 'startName', 'endName'),
+		...omit(
+			withDefaults,
+			'defaultValue',
+			'onValueChange',
+			'startName',
+			'endName',
+			'readonlySegments'
+		),
 		value: startValue,
 		name: withDefaults.startName,
+		readonlySegments: withDefaults.readonlySegments?.start,
 		ids: {
 			...generatedIds,
 			...withDefaults.ids,
@@ -97,9 +106,17 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 	});
 
 	const endField = createDateField({
-		...omit(withDefaults, 'defaultValue', 'onValueChange', 'endName', 'startName'),
+		...omit(
+			withDefaults,
+			'defaultValue',
+			'onValueChange',
+			'endName',
+			'startName',
+			'readonlySegments'
+		),
 		value: endValue,
 		name: withDefaults.endName,
+		readonlySegments: withDefaults.readonlySegments?.end,
 		ids: {
 			...generatedIds,
 			...withDefaults.ids,
@@ -315,6 +332,10 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 		startField.options.readonly.set($readonly);
 		endField.options.readonly.set($readonly);
 	});
+	effect([options.readonlySegments], ([$readonlySegments]) => {
+		startField.options.readonlySegments.set($readonlySegments?.start);
+		endField.options.readonlySegments.set($readonlySegments?.end);
+	});
 	effect([options.minValue], ([$minValue]) => {
 		startField.options.minValue.set($minValue);
 		endField.options.minValue.set($minValue);
diff --git a/src/lib/builders/date-range-field/types.ts b/src/lib/builders/date-range-field/types.ts
index 8ffe1948..e7fd3ea8 100644
--- a/src/lib/builders/date-range-field/types.ts
+++ b/src/lib/builders/date-range-field/types.ts
@@ -3,6 +3,7 @@ import type { ChangeFn, IdObj } from '$lib/internal/helpers/index.js';
 import type { DateRangeFieldIdParts, createDateRangeField } from './create.js';
 import type { DateValue } from '@internationalized/date';
 import type { CreateDateFieldProps, DateFieldIdParts, DateRange, Matcher } from '$lib/index.js';
+import type { EditableSegmentPart } from '$lib/shared/index.js';
 
 export type DateRangeFieldProps = {
 	/**
@@ -99,6 +100,14 @@ export type DateRangeFieldProps = {
 	 */
 	endName?: string;
 
+	/**
+	 *
+	 * Lists of segment names that will be readonly on the start and end fields.
+	 *
+	 * @default undefined
+	 */
+	readonlySegments?: { start: EditableSegmentPart[]; end: EditableSegmentPart[] };
+
 	/**
 	 * Override any of the element IDs set by the builder.
 	 *
@@ -114,6 +123,9 @@ export type DateRangeFieldProps = {
 
 export type CreateDateRangeFieldProps = Expand<
 	DateRangeFieldProps &
-		Omit<CreateDateFieldProps, 'value' | 'defaultValue' | 'onValueChange' | 'ids' | 'name'>
+		Omit<
+			CreateDateFieldProps,
+			'value' | 'defaultValue' | 'onValueChange' | 'ids' | 'name' | 'readonlySegments'
+		>
 >;
 export type DateRangeField = ReturnType<typeof createDateRangeField>;
diff --git a/src/lib/builders/date-range-picker/types.ts b/src/lib/builders/date-range-picker/types.ts
index 35fa6734..c224bff3 100644
--- a/src/lib/builders/date-range-picker/types.ts
+++ b/src/lib/builders/date-range-picker/types.ts
@@ -8,7 +8,7 @@ import type { RenameProperties } from '$lib/internal/types.js';
 import type { CreateDateRangeFieldProps } from '../date-range-field/types.js';
 import type { CreateRangeCalendarProps } from '../range-calendar/types.js';
 
-export type DateRangePickerProps = {
+type DateRangePickerProps = {
 	/**
 	 * The default value for the date field. When provided,
 	 * the `placeholder` will also assume this value.
@@ -125,7 +125,10 @@ export type DateRangePickerProps = {
 	locale?: string;
 };
 
-type ModifiedDateFieldProps = RenameProperties<CreateDateRangeFieldProps, { ids: 'dateFieldIds' }>;
+type ModifiedDateFieldProps = Omit<
+	RenameProperties<CreateDateRangeFieldProps, { ids: 'dateFieldIds' }>,
+	'readonlySegments'
+>;
 type ModifiedCalendarProps = RenameProperties<CreateRangeCalendarProps, { ids: 'calendarIds' }>;
 type ModifiedPopoverProps = Omit<
 	RenameProperties<CreatePopoverProps, { ids: 'popoverIds' }>,
diff --git a/src/lib/builders/menu/create.ts b/src/lib/builders/menu/create.ts
index a393915a..1fb3b168 100644
--- a/src/lib/builders/menu/create.ts
+++ b/src/lib/builders/menu/create.ts
@@ -1138,55 +1138,50 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		}
 	});
 
-	effect(
-		[rootOpen, rootActiveTrigger, preventScroll],
-		([$rootOpen, $rootActiveTrigger, $preventScroll]) => {
-			if (!isBrowser) return;
-
-			const unsubs: Array<() => void> = [];
+	effect([rootOpen], ([$rootOpen]) => {
+		if (!isBrowser) return;
+		if (!$rootOpen) {
+			const $rootActiveTrigger = get(rootActiveTrigger);
+			if (!$rootActiveTrigger) return;
+			const $closeFocus = get(closeFocus);
 
-			if (opts.removeScroll && $rootOpen && $preventScroll) {
-				unsubs.push(removeScroll());
+			if (!$rootOpen && $rootActiveTrigger) {
+				handleFocus({ prop: $closeFocus, defaultEl: $rootActiveTrigger });
 			}
+		}
+	});
 
-			const $closeFocus = get(closeFocus);
+	effect([rootOpen, preventScroll], ([$rootOpen, $preventScroll]) => {
+		if (!isBrowser) return;
 
-			if (!$rootOpen) {
-				if ($rootActiveTrigger) {
-					// If we already have a reference to the trigger, focus it
-					handleFocus({ prop: $closeFocus, defaultEl: $rootActiveTrigger });
-				} else {
-					// otherwise we'll get the trigger el and focus it
-					handleFocus({
-						prop: $closeFocus,
-						defaultEl: document.getElementById(get(rootIds.trigger)),
-					});
-				}
-			}
+		const unsubs: Array<() => void> = [];
 
-			// if the menu is open, we'll sleep for a sec so the menu can render
-			// before we focus on either the first item or the menu itself.
-			sleep(1).then(() => {
-				const menuEl = document.getElementById(get(rootIds.menu));
-				if (menuEl && $rootOpen && get(isUsingKeyboard)) {
-					if (get(disableFocusFirstItem)) {
-						handleRovingFocus(menuEl);
-						return;
-					}
-					// Get menu items belonging to the root menu
-					const menuItems = getMenuItems(menuEl);
-					if (!menuItems.length) return;
+		if (opts.removeScroll && $rootOpen && $preventScroll) {
+			unsubs.push(removeScroll());
+		}
 
-					// Focus on first menu item
-					handleRovingFocus(menuItems[0]);
+		// if the menu is open, we'll sleep for a sec so the menu can render
+		// before we focus on either the first item or the menu itself.
+		sleep(1).then(() => {
+			const menuEl = document.getElementById(get(rootIds.menu));
+			if (menuEl && $rootOpen && get(isUsingKeyboard)) {
+				if (get(disableFocusFirstItem)) {
+					handleRovingFocus(menuEl);
+					return;
 				}
-			});
+				// Get menu items belonging to the root menu
+				const menuItems = getMenuItems(menuEl);
+				if (!menuItems.length) return;
 
-			return () => {
-				unsubs.forEach((unsub) => unsub());
-			};
-		}
-	);
+				// Focus on first menu item
+				handleRovingFocus(menuItems[0]);
+			}
+		});
+
+		return () => {
+			unsubs.forEach((unsub) => unsub());
+		};
+	});
 
 	effect(rootOpen, ($rootOpen) => {
 		if (!isBrowser) return;
diff --git a/src/lib/builders/menubar/create.ts b/src/lib/builders/menubar/create.ts
index 22172dbe..6da83a83 100644
--- a/src/lib/builders/menubar/create.ts
+++ b/src/lib/builders/menubar/create.ts
@@ -23,13 +23,13 @@ import {
 	removeScroll,
 	styleToString,
 	toWritableStores,
+	generateIds,
+	omit,
 } from '$lib/internal/helpers/index.js';
 import { safeOnDestroy, safeOnMount } from '$lib/internal/helpers/lifecycle.js';
 import type { MeltActionReturn } from '$lib/internal/types.js';
 import { tick } from 'svelte';
 import { get, writable } from 'svelte/store';
-import { generateIds } from '../../internal/helpers/id';
-import { omit } from '../../internal/helpers/object';
 import {
 	applyAttrsIfDisabled,
 	createMenuBuilder,
@@ -48,6 +48,7 @@ const { name } = createElHelpers<_MenuParts | 'menu'>('menubar');
 const defaults = {
 	loop: true,
 	closeOnEscape: true,
+	preventScroll: false,
 } satisfies CreateMenubarProps;
 
 export const menubarIdParts = ['menubar'] as const;
@@ -57,7 +58,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 	const withDefaults = { ...defaults, ...props } satisfies CreateMenubarProps;
 
 	const options = toWritableStores(omit(withDefaults, 'ids'));
-	const { loop, closeOnEscape } = options;
+	const { loop, closeOnEscape, preventScroll } = options;
 	const activeMenu = writable<string>('');
 
 	const nextFocusable = writable<HTMLElement | null>(null);
@@ -93,7 +94,6 @@ export function createMenubar(props?: CreateMenubarProps) {
 		positioning: {
 			placement: 'bottom-start',
 		},
-		preventScroll: true,
 		arrowSize: 8,
 		dir: 'ltr',
 		loop: false,
@@ -119,7 +119,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 		const { positioning, portal, forceVisible, closeOnOutsideClick, onOutsideClick } = options;
 
 		const m = createMenuBuilder({
-			rootOptions: options,
+			rootOptions: { ...options, preventScroll },
 			rootOpen,
 			rootActiveTrigger,
 			nextFocusable,
@@ -588,8 +588,8 @@ export function createMenubar(props?: CreateMenubarProps) {
 	});
 
 	const unsubs: Array<() => void> = [];
-	effect([activeMenu], ([$activeMenu]) => {
-		if (!isBrowser) return;
+	effect([activeMenu, preventScroll], ([$activeMenu, $preventScroll]) => {
+		if (!isBrowser || !$preventScroll) return;
 
 		/**
 		 * To prevent adding/removing the scroll as we cycle through
diff --git a/src/lib/builders/menubar/types.ts b/src/lib/builders/menubar/types.ts
index 9fd7b415..4e12e494 100644
--- a/src/lib/builders/menubar/types.ts
+++ b/src/lib/builders/menubar/types.ts
@@ -16,16 +16,26 @@ export type CreateMenubarProps = {
 
 	/**
 	 * Whether to close the active menu when the escape key is pressed.
+	 *
+	 * @default true
 	 */
 	closeOnEscape?: boolean;
 
+	/**
+	 * Whether to prevent scrolling the body when any menu within
+	 * the menubar is open.
+	 *
+	 * @default true
+	 */
+	preventScroll?: boolean;
+
 	/**
 	 * Optionally override the default ids we assign to the elements
 	 */
 	ids?: Partial<IdObj<MenubarIdParts>>;
 };
 
-export type CreateMenubarMenuProps = _Menu['builder'];
+export type CreateMenubarMenuProps = Omit<_Menu['builder'], 'preventScroll'>;
 export type CreateMenubarSubmenuProps = _Menu['submenu'];
 export type MenubarMenuItemProps = _Menu['item'];
 export type CreateMenuRadioGroupProps = _Menu['radioGroup'];
diff --git a/src/lib/builders/radio-group/create.ts b/src/lib/builders/radio-group/create.ts
index be95c8b5..1c693bc4 100644
--- a/src/lib/builders/radio-group/create.ts
+++ b/src/lib/builders/radio-group/create.ts
@@ -128,7 +128,7 @@ export function createRadioGroup(props?: CreateRadioGroupProps) {
 					if (!isHTMLElement(root)) return;
 
 					const items = Array.from(root.querySelectorAll(selector('item'))).filter(
-						(el): el is HTMLElement => isHTMLElement(el)
+						(el): el is HTMLElement => isHTMLElement(el) && !el.hasAttribute('data-disabled')
 					);
 					const currentIndex = items.indexOf(el);
 
diff --git a/src/lib/builders/tooltip/create.ts b/src/lib/builders/tooltip/create.ts
index 5dc622a5..e81717c5 100644
--- a/src/lib/builders/tooltip/create.ts
+++ b/src/lib/builders/tooltip/create.ts
@@ -126,12 +126,17 @@ export function createTooltip(props?: CreateTooltipProps) {
 		}
 	}
 
+	const isVisible = derived([open, forceVisible], ([$open, $forceVisible]) => {
+		return $open || $forceVisible;
+	});
+
 	const trigger = builder(name('trigger'), {
-		stores: [ids.content, ids.trigger],
-		returned: ([$contentId, $triggerId]) => {
+		stores: [ids.content, ids.trigger, open],
+		returned: ([$contentId, $triggerId, $open]) => {
 			return {
 				'aria-describedby': $contentId,
 				id: $triggerId,
+				'data-state': $open ? 'open' : 'closed',
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<TooltipEvents['trigger']> => {
@@ -183,10 +188,6 @@ export function createTooltip(props?: CreateTooltipProps) {
 		},
 	});
 
-	const isVisible = derived([open, forceVisible], ([$open, $forceVisible]) => {
-		return $open || $forceVisible;
-	});
-
 	const content = builder(name('content'), {
 		stores: [isVisible, portal, ids.content],
 		returned: ([$isVisible, $portal, $contentId]) => {
@@ -199,6 +200,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 				}),
 				id: $contentId,
 				'data-portal': $portal ? '' : undefined,
+				'data-state': $isVisible ? 'open' : 'closed',
 			};
 		},
 		action: (node: HTMLElement): MeltActionReturn<TooltipEvents['content']> => {
diff --git a/src/lib/shared/index.ts b/src/lib/shared/index.ts
index b9513b12..f9b788d1 100644
--- a/src/lib/shared/index.ts
+++ b/src/lib/shared/index.ts
@@ -4,8 +4,17 @@
  * use, they should be exported from this file to prevent conflicts
  * with other builders that may export the same types or functions.
  */
-import type { SegmentPart } from '$lib/builders/date-field/_internal/types.js';
+import type { SegmentPart, EditableSegmentPart } from '$lib/builders/date-field/_internal/types.js';
 import type { Granularity, Matcher, DateRange, Month } from '$lib/internal/helpers/date/index.js';
 import type { FocusProp, FocusTarget } from '$lib/internal/helpers/index.js';
 
-export type { Granularity, FocusProp, FocusTarget, Matcher, DateRange, Month, SegmentPart };
+export type {
+	Granularity,
+	FocusProp,
+	FocusTarget,
+	Matcher,
+	DateRange,
+	Month,
+	SegmentPart,
+	EditableSegmentPart,
+};
diff --git a/src/stories/DateField/DateField.svelte b/src/stories/DateField/DateField.svelte
index 900a6ae7..214850f3 100644
--- a/src/stories/DateField/DateField.svelte
+++ b/src/stories/DateField/DateField.svelte
@@ -11,6 +11,7 @@
 	export let isDateUnavailable: CreateDateFieldProps['isDateUnavailable'] = undefined;
 	export let disabled: CreateDateFieldProps['disabled'] = undefined;
 	export let readonly: CreateDateFieldProps['readonly'] = undefined;
+	export let readonlySegments: CreateDateFieldProps['readonlySegments'] = undefined;
 	export let hourCycle: CreateDateFieldProps['hourCycle'] = undefined;
 	export let locale: CreateDateFieldProps['locale'] = 'en';
 	export let hideTimeZone: CreateDateFieldProps['hideTimeZone'] = undefined;
@@ -29,6 +30,7 @@
 		isDateUnavailable,
 		disabled,
 		readonly,
+		readonlySegments,
 		hourCycle,
 		locale,
 		hideTimeZone,
diff --git a/svelte.config.js b/svelte.config.js
index 6d258347..58c849e1 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -1,7 +1,7 @@
 import { mdsvex } from '@huntabyte/mdsvex';
 import { preprocessMeltUI } from '@melt-ui/pp';
 import adapterStatic from '@sveltejs/adapter-static';
-import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+import { vitePreprocess } from '@sveltejs/kit/vite';
 import sequence from 'svelte-sequential-preprocessor';
 import { mdsvexOptions } from './mdsvex.config.js';
 


Test patch:
diff --git a/src/tests/date-field/DateField.spec.ts b/src/tests/date-field/DateField.spec.ts
index 1c998349..63da56dd 100644
--- a/src/tests/date-field/DateField.spec.ts
+++ b/src/tests/date-field/DateField.spec.ts
@@ -258,6 +258,29 @@ describe('DateField', () => {
 		}
 	});
 
+	test('readonlySegments prop prevents modifying given segments', async () => {
+		const { getByTestId, user } = setup({
+			readonlySegments: ['month'],
+			defaultValue: calendarDateOther,
+		});
+
+		// month should not change
+		const monthSegment = getByTestId('month');
+		expect(monthSegment).toHaveTextContent(String(calendarDateOther['month']));
+		await user.click(monthSegment);
+		expect(monthSegment).toHaveFocus();
+		await user.keyboard(kbd.ARROW_UP);
+		expect(monthSegment).toHaveTextContent(String(calendarDateOther['month']));
+
+		// day should change
+		const daySegment = getByTestId('day');
+		expect(daySegment).toHaveTextContent(String(calendarDateOther['day']));
+		await user.click(daySegment);
+		expect(daySegment).toHaveFocus();
+		await user.keyboard(kbd.ARROW_UP);
+		expect(daySegment).toHaveTextContent(String(calendarDateOther['day'] + 1));
+	});
+
 	test('if selected date unavailable, mark field as invalid', async () => {
 		const { getByTestId, user } = setup({
 			granularity: 'day',
diff --git a/src/tests/date-field/DateFieldTest.svelte b/src/tests/date-field/DateFieldTest.svelte
index 82db773f..32bbb35f 100644
--- a/src/tests/date-field/DateFieldTest.svelte
+++ b/src/tests/date-field/DateFieldTest.svelte
@@ -1,6 +1,7 @@
 <script lang="ts">
 	import { createDateField, type CreateDateFieldProps } from '$lib/builders';
 	import { melt } from '$lib';
+	import { removeUndefined } from '../utils';
 
 	export let value: CreateDateFieldProps['value'] = undefined;
 	export let defaultValue: CreateDateFieldProps['defaultValue'] = undefined;
@@ -10,6 +11,7 @@
 	export let isDateUnavailable: CreateDateFieldProps['isDateUnavailable'] = undefined;
 	export let disabled: CreateDateFieldProps['disabled'] = undefined;
 	export let readonly: CreateDateFieldProps['readonly'] = undefined;
+	export let readonlySegments: CreateDateFieldProps['readonlySegments'] = undefined;
 	export let hourCycle: CreateDateFieldProps['hourCycle'] = undefined;
 	export let locale: CreateDateFieldProps['locale'] = 'en';
 	export let hideTimeZone: CreateDateFieldProps['hideTimeZone'] = undefined;
@@ -19,21 +21,24 @@
 	const {
 		elements: { field, segment, label, validation },
 		states: { value: insideValue, segmentContents },
-	} = createDateField({
-		value,
-		defaultValue,
-		defaultPlaceholder,
-		onValueChange,
-		onPlaceholderChange,
-		isDateUnavailable,
-		disabled,
-		readonly,
-		hourCycle,
-		locale,
-		hideTimeZone,
-		ids,
-		granularity,
-	});
+	} = createDateField(
+		removeUndefined({
+			value,
+			defaultValue,
+			defaultPlaceholder,
+			onValueChange,
+			onPlaceholderChange,
+			isDateUnavailable,
+			disabled,
+			readonly,
+			readonlySegments,
+			hourCycle,
+			locale,
+			hideTimeZone,
+			ids,
+			granularity,
+		})
+	);
 </script>
 
 <main>
diff --git a/src/tests/date-range-field/DateRangeField.spec.ts b/src/tests/date-range-field/DateRangeField.spec.ts
index e59405f2..107c3784 100644
--- a/src/tests/date-range-field/DateRangeField.spec.ts
+++ b/src/tests/date-range-field/DateRangeField.spec.ts
@@ -258,4 +258,68 @@ describe('DateField', () => {
 		expect(getByTestId('start-value')).toHaveTextContent(overrideDay.start.toString());
 		expect(getByTestId('end-value')).toHaveTextContent(overrideDay.end.toString());
 	});
+
+	test('readonlySegments prop prevents modifying given segments', async () => {
+		const { getByTestId, user } = setup({
+			defaultValue: exampleDate,
+			readonlySegments: {
+				start: ['month'],
+				end: ['month', 'year'],
+			},
+		});
+
+		// start field
+		{
+			// start month should not change
+			const monthSegment = getByTestId('start-month');
+			expect(monthSegment).toHaveTextContent(String(exampleDate.start['month']));
+			await user.click(monthSegment);
+			expect(monthSegment).toHaveFocus();
+			await user.keyboard(kbd.ARROW_UP);
+			expect(monthSegment).toHaveTextContent(String(exampleDate.start['month']));
+
+			// start day should change
+			const daySegment = getByTestId('start-day');
+			expect(daySegment).toHaveTextContent(String(exampleDate.start['day']));
+			await user.click(daySegment);
+			expect(daySegment).toHaveFocus();
+			await user.keyboard(kbd.ARROW_UP);
+			expect(daySegment).toHaveTextContent(String(exampleDate.start['day'] + 1));
+
+			// start year should change
+			const yearSegment = getByTestId('start-year');
+			expect(yearSegment).toHaveTextContent(String(exampleDate.start['year']));
+			await user.click(yearSegment);
+			expect(yearSegment).toHaveFocus();
+			await user.keyboard(kbd.ARROW_UP);
+			expect(yearSegment).toHaveTextContent(String(exampleDate.start['year'] + 1));
+		}
+
+		// end field
+		{
+			// end month should not change
+			const monthSegment = getByTestId('end-month');
+			expect(monthSegment).toHaveTextContent(String(exampleDate.end['month']));
+			await user.click(monthSegment);
+			expect(monthSegment).toHaveFocus();
+			await user.keyboard(kbd.ARROW_UP);
+			expect(monthSegment).toHaveTextContent(String(exampleDate.end['month']));
+
+			// end day should change
+			const daySegment = getByTestId('end-day');
+			expect(daySegment).toHaveTextContent(String(exampleDate.end['day']));
+			await user.click(daySegment);
+			expect(daySegment).toHaveFocus();
+			await user.keyboard(kbd.ARROW_UP);
+			expect(daySegment).toHaveTextContent(String(exampleDate.end['day'] + 1));
+
+			// end year should not change
+			const yearSegment = getByTestId('end-year');
+			expect(yearSegment).toHaveTextContent(String(exampleDate.end['year']));
+			await user.click(yearSegment);
+			expect(yearSegment).toHaveFocus();
+			await user.keyboard(kbd.ARROW_UP);
+			expect(yearSegment).toHaveTextContent(String(exampleDate.end['year']));
+		}
+	});
 });
diff --git a/src/tests/date-range-field/DateRangeFieldTest.svelte b/src/tests/date-range-field/DateRangeFieldTest.svelte
index cefd943b..b7aca709 100644
--- a/src/tests/date-range-field/DateRangeFieldTest.svelte
+++ b/src/tests/date-range-field/DateRangeFieldTest.svelte
@@ -14,6 +14,7 @@
 	export let isDateUnavailable: CreateDateRangeFieldProps['isDateUnavailable'] = undefined;
 	export let disabled: CreateDateRangeFieldProps['disabled'] = undefined;
 	export let readonly: CreateDateRangeFieldProps['readonly'] = undefined;
+	export let readonlySegments: CreateDateRangeFieldProps['readonlySegments'] = undefined;
 	export let hourCycle: CreateDateRangeFieldProps['hourCycle'] = undefined;
 	export let locale: CreateDateRangeFieldProps['locale'] = 'en';
 	export let hideTimeZone: CreateDateRangeFieldProps['hideTimeZone'] = undefined;
@@ -35,6 +36,7 @@
 			isDateUnavailable,
 			disabled,
 			readonly,
+			readonlySegments,
 			hourCycle,
 			locale,
 			hideTimeZone,
diff --git a/src/tests/dropdown-menu/DropdownMenu.spec.ts b/src/tests/dropdown-menu/DropdownMenu.spec.ts
index 70ea94ac..5c539c6f 100644
--- a/src/tests/dropdown-menu/DropdownMenu.spec.ts
+++ b/src/tests/dropdown-menu/DropdownMenu.spec.ts
@@ -7,6 +7,7 @@ import DropdownMenuTest from './DropdownMenuTest.svelte';
 import DropdownMenuForceVisible from './DropdownMenuForceVisibleTest.svelte';
 import type { CreateDropdownMenuProps } from '$lib';
 import { sleep } from '$lib/internal/helpers/sleep.js';
+import { tick } from 'svelte';
 
 const OPEN_KEYS = [kbd.ENTER, kbd.ARROW_DOWN, kbd.SPACE];
 
@@ -45,6 +46,19 @@ describe('Dropdown Menu (Default)', () => {
 		expect(menu).not.toBeVisible();
 	});
 
+	test('Refocuses the trigger when menu closed with keyboard', async () => {
+		const { user, trigger, getByTestId } = setup();
+		const menu = getByTestId('menu');
+
+		expect(menu).not.toBeVisible();
+		await user.click(trigger);
+		expect(menu).toBeVisible();
+
+		await user.keyboard(kbd.ESCAPE);
+		expect(menu).not.toBeVisible();
+		expect(trigger).toHaveFocus();
+	});
+
 	test.each(OPEN_KEYS)('Opens when %s is pressed', async (key) => {
 		const { user, trigger, getByTestId } = setup();
 		const menu = getByTestId('menu');
@@ -303,6 +317,21 @@ describe('Dropdown Menu (forceVisible)', () => {
 		await waitFor(() => expect(queryByTestId('menu')).toBeNull());
 	});
 
+	test('Refocuses the trigger when menu closed with keyboard', async () => {
+		const { queryByTestId, user, trigger } = setupForceVis();
+
+		expect(queryByTestId('menu')).toBeNull();
+		await user.click(trigger);
+		await waitFor(() => expect(queryByTestId('menu')).not.toBeNull());
+		await user.keyboard(kbd.ARROW_DOWN);
+
+		expect(trigger).not.toHaveFocus();
+		const spy = vi.spyOn(trigger, 'focus');
+		await user.keyboard(kbd.ESCAPE);
+		await waitFor(() => expect(queryByTestId('menu')).toBeNull());
+		expect(spy).toHaveBeenCalled();
+	});
+
 	test.each(OPEN_KEYS)('Opens when %s is pressed', async (key) => {
 		const { queryByTestId, user, trigger } = setupForceVis();
 		expect(queryByTestId('menu')).toBeNull();
diff --git a/src/tests/radio-group/RadioGroup.spec.ts b/src/tests/radio-group/RadioGroup.spec.ts
new file mode 100644
index 00000000..095816fd
--- /dev/null
+++ b/src/tests/radio-group/RadioGroup.spec.ts
@@ -0,0 +1,87 @@
+import { render, waitFor } from '@testing-library/svelte';
+import { axe } from 'jest-axe';
+import { describe, it } from 'vitest';
+import RadioGroupTest from './RadioGroupTest.svelte';
+import userEvent from '@testing-library/user-event';
+import { testKbd as kbd } from '../utils.js';
+import type { CreateRadioGroupProps } from '$lib';
+
+const items = [
+	{
+		value: 'a',
+		disabled: false,
+	},
+	{
+		value: 'b',
+		disabled: false,
+	},
+	{
+		value: 'c',
+		disabled: false,
+	},
+	{
+		value: 'd',
+		disabled: false,
+	},
+];
+
+function setup(
+	props: CreateRadioGroupProps & { items?: { value: string; disabled: boolean }[] } = {}
+) {
+	const user = userEvent.setup();
+	return { ...render(RadioGroupTest, props), user };
+}
+
+describe('Radio Group', () => {
+	it('No accessibility violations', async () => {
+		const { container } = render(RadioGroupTest);
+		expect(await axe(container)).toHaveNoViolations();
+	});
+
+	it('doesnt require a default value', async () => {
+		const { getByTestId } = setup();
+		expect(getByTestId('value')).toHaveTextContent('undefined');
+	});
+
+	it('allows changing value by clicking on a radio button', async () => {
+		const { user, getByTestId } = setup();
+		const a = getByTestId('a');
+		const b = getByTestId('b');
+		const value = getByTestId('value');
+		await user.click(a);
+		expect(value).toHaveTextContent('a');
+		await user.click(b);
+		expect(value).toHaveTextContent('b');
+	});
+
+	it('navigates through the buttons with arrow keys', async () => {
+		const { user, getByTestId } = setup();
+
+		const els = ['a', 'b', 'c', 'd'].map((v) => getByTestId(v));
+
+		await user.click(els[0]);
+		expect(els[0]).toHaveFocus();
+		await user.keyboard(kbd.ARROW_DOWN);
+		expect(els[1]).toHaveFocus();
+		await user.keyboard(kbd.ARROW_DOWN);
+		expect(els[2]).toHaveFocus();
+		await user.keyboard(kbd.ARROW_DOWN);
+		expect(els[3]).toHaveFocus();
+		await user.keyboard(kbd.ARROW_DOWN);
+		expect(els[0]).toHaveFocus();
+	});
+
+	it('skips over disabled buttons', async () => {
+		const { user, getByTestId } = setup({
+			items: items.map((item) => ({ ...item, disabled: item.value === 'b' })),
+		});
+
+		const els = ['a', 'b', 'c', 'd'].map((v) => getByTestId(v));
+
+		await user.click(els[0]);
+		expect(els[0]).toHaveFocus();
+		await user.keyboard(kbd.ARROW_DOWN);
+		expect(els[2]).toHaveFocus();
+		expect(els[1]).not.toHaveFocus();
+	});
+});
diff --git a/src/tests/radio-group/RadioGroupTest.svelte b/src/tests/radio-group/RadioGroupTest.svelte
new file mode 100644
index 00000000..111c1785
--- /dev/null
+++ b/src/tests/radio-group/RadioGroupTest.svelte
@@ -0,0 +1,49 @@
+<script lang="ts">
+	import { createRadioGroup, melt, type CreateRadioGroupProps } from '$lib/index.js';
+	import { removeUndefined } from '../utils.js';
+
+	type $$Props = CreateRadioGroupProps & {
+		items?: Item[];
+	};
+
+	type Item = {
+		value: string;
+		disabled: boolean;
+	};
+
+	export let value: CreateRadioGroupProps['value'] = undefined;
+	export let defaultValue: CreateRadioGroupProps['defaultValue'] = undefined;
+	export let disabled: CreateRadioGroupProps['disabled'] = undefined;
+	export let onValueChange: CreateRadioGroupProps['onValueChange'] = undefined;
+	export let required: CreateRadioGroupProps['required'] = undefined;
+	export let items: Item[] = [
+		{ value: 'a', disabled: false },
+		{ value: 'b', disabled: false },
+		{ value: 'c', disabled: false },
+		{ value: 'd', disabled: false },
+	];
+
+	const {
+		elements: { root, item },
+		states: { value: localValue },
+	} = createRadioGroup({
+		value,
+		defaultValue,
+		onValueChange,
+		disabled,
+		required,
+		...removeUndefined($$restProps),
+	});
+</script>
+
+<main>
+	<div data-testid="value">{$localValue}</div>
+	<form>
+		<label id="radio-group-label" for="radio-group" data-testid="label"> Airplane mode </label>
+		<div use:melt={$root} data-testid="root">
+			{#each items as radioItem}
+				<button use:melt={$item(radioItem)} data-testid={radioItem.value}>{radioItem.value}</button>
+			{/each}
+		</div>
+	</form>
+</main>