From fb02c2366c9126886ef1eec731954bd3846076f2 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Fri, 26 Jul 2024 11:27:32 -0400 Subject: [PATCH] scroll area work --- .../bits/scroll-area/scroll-area.svelte.ts | 77 +++++++++++-------- sites/docs/content/components/scroll-area.md | 14 +++- .../demos/scroll-area-demo-custom.svelte | 8 +- .../components/demos/scroll-area-demo.svelte | 8 +- 4 files changed, 61 insertions(+), 46 deletions(-) diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index 7bea0adb7..fd1ad913d 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -222,32 +222,31 @@ class ScrollAreaScrollbarHoverState { const scrollAreaNode = this.root.scrollAreaNode; const hideDelay = this.root.scrollHideDelay.current; let hideTimer = 0; - if (scrollAreaNode) { - const handlePointerEnter = () => { - window.clearTimeout(hideTimer); - untrack(() => (this.isVisible = true)); - }; - - const handlePointerLeave = () => { - if (hideTimer) window.clearTimeout(hideTimer); - hideTimer = window.setTimeout(() => { - untrack(() => { - this.scrollbar.hasThumb = false; - this.isVisible = false; - }); - }, hideDelay); - }; - - const unsubListeners = executeCallbacks( - addEventListener(scrollAreaNode, "pointerenter", handlePointerEnter), - addEventListener(scrollAreaNode, "pointerleave", handlePointerLeave) - ); + if (!scrollAreaNode) return; + const handlePointerEnter = () => { + window.clearTimeout(hideTimer); + untrack(() => (this.isVisible = true)); + }; - return () => { - window.clearTimeout(hideTimer); - unsubListeners(); - }; - } + const handlePointerLeave = () => { + if (hideTimer) window.clearTimeout(hideTimer); + hideTimer = window.setTimeout(() => { + untrack(() => { + this.scrollbar.hasThumb = false; + this.isVisible = false; + }); + }, hideDelay); + }; + + const unsubListeners = executeCallbacks( + addEventListener(scrollAreaNode, "pointerenter", handlePointerEnter), + addEventListener(scrollAreaNode, "pointerleave", handlePointerLeave) + ); + + return () => { + window.clearTimeout(hideTimer); + unsubListeners(); + }; }); } @@ -382,6 +381,7 @@ class ScrollAreaScrollbarVisibleState { }); thumbRatio = $derived.by(() => getThumbRatio(this.sizes.viewport, this.sizes.content)); hasThumb = $derived.by(() => Boolean(this.thumbRatio > 0 && this.thumbRatio < 1)); + prevTransformStyle = ""; constructor(scrollbar: ScrollAreaScrollbarState) { this.scrollbar = scrollbar; @@ -390,6 +390,12 @@ class ScrollAreaScrollbarVisibleState { $effect(() => { this.scrollbar.hasThumb = this.hasThumb; }); + + $effect.pre(() => { + if (!this.scrollbar.hasThumb && this.thumbNode) { + this.prevTransformStyle = this.thumbNode.style.transform; + } + }); } setSizes = (sizes: Sizes) => { @@ -421,7 +427,8 @@ class ScrollAreaScrollbarVisibleState { sizes: this.sizes, dir: this.root.dir.current, }); - this.thumbNode.style.transform = `translate3d(${offset}px, 0, 0)`; + const transformStyle = `translate3d(${offset}px, 0, 0)`; + this.thumbNode.style.transform = transformStyle; }; xOnWheelScroll = (scrollPos: number) => { @@ -441,7 +448,8 @@ class ScrollAreaScrollbarVisibleState { if (!(this.root.viewportNode && this.thumbNode)) return; const scrollPos = this.root.viewportNode.scrollTop; const offset = getThumbOffsetFromScroll({ scrollPos, sizes: this.sizes }); - this.thumbNode.style.transform = `translate3d(0, ${offset}px, 0)`; + const transformStyle = `translate3d(0, ${offset}px, 0)`; + this.thumbNode.style.transform = transformStyle; }; yOnWheelScroll = (scrollPos: number) => { @@ -451,7 +459,10 @@ class ScrollAreaScrollbarVisibleState { yOnDragScroll = (pointerPos: number) => { if (!this.root.viewportNode) return; - this.root.viewportNode.scrollTop = this.getScrollPosition(pointerPos); + this.root.viewportNode.scrollTop = this.getScrollPosition( + pointerPos, + this.root.dir.current + ); }; createScrollbarX(props: ScrollbarAxisStateProps) { @@ -554,8 +565,7 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { }; thumbSize = $derived.by(() => { - const ts = getThumbSize(this.scrollbarVis.sizes); - return ts; + return getThumbSize(this.scrollbarVis.sizes); }); props = $derived.by( @@ -731,6 +741,10 @@ class ScrollAreaScrollbarSharedState { untrack(() => this.handleThumbPositionChange()); }); + $effect(() => { + this.handleThumbPositionChange(); + }); + useResizeObserver(() => this.scrollbarState.ref.current, this.handleResize); useResizeObserver(() => this.root.contentNode, this.handleResize); } @@ -854,7 +868,7 @@ class ScrollAreaThumbImplState { this.#scrollbarState.handleThumbPointerDown({ x, y }); }; - #onpointerup = (e: PointerEvent) => { + #onpointerup = () => { this.#scrollbarState.handleThumbPointerUp(); }; @@ -866,6 +880,7 @@ class ScrollAreaThumbImplState { style: { width: "var(--bits-scroll-area-thumb-width)", height: "var(--bits-scroll-area-thumb-height)", + transform: this.#scrollbarState.scrollbarVis.prevTransformStyle, }, onpointerdowncapture: this.#onpointerdowncapture, onpointerup: this.#onpointerup, diff --git a/sites/docs/content/components/scroll-area.md b/sites/docs/content/components/scroll-area.md index a12c8b03c..b6919ec5e 100644 --- a/sites/docs/content/components/scroll-area.md +++ b/sites/docs/content/components/scroll-area.md @@ -89,6 +89,14 @@ We'll use this custom component in the following examples to demonstrate how to The `hover` type is the default type of the scroll area, demonstrated in the featured example above. It only shows scrollbars when the user hovers over the scroll area and the content is larger than the viewport. +```svelte {1} + + + +``` + + + ### Scroll The `scroll` type displays the scrollbars when the user scrolls the content. This is similar to the behavior of MacOS. @@ -115,15 +123,15 @@ The `auto` type behaves similarly to your typical browser scrollbars. When the c ### Always -The `always` type behaves as if you set `overflow: scroll` on the scroll area. Scrollbars will always be visible, even when the content is smaller than the viewport. +The `always` type behaves as if you set `overflow: scroll` on the scroll area. Scrollbars will always be visible, even when the content is smaller than the viewport. We've also set the `orientation` prop on the `CustomScrollArea` to `'both'` to ensure both scrollbars are rendered. ```svelte {1} - + ``` - + ## Customizing the Hide Delay diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo-custom.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo-custom.svelte index ded210e3a..24d313262 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo-custom.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo-custom.svelte @@ -23,18 +23,14 @@ {orientation} class="flex w-2.5 touch-none select-none rounded-full border-l border-l-transparent bg-muted p-px transition-all duration-200 hover:w-3 hover:bg-dark-10 data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out-0 data-[state=visible]:fade-in-0" > - + {:else} - + {/if} {/snippet} diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte index ff5fe9529..4a651521d 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte @@ -22,17 +22,13 @@ orientation="vertical" class="flex w-2.5 touch-none select-none rounded-full border-l border-l-transparent bg-muted p-px transition-all duration-200 hover:w-3 hover:bg-dark-10 data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out-0 data-[state=visible]:fade-in-0" > - + - +