Repository: melt-ui/melt-ui
Pull request #1046 (2024-03-04 04:39:02)

fix: ScrollbarX issues
This PR fixes issues with the style of `scrollbarX` due to a missing parenthesis. It also fixes the case where `type: 'hover'` scrollbars would still display even when the content isn't overflowing. 

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

Latest commit: 3411b8c872fd925b7194c5d0e81952926cf0c1a3

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

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

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

</details>

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

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/huntabyte/melt-ui/new/fix/scroll-area-scrollbar-x?filename=.changeset/red-socks-give.md&value=---%0A%22%40melt-ui%2Fsvelte%22%3A%20patch%0A---%0A%0Afix%3A%20ScrollbarX%20issues%0A)


github-actions[bot]: <!-- refined-cf-pages-action:deployment-summary:melt-ui -->

###### built with [Refined Cloudflare Pages Action](https://github.com/AdrianGonz97/refined-cf-pages-action)
## ⚡ Cloudflare Pages Deployment
| Name | Status | Preview | Last Commit |
| :--- | :----- | :------ | :---------- |
| **melt-ui** | 🔨 Building ([View Log](https://github.com/melt-ui/melt-ui/actions/runs/8135799917)) |  | c3477a92db716285b66bbfd8fd45cb819846c173 |


Patch:
diff --git a/.changeset/breezy-dots-happen.md b/.changeset/breezy-dots-happen.md
new file mode 100644
index 00000000..4aa75cec
--- /dev/null
+++ b/.changeset/breezy-dots-happen.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': patch
+---
+
+Scroll Area: fixed bug with `type: 'hover'` scrollbars always showing on hover even if content not overflowing
diff --git a/.changeset/six-vans-love.md b/.changeset/six-vans-love.md
new file mode 100644
index 00000000..7a75d2f8
--- /dev/null
+++ b/.changeset/six-vans-love.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': patch
+---
+
+Scroll Area: fixed bug with `scrollbarX` styles
diff --git a/src/docs/previews/scroll-area/main/tailwind/index.svelte b/src/docs/previews/scroll-area/main/tailwind/index.svelte
index 78b8e851..7f5c6362 100644
--- a/src/docs/previews/scroll-area/main/tailwind/index.svelte
+++ b/src/docs/previews/scroll-area/main/tailwind/index.svelte
@@ -3,7 +3,16 @@
 	import { flavors } from './flavors.js';
 
 	const {
-		elements: { root, content, viewport, corner, scrollbarY, thumbY },
+		elements: {
+			root,
+			content,
+			viewport,
+			corner,
+			scrollbarY,
+			thumbY,
+			thumbX,
+			scrollbarX,
+		},
 	} = createScrollArea({
 		type: 'hover',
 		dir: 'ltr',
@@ -12,7 +21,7 @@
 
 <div
 	use:melt={$root}
-	class="relative h-72 w-56 overflow-hidden rounded-md border bg-white text-magnum-900 shadow-lg"
+	class="relative h-72 w-52 overflow-hidden rounded-md border bg-white text-magnum-900 shadow-lg"
 >
 	<div use:melt={$viewport} class="h-full w-full rounded-[inherit]">
 		<div use:melt={$content}>
@@ -36,5 +45,11 @@
 			class="relative flex-1 rounded-full bg-magnum-600"
 		/>
 	</div>
+	<div
+		use:melt={$scrollbarX}
+		class="flex h-2.5 w-full touch-none select-none border-t border-t-transparent bg-magnum-800/10 p-px"
+	>
+		<div use:melt={$thumbX} class="relative rounded-full bg-magnum-600" />
+	</div>
 	<div use:melt={$corner} />
 </div>
diff --git a/src/lib/builders/scroll-area/scrollbars.ts b/src/lib/builders/scroll-area/scrollbars.ts
index bbab8be6..cc724a0a 100644
--- a/src/lib/builders/scroll-area/scrollbars.ts
+++ b/src/lib/builders/scroll-area/scrollbars.ts
@@ -189,7 +189,12 @@ export function createHoverScrollbarAction(state: ScrollAreaState) {
 	function handlePointerEnter() {
 		window.clearTimeout(timeout);
 		if (scrollbarState.isVisible.get()) return;
-		scrollbarState.isVisible.set(true);
+		const $viewportEl = rootState.viewportEl.get();
+		if (!$viewportEl) return;
+		const isOverflowX = $viewportEl.offsetWidth < $viewportEl.scrollWidth;
+		const isOverflowY = $viewportEl.offsetHeight < $viewportEl.scrollHeight;
+
+		scrollbarState.isVisible.set(scrollbarState.isHorizontal.get() ? isOverflowX : isOverflowY);
 	}
 
 	function handlePointerLeave() {
@@ -347,9 +352,9 @@ export function createScrollbarX(state: ScrollAreaState, createAction: CreateScr
 					position: 'absolute',
 					bottom: 0,
 					left: $dir === 'rtl' ? 'var(--melt-scroll-area-corner-width)' : 0,
-					right: $dir === 'ltr' ? 'var(--melt-scroll-area-corner-width' : 0,
-					'--melt-scroll-area-thumb-width': $sizes ? `${getThumbSize($sizes)}px` : undefined,
-					display: !$isVisible ? 'none' : undefined,
+					right: $dir === 'ltr' ? 'var(--melt-scroll-area-corner-width)' : 0,
+					'--melt-scroll-area-thumb-width': `${getThumbSize($sizes)}px`,
+					visibility: !$isVisible ? 'hidden' : undefined,
 				}),
 				'data-state': $isVisible ? 'visible' : 'hidden',
 			};


Test patch:
diff --git a/src/tests/scroll-area/ScrollArea.spec.ts b/src/tests/scroll-area/ScrollArea.spec.ts
new file mode 100644
index 00000000..4b7a0c77
--- /dev/null
+++ b/src/tests/scroll-area/ScrollArea.spec.ts
@@ -0,0 +1,96 @@
+import { render, waitFor, fireEvent } from '@testing-library/svelte';
+import { describe, it } from 'vitest';
+import { userEvent } from '@testing-library/user-event';
+import ScrollAreaTest from './ScrollAreaTest.svelte';
+import type { CreateScrollAreaProps } from '$lib/index.js';
+
+function setup(
+	props?: CreateScrollAreaProps & {
+		height?: string;
+		width?: string;
+	}
+) {
+	const user = userEvent.setup();
+	const returned = render(ScrollAreaTest, { props });
+	const root = returned.getByTestId('root');
+	const scrollbarX = returned.getByTestId('scrollbar-x');
+	const scrollbarY = returned.getByTestId('scrollbar-y');
+	const content = returned.getByTestId('content');
+	const thumbX = returned.getByTestId('thumb-x');
+	const thumbY = returned.getByTestId('thumb-y');
+
+	return {
+		user,
+		elements: {
+			scrollbarX,
+			scrollbarY,
+			content,
+			thumbX,
+			thumbY,
+			root,
+		},
+		...returned,
+	};
+}
+
+describe('Scroll Area', () => {
+	it('Displays the scrollbars when `type` is "always"', async () => {
+		const { elements } = setup({
+			type: 'always',
+		});
+
+		expect(elements.scrollbarX).toBeVisible();
+		expect(elements.scrollbarY).toBeVisible();
+	});
+
+	it('Displays the x scrollbar when `type` is "auto" and content overflows', async () => {
+		const { elements } = setup({
+			type: 'always',
+			width: '10px',
+		});
+
+		expect(elements.scrollbarX).toBeVisible();
+	});
+
+	it('Displays the y scrollbar when `type` is "auto" and content overflows', async () => {
+		const { elements } = setup({
+			type: 'always',
+			height: '10px',
+		});
+
+		expect(elements.scrollbarY).toBeVisible();
+	});
+
+	it("Doesn't display the scrollbars when `type` is 'auto' and content doesn't overflow", async () => {
+		const { elements } = setup({
+			type: 'auto',
+			width: '5000px',
+			height: '5000px',
+		});
+
+		await waitFor(() => expect(elements.scrollbarX).not.toBeVisible());
+		await waitFor(() => expect(elements.scrollbarY).not.toBeVisible());
+	});
+
+	// I hate vitest/testing library sometimes.
+	it.skip('Displays the scrollbars on hover', async () => {
+		const { elements } = setup({
+			type: 'hover',
+			width: '10px',
+			height: '10px',
+		});
+
+		await waitFor(() => expect(elements.scrollbarX).not.toBeVisible());
+		await waitFor(() => expect(elements.scrollbarY).not.toBeVisible());
+
+		await fireEvent.pointerEnter(elements.root);
+
+		await waitFor(() => expect(elements.scrollbarX).toBeVisible());
+		await waitFor(() => expect(elements.scrollbarY).toBeVisible());
+
+		await fireEvent.pointerLeave(elements.root);
+
+		await waitFor(() => expect(elements.scrollbarX).not.toBeVisible());
+		await waitFor(() => expect(elements.scrollbarY).not.toBeVisible());
+	});
+});
diff --git a/src/tests/scroll-area/ScrollAreaTest.svelte b/src/tests/scroll-area/ScrollAreaTest.svelte
new file mode 100644
index 00000000..b8226e6e
--- /dev/null
+++ b/src/tests/scroll-area/ScrollAreaTest.svelte
@@ -0,0 +1,99 @@
+<script lang="ts">
+	import { createScrollArea, melt, type CreateScrollAreaProps } from '$lib/index.js';
+	import { removeUndefined } from '../utils.js';
+
+	const flavors = [
+		'Vanilla',
+		'Chocolate',
+		'Strawberry',
+		'Mint Chocolate Chip',
+		'Cookies and Cream',
+		'Rocky Road',
+		'Pistachio',
+		'Neapolitan',
+		'Butter Pecan',
+		'Salted Caramel',
+		'Coffee',
+		'Mango',
+		'Raspberry Ripple',
+		'Lemon Sorbet',
+		'Green Tea',
+		'Coconut',
+		'Black Cherry',
+		'Banana',
+		'Almond Fudge',
+		'Cinnamon',
+		'Blueberry Cheesecake',
+		'Tiramisu',
+		'Red Velvet',
+		'Matcha',
+		'Peanut Butter Cup',
+		'Cookie Dough',
+		'Rum Raisin',
+		'Birthday Cake',
+		'Lychee',
+		'Honey Lavender',
+	];
+
+	type $$Props = CreateScrollAreaProps & {
+		height?: string;
+		width?: string;
+	};
+
+	export let height = '288px';
+	export let width = '490px';
+
+	export let type: $$Props['type'] = 'hover';
+
+	const {
+		elements: { root, content, viewport, corner, scrollbarY, thumbY, scrollbarX, thumbX },
+	} = createScrollArea(
+		removeUndefined({
+			type,
+			...$$restProps,
+		})
+	);
+</script>
+
+<div
+	use:melt={$root}
+	class="relative overflow-hidden rounded-md border bg-white text-magnum-900 shadow-lg"
+	data-testid="root"
+	style:width
+	style:height
+>
+	<div use:melt={$viewport} class="h-full w-full rounded-[inherit]" data-testid="viewport">
+		<div use:melt={$content} data-testid="content">
+			<div class="w-[300px] p-4" data-testid="inner-content">
+				<h4 class="mb-4 font-semibold leading-none">Endless Flavors</h4>
+				<div data-testid="hover">Hover me please</div>
+				{#each flavors as flavor (flavor)}
+					<div class="text-sm">
+						{flavor}
+					</div>
+					<div role="separator" class="my-2 h-px w-full bg-magnum-600" />
+				{/each}
+			</div>
+		</div>
+	</div>
+	<div
+		use:melt={$scrollbarY}
+		data-testid="scrollbar-y"
+		class="flex h-full w-2.5 border-l border-l-transparent bg-magnum-800/10 p-px"
+	>
+		<div
+			use:melt={$thumbY}
+			data-testid="thumb-y"
+			class="relative flex-1 rounded-full bg-magnum-600"
+		/>
+	</div>
+
+	<div
+		use:melt={$scrollbarX}
+		data-testid="scrollbar-x"
+		class="flex h-full w-2.5 border-l border-l-transparent bg-magnum-800/10 p-px"
+	>
+		<div use:melt={$thumbX} data-testid="thumb-x" class="relative rounded-full bg-magnum-600" />
+	</div>
+	<div use:melt={$corner} />
+</div>