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

Issue:
📁Docs: <Tree/> docs outdated
Found that the docs for `<Tree />` are mismatching the types and working while implementing. Some examples:


```ts
// parent.svelte

let ctx = createTreeView()
setContext('tree', ctx)

let {
	elements: { tree },
	states: { selectedItem, expanded } // vs. selectedItem, collapsedGroups on docs
} = ctx

// MyTree.svelte
import { melt, type TreeView } from '@melt-ui/svelte'
import { getContext } from 'svelte'

const {
	elements: { item, group },
	helpers: { isExpanded, isSelected } // vs. isExpanded, isCollapsedGroup in docs
} = getContext<TreeView>('tree')
```

There may be more issues there, but these are the ones I found in my implementation.

Pull request:
Fix: TreeView Docs
Fixes #1000 

Patch:
diff --git a/.changeset/forty-beers-reply.md b/.changeset/forty-beers-reply.md
deleted file mode 100644
index 0aa3e0de..00000000
--- a/.changeset/forty-beers-reply.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@melt-ui/svelte": patch
----
-
-Dialog: fixed interact outside pointerup/mouseup issue where the dialog would close if you clicked and dragged outside of the dialog before releasing the press (Closes: #750)
diff --git a/.changeset/happy-cheetahs-appear.md b/.changeset/happy-cheetahs-appear.md
new file mode 100644
index 00000000..b900ec5a
--- /dev/null
+++ b/.changeset/happy-cheetahs-appear.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': patch
+---
+
+fix(Calendar/Range Calendar): use `'button'` for button elements' `type`
diff --git a/.changeset/popular-dolls-attend.md b/.changeset/popular-dolls-attend.md
deleted file mode 100644
index bfc013a7..00000000
--- a/.changeset/popular-dolls-attend.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@melt-ui/svelte': patch
----
-
-Toggle Group: fixed bug where `single` groups wouldn't focus (Closes #810)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0472ac1b..85e1bd31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @melt-ui/svelte
 
+## 0.74.3
+
+### Patch Changes
+
+- Revert regressions caused by PRs #977 and #995 ([#1016](https://github.com/melt-ui/melt-ui/pull/1016))
+
+- Toolbar: set correct tabindex for buttons and links ([#1004](https://github.com/melt-ui/melt-ui/pull/1004))
+
+## 0.74.2
+
+### Patch Changes
+
+- Fix custom portal issues ([#995](https://github.com/melt-ui/melt-ui/pull/995))
+
+- Dialog: fixed interact outside pointerup/mouseup issue where the dialog would close if you clicked and dragged outside of the dialog before releasing the press (Closes: #750) ([#994](https://github.com/melt-ui/melt-ui/pull/994))
+
+- Toggle Group: fixed bug where `single` groups wouldn't focus (Closes #810) ([#990](https://github.com/melt-ui/melt-ui/pull/990))
+
 ## 0.74.1
 
 ### Patch Changes
diff --git a/package.json b/package.json
index 798f88d5..f4031476 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@melt-ui/svelte",
-	"version": "0.74.1",
+	"version": "0.74.3",
 	"license": "MIT",
 	"repository": "github:melt-ui/melt-ui",
 	"author": "Thomas G. Lopes",
diff --git a/src/docs/components/tooltip.svelte b/src/docs/components/tooltip.svelte
index a0cdb4d7..be92e102 100644
--- a/src/docs/components/tooltip.svelte
+++ b/src/docs/components/tooltip.svelte
@@ -36,7 +36,7 @@
 	[data-melt-tooltip-content] {
 		opacity: 0;
 		visibility: hidden;
-		transition: 150ms ease;
+		transition: opacity 150ms ease;
 
 		&[data-open] {
 			opacity: 1;
diff --git a/src/docs/content/builders/tree.md b/src/docs/content/builders/tree.md
index ea276d16..0a974eea 100644
--- a/src/docs/content/builders/tree.md
+++ b/src/docs/content/builders/tree.md
@@ -13,7 +13,6 @@ description:
 ## Anatomy
 
 - **Tree**: The container in which the tree lies.
-- **Label**: The element that describes the tree.
 - **Item**: The individual tree item element.
 - **Group**: An element where a subtree is nested.
 
diff --git a/src/docs/data/builders/tree.ts b/src/docs/data/builders/tree.ts
index fa630d6f..b34d23d3 100644
--- a/src/docs/data/builders/tree.ts
+++ b/src/docs/data/builders/tree.ts
@@ -1,4 +1,4 @@
-import { DESCRIPTIONS, KBD, PROPS } from '$docs/constants.js';
+import { DESCRIPTIONS, KBD, PROPS, SEE } from '$docs/constants.js';
 import type { KeyboardSchema } from '$docs/types.js';
 import { builderSchema, elementSchema } from '$docs/utils/index.js';
 import { treeEvents } from '$lib/builders/tree/events.js';
@@ -7,19 +7,38 @@ import type { BuilderData } from './index.js';
 const BUILDER_NAME = 'tree-view';
 
 const builder = builderSchema(BUILDER_NAME, {
-	title: 'createTreeViewBuilder',
+	title: 'createTreeView',
 	description: DESCRIPTIONS.BUILDER('tree-view'),
-	props: [PROPS.FORCE_VISIBLE],
+	props: [
+		PROPS.FORCE_VISIBLE,
+		{
+			name: 'defaultExpanded',
+			type: 'string[]',
+			description: 'Which tree items are expanded by default.',
+			default: '[]',
+		},
+		{
+			name: 'expanded',
+			type: 'Writable<string[]>',
+			description:
+				'Optionally pass a writable store to control the expanded items of the tree. If provided, this will override the value of the `defaultTree` prop.',
+			see: SEE.BRING_YOUR_OWN_STORE,
+		},
+		{
+			name: 'onExpandedChange',
+			type: 'ChangeFn<string[]>',
+			description: 'A callback called when the value of the `expanded` store should be changed.',
+			see: SEE.CHANGE_FUNCTIONS,
+		},
+	],
 	elements: [
 		{
 			name: 'tree',
 			description: 'The builder store used to create the tree.',
-			link: '#tree',
 		},
 		{
 			name: 'item',
 			description: 'The builder store used to create a tree item.',
-			link: '#item',
 		},
 		{
 			name: 'group',
@@ -28,29 +47,25 @@ const builder = builderSchema(BUILDER_NAME, {
 	],
 	states: [
 		{
-			name: 'collapsedGroups',
-			description: 'The list of IDs of items, who have a group that is not expanded.',
-		},
-		{
-			name: 'focusedItem',
-			description: 'The list item that is currently focused.',
+			name: 'expanded',
+			type: 'Writable<string[]>',
+			description: 'The list of IDs of items that are expanded.',
 		},
 		{
 			name: 'selectedItem',
-			description: 'The list item that is currently selected.',
+			type: 'Writable<HTMLElement | null>',
+			description: 'The currently selected item, or `null` if no item is selected.',
 		},
 	],
 	helpers: [
 		{
-			name: 'isCollapsedGroup',
-			description:
-				'A derived store that returns a function that returns whether or not the item is collapsed.',
+			name: 'isExpanded',
+			description: 'A function that returns whether or not the item is expanded.',
 			type: 'Readable<(itemId: string) => boolean>',
 		},
 		{
 			name: 'isSelected',
-			description:
-				'A derived store that returns a function that returns whether or not the item is selected.',
+			description: 'A function that returns whether or not the item is selected.',
 			type: 'Readable<(itemId: string) => boolean>',
 		},
 	],
@@ -58,7 +73,7 @@ const builder = builderSchema(BUILDER_NAME, {
 
 const tree = elementSchema('tree', {
 	title: 'tree',
-	description: 'The tree <ul> element.',
+	description: 'The tree list element.',
 	dataAttributes: [
 		{
 			name: 'data-melt-id',
@@ -69,12 +84,13 @@ const tree = elementSchema('tree', {
 
 const item = elementSchema('item', {
 	title: 'item',
-	description: 'The list tree item.',
+	description: 'The list item.',
 	props: [
 		{
 			name: 'id',
 			description: 'The unique ID of the item.',
 			type: 'string',
+			required: true,
 		},
 	],
 	dataAttributes: [
@@ -82,10 +98,6 @@ const item = elementSchema('item', {
 			name: 'data-id',
 			value: 'A unique ID.',
 		},
-		{
-			name: 'data-value',
-			value: 'The value of the tree item.',
-		},
 	],
 	events: treeEvents['item'],
 });
@@ -98,6 +110,7 @@ const group = elementSchema('group', {
 			name: 'id',
 			description: 'The unique ID of the group. Must match the ID of the item it belongs to.',
 			type: 'string',
+			required: true,
 		},
 	],
 	dataAttributes: [
diff --git a/src/lib/builders/calendar/create.ts b/src/lib/builders/calendar/create.ts
index 98406db8..b3073b0a 100644
--- a/src/lib/builders/calendar/create.ts
+++ b/src/lib/builders/calendar/create.ts
@@ -360,6 +360,7 @@ export function createCalendar<
 			const disabled = $isPrevButtonDisabled;
 			return {
 				role: 'button',
+				type: 'button' as const,
 				'aria-label': 'Previous',
 				'aria-disabled': disabled ? ('true' as const) : undefined,
 				'data-disabled': disabled ? '' : undefined,
@@ -391,6 +392,7 @@ export function createCalendar<
 			const disabled = $isNextButtonDisabled;
 			return {
 				role: 'button',
+				type: 'button' as const,
 				'aria-label': 'Next',
 				'aria-disabled': disabled ? ('true' as const) : undefined,
 				'data-disabled': disabled ? '' : undefined,
diff --git a/src/lib/builders/range-calendar/create.ts b/src/lib/builders/range-calendar/create.ts
index 8702c21d..85dc8036 100644
--- a/src/lib/builders/range-calendar/create.ts
+++ b/src/lib/builders/range-calendar/create.ts
@@ -334,6 +334,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 			const disabled = $isPrevButtonDisabled;
 			return {
 				role: 'button',
+				type: 'button' as const,
 				'aria-label': 'Previous',
 				'aria-disabled': disabled ? ('true' as const) : undefined,
 				disabled: disabled ? true : undefined,
@@ -358,6 +359,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 			const disabled = $isNextButtonDisabled;
 			return {
 				role: 'button',
+				type: 'button' as const,
 				'aria-label': 'Next',
 				'aria-disabled': disabled ? ('true' as const) : undefined,
 				disabled: disabled ? true : undefined,
diff --git a/src/lib/builders/toolbar/create.ts b/src/lib/builders/toolbar/create.ts
index 11641e0f..051ab1c4 100644
--- a/src/lib/builders/toolbar/create.ts
+++ b/src/lib/builders/toolbar/create.ts
@@ -48,9 +48,10 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 			({
 				role: 'button',
 				type: 'button',
-				tabIndex: -1,
 			} as const),
 		action: (node: HTMLElement): MeltActionReturn<ToolbarEvents['button']> => {
+			setNodeTabIndex(node);
+
 			const unsub = addMeltEventListener(node, 'keydown', handleKeyDown);
 
 			return {
@@ -63,10 +64,10 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 		returned: () =>
 			({
 				role: 'link',
-				'data-melt-toolbar-item': '',
-				tabIndex: -1,
 			} as const),
 		action: (node: HTMLElement): MeltActionReturn<ToolbarEvents['link']> => {
+			setNodeTabIndex(node);
+
 			const unsub = addMeltEventListener(node, 'keydown', handleKeyDown);
 
 			return {
@@ -149,6 +150,8 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 				};
 			},
 			action: (node: HTMLElement): MeltActionReturn<ToolbarEvents['item']> => {
+				setNodeTabIndex(node);
+
 				function getNodeProps() {
 					const itemValue = node.dataset.value;
 					const disabled = node.dataset.disabled === 'true';
@@ -172,17 +175,6 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 					});
 				}
 
-				const parentToolbar = node.closest('[data-melt-toolbar]');
-				if (!isHTMLElement(parentToolbar)) return {};
-
-				const items = getToolbarItems(parentToolbar);
-
-				if (items[0] === node) {
-					node.tabIndex = 0;
-				} else {
-					node.tabIndex = -1;
-				}
-
 				const unsub = executeCallbacks(
 					addMeltEventListener(node, 'click', () => {
 						handleValueUpdate();
@@ -224,12 +216,6 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 		};
 	};
 
-	function getToolbarItems(element: HTMLElement) {
-		return Array.from(
-			element.querySelectorAll(`${selector('item')}, ${selector('button')}`)
-		).filter((el): el is HTMLElement => isHTMLElement(el));
-	}
-
 	function handleKeyDown(e: KeyboardEvent) {
 		const $orientation = orientation.get();
 		const $loop = loop.get();
@@ -250,9 +236,7 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 		const root = el.closest('[data-melt-toolbar]');
 		if (!isHTMLElement(root)) return;
 
-		const items = Array.from(
-			root.querySelectorAll(`${selector('item')}, ${selector('button')}`)
-		).filter((el): el is HTMLElement => isHTMLElement(el));
+		const items = getToolbarItems(root);
 
 		const currentIndex = items.indexOf(el);
 
@@ -294,3 +278,30 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 		options,
 	};
 };
+
+/**
+ * Sets the appropriate tabIndex for the node based on its position in the
+ * parent toolbar.
+ */
+function setNodeTabIndex(node: HTMLElement) {
+	const parentToolbar = node.closest('[data-melt-toolbar]');
+
+	if (!isHTMLElement(parentToolbar)) return;
+
+	const items = getToolbarItems(parentToolbar);
+
+	if (items[0] === node) {
+		node.tabIndex = 0;
+	} else {
+		node.tabIndex = -1;
+	}
+}
+
+/**
+ * Returns an array of all toolbar items within the given element.
+ */
+function getToolbarItems(element: HTMLElement) {
+	return Array.from(
+		element.querySelectorAll(`${selector('item')}, ${selector('button')}, ${selector('link')}`)
+	).filter((el): el is HTMLElement => isHTMLElement(el));
+}
diff --git a/src/lib/builders/tree/events.ts b/src/lib/builders/tree/events.ts
index 7852677e..002d373c 100644
--- a/src/lib/builders/tree/events.ts
+++ b/src/lib/builders/tree/events.ts
@@ -1,7 +1,7 @@
 import type { GroupedEvents, MeltComponentEvents } from '$lib/internal/types.js';
 
 export const treeEvents = {
-	item: ['keydown', 'click'] as const,
+	item: ['keydown', 'click', 'focus'] as const,
 };
 
 export type TreeEvents = GroupedEvents<typeof treeEvents>;
diff --git a/src/lib/builders/tree/types.ts b/src/lib/builders/tree/types.ts
index 1e829391..52853570 100644
--- a/src/lib/builders/tree/types.ts
+++ b/src/lib/builders/tree/types.ts
@@ -3,12 +3,19 @@ import type { Writable } from 'svelte/store';
 import type { createTreeView } from './create.js';
 
 export type CreateTreeViewProps = {
+	/**
+	 * Whether or not to force the tree to always be visible.
+	 *
+	 * This is useful for custom transitions and animations using conditional blocks.
+	 *
+	 * @default false
+	 */
 	forceVisible?: boolean;
 
 	/**
 	 * Which tree items are expanded by default.
 	 *
-	 * @default false
+	 * @default []
 	 */
 	defaultExpanded?: string[];
 
diff --git a/src/routes/(landing-ui)/search.svelte b/src/routes/(landing-ui)/search.svelte
index 6c8484a0..0614fac9 100644
--- a/src/routes/(landing-ui)/search.svelte
+++ b/src/routes/(landing-ui)/search.svelte
@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { goto } from '$app/navigation';
 	import { Tooltip } from '$docs/components/index.js';
-	import { createCombobox, createDialog, melt } from '$lib/index.js';
+	import { createCombobox, createDialog, createSync, melt } from '$lib/index.js';
 	import { CornerDownRight, LoaderIcon, Search as SearchIcon } from '$icons/index.js';
 	import { onMount } from 'svelte';
 	import type { Pagefind, PagefindSearchFragment, PagefindSubResult } from '../../pagefind.js';
@@ -35,7 +35,6 @@
 			}
 			return undefined;
 		},
-		forceVisible: true,
 		preventScroll: false,
 		highlightOnHover: false,
 	});
diff --git a/src/routes/repro/+page.ts b/src/routes/repro/+page.ts
index b88b1711..28882678 100644
--- a/src/routes/repro/+page.ts
+++ b/src/routes/repro/+page.ts
@@ -2,5 +2,5 @@ import { redirect } from '@sveltejs/kit';
 import type { PageLoad } from './$types.js';
 
 export const load: PageLoad = async () => {
-	redirect(303, 'https://stackblitz.com/github/melt-ui/playground');
+	redirect(303, 'https://stackblitz.com/github/melt-ui/template');
 };
diff --git a/vite.config.ts b/vite.config.ts
index 5f4b7ad3..7df879ab 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -24,6 +24,6 @@ export default defineConfig({
 		deps: {
 			inline: ['clsx'],
 		},
-		retry: 3,
+		retry: 5,
 	},
 });


Test patch:
diff --git a/src/tests/portal/PortalNested.spec.ts b/src/tests/portal/PortalNested.spec.ts
index 6e961296..ae35084f 100644
--- a/src/tests/portal/PortalNested.spec.ts
+++ b/src/tests/portal/PortalNested.spec.ts
@@ -63,7 +63,7 @@ const testComponent = async (component: Structure, level = 0) => {
 };
 
 // Execute the test
-test.skip('recursive component test', async () => {
+test('recursive component test', async () => {
 	await render(PortalNestedTest);
 	await testComponent(structure);
 });
diff --git a/src/tests/portal/PortalNested.svelte b/src/tests/portal/PortalNested.svelte
index 099c45ca..fdda1a15 100644
--- a/src/tests/portal/PortalNested.svelte
+++ b/src/tests/portal/PortalNested.svelte
@@ -16,14 +16,14 @@
 	export const structure: Structure = {
 		name: 'dialog',
 		children: [
-			{
-				name: 'dialog',
-				children: [
-					{
-						name: 'select',
-					},
-				],
-			},
+			// {
+			// 	name: 'dialog',
+			// 	children: [
+			// 		{
+			// 			name: 'select',
+			// 		},
+			// 	],
+			// },
 			{
 				name: 'popover',
 				children: [{ name: 'dialog' }, { name: 'popover' }],
diff --git a/src/tests/portal/index.ts b/src/tests/portal/index.ts
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/tests/toolbar/Toolbar.spec.ts b/src/tests/toolbar/Toolbar.spec.ts
index 67bdb009..1f93f658 100644
--- a/src/tests/toolbar/Toolbar.spec.ts
+++ b/src/tests/toolbar/Toolbar.spec.ts
@@ -6,6 +6,7 @@ import { describe } from 'vitest';
 import ToolbarTest from './ToolbarTest.svelte';
 import { writable } from 'svelte/store';
 import { tick } from 'svelte';
+import { type CreateToolbarProps } from '$lib/index.js';
 
 const group1Items = ['item-1', 'item-2', 'item-3'];
 const group2Items = ['item-4', 'item-5', 'item-6'];
@@ -13,8 +14,15 @@ const tbItems = [...group1Items, ...group2Items];
 
 const tbLinks = ['link-1'];
 const tbButtons = ['button-1'];
+const tbLinksAndButtons = [...tbLinks, ...tbButtons];
+const tbButtonsAndLinks = [...tbButtons, ...tbLinks];
 
 const allToolbarItems = [...tbItems, ...tbLinks, ...tbButtons];
+const allToolbarItemsLinksFirst = [...tbLinks, ...tbItems, ...tbButtons];
+const allToolbarItemsButtonsFirst = [...tbButtons, ...tbItems, ...tbLinks];
+
+const horizontal: CreateToolbarProps['orientation'] = 'horizontal';
+const vertical: CreateToolbarProps['orientation'] = 'vertical';
 
 describe('Toolbar', () => {
 	test('has no accessibility violations', async () => {
@@ -22,62 +30,287 @@ describe('Toolbar', () => {
 		expect(await axe(container)).toHaveNoViolations();
 	});
 
-	test("tab enters toolbar but doesn't navigate between items", async () => {
+	test.each([
+		{ desc: '', props: {}, items: allToolbarItems },
+		{
+			desc: 'first item is a link',
+			props: { linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'first item is a button',
+			props: { buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'no group items, first item is a link',
+			props: { linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'no group items, first item is a button',
+			props: { linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+	])("tab enters toolbar but doesn't navigate between items - $desc", async ({ props, items }) => {
 		const user = userEvent.setup();
-		const { getByTestId } = render(ToolbarTest);
+		const { getByTestId } = render(ToolbarTest, props);
 
 		await user.tab();
-		expect(getByTestId('item-1')).toHaveFocus();
+		expect(getByTestId(items[0])).toHaveFocus();
 		await user.tab();
-		expect(getByTestId('item-2')).not.toHaveFocus();
+		expect(getByTestId(items[1])).not.toHaveFocus();
 		await user.tab({ shift: true });
-		expect(getByTestId('item-1')).toHaveFocus();
+		expect(getByTestId(items[0])).toHaveFocus();
 		await user.tab({ shift: true });
-		expect(getByTestId('item-1')).not.toHaveFocus();
+		expect(getByTestId(items[0])).not.toHaveFocus();
 	});
 
-	test('arrow keys navigate between items', async () => {
+	test.each([
+		{ desc: 'horizontal', props: { orientation: horizontal }, items: allToolbarItems },
+		{ desc: 'vertical', props: { orientation: vertical }, items: allToolbarItems },
+		{
+			desc: 'horizontal, first item is a link',
+			props: { orientation: horizontal, linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'vertical, first item is a link',
+			props: { orientation: vertical, linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'horizontal, first item is a button',
+			props: { orientation: horizontal, buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'vertical, first item is a button',
+			props: { orientation: vertical, buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'horizontal, no group items, first item is a link',
+			props: { orientation: horizontal, linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'vertical, no group items, first item is a link',
+			props: { orientation: vertical, linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'horizontal, no group items, first item is a button',
+			props: { orientation: horizontal, linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+		{
+			desc: 'vertical, no group items, first item is a button',
+			props: { orientation: vertical, linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+	])('arrow keys navigate between items - $desc', async ({ props, items }) => {
 		const user = userEvent.setup();
-		const { getByTestId } = render(ToolbarTest);
+		const { getByTestId } = render(ToolbarTest, props);
 
-		await user.tab();
+		const nextKey = `{${props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN}}`;
+		const prevKey = `{${props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP}}`;
+
+		const reversedItems = [...items].reverse();
 
-		const reversedItems = [...allToolbarItems].reverse();
+		await user.tab();
 
-		for (const item of allToolbarItems) {
+		for (const item of items) {
 			expect(getByTestId(item)).toHaveFocus();
-			await user.keyboard(`{${kbd.ARROW_RIGHT}}`);
+			await user.keyboard(nextKey);
 		}
 
 		for (const item of reversedItems) {
 			expect(getByTestId(item)).toHaveFocus();
-			await user.keyboard(`{${kbd.ARROW_LEFT}}`);
+			await user.keyboard(prevKey);
 		}
 	});
 
-	test('home and end keys focus first and last items - horizontal', async () => {
-		const user = userEvent.setup();
-		const { getByTestId } = render(ToolbarTest);
-
-		await user.tab();
-		const firstItem = getByTestId('item-1');
-		const lastItem = getByTestId('button-1');
-		expect(firstItem).toHaveFocus();
-		await user.keyboard(`{${kbd.END}}`);
-		expect(lastItem).toHaveFocus();
-		await user.keyboard(`{${kbd.HOME}}`);
-		expect(firstItem).toHaveFocus();
-	});
-
-	test('home and end keys focus first and last items - vertical', async () => {
+	test.each([
+		{ desc: 'horizontal', props: { orientation: horizontal }, items: allToolbarItems },
+		{ desc: 'vertical', props: { orientation: vertical }, items: allToolbarItems },
+		{
+			desc: 'horizontal, first item is a link',
+			props: { orientation: horizontal, linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'vertical, first item is a link',
+			props: { orientation: vertical, linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'horizontal, first item is a button',
+			props: { orientation: horizontal, buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'vertical, first item is a button',
+			props: { orientation: vertical, buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'horizontal, no group items, first item is a link',
+			props: { orientation: horizontal, linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'vertical, no group items, first item is a link',
+			props: { orientation: vertical, linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'horizontal, no group items, first item is a button',
+			props: { orientation: horizontal, linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+		{
+			desc: 'vertical, no group items, first item is a button',
+			props: { orientation: vertical, linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+	])(
+		'arrow key navigation loops around when looping is enabled - $desc',
+		async ({ props, items }) => {
+			const user = userEvent.setup();
+			const { getByTestId } = render(ToolbarTest, { loop: true, ...props });
+
+			const nextKey = `{${props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN}}`;
+			const prevKey = `{${props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP}}`;
+
+			await user.tab();
+			const firstItem = getByTestId(items[0]);
+			const lastItem = getByTestId(items[items.length - 1]);
+			expect(firstItem).toHaveFocus();
+			await user.keyboard(prevKey);
+			expect(lastItem).toHaveFocus();
+			await user.keyboard(nextKey);
+			expect(firstItem).toHaveFocus();
+		}
+	);
+
+	test.each([
+		{ desc: 'horizontal', props: { orientation: horizontal }, items: allToolbarItems },
+		{ desc: 'vertical', props: { orientation: vertical }, items: allToolbarItems },
+		{
+			desc: 'horizontal, first item is a link',
+			props: { orientation: horizontal, linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'vertical, first item is a link',
+			props: { orientation: vertical, linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'horizontal, first item is a button',
+			props: { orientation: horizontal, buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'vertical, first item is a button',
+			props: { orientation: vertical, buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'horizontal, no group items, first item is a link',
+			props: { orientation: horizontal, linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'vertical, no group items, first item is a link',
+			props: { orientation: vertical, linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'horizontal, no group items, first item is a button',
+			props: { orientation: horizontal, linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+		{
+			desc: 'vertical, no group items, first item is a button',
+			props: { orientation: vertical, linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+	])(
+		'arrow key navigation does not loop around when looping is disabled - $desc',
+		async ({ props, items }) => {
+			const user = userEvent.setup();
+			const { getByTestId } = render(ToolbarTest, { loop: false, ...props });
+
+			const nextKey = `{${props.orientation === horizontal ? kbd.ARROW_RIGHT : kbd.ARROW_DOWN}}`;
+			const prevKey = `{${props.orientation === horizontal ? kbd.ARROW_LEFT : kbd.ARROW_UP}}`;
+
+			const firstItem = getByTestId(items[0]);
+			const lastItem = getByTestId(items[items.length - 1]);
+
+			firstItem.focus();
+			expect(firstItem).toHaveFocus();
+			await user.keyboard(prevKey);
+			expect(firstItem).toHaveFocus();
+
+			lastItem.focus();
+			expect(lastItem).toHaveFocus();
+			await user.keyboard(nextKey);
+			expect(lastItem).toHaveFocus();
+		}
+	);
+
+	test.each([
+		{ desc: 'horizontal', props: { orientation: horizontal }, items: allToolbarItems },
+		{ desc: 'vertical', props: { orientation: vertical }, items: allToolbarItems },
+		{
+			desc: 'horizontal, first item is a link',
+			props: { orientation: horizontal, linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'vertical, first item is a link',
+			props: { orientation: vertical, linksFirst: true },
+			items: allToolbarItemsLinksFirst,
+		},
+		{
+			desc: 'horizontal, first item is a button',
+			props: { orientation: horizontal, buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'vertical, first item is a button',
+			props: { orientation: vertical, buttonsFirst: true },
+			items: allToolbarItemsButtonsFirst,
+		},
+		{
+			desc: 'horizontal, no group items, first item is a link',
+			props: { orientation: horizontal, linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'vertical, no group items, first item is a link',
+			props: { orientation: vertical, linksButtonsOnly: true, linksFirst: true },
+			items: tbLinksAndButtons,
+		},
+		{
+			desc: 'horizontal, no group items, first item is a button',
+			props: { orientation: horizontal, linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+		{
+			desc: 'vertical, no group items, first item is a button',
+			props: { orientation: vertical, linksButtonsOnly: true, buttonsFirst: true },
+			items: tbButtonsAndLinks,
+		},
+	])('home and end keys focus first and last items - $desc', async ({ props, items }) => {
 		const user = userEvent.setup();
-		const { getByTestId } = render(ToolbarTest, {
-			orientation: 'vertical',
-		});
+		const { getByTestId } = render(ToolbarTest, props);
 
 		await user.tab();
-		const firstItem = getByTestId('item-1');
-		const lastItem = getByTestId('button-1');
+		const firstItem = getByTestId(items[0]);
+		const lastItem = getByTestId(items[items.length - 1]);
 		expect(firstItem).toHaveFocus();
 		await user.keyboard(`{${kbd.END}}`);
 		expect(lastItem).toHaveFocus();
@@ -157,27 +390,6 @@ describe('Toolbar', () => {
 		expect(group2OnItems.length).toBe(1);
 	});
 
-	test('orientation changes keyboard nav', async () => {
-		const user = userEvent.setup();
-		const { getByTestId } = render(ToolbarTest, {
-			orientation: 'vertical',
-		});
-
-		await user.tab();
-
-		const reversedItems = [...allToolbarItems].reverse();
-
-		for (const item of allToolbarItems) {
-			expect(getByTestId(item)).toHaveFocus();
-			await user.keyboard(`{${kbd.ARROW_DOWN}}`);
-		}
-
-		for (const item of reversedItems) {
-			expect(getByTestId(item)).toHaveFocus();
-			await user.keyboard(`{${kbd.ARROW_UP}}`);
-		}
-	});
-
 	test('default value sets initial pressed/checked items', async () => {
 		const { getByTestId } = render(ToolbarTest, {
 			orientation: 'vertical',
diff --git a/src/tests/toolbar/ToolbarTest.svelte b/src/tests/toolbar/ToolbarTest.svelte
index 0af7dd03..840c50ab 100644
--- a/src/tests/toolbar/ToolbarTest.svelte
+++ b/src/tests/toolbar/ToolbarTest.svelte
@@ -25,6 +25,10 @@
 		value: undefined,
 	};
 
+	export let linksFirst = false;
+	export let buttonsFirst = false;
+	export let linksButtonsOnly = false;
+
 	const {
 		elements: { root, button, link, separator },
 		builders: { createToolbarGroup },
@@ -44,19 +48,31 @@
 
 <main>
 	<div use:melt={$root} data-testid="root">
-		<div use:melt={$fontGroup} data-testid="group-1">
-			<button use:melt={$fontItem('item-1')} data-testid="item-1"> item-1 </button>
-			<button use:melt={$fontItem('item-2')} data-testid="item-2"> item-2 </button>
-			<button use:melt={$fontItem('item-3')} data-testid="item-3"> item-3 </button>
-		</div>
-		<div use:melt={$separator} />
-		<div use:melt={$alignGroup} data-testid="group-2">
-			<button use:melt={$alignItem('item-4')} data-testid="item-4"> item-4 </button>
-			<button use:melt={$alignItem('item-5')} data-testid="item-5"> item-5 </button>
-			<button use:melt={$alignItem('item-6')} data-testid="item-6"> item-6</button>
-		</div>
-		<div use:melt={$separator} />
-		<a href="/" use:melt={$link} data-testid="link-1"> link-1 </a>
-		<button use:melt={$button} data-testid="button-1">button-1</button>
+		{#if linksFirst}
+			<a href="/" use:melt={$link} data-testid="link-1"> link-1 </a>
+		{/if}
+		{#if buttonsFirst}
+			<button use:melt={$button} data-testid="button-1"> button-1 </button>
+		{/if}
+		{#if !linksButtonsOnly}
+			<div use:melt={$fontGroup} data-testid="group-1">
+				<button use:melt={$fontItem('item-1')} data-testid="item-1"> item-1 </button>
+				<button use:melt={$fontItem('item-2')} data-testid="item-2"> item-2 </button>
+				<button use:melt={$fontItem('item-3')} data-testid="item-3"> item-3 </button>
+			</div>
+			<div use:melt={$separator} />
+			<div use:melt={$alignGroup} data-testid="group-2">
+				<button use:melt={$alignItem('item-4')} data-testid="item-4"> item-4 </button>
+				<button use:melt={$alignItem('item-5')} data-testid="item-5"> item-5 </button>
+				<button use:melt={$alignItem('item-6')} data-testid="item-6"> item-6</button>
+			</div>
+			<div use:melt={$separator} />
+		{/if}
+		{#if !linksFirst}
+			<a href="/" use:melt={$link} data-testid="link-1"> link-1 </a>
+		{/if}
+		{#if !buttonsFirst}
+			<button use:melt={$button} data-testid="button-1"> button-1 </button>
+		{/if}
 	</div>
 </main>