Repository: melt-ui/melt-ui
Pull request #411 (2023-08-18 22:03:12)

[Menubar] Hotfix: menubar race conditions


Patch:
diff --git a/.changeset/poor-oranges-joke.md b/.changeset/poor-oranges-joke.md
new file mode 100644
index 00000000..ec132089
--- /dev/null
+++ b/.changeset/poor-oranges-joke.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+[Menubar] Hotfix: menubar race conditions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44bc706d..d8c96213 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,30 +1,5 @@
 # @melt-ui/svelte
 
-## 0.37.0
-
-### Minor Changes
-
-- 77a39a59: fix pin input controlled value
-- 908f68ef: Rename Hover Card to Link Preview
-
-### Patch Changes
-
-- 4d9212fc: Keep Select menu open on select when `multiple` prop is `true`
-- abcae57d: fix: context menu not closing when clicking on svg element
-- 698a22d6: fix combobox controlled reset
-
-## 0.36.0
-
-### Minor Changes
-
-- 886ea40b: improve event canceling; replace e.detail.cancel() with e.preventDefault()
-
-## 0.35.1
-
-### Patch Changes
-
-- 7ab57673: fix accordion defaultValue
-
 ## 0.35.0
 
 ### Minor Changes
diff --git a/README.md b/README.md
index 8348e51f..103b51c6 100644
--- a/README.md
+++ b/README.md
@@ -74,57 +74,61 @@ we'd love to have you there.
 
 ## Roadmap
 
-| Component name    | Status |
-| ----------------- | ------ |
-| Accordion         | ✅     |
-| Avatar            | ✅     |
-| Calendar          |        |
-| Checkbox          | ✅     |
-| Collapsible       | ✅     |
-| ComboBox          | ✅     |
-| Command Center    |        |
-| Context Menu      | ✅     |
-| Dialog            | ✅     |
-| Dropdown Menu     | ✅     |
-| Dropzone          |        |
-| Label             | ✅     |
-| Link Preview      | ✅     |
-| Menubar           | ✅     |
-| Navigation Menu   |        |
-| Pagination        | ✅     |
-| Pin Input         | ✅     |
-| Popover           | ✅     |
-| Progress          | ✅     |
-| Radio Group       | ✅     |
-| Scroll Area       |        |
-| Select            | ✅     |
-| Separator         | ✅     |
-| Slider            | ✅     |
-| Spin Button       |        |
-| Switch            | ✅     |
-| Table of Contents | ✅     |
-| Tabs              | ✅     |
-| Tags Input        | ✅     |
-| Toast             | ✅     |
-| Toggle            | ✅     |
-| Toggle Group      | ✅     |
-| Toolbar           | ✅     |
-| Tooltip           | ✅     |
-| Tree View         |        |
-| ???               |        |
+| Component name  | Status |
+| --------------- | ------ |
+| Accordion       | ✅     |
+| Avatar          | ✅     |
+| Calendar        |        |
+| Checkbox        | ✅     |
+| Collapsible     | ✅     |
+| ComboBox        | ✅     |
+| Command Menu    |        |
+| Context Menu    | ✅     |
+| Dialog          | ✅     |
+| Dropdown Menu   | ✅     |
+| Dropzone        |        |
+| Hover Card      | ✅     |
+| Label           | ✅     |
+| Menubar         | ✅     |
+| Navigation Menu |        |
+| Pagination      | ✅     |
+| Pin Input       | ✅     |
+| Popover         | ✅     |
+| Progress        | ✅     |
+| Radio Group     | ✅     |
+| Scroll Area     |        |
+| Select          | ✅     |
+| Separator       | ✅     |
+| Slider          | ✅     |
+| Spin Button     |        |
+| Switch          | ✅     |
+| Tabs            | ✅     |
+| Tags Input      | ✅     |
+| Toast           | ✅     |
+| Toggle          | ✅     |
+| Toggle Group    | ✅     |
+| Toolbar         | ✅     |
+| Tooltip         | ✅     |
+| Tree View       |        |
+| ???             |        |
+
+**Progress:** 26/???
 
 ## Similar projects
 
 Looking for more? Check out these other projects too:
 
 - [Shadcn Svelte](https://shadcn-svelte.com/)
+- [Radix Svelte](https://radix-svelte.com/)
 - [Skeleton](https://skeleton.dev/)
 - [Svelte Headless UI](https://svelte-headlessui.goss.io/docs)
 - [Flowbite Svelte](https://flowbite-svelte.com/)
-- [Svelte Put](https://svelte-put.vnphanquang.com/)
 - [Carbon Components Svelte](https://github.com/carbon-design-system/carbon-components-svelte/)
 - [Svelte Material UI](https://sveltematerialui.com/)
 - [SvelteStrap](https://github.com/bestguy/sveltestrap)
+- [Noir UI](https://github.com/khairulhaaziq/noir-ui)
+- [Zag](https://github.com/chakra-ui/zag)
+- [Grail UI](https://grail-ui.vercel.app/)
 
 This list is not exhaustive or sorted in any particular order. If you know of any other similar
 projects for Svelte, feel free to open a PR to add them here.
diff --git a/package.json b/package.json
index b9a70a11..744d47bb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@melt-ui/svelte",
-	"version": "0.37.0",
+	"version": "0.35.0",
 	"license": "MIT",
 	"scripts": {
 		"dev": "vite dev",
diff --git a/src/docs/components/tables/custom-events-table.svelte b/src/docs/components/tables/custom-events-table.svelte
index 423a5567..ec042ffd 100644
--- a/src/docs/components/tables/custom-events-table.svelte
+++ b/src/docs/components/tables/custom-events-table.svelte
@@ -12,7 +12,7 @@
 		<svelte:fragment slot="info">
 			Custom events are dispatched with each normal event that we handle. You can override the way
 			we handle these events by calling
-			<code class="neutral">e.preventDefault()</code> in your event listener.
+			<code class="neutral">e.detail.cancel()</code> in your event listener.
 		</svelte:fragment>
 	</APITableHeading>
 
diff --git a/src/docs/content/builders/context-menu.md b/src/docs/content/builders/context-menu.md
index 8094e58b..9e1e0fd8 100644
--- a/src/docs/content/builders/context-menu.md
+++ b/src/docs/content/builders/context-menu.md
@@ -71,14 +71,14 @@ event for items).
 ```
 
 If you wanted to prevent the default behavior that occurs when you select an item, you can call
-`e.preventDefault()` in your `on:m-click` listener, which will prevent the default behavior from
+`e.detail.cancel()` in your `on:m-click` listener, which will prevent the default behavior from
 occurring.
 
 ```svelte {4}
 <div
 	use:melt={$item}
 	on:m-click={(e) => {
-		e.preventDefault()
+		e.detail.cancel()
 	}}>
 	Item 2
 </div>
diff --git a/src/docs/content/builders/dropdown-menu.md b/src/docs/content/builders/dropdown-menu.md
index 1365443b..37d92706 100644
--- a/src/docs/content/builders/dropdown-menu.md
+++ b/src/docs/content/builders/dropdown-menu.md
@@ -71,14 +71,14 @@ event for items).
 ```
 
 If you wanted to prevent the default behavior that occurs when you select an item, you can call
-`e.preventDefault()` in your `on:m-click` listener, which will prevent the default behavior from
+`e.detail.cancel()` in your `on:m-click` listener, which will prevent the default behavior from
 occurring.
 
 ```svelte {4}
 <div
 	use:melt={$item}
 	on:m-click={(e) => {
-		e.preventDefault()
+		e.detail.cancel()
 	}}>
 	Item 2
 </div>
diff --git a/src/docs/content/builders/hover-card.md b/src/docs/content/builders/hover-card.md
new file mode 100644
index 00000000..6349576f
--- /dev/null
+++ b/src/docs/content/builders/hover-card.md
@@ -0,0 +1,66 @@
+---
+title: Hover Card
+description: Enable sighted users to preview content behind a link.
+---
+
+<script>
+    import { KbdTable, APIReference, Callout } from '$docs/components'
+    export let schemas
+</script>
+
+<Callout type="warning">
+
+⚠️ The Hover Card is only intended to be used with a mouse or other pointing device and doesn't
+respond to keyboard or touch events. On touch devices, the link will be followed immediately.
+
+</Callout>
+
+## Anatomy
+
+- **Trigger**: The element that opens the hovercard on hover.
+- **Content**: The element containing the content for the hovercard.
+- **Arrow**: An optional arrow component
+
+## Usage
+
+Create a hovercard using the `createHoverCard` builder function.
+
+```svelte {3-5}
+<script lang="ts">
+	import { createHoverCard, melt } from '@melt-ui/svelte'
+	const {
+		elements: { trigger, content, arrow }
+	} = createHoverCard()
+</script>
+```
+
+Then you can use the `trigger`, `content`, and `arrow` to construct a hovercard. A high level
+example of how to structure the hovercard is shown below.
+
+```svelte
+<script lang="ts">
+	import { createHoverCard, melt } from '@melt-ui/svelte'
+	const {
+		elements: { trigger, content, arrow }
+	} = createHoverCard()
+</script>
+
+<button use:melt={$trigger}>Hover Me</button>
+
+<div use:melt={$content}>
+	<div>I am content inside the hover card</div>
+	<div use:melt={$arrow} />
+</div>
+```
+
+It's also possible to use Svelte Transitions, as demonstrated in the example at the top of this
+page.
+
+## API Reference
+
+<APIReference {schemas} />
+
+## Accessibility
+
+The hover card is only intended to be used with a mouse or other pointing device and doesn't respond
+to keyboard events.
diff --git a/src/docs/content/builders/link-preview.md b/src/docs/content/builders/link-preview.md
deleted file mode 100644
index ea3c325d..00000000
--- a/src/docs/content/builders/link-preview.md
+++ /dev/null
@@ -1,90 +0,0 @@
----
-title: Link Preview
-description: Enable sighted users to preview content behind a link.
----
-
-<script>
-    import { KbdTable, APIReference, Callout } from '$docs/components'
-    export let schemas
-</script>
-
-<Callout type="warning">
-
-⚠️ The Link Preview is only intended to be used with a mouse or other pointing device. It doesn't
-respond to touch events, and the preview content cannot be accessed via the keyboard. On touch
-devices, the link will be followed immediately. As it is not accessible to all users, the preview
-should not contain vital information.
-
-</Callout>
-
-## Anatomy
-
-- **Trigger**: The element that opens the link preview on hover.
-- **Content**: The element containing the content for the link preview.
-- **Arrow**: An optional arrow component
-
-## Usage
-
-Create a link preview using the `createLinkPreview` builder function.
-
-```svelte {3-5}
-<script lang="ts">
-	import { createLinkPreview, melt } from '@melt-ui/svelte'
-	const {
-		elements: { trigger, content, arrow }
-	} = createLinkPreview()
-</script>
-```
-
-Then you can use the `trigger`, `content`, and `arrow` to construct a link preview. A high level
-example of how to structure the link preview is shown below.
-
-```svelte
-<script lang="ts">
-	import { createLinkPreview, melt } from '@melt-ui/svelte'
-	const {
-		elements: { trigger, content, arrow }
-	} = createLinkPreview()
-</script>
-
-<button use:melt={$trigger}>Hover Me</button>
-
-<div use:melt={$content}>
-	<div>I am content inside the link preview</div>
-	<div use:melt={$arrow} />
-</div>
-```
-
-It's also possible to use Svelte Transitions, as demonstrated in the example at the top of this
-page.
-
-### Focus handling
-
-By default the link preview will be shown when focused by the keyboard (or more specifically if
-`:focus-visible` would apply), and hide on blur.
-
-You can disable this functionality by listening to and cancelling the custom `m-focus` and `m-blur`
-events.
-
-```svelte {3-8}
-<button
-	use:melt={$trigger}
-	on:m-focus={(e) => {
-		e.preventDefault()
-	}}
-	on:m-blur={(e) => {
-		e.preventDefault()
-	}}>
-	Hover Me
-</button>
-```
-
-## API Reference
-
-<APIReference {schemas} />
-
-## Accessibility
-
-The link preview is only intended to be used with a mouse or other pointing device and the preview
-content cannot be accessed via the keyboard and is not exposed to screen readers. As a result the
-preview should contain content that is otherwise not visible elswhere.
diff --git a/src/docs/content/controlled.md b/src/docs/content/controlled.md
index 8d78eab0..829c461c 100644
--- a/src/docs/content/controlled.md
+++ b/src/docs/content/controlled.md
@@ -150,14 +150,17 @@ const customMeltEvent: MeltEvent<typeof originalEvent> = new CustomEvent(
 	`m-${originalEvent.type}`,
 	{
 		detail: {
+			cancel: () => {
+				originalEvent.preventDefault()
+			},
 			originalEvent
-		},
-		cancelable: true
+		}
 	}
 )
 ```
 
-By marking the custom event as `cancelable`, you may use `preventDefault` on it, which signals Melt to immediately stop handling the event, thus preventing the builder's default behavior from happening.
+The `cancel` function calls `preventDefault` on the original event. When this function is invoked in
+, Melt immediately stops handling the event, thus preventing the default behavior from happening.
 
 We also pass the original event as a property on the `detail` object so that you can access it in
 case you'd like to do something else with it.
@@ -169,7 +172,7 @@ then you could do something like this:
 <button
 	use:melt={$trigger}
 	on:m-click={(e) => {
-		e.preventDefault();
+		e.detail.cancel()
 		// do something else
 	}}>
 	Trigger
diff --git a/src/docs/data/builders/link-preview.ts b/src/docs/data/builders/hover-card.ts
similarity index 63%
rename from src/docs/data/builders/link-preview.ts
rename to src/docs/data/builders/hover-card.ts
index 7a15b010..b38744d1 100644
--- a/src/docs/data/builders/link-preview.ts
+++ b/src/docs/data/builders/hover-card.ts
@@ -1,6 +1,6 @@
 import { ATTRS, PROPS } from '$docs/constants.js';
 import { builderSchema, elementSchema } from '$docs/utils/index.js';
-import { linkPreviewEvents } from '$lib/builders/link-preview/events.js';
+import { hoverCardEvents } from '$lib/builders/hover-card/events.js';
 import type { BuilderData } from './index.js';
 import { getMenuArrowSchema } from './menu.js';
 
@@ -12,13 +12,13 @@ const OPTION_PROPS = [
 		name: 'openDelay',
 		type: 'number',
 		default: '700',
-		description: 'The delay in milliseconds before the link preview opens.',
+		description: 'The delay in milliseconds before the hover card opens.',
 	},
 	{
 		name: 'closeDelay',
 		type: 'number',
 		default: '300',
-		description: 'The delay in milliseconds before the link preview closes.',
+		description: 'The delay in milliseconds before the hover card closes.',
 	},
 	PROPS.POSITIONING({ default: "placement: 'bottom'" }),
 	PROPS.ARROW_SIZE,
@@ -28,63 +28,63 @@ const OPTION_PROPS = [
 	PROPS.PORTAL,
 ];
 
-const BUILDER_NAME = 'link preview';
+const BUILDER_NAME = 'hover card';
 
 const builder = builderSchema(BUILDER_NAME, {
-	title: 'createLinkPreview',
+	title: 'createHoverCard',
 	props: [...OPTION_PROPS, PROPS.DEFAULT_OPEN, PROPS.OPEN, PROPS.ON_OPEN_CHANGE],
 	elements: [
 		{
 			name: 'trigger',
-			description: 'The builder store used to create the link preview trigger.',
+			description: 'The builder store used to create the hover card trigger.',
 		},
 		{
 			name: 'content',
-			description: 'The builder store used to create the link preview content.',
+			description: 'The builder store used to create the hover card content.',
 		},
 		{
 			name: 'arrow',
-			description: 'The builder store used to create the link preview arrow.',
+			description: 'The builder store used to create the hover card arrow.',
 		},
 	],
 	states: [
 		{
 			name: 'open',
 			type: 'Writable<boolean>',
-			description: 'A writable store with the open state of the link preview.',
+			description: 'A writable store with the open state of the hover card.',
 		},
 	],
 	options: OPTION_PROPS,
 });
 
 const trigger = elementSchema('trigger', {
-	description: 'The link preview trigger element.',
+	description: 'The hover card trigger element.',
 	dataAttributes: [
 		{
 			name: 'data-state',
 			value: ATTRS.OPEN_CLOSED,
 		},
 		{
-			name: 'data-melt-link-preview-trigger',
+			name: 'data-melt-hover-card-trigger',
 			value: ATTRS.MELT('trigger'),
 		},
 	],
-	events: linkPreviewEvents['trigger'],
+	events: hoverCardEvents['trigger'],
 });
 
 const content = elementSchema('content', {
-	description: 'The content displayed in the linkpreview',
+	description: 'The content displayed in the hovercard',
 	dataAttributes: [
 		{
 			name: 'data-state',
 			value: ATTRS.OPEN_CLOSED,
 		},
 		{
-			name: 'data-melt-link-preview-content',
+			name: 'data-melt-hover-card-content',
 			value: ATTRS.MELT('content'),
 		},
 	],
-	events: linkPreviewEvents['content'],
+	events: hoverCardEvents['content'],
 });
 
 const arrow = getMenuArrowSchema(BUILDER_NAME);
@@ -98,7 +98,7 @@ const features = [
 	'Positioning and alignment customization',
 ];
 
-export const linkPreviewData: BuilderData = {
+export const hoverCardData: BuilderData = {
 	schemas,
 	features,
 };
diff --git a/src/docs/data/builders/index.ts b/src/docs/data/builders/index.ts
index 3411a0c1..37a4dd57 100644
--- a/src/docs/data/builders/index.ts
+++ b/src/docs/data/builders/index.ts
@@ -7,8 +7,8 @@ import { comboboxData } from './combobox.js';
 import { contextMenuData } from './context-menu.js';
 import { dialogData } from './dialog.js';
 import { dropdownMenuData } from './dropdown-menu.js';
+import { hoverCardData } from './hover-card.js';
 import { labelData } from './label.js';
-import { linkPreviewData } from './link-preview.js';
 import { menubarData } from './menubar.js';
 import { paginationData } from './pagination.js';
 import { pinInputData } from './pin-input.js';
@@ -37,8 +37,8 @@ export const builderList = [
 	'context-menu',
 	'dialog',
 	'dropdown-menu',
+	'hover-card',
 	'label',
-	'link-preview',
 	'menubar',
 	'pagination',
 	'pin-input',
@@ -82,8 +82,8 @@ export const data: Builders = {
 	'context-menu': contextMenuData,
 	dialog: dialogData,
 	'dropdown-menu': dropdownMenuData,
+	'hover-card': hoverCardData,
 	label: labelData,
-	'link-preview': linkPreviewData,
 	menubar: menubarData,
 	pagination: paginationData,
 	'pin-input': pinInputData,
diff --git a/src/docs/previews/combobox/main/tailwind/index.svelte b/src/docs/previews/combobox/main/tailwind/index.svelte
index 181e79f8..64192a23 100644
--- a/src/docs/previews/combobox/main/tailwind/index.svelte
+++ b/src/docs/previews/combobox/main/tailwind/index.svelte
@@ -130,9 +130,9 @@
 						label: book.title,
 						disabled: book.disabled,
 					})}
-					class="relative cursor-pointer scroll-my-2 rounded-md py-1 pl-8 pr-4 text-neutral-800
-				data-[highlighted]:bg-magnum-100 data-[highlighted]:text-magnum-700
-					data-[disabled]:opacity-50"
+					class="relative cursor-pointer rounded-md py-1 pl-8 pr-4 text-neutral-800
+                        data-[highlighted]:bg-magnum-100 data-[highlighted]:text-magnum-700
+                        data-[disabled]:opacity-50"
 				>
 					{#if $isSelected(book)}
 						<div class="check">
diff --git a/src/docs/previews/link-preview/main/tailwind/index.svelte b/src/docs/previews/hover-card/main/tailwind/index.svelte
similarity index 95%
rename from src/docs/previews/link-preview/main/tailwind/index.svelte
rename to src/docs/previews/hover-card/main/tailwind/index.svelte
index 6846274a..ed10cb8d 100644
--- a/src/docs/previews/link-preview/main/tailwind/index.svelte
+++ b/src/docs/previews/hover-card/main/tailwind/index.svelte
@@ -1,11 +1,11 @@
 <script lang="ts">
-	import { createLinkPreview, melt } from '$lib/index.js';
+	import { createHoverCard, melt } from '$lib/index.js';
 	import { fly } from 'svelte/transition';
 
 	const {
 		elements: { trigger, content, arrow },
 		states: { open },
-	} = createLinkPreview({
+	} = createHoverCard({
 		forceVisible: true,
 	});
 </script>
diff --git a/src/docs/previews/pin-input/main/tailwind/index.svelte b/src/docs/previews/pin-input/main/tailwind/index.svelte
index 09df4b73..53c25c9a 100644
--- a/src/docs/previews/pin-input/main/tailwind/index.svelte
+++ b/src/docs/previews/pin-input/main/tailwind/index.svelte
@@ -10,7 +10,7 @@
 	{#each Array.from({ length: 5 }) as _, i}
 		<input
 			class="rounded-md bg-white text-center text-lg text-magnum-900 shadow-sm square-12"
-			use:melt={$input()}
+			use:melt={$input}
 		/>
 	{/each}
 </div>
diff --git a/src/docs/previews/select/keyboard/tailwind/index.svelte b/src/docs/previews/select/keyboard/tailwind/index.svelte
index c27e8d63..6396994d 100644
--- a/src/docs/previews/select/keyboard/tailwind/index.svelte
+++ b/src/docs/previews/select/keyboard/tailwind/index.svelte
@@ -30,22 +30,19 @@
 	py-2 text-magnum-700 transition-opacity hover:opacity-90"
 		use:melt={$trigger}
 		aria-label="Food"
-		on:m-keydown={(e) => {
-			e.preventDefault(); // Cancel default builder behabiour
-			e.detail.originalEvent.preventDefault(); // Cancel page scroll
+		on:m-keydown={({ detail: { cancel, originalEvent: e } }) => {
+			cancel();
 
-			const { key } = e.detail.originalEvent;
-
-			if (!['ArrowDown', 'ArrowUp', 'Space', 'Enter'].includes(key)) return;
+			if (!['ArrowDown', 'ArrowUp', 'Space', 'Enter'].includes(e.key)) return;
 
 			const allOptions = Object.values(options).flat();
 			const index = allOptions.indexOf(`${$valueLabel}`);
 
-			if (key === 'ArrowDown') {
+			if (e.key === 'ArrowDown') {
 				const nextIndex = index + 1;
 				const nextOption = allOptions[nextIndex] || allOptions[0];
 				value.set(nextOption);
-			} else if (key === 'ArrowUp') {
+			} else if (e.key === 'ArrowUp') {
 				const prevIndex = index - 1;
 				const prevOption =
 					allOptions[prevIndex] || allOptions[allOptions.length - 1];
diff --git a/src/docs/utils/preview.ts b/src/docs/utils/preview.ts
index de604dc0..766df931 100644
--- a/src/docs/utils/preview.ts
+++ b/src/docs/utils/preview.ts
@@ -140,10 +140,12 @@ type GetAllPreviewSnippetsArgs = {
 	fetcher?: typeof fetch;
 };
 
+const replaceLibEntriesRegex = /import (.*) from ["|'](?:\$lib.*)["|']/;
+
 function replaceLibEntries(code: string) {
 	// avoid executing the regex if it doesn't have $lib in the code for performance
 	if (!code.includes('$lib/index.js')) return code;
-	return code.replace(/\$lib\/index\.js/g, '@melt-ui/svelte');
+	return code.replace(replaceLibEntriesRegex, "import $1 from '@melt-ui/svelte'");
 }
 
 export async function getAllPreviewSnippets({ slug, fetcher }: GetAllPreviewSnippetsArgs) {
diff --git a/src/lib/builders/accordion/create.ts b/src/lib/builders/accordion/create.ts
index ae966cb8..34496f4f 100644
--- a/src/lib/builders/accordion/create.ts
+++ b/src/lib/builders/accordion/create.ts
@@ -35,8 +35,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 	const options = toWritableStores(omit(withDefaults, 'value', 'onValueChange', 'defaultValue'));
 	const { disabled, forceVisible } = options;
 
-	const valueWritable = withDefaults.value ?? writable(withDefaults.defaultValue);
-
+	const valueWritable = withDefaults.value ?? writable(withDefaults.value);
 	const value = overridable<string | string[] | undefined>(
 		valueWritable,
 		withDefaults?.onValueChange as ChangeFn<string | string[] | undefined>
diff --git a/src/lib/builders/combobox/create.ts b/src/lib/builders/combobox/create.ts
index 2f982573..30043246 100644
--- a/src/lib/builders/combobox/create.ts
+++ b/src/lib/builders/combobox/create.ts
@@ -627,11 +627,7 @@ export function createCombobox<ItemValue>(props?: CreateComboboxProps<ItemValue>
 	});
 
 	effect(value, function setInputValue($value) {
-		if (!$value) {
-			inputValue.set('');
-			return;
-		}
-
+		if (!$value) return;
 		tick().then(() => {
 			inputValue.set(getSelectedLabel() ?? '');
 		});
diff --git a/src/lib/builders/context-menu/create.ts b/src/lib/builders/context-menu/create.ts
index 17f1e99b..4a2f7748 100644
--- a/src/lib/builders/context-menu/create.ts
+++ b/src/lib/builders/context-menu/create.ts
@@ -108,7 +108,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 		if (e.defaultPrevented) return;
 
 		const target = e.target;
-		if (!(target instanceof Element)) return;
+		if (!isHTMLElement(target)) return;
 
 		if (target.id === rootIds.trigger && isLeftClick(e)) {
 			rootOpen.set(false);
diff --git a/src/lib/builders/link-preview/create.ts b/src/lib/builders/hover-card/create.ts
similarity index 90%
rename from src/lib/builders/link-preview/create.ts
rename to src/lib/builders/hover-card/create.ts
index dec94f7b..28c6e16b 100644
--- a/src/lib/builders/link-preview/create.ts
+++ b/src/lib/builders/hover-card/create.ts
@@ -10,8 +10,6 @@ import {
 	getPortalDestination,
 	getTabbableNodes,
 	isBrowser,
-	isElement,
-	isFocusVisible,
 	isHTMLElement,
 	isTouch,
 	noop,
@@ -23,11 +21,11 @@ import {
 import type { MeltActionReturn } from '$lib/internal/types.js';
 import { onMount, tick } from 'svelte';
 import { derived, get, writable, type Readable } from 'svelte/store';
-import type { LinkPreviewEvents } from './events.js';
-import type { CreateLinkPreviewProps } from './types.js';
+import type { HoverCardEvents } from './events.js';
+import type { CreateHoverCardProps } from './types.js';
 
-type LinkPreviewParts = 'trigger' | 'content' | 'arrow';
-const { name } = createElHelpers<LinkPreviewParts>('hover-card');
+type HoverCardParts = 'trigger' | 'content' | 'arrow';
+const { name } = createElHelpers<HoverCardParts>('hover-card');
 
 const defaults = {
 	defaultOpen: false,
@@ -41,10 +39,10 @@ const defaults = {
 	forceVisible: false,
 	portal: 'body',
 	closeOnEscape: true,
-} satisfies CreateLinkPreviewProps;
+} satisfies CreateHoverCardProps;
 
-export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
-	const withDefaults = { ...defaults, ...props } satisfies CreateLinkPreviewProps;
+export function createHoverCard(props: CreateHoverCardProps = {}) {
+	const withDefaults = { ...defaults, ...props } satisfies CreateHoverCardProps;
 
 	const openWritable = withDefaults.open ?? writable(withDefaults.defaultOpen);
 	const open = overridable(openWritable, withDefaults?.onOpenChange);
@@ -116,7 +114,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 				id: ids.trigger,
 			};
 		},
-		action: (node: HTMLElement): MeltActionReturn<LinkPreviewEvents['trigger']> => {
+		action: (node: HTMLElement): MeltActionReturn<HoverCardEvents['trigger']> => {
 			const unsub = executeCallbacks(
 				addMeltEventListener(node, 'pointerenter', (e) => {
 					if (isTouch(e)) return;
@@ -126,11 +124,16 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 					if (isTouch(e)) return;
 					get(handleClose)();
 				}),
-				addMeltEventListener(node, 'focus', (e) => {
-					if (!isElement(e.currentTarget) || !isFocusVisible(e.currentTarget)) return;
-					get(handleOpen)();
-				}),
-				addMeltEventListener(node, 'blur', () => get(handleClose)())
+				addMeltEventListener(node, 'focus', () => get(handleOpen)()),
+
+				addMeltEventListener(node, 'blur', () => get(handleClose)()),
+				addMeltEventListener(node, 'touchstart', (e) => {
+					// prevent focus on touch devices
+					e.preventDefault();
+					const currentTarget = e.currentTarget;
+					if (!isHTMLElement(currentTarget)) return;
+					currentTarget.click();
+				})
 			);
 
 			return {
@@ -157,7 +160,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 				'data-portal': $portal ? '' : undefined,
 			};
 		},
-		action: (node: HTMLElement): MeltActionReturn<LinkPreviewEvents['content']> => {
+		action: (node: HTMLElement): MeltActionReturn<HoverCardEvents['content']> => {
 			let unsub = noop;
 
 			const unsubTimers = () => {
diff --git a/src/lib/builders/hover-card/events.ts b/src/lib/builders/hover-card/events.ts
new file mode 100644
index 00000000..ee671938
--- /dev/null
+++ b/src/lib/builders/hover-card/events.ts
@@ -0,0 +1,9 @@
+import type { GroupedEvents, MeltComponentEvents } from '$lib/internal/types.js';
+
+export const hoverCardEvents = {
+	trigger: ['pointerenter', 'pointerleave', 'focus', 'blur', 'touchstart'] as const,
+	content: ['pointerdown', 'pointerenter', 'pointerleave', 'focusout'] as const,
+};
+
+export type HoverCardEvents = GroupedEvents<typeof hoverCardEvents>;
+export type HoverCardComponentEvents = MeltComponentEvents<HoverCardEvents>;
diff --git a/src/lib/builders/link-preview/index.ts b/src/lib/builders/hover-card/index.ts
similarity index 100%
rename from src/lib/builders/link-preview/index.ts
rename to src/lib/builders/hover-card/index.ts
diff --git a/src/lib/builders/link-preview/types.ts b/src/lib/builders/hover-card/types.ts
similarity index 67%
rename from src/lib/builders/link-preview/types.ts
rename to src/lib/builders/hover-card/types.ts
index 789c7de3..6cb79f0e 100644
--- a/src/lib/builders/link-preview/types.ts
+++ b/src/lib/builders/hover-card/types.ts
@@ -1,10 +1,10 @@
 import type { FloatingConfig } from '$lib/internal/actions/index.js';
 import type { BuilderReturn } from '$lib/internal/types.js';
 import type { Writable } from 'svelte/store';
-import type { createLinkPreview } from './create.js';
+import type { createHoverCard } from './create.js';
 import type { ChangeFn } from '$lib/internal/helpers/index.js';
-export type { LinkPreviewComponentEvents } from './events.js';
-export type CreateLinkPreviewProps = {
+export type { HoverCardComponentEvents } from './events.js';
+export type CreateHoverCardProps = {
 	/**
 	 * Options for positioning the popover menu.
 	 *
@@ -13,14 +13,14 @@ export type CreateLinkPreviewProps = {
 	positioning?: FloatingConfig;
 
 	/**
-	 * Whether or not the linkpreview is open by default.
+	 * Whether or not the hovercard is open by default.
 	 *
 	 * @default false
 	 */
 	defaultOpen?: boolean;
 
 	/**
-	 * A controlled open state store for the linkpreview. If provided, the
+	 * A controlled open state store for the hovercard. If provided, the
 	 * value of this store will override the `defaultOpen` prop.
 	 *
 	 * @see https://melt-ui.com/docs/controlled#bring-your-own-store
@@ -35,7 +35,7 @@ export type CreateLinkPreviewProps = {
 	onOpenChange?: ChangeFn<boolean>;
 
 	/**
-	 * The delay in milliseconds to hover before opening the linkpreview
+	 * The delay in milliseconds to hover before opening the hovercard
 	 *
 	 * @default 700
 	 */
@@ -43,14 +43,14 @@ export type CreateLinkPreviewProps = {
 
 	/**
 	 * The delay in milliseconds after the pointer leaves the
-	 * linkpreview before closing it.
+	 * hovercard before closing it.
 	 *
 	 * @default 300
 	 */
 	closeDelay?: number;
 
 	/**
-	 * Whether or not to close the linkpreview when the pointer is clicked
+	 * Whether or not to close the hovercard when the pointer is clicked
 	 * outside of it.
 	 *
 	 * @default true
@@ -58,7 +58,7 @@ export type CreateLinkPreviewProps = {
 	closeOnOutsideClick?: boolean;
 
 	/**
-	 * Whether or not to close the linkpreview when the escape key is pressed
+	 * Whether or not to close the hovercard when the escape key is pressed
 	 * while it is open.
 	 *
 	 * @default true
@@ -90,7 +90,7 @@ export type CreateLinkPreviewProps = {
 	portal?: HTMLElement | string | null;
 };
 
-export type LinkPreview = BuilderReturn<typeof createLinkPreview>;
-export type LinkPreviewElements = LinkPreview['elements'];
-export type LinkPreviewOptions = LinkPreview['options'];
-export type LinkPreviewStates = LinkPreview['states'];
+export type HoverCard = BuilderReturn<typeof createHoverCard>;
+export type HoverCardElements = HoverCard['elements'];
+export type HoverCardOptions = HoverCard['options'];
+export type HoverCardStates = HoverCard['states'];
diff --git a/src/lib/builders/index.ts b/src/lib/builders/index.ts
index 0c392ce2..2712f460 100644
--- a/src/lib/builders/index.ts
+++ b/src/lib/builders/index.ts
@@ -6,7 +6,7 @@ export * from './combobox/index.js';
 export * from './context-menu/index.js';
 export * from './dialog/index.js';
 export * from './dropdown-menu/index.js';
-export * from './link-preview/index.js';
+export * from './hover-card/index.js';
 export * from './label/index.js';
 export * from './menubar/index.js';
 export * from './pagination/index.js';
diff --git a/src/lib/builders/link-preview/events.ts b/src/lib/builders/link-preview/events.ts
deleted file mode 100644
index cb636d21..00000000
--- a/src/lib/builders/link-preview/events.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { GroupedEvents, MeltComponentEvents } from '$lib/internal/types.js';
-
-export const linkPreviewEvents = {
-	trigger: ['pointerenter', 'pointerleave', 'focus', 'blur'] as const,
-	content: ['pointerdown', 'pointerenter', 'pointerleave', 'focusout'] as const,
-};
-
-export type LinkPreviewEvents = GroupedEvents<typeof linkPreviewEvents>;
-export type LinkPreviewComponentEvents = MeltComponentEvents<LinkPreviewEvents>;
diff --git a/src/lib/builders/menubar/create.ts b/src/lib/builders/menubar/create.ts
index 72a30bde..d3072b2f 100644
--- a/src/lib/builders/menubar/create.ts
+++ b/src/lib/builders/menubar/create.ts
@@ -250,9 +250,11 @@ export function createMenubar(props?: CreateMenubarProps) {
 					menubarEl.querySelectorAll('[data-melt-menubar-trigger]')
 				);
 
-				scopedMenus = Array.from(menubarEl.querySelectorAll('[data-melt-menubar-menu]')).filter(
-					(el): el is HTMLElement => isHTMLElement(el)
-				);
+				tick().then(() => {
+					scopedMenus = Array.from(menubarEl.querySelectorAll('[data-melt-menubar-menu]')).filter(
+						(el): el is HTMLElement => isHTMLElement(el)
+					);
+				});
 
 				if (!menubarTriggers.length) return {};
 
diff --git a/src/lib/builders/pin-input/create.ts b/src/lib/builders/pin-input/create.ts
index f328a611..bc0c5640 100644
--- a/src/lib/builders/pin-input/create.ts
+++ b/src/lib/builders/pin-input/create.ts
@@ -12,9 +12,6 @@ import {
 	styleToString,
 	toWritableStores,
 	addMeltEventListener,
-	effect,
-	generateId,
-	isBrowser,
 } from '$lib/internal/helpers/index.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
 import { tick } from 'svelte';
@@ -56,47 +53,23 @@ export function createPinInput(props?: CreatePinInputProps) {
 	const value = overridable(valueWritable, withDefaults?.onValueChange);
 	const valueStr = derived(value, (v) => v.join(''));
 
-	const ids = {
-		root: generateId(),
-	};
-
 	const root = builder(name(), {
 		stores: value,
 		returned: ($value) => {
 			return {
-				id: ids.root,
 				'data-complete': $value.length && $value.every((v) => v.length > 0) ? '' : undefined,
 			};
 		},
 	});
 
-	let index = 0;
-
-	const getTotalItems = () => {
-		if (!isBrowser) return Infinity;
-		const rootEl = document.getElementById(ids.root);
-		if (!rootEl) return Infinity;
-
-		const inputs = Array.from(rootEl.querySelectorAll(selector('input')));
-		return inputs.length;
-	};
-
 	const input = builder(name('input'), {
 		stores: [value, placeholder, disabled, type],
 		returned: ([$value, $placeholder, $disabled, $type]) => {
-			return () => {
-				const totalItems = getTotalItems();
-				const currIndex = index % totalItems;
-				index = (index + 1) % totalItems;
-				const currValue = $value[currIndex] ?? '';
-
-				return {
-					'data-complete': $value.length && $value.every((v) => v.length > 0) ? '' : undefined,
-					placeholder: $placeholder,
-					disabled: $disabled,
-					type: $type,
-					value: currValue,
-				};
+			return {
+				'data-complete': $value.length && $value.every((v) => v.length > 0) ? '' : undefined,
+				placeholder: $placeholder,
+				disabled: $disabled,
+				type: $type,
 			};
 		},
 		action: (node: HTMLInputElement): MeltActionReturn<PinInputEvents['input']> => {
diff --git a/src/lib/builders/select/create.ts b/src/lib/builders/select/create.ts
index de882117..77bda369 100644
--- a/src/lib/builders/select/create.ts
+++ b/src/lib/builders/select/create.ts
@@ -514,8 +514,7 @@ export function createSelect<
 					handleRovingFocus(itemElement);
 
 					setValue(props.value);
-					const $multiple = get(multiple);
-					if (!$multiple) open.set(false);
+					open.set(false);
 				}),
 
 				addMeltEventListener(node, 'keydown', (e) => {
@@ -531,8 +530,7 @@ export function createSelect<
 						node.setAttribute('data-selected', '');
 
 						setValue(props.value);
-						const $multiple = get(multiple);
-						if (!$multiple) open.set(false);
+						open.set(false);
 					}
 				}),
 				addMeltEventListener(node, 'pointermove', (e) => {
diff --git a/src/lib/internal/helpers/event.ts b/src/lib/internal/helpers/event.ts
index b651ca4a..d4410e74 100644
--- a/src/lib/internal/helpers/event.ts
+++ b/src/lib/internal/helpers/event.ts
@@ -108,29 +108,29 @@ export function dispatchMeltEvent<E extends keyof HTMLElementEventMap>(
 	originalEvent: HTMLElementEventMap[E]
 ) {
 	const node = originalEvent.currentTarget;
-	if (!isHTMLElement(node)) return null;
+	if (!isHTMLElement(node)) return;
 
 	const customMeltEvent: MeltEvent<typeof originalEvent> = new CustomEvent(
 		`m-${originalEvent.type}`,
 		{
 			detail: {
+				cancel: () => {
+					originalEvent.preventDefault();
+				},
 				originalEvent,
 			},
-			cancelable: true,
 		}
 	);
 
 	node.dispatchEvent(customMeltEvent);
-	return customMeltEvent;
 }
 
 export function withMelt<E extends keyof HTMLElementEventMap>(
 	handler: (event: HTMLElementEventMap[E]) => void
 ) {
 	return (event: HTMLElementEventMap[E]) => {
-		const customEvent = dispatchMeltEvent(event);
-
-		if (customEvent?.defaultPrevented) return;
+		dispatchMeltEvent(event);
+		if (event.defaultPrevented) return;
 		return handler(event);
 	};
 }
diff --git a/src/lib/internal/helpers/is.ts b/src/lib/internal/helpers/is.ts
index 0ba61fc4..14aeea71 100644
--- a/src/lib/internal/helpers/is.ts
+++ b/src/lib/internal/helpers/is.ts
@@ -2,10 +2,6 @@ export const isBrowser = typeof document !== 'undefined';
 // eslint-disable-next-line @typescript-eslint/ban-types
 export const isFunction = (v: unknown): v is Function => typeof v === 'function';
 
-export function isElement(element: unknown): element is Element {
-	return element instanceof Element;
-}
-
 export function isHTMLElement(element: unknown): element is HTMLElement {
 	return element instanceof HTMLElement;
 }
@@ -33,7 +29,3 @@ export function isTouch(event: PointerEvent): boolean {
 export function isLeftClick(event: PointerEvent | MouseEvent): boolean {
 	return event.button === 0 && event.ctrlKey === false && event.metaKey === false;
 }
-
-export function isFocusVisible(element: Element): boolean {
-	return element.matches(':focus-visible');
-}
diff --git a/src/lib/internal/types.ts b/src/lib/internal/types.ts
index 44efa444..19623331 100644
--- a/src/lib/internal/types.ts
+++ b/src/lib/internal/types.ts
@@ -59,7 +59,7 @@ export type BuilderReturn<T extends (...args: any) => any> = {
 
 export type EventHandler<T extends Event = Event> = (event: T) => void;
 
-export type MeltEvent<E extends Event> = CustomEvent<{ originalEvent: E }>;
+export type MeltEvent<E extends Event> = CustomEvent<{ cancel: () => void; originalEvent: E }>;
 
 export type MeltEventHandler<E extends Event> = EventHandler<
 	Expand<Omit<MeltEvent<E>, 'initCustomEvent'>>


Test patch:
diff --git a/src/tests/link-preview/LinkPreview.spec.ts b/src/tests/hover-card/HoverCard.spec.ts
similarity index 84%
rename from src/tests/link-preview/LinkPreview.spec.ts
rename to src/tests/hover-card/HoverCard.spec.ts
index cbb6c746..c15d92de 100644
--- a/src/tests/link-preview/LinkPreview.spec.ts
+++ b/src/tests/hover-card/HoverCard.spec.ts
@@ -1,18 +1,18 @@
 import { render, waitFor } from '@testing-library/svelte';
 import { axe } from 'jest-axe';
-import LinkPreviewTest from './LinkPreviewTest.svelte';
+import HoverCardTest from './HoverCardTest.svelte';
 import userEvent from '@testing-library/user-event';
 import { kbd } from '$lib/internal/helpers/index.js';
 
-describe('LinkPreview (Default)', () => {
+describe('HoverCard (Default)', () => {
 	test('No accessibility violations', async () => {
-		const { container } = await render(LinkPreviewTest);
+		const { container } = await render(HoverCardTest);
 
 		expect(await axe(container)).toHaveNoViolations();
 	});
 
 	test('Opens on hover', async () => {
-		const { getByTestId } = await render(LinkPreviewTest);
+		const { getByTestId } = await render(HoverCardTest);
 		const user = userEvent.setup();
 		const trigger = getByTestId('trigger');
 		const content = getByTestId('content');
@@ -26,7 +26,7 @@ describe('LinkPreview (Default)', () => {
 	});
 
 	test('Closes on escape', async () => {
-		const { getByTestId } = await render(LinkPreviewTest);
+		const { getByTestId } = await render(HoverCardTest);
 		const user = userEvent.setup();
 		const trigger = getByTestId('trigger');
 		const content = getByTestId('content');
@@ -42,7 +42,7 @@ describe('LinkPreview (Default)', () => {
 	});
 
 	test('Closes when pointer moves outside', async () => {
-		const { getByTestId } = await render(LinkPreviewTest);
+		const { getByTestId } = await render(HoverCardTest);
 		const user = userEvent.setup();
 		const trigger = getByTestId('trigger');
 		const content = getByTestId('content');
diff --git a/src/tests/link-preview/LinkPreviewTest.svelte b/src/tests/hover-card/HoverCardTest.svelte
similarity index 95%
rename from src/tests/link-preview/LinkPreviewTest.svelte
rename to src/tests/hover-card/HoverCardTest.svelte
index 6341958a..1089d252 100644
--- a/src/tests/link-preview/LinkPreviewTest.svelte
+++ b/src/tests/hover-card/HoverCardTest.svelte
@@ -1,9 +1,9 @@
 <script lang="ts">
-	import { createLinkPreview, melt } from '$lib/index.js';
+	import { createHoverCard, melt } from '$lib/index.js';
 
 	const {
 		elements: { trigger, content, arrow },
-	} = createLinkPreview({
+	} = createHoverCard({
 		openDelay: 0,
 	});
 </script>
diff --git a/src/tests/popover/Popover.spec.ts b/src/tests/popover/Popover.spec.ts
index 8bd759af..d028168c 100644
--- a/src/tests/popover/Popover.spec.ts
+++ b/src/tests/popover/Popover.spec.ts
@@ -4,7 +4,7 @@ import PopoverTest from './PopoverTest.svelte';
 import userEvent from '@testing-library/user-event';
 import { kbd } from '$lib/internal/helpers/index.js';
 
-describe('Popover (Default)', () => {
+describe('HoverCard (Default)', () => {
 	test('No accessibility violations', async () => {
 		const { container } = await render(PopoverTest);
 
diff --git a/src/tests/select/Select.spec.ts b/src/tests/select/Select.spec.ts
index 854a6a82..b5f26013 100644
--- a/src/tests/select/Select.spec.ts
+++ b/src/tests/select/Select.spec.ts
@@ -126,9 +126,12 @@ describe('Select (Default)', () => {
 		await expect(firstItem).toHaveAttribute('data-selected');
 		await expect(firstItem).toHaveAttribute('aria-selected', 'true');
 
-		await expect(menu).toBeVisible();
+		await expect(menu).not.toBeVisible();
 		await expect(trigger).toHaveTextContent('Caramel');
 
+		await user.click(trigger);
+		await expect(menu).toBeVisible();
+
 		const secondItem = menu.querySelectorAll('[data-melt-select-option]')[1];
 		if (!secondItem) throw new Error('No option found');
 		await user.click(secondItem);
@@ -136,11 +139,8 @@ describe('Select (Default)', () => {
 		await expect(secondItem).toHaveAttribute('data-selected');
 		await expect(secondItem).toHaveAttribute('aria-selected', 'true');
 
-		await expect(menu).toBeVisible();
-		await expect(trigger).toHaveTextContent('Caramel, Chocolate');
-
-		await user.keyboard(`{${kbd.ESCAPE}}`);
 		await expect(menu).not.toBeVisible();
+		await expect(trigger).toHaveTextContent('Caramel, Chocolate');
 	});
 
 	test('Shows correct label when defaultValue is provided', async () => {