Repository: melt-ui/melt-ui
Pull request #590 (2023-10-04 06:25:44)

Fix bug with Tooltip `group` parameter
Tooltips with a `group` would prevent themselves from reopening again unless another tooltip in the same group was opened. This is due to the effect not cleaning up after the tooltip closes, so it stays in the "open tooltips map" forever.

This PR fixes the bug and adds a test to validate the fix.

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

Latest commit: c0519704817e3d2fb502f143a01c683c7d032d7d

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/abdel-17/melt-ui/new/fix-tooltip-group-bug?filename=.changeset/lemon-bottles-do.md&value=---%0A%22%40melt-ui%2Fsvelte%22%3A%20patch%0A---%0A%0AFix%20bug%20with%20Tooltip%20%60group%60%20parameter%0A)


vercel[bot]: [vc]: #R/mzHXM7PfnN74d7R+HkDDKQXt1eb9CVTSVDGorv03o=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJtZWx0LXVpLXN0b3J5Ym9vayIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9tZWx0LXVpL21lbHQtdWktc3Rvcnlib29rL0VMNTRlVGdTYWN0M1FiWjViR1pxcVhFbmFadmYiLCJwcmV2aWV3VXJsIjoibWVsdC11aS1zdG9yeWJvb2stZ2l0LWZvcmstYWJkZWwtMTctZml4LXRvb2x0aXAtMjQxYmU4LW1lbHQtdWkudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJQRU5ESU5HIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6Im1lbHQtdWktc3Rvcnlib29rLWdpdC1mb3JrLWFiZGVsLTE3LWZpeC10b29sdGlwLTI0MWJlOC1tZWx0LXVpLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **melt-ui-storybook** | 🔄 Building ([Inspect](https://vercel.com/melt-ui/melt-ui-storybook/EL54eTgSact3QbZ5bGZqqXEnaZvf)) | [Visit Preview](https://vercel.live/open-feedback/melt-ui-storybook-git-fork-abdel-17-fix-tooltip-241be8-melt-ui.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/melt-ui-storybook-git-fork-abdel-17-fix-tooltip-241be8-melt-ui.vercel.app?via=pr-comment-feedback-link) | Oct 4, 2023 6:25am |



Patch:
diff --git a/.changeset/fair-yaks-travel.md b/.changeset/fair-yaks-travel.md
new file mode 100644
index 00000000..a4ffa9c4
--- /dev/null
+++ b/.changeset/fair-yaks-travel.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+Tooltip: Fix bug with `group` parameter
diff --git a/.changeset/hungry-queens-hide.md b/.changeset/hungry-queens-hide.md
new file mode 100644
index 00000000..74ab9f7f
--- /dev/null
+++ b/.changeset/hungry-queens-hide.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": minor
+---
+
+- return an `ids` object from builders which contains the `id`s Melt applies to certain elements internally
diff --git a/README.md b/README.md
index 6eddb236..493f10d2 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,12 @@
 
 [Melt UI](https://www.melt-ui.com/) is a set of headless, accessible component builders for Svelte.
 
-[![](https://img.shields.io/npm/v/@melt-ui/svelte?style=flat-square)](https://www.npmjs.com/package/@melt-ui/svelte)
-[![](https://img.shields.io/github/actions/workflow/status/melt-ui/melt-ui/ci.yaml?style=flat-square)](https://github.com/melt-ui/melt-ui/actions/workflows/ci.yaml)
-![](https://img.shields.io/github/license/melt-ui/melt-ui?style=flat-square)
-[![](https://dcbadge.vercel.app/api/server/2QDjZkYunf?style=flat-square)](https://discord.gg/2QDjZkYunf)
+[![](https://img.shields.io/npm/v/@melt-ui/svelte?style=flat)](https://www.npmjs.com/package/@melt-ui/svelte)
+![npm](https://img.shields.io/npm/dw/%40melt-ui%2Fsvelte?style=flat&color=orange)
+
+[![](https://img.shields.io/github/actions/workflow/status/melt-ui/melt-ui/ci.yaml?style=flat)](https://github.com/melt-ui/melt-ui/actions/workflows/ci.yaml)
+![](https://img.shields.io/github/license/melt-ui/melt-ui?style=flat)
+[![](https://dcbadge.vercel.app/api/server/2QDjZkYunf?style=flat)](https://discord.gg/2QDjZkYunf)
 
 ## About
 
diff --git a/src/docs/content/builders/dialog.md b/src/docs/content/builders/dialog.md
index a6a5f64c..b7a64c98 100644
--- a/src/docs/content/builders/dialog.md
+++ b/src/docs/content/builders/dialog.md
@@ -20,7 +20,18 @@ At a high level, the anatomy of a dialog looks like this:
 ```svelte
 <script lang="ts">
 	import { createDialog, melt } from '@melt-ui/svelte'
-	const { trigger, portal, overlay, content, title, description, close, open } = createDialog()
+	const { 
+		elements: {
+			trigger, 
+			portalled, 
+			overlay, 
+			content, 
+			title, 
+			description, 
+			close, 
+			open,
+		},
+	} = createDialog();
 </script>
 
 <button use:melt={$trigger}> Open Dialog </button>
@@ -38,6 +49,7 @@ At a high level, the anatomy of a dialog looks like this:
 ```
 
 - **Trigger**: The button(s) that open the dialog
+- **Portalled**: The container that is portalled (to `body`, by default)
 - **Overlay**: The dim background that is typically behind a dialog element.
 - **Content**: Container for the content within the dialog.
   - **Title**: The title of the dialog
@@ -63,9 +75,9 @@ By default, we set the `role` attribute to `dialog`. If you want it to be consid
 dialog, you can set the `role` builder prop to `alertdialog`.
 
 ```ts {2}
-const { trigger, portal, overlay, content } = createDialog({
-	role: 'alertdialog'
-})
+const { /* ... */ } = createDialog({
+	role: 'alertdialog',
+});
 ```
 
 ### Disable Scroll Prevention
@@ -74,9 +86,9 @@ By default, scrolling is prevented on the body when a dialog is open. You can di
 by setting the `preventScroll` builder prop to `false`.
 
 ```ts {2}
-const { trigger, portal, overlay, content } = createDialog({
-	preventScroll: false
-})
+const { /* ... */ } = createDialog({
+	preventScroll: false,
+});
 ```
 
 ### Disable Close on Outside Click
@@ -85,9 +97,9 @@ By default, clicking outside of the dialog will close it. You can disable this b
 the `closeOnOutsideClick` builder prop to `false`.
 
 ```ts {2}
-const { trigger, portal, overlay, content } = createDialog({
-	closeOnOutsideClick: false
-})
+const { /* ... */ } = createDialog({
+	closeOnOutsideClick: false,
+});
 ```
 
 ### Disable Close on Escape
@@ -96,9 +108,9 @@ By default, pressing the escape key will close the dialog. You can disable this
 the `closeOnEscape` builder prop to `false`.
 
 ```ts {2}
-const { trigger, portal, overlay, content } = createDialog({
-	closeOnEscape: false
-})
+const { /* ... */ } = createDialog({
+	closeOnEscape: false,
+});
 ```
 
 ## Example Components
@@ -137,7 +149,7 @@ Dialogs can be nested. For example, here's a dialog that opens another dialog.
 
 ## Accessibility
 
-Adheres to the [Dialog WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog/) &
+Adheres to the [Dialog WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) &
 [Alert Dialog WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/)
 
 <KbdTable {keyboard} />
diff --git a/src/docs/data/builders/dialog.ts b/src/docs/data/builders/dialog.ts
index 2e42246b..f54a12b7 100644
--- a/src/docs/data/builders/dialog.ts
+++ b/src/docs/data/builders/dialog.ts
@@ -73,6 +73,20 @@ const trigger = elementSchema('trigger', {
 	events: dialogEvents['trigger'],
 });
 
+const portalled = elementSchema('portalled', {
+	description: "The element that will be portalled (or moved) to a different location in the DOM based on the `portal` prop value.",
+	dataAttributes: [
+		{
+			name: 'data-portal',
+			value: ATTRS.MELT('portalled'),
+		},
+		{
+			name: 'data-melt-dialog-portalled',
+			value: ATTRS.MELT('portalled'),
+		}
+	],
+});	
+
 const overlay = elementSchema('overlay', {
 	description: 'The overlay element which covers the page when the dialog is open.',
 	dataAttributes: [
@@ -155,7 +169,7 @@ const keyboard: KeyboardSchema = [
 	},
 ];
 
-const schemas = [builder, trigger, overlay, content, close, title, description];
+const schemas = [builder, trigger, portalled, overlay, content, close, title, description];
 const features = [
 	'Fully managed focus',
 	'Can be controlled or uncontrolled',
diff --git a/src/lib/builders/accordion/create.ts b/src/lib/builders/accordion/create.ts
index 3c7b34d1..3dd09ce9 100644
--- a/src/lib/builders/accordion/create.ts
+++ b/src/lib/builders/accordion/create.ts
@@ -233,6 +233,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 	}
 
 	return {
+		ids,
 		elements: {
 			root,
 			item,
diff --git a/src/lib/builders/combobox/create.ts b/src/lib/builders/combobox/create.ts
index ed0d501e..38dea244 100644
--- a/src/lib/builders/combobox/create.ts
+++ b/src/lib/builders/combobox/create.ts
@@ -654,6 +654,7 @@ export function createCombobox<
 	});
 
 	return {
+		ids,
 		elements: {
 			input,
 			option,
diff --git a/src/lib/builders/context-menu/create.ts b/src/lib/builders/context-menu/create.ts
index 5b0d4bfe..799d5bc1 100644
--- a/src/lib/builders/context-menu/create.ts
+++ b/src/lib/builders/context-menu/create.ts
@@ -73,7 +73,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 		arrow,
 		createSubmenu,
 		createMenuRadioGroup,
-		rootIds,
+		ids,
 		separator,
 		handleTypeaheadSearch,
 		group,
@@ -111,12 +111,12 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 		const target = e.target;
 		if (!(target instanceof Element)) return;
 
-		if (target.id === rootIds.trigger && isLeftClick(e)) {
+		if (target.id === ids.trigger && isLeftClick(e)) {
 			rootOpen.set(false);
 			return;
 		}
 
-		if (target.id !== rootIds.trigger && !target.closest(selector())) {
+		if (target.id !== ids.trigger && !target.closest(selector())) {
 			rootOpen.set(false);
 		}
 	}
@@ -137,8 +137,8 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 				style: styleToString({
 					display: $isVisible ? undefined : 'none',
 				}),
-				id: rootIds.menu,
-				'aria-labelledby': rootIds.trigger,
+				id: ids.menu,
+				'aria-labelledby': ids.trigger,
 				'data-state': $isVisible ? 'open' : 'closed',
 				'data-portal': $portal ? '' : undefined,
 				tabindex: -1,
@@ -233,10 +233,10 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 		stores: rootOpen,
 		returned: ($rootOpen) => {
 			return {
-				'aria-controls': rootIds.menu,
+				'aria-controls': ids.menu,
 				'aria-expanded': $rootOpen,
 				'data-state': $rootOpen ? 'open' : 'closed',
-				id: rootIds.trigger,
+				id: ids.trigger,
 				style: styleToString({
 					WebkitTouchCallout: 'none',
 				}),
diff --git a/src/lib/builders/dialog/create.ts b/src/lib/builders/dialog/create.ts
index b6e06c48..9be5f518 100644
--- a/src/lib/builders/dialog/create.ts
+++ b/src/lib/builders/dialog/create.ts
@@ -329,6 +329,7 @@ export function createDialog(props?: CreateDialogProps) {
 	});
 
 	return {
+		ids,
 		elements: {
 			content,
 			trigger,
diff --git a/src/lib/builders/link-preview/create.ts b/src/lib/builders/link-preview/create.ts
index bafd8bbb..545ba7e9 100644
--- a/src/lib/builders/link-preview/create.ts
+++ b/src/lib/builders/link-preview/create.ts
@@ -307,6 +307,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 	});
 
 	return {
+		ids,
 		elements: {
 			trigger,
 			content,
diff --git a/src/lib/builders/menu/create.ts b/src/lib/builders/menu/create.ts
index 2e5ff259..7fc150de 100644
--- a/src/lib/builders/menu/create.ts
+++ b/src/lib/builders/menu/create.ts
@@ -1028,6 +1028,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		});
 
 		return {
+			ids: subIds,
 			elements: {
 				subTrigger,
 				subMenu,
@@ -1316,6 +1317,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 	}
 
 	return {
+		ids: rootIds,
 		trigger: rootTrigger,
 		menu: rootMenu,
 		open: rootOpen,
@@ -1328,7 +1330,6 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		createSubmenu,
 		createMenuRadioGroup,
 		separator,
-		rootIds,
 		handleTypeaheadSearch,
 	};
 }
diff --git a/src/lib/builders/menubar/create.ts b/src/lib/builders/menubar/create.ts
index 8904156f..304d6179 100644
--- a/src/lib/builders/menubar/create.ts
+++ b/src/lib/builders/menubar/create.ts
@@ -61,7 +61,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 	const closeTimer = writable(0);
 	let scrollRemoved = false;
 
-	const rootIds = {
+	const ids = {
 		menubar: generateId(),
 	};
 
@@ -71,7 +71,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 				role: 'menubar',
 				'data-melt-menubar': '',
 				'data-orientation': 'horizontal',
-				id: rootIds.menubar,
+				id: ids.menubar,
 			};
 		},
 		action: (node: HTMLElement) => {
@@ -137,10 +137,10 @@ export function createMenubar(props?: CreateMenubarProps) {
 					style: styleToString({
 						display: $isVisible ? undefined : 'none',
 					}),
-					id: m.rootIds.menu,
-					'aria-labelledby': m.rootIds.trigger,
+					id: m.ids.menu,
+					'aria-labelledby': m.ids.trigger,
 					'data-state': $isVisible ? 'open' : 'closed',
-					'data-melt-scope': rootIds.menubar,
+					'data-melt-scope': ids.menubar,
 					'data-portal': $portal ? '' : undefined,
 					tabindex: -1,
 				} as const;
@@ -164,7 +164,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 									clickOutside: {
 										ignore: (e) => {
 											const target = e.target;
-											const menubarEl = document.getElementById(rootIds.menubar);
+											const menubarEl = document.getElementById(ids.menubar);
 											if (!menubarEl || !isElement(target)) return false;
 											return menubarEl.contains(target);
 										},
@@ -239,10 +239,10 @@ export function createMenubar(props?: CreateMenubarProps) {
 			stores: [rootOpen],
 			returned: ([$rootOpen]) => {
 				return {
-					'aria-controls': m.rootIds.menu,
+					'aria-controls': m.ids.menu,
 					'aria-expanded': $rootOpen,
 					'data-state': $rootOpen ? 'open' : 'closed',
-					id: m.rootIds.trigger,
+					id: m.ids.trigger,
 					'aria-haspopup': 'menu',
 					'data-orientation': 'horizontal',
 					role: 'menuitem',
@@ -251,7 +251,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 			action: (node: HTMLElement): MeltActionReturn<MenubarEvents['trigger']> => {
 				applyAttrsIfDisabled(node);
 
-				const menubarEl = document.getElementById(rootIds.menubar);
+				const menubarEl = document.getElementById(ids.menubar);
 				if (!menubarEl) return {};
 
 				const menubarTriggers = Array.from(
@@ -313,7 +313,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 						const $rootOpen = get(rootOpen);
 						if ($activeMenu && !$rootOpen) {
 							rootOpen.set(true);
-							activeMenu.set(m.rootIds.menu);
+							activeMenu.set(m.ids.menu);
 							rootActiveTrigger.set(triggerEl);
 						}
 					})
@@ -335,7 +335,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 					nextFocusable.set(getNextFocusable(triggerEl));
 					prevFocusable.set(getPreviousFocusable(triggerEl));
 					rootActiveTrigger.set(triggerEl);
-					activeMenu.set(m.rootIds.menu);
+					activeMenu.set(m.ids.menu);
 				} else {
 					rootActiveTrigger.set(null);
 				}
@@ -346,10 +346,10 @@ export function createMenubar(props?: CreateMenubarProps) {
 
 		effect([activeMenu], ([$activeMenu]) => {
 			if (!isBrowser) return;
-			if ($activeMenu === m.rootIds.menu) {
+			if ($activeMenu === m.ids.menu) {
 				if (get(rootOpen)) return;
 
-				const triggerEl = document.getElementById(m.rootIds.trigger);
+				const triggerEl = document.getElementById(m.ids.trigger);
 				if (!triggerEl) return;
 				rootActiveTrigger.set(triggerEl);
 				addHighlight(triggerEl);
@@ -357,10 +357,10 @@ export function createMenubar(props?: CreateMenubarProps) {
 				return;
 			}
 
-			if ($activeMenu !== m.rootIds.menu) {
+			if ($activeMenu !== m.ids.menu) {
 				if (!isBrowser) return;
 				if (get(rootOpen)) {
-					const triggerEl = document.getElementById(m.rootIds.trigger);
+					const triggerEl = document.getElementById(m.ids.trigger);
 					if (!triggerEl) return;
 					rootActiveTrigger.set(null);
 					rootOpen.set(false);
@@ -372,9 +372,9 @@ export function createMenubar(props?: CreateMenubarProps) {
 
 		effect([rootOpen], ([$rootOpen]) => {
 			if (!isBrowser) return;
-			const triggerEl = document.getElementById(m.rootIds.trigger);
+			const triggerEl = document.getElementById(m.ids.trigger);
 			if (!triggerEl) return;
-			if (!$rootOpen && get(activeMenu) === m.rootIds.menu) {
+			if (!$rootOpen && get(activeMenu) === m.ids.menu) {
 				rootActiveTrigger.set(null);
 				activeMenu.set('');
 				removeHighlight(triggerEl);
@@ -388,13 +388,14 @@ export function createMenubar(props?: CreateMenubarProps) {
 
 		onMount(() => {
 			if (!isBrowser) return;
-			const triggerEl = document.getElementById(m.rootIds.trigger);
+			const triggerEl = document.getElementById(m.ids.trigger);
 			if (isHTMLElement(triggerEl) && get(rootOpen)) {
 				rootActiveTrigger.set(triggerEl);
 			}
 		});
 
 		return {
+			ids,
 			elements: {
 				menu,
 				trigger,
@@ -446,7 +447,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 		if (isPrevKey && isKeyDownInsideSubMenu) return;
 
 		// Index of the currently focused item in the candidate nodes array
-		const menubarEl = document.getElementById(rootIds.menubar);
+		const menubarEl = document.getElementById(ids.menubar);
 		if (!isHTMLElement(menubarEl)) return;
 		const triggers = getMenuTriggers(menubarEl);
 		const currTriggerId = currentTarget.getAttribute('aria-labelledby');
@@ -545,7 +546,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 	onMount(() => {
 		if (!isBrowser) return;
 
-		const menubarEl = document.getElementById(rootIds.menubar);
+		const menubarEl = document.getElementById(ids.menubar);
 		if (!menubarEl) return;
 		const unsubEvents = executeCallbacks(
 			addMeltEventListener(menubarEl, 'keydown', (e) => {
@@ -601,6 +602,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 	});
 
 	return {
+		ids: ids,
 		elements: {
 			menubar,
 		},
diff --git a/src/lib/builders/pin-input/create.ts b/src/lib/builders/pin-input/create.ts
index 29949c39..96483eed 100644
--- a/src/lib/builders/pin-input/create.ts
+++ b/src/lib/builders/pin-input/create.ts
@@ -249,6 +249,7 @@ export function createPinInput(props?: CreatePinInputProps) {
 	};
 
 	return {
+		ids,
 		elements: {
 			root,
 			input,
diff --git a/src/lib/builders/popover/create.ts b/src/lib/builders/popover/create.ts
index c779e1f8..3092c4a7 100644
--- a/src/lib/builders/popover/create.ts
+++ b/src/lib/builders/popover/create.ts
@@ -254,6 +254,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		};
 	});
 	return {
+		ids,
 		elements: {
 			trigger,
 			content,
diff --git a/src/lib/builders/select/create.ts b/src/lib/builders/select/create.ts
index 65e5a3a5..f9650c73 100644
--- a/src/lib/builders/select/create.ts
+++ b/src/lib/builders/select/create.ts
@@ -756,6 +756,7 @@ export function createSelect<
 	});
 
 	return {
+		ids,
 		elements: {
 			menu,
 			trigger,
diff --git a/src/lib/builders/slider/create.ts b/src/lib/builders/slider/create.ts
index 2bfdc820..8915c3d2 100644
--- a/src/lib/builders/slider/create.ts
+++ b/src/lib/builders/slider/create.ts
@@ -426,6 +426,7 @@ export const createSlider = (props?: CreateSliderProps) => {
 	);
 
 	return {
+		ids,
 		elements: {
 			root,
 			thumb,
diff --git a/src/lib/builders/tags-input/create.ts b/src/lib/builders/tags-input/create.ts
index 49fadea4..cfe5a243 100644
--- a/src/lib/builders/tags-input/create.ts
+++ b/src/lib/builders/tags-input/create.ts
@@ -744,6 +744,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 	});
 
 	return {
+		ids,
 		elements: {
 			root,
 			input,
diff --git a/src/lib/builders/tooltip/create.ts b/src/lib/builders/tooltip/create.ts
index 755e90aa..9fcaafe7 100644
--- a/src/lib/builders/tooltip/create.ts
+++ b/src/lib/builders/tooltip/create.ts
@@ -258,18 +258,24 @@ export function createTooltip(props?: CreateTooltipProps) {
 	let isMouseInTooltipArea = false;
 
 	effect(open, ($open) => {
-		if (!$open) return;
+		const currentGroup = get(group);
+		if (currentGroup === undefined || currentGroup === false) {
+			return;
+		}
 
-		const groupValue = get(group);
-		if (groupValue === undefined || groupValue === false) {
+		if (!$open) {
+			if (groupMap.get(currentGroup) === open) {
+				// Tooltip is no longer open
+				groupMap.delete(currentGroup);
+			}
 			return;
 		}
 
 		// Close the currently open tooltip in the same group
 		// and set this tooltip as the open one.
-		const currentOpen = groupMap.get(groupValue);
+		const currentOpen = groupMap.get(currentGroup);
 		currentOpen?.set(false);
-		groupMap.set(groupValue, open);
+		groupMap.set(currentGroup, open);
 	});
 
 	effect([open, openReason], ([$open, $openReason]) => {
@@ -303,6 +309,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 	});
 
 	return {
+		ids,
 		elements: {
 			trigger,
 			content,
diff --git a/src/lib/builders/tree/create.ts b/src/lib/builders/tree/create.ts
index be01f2c0..406af17b 100644
--- a/src/lib/builders/tree/create.ts
+++ b/src/lib/builders/tree/create.ts
@@ -67,7 +67,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 		return (itemId: string) => $expanded.includes(itemId);
 	});
 
-	const rootIds = {
+	const ids = {
 		tree: generateId(),
 	};
 
@@ -75,7 +75,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 		returned: () => {
 			return {
 				role: 'tree',
-				'data-melt-id': rootIds.tree,
+				'data-melt-id': ids.tree,
 			} as const;
 		},
 	});
@@ -124,7 +124,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 						return;
 					}
 
-					const rootEl = getElementByMeltId(rootIds.tree);
+					const rootEl = getElementByMeltId(ids.tree);
 					if (!rootEl || !isHTMLElement(node) || node.getAttribute('role') !== 'treeitem') {
 						return;
 					}
@@ -279,7 +279,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 
 	function getItems(): HTMLElement[] {
 		let items = [] as HTMLElement[];
-		const rootEl = getElementByMeltId(rootIds.tree);
+		const rootEl = getElementByMeltId(ids.tree);
 		if (!rootEl) return items;
 
 		// Select all 'treeitem' li elements within our root element.
@@ -322,6 +322,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 	}
 
 	return {
+		ids,
 		elements: {
 			tree: rootTree,
 			item,


Test patch:
diff --git a/src/tests/tooltip/Tooltip.spec.ts b/src/tests/tooltip/Tooltip.spec.ts
index 76ad53ea..c4b99025 100644
--- a/src/tests/tooltip/Tooltip.spec.ts
+++ b/src/tests/tooltip/Tooltip.spec.ts
@@ -197,3 +197,18 @@ describe('2 tooltips, group=undefined', () => {
 		expect(get(open2)).toBe(true);
 	});
 });
+
+describe('Single tooltip, group=true', () => {
+	test('Tooltip does not prevent itself from opening', () => {
+		const open = writable(true);
+		render(Tooltip, {
+			open,
+			group: true,
+		});
+
+		open.set(false);
+
+		open.set(true);
+		expect(get(open)).toBe(true);
+	});
+});