INIT
This commit is contained in:
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { ScrollAreaCornerProps } from "../types.js";
|
||||
import { ScrollAreaCornerImplState } from "../scroll-area.svelte.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: Omit<ScrollAreaCornerProps, "id"> & {
|
||||
id: string;
|
||||
} = $props();
|
||||
|
||||
const cornerState = ScrollAreaCornerImplState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, cornerState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import type { ScrollAreaCornerProps } from "../types.js";
|
||||
type $$ComponentProps = Omit<ScrollAreaCornerProps, "id"> & {
|
||||
id: string;
|
||||
};
|
||||
declare const ScrollAreaCornerImpl: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
||||
type ScrollAreaCornerImpl = ReturnType<typeof ScrollAreaCornerImpl>;
|
||||
export default ScrollAreaCornerImpl;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type { ScrollAreaCornerProps } from "../types.js";
|
||||
import { ScrollAreaRootContext } from "../scroll-area.svelte.js";
|
||||
import ScrollAreaCornerImpl from "./scroll-area-corner-impl.svelte";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
...restProps
|
||||
}: ScrollAreaCornerProps = $props();
|
||||
|
||||
const scrollAreaState = ScrollAreaRootContext.get();
|
||||
|
||||
const hasBothScrollbarsVisible = $derived(
|
||||
Boolean(scrollAreaState.scrollbarXNode && scrollAreaState.scrollbarYNode)
|
||||
);
|
||||
const hasCorner = $derived(
|
||||
scrollAreaState.opts.type.current !== "scroll" && hasBothScrollbarsVisible
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if hasCorner}
|
||||
<ScrollAreaCornerImpl {...restProps} {id} bind:ref />
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { ScrollAreaCornerProps } from "../types.js";
|
||||
declare const ScrollAreaCorner: import("svelte").Component<ScrollAreaCornerProps, {}, "ref">;
|
||||
type ScrollAreaCorner = ReturnType<typeof ScrollAreaCorner>;
|
||||
export default ScrollAreaCorner;
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from "svelte-toolbelt";
|
||||
import { ScrollAreaScrollbarAutoState } from "../scroll-area.svelte.js";
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
import ScrollAreaScrollbarVisible from "./scroll-area-scrollbar-visible.svelte";
|
||||
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
||||
|
||||
let { forceMount = false, ...restProps }: _ScrollbarStubProps = $props();
|
||||
|
||||
const scrollbarAutoState = ScrollAreaScrollbarAutoState.create();
|
||||
const mergedProps = $derived(mergeProps(restProps, scrollbarAutoState.props));
|
||||
</script>
|
||||
|
||||
<PresenceLayer
|
||||
open={forceMount || scrollbarAutoState.isVisible}
|
||||
ref={scrollbarAutoState.scrollbar.opts.ref}
|
||||
>
|
||||
{#snippet presence()}
|
||||
<ScrollAreaScrollbarVisible {...mergedProps} />
|
||||
{/snippet}
|
||||
</PresenceLayer>
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
declare const ScrollAreaScrollbarAuto: import("svelte").Component<_ScrollbarStubProps, {}, "">;
|
||||
type ScrollAreaScrollbarAuto = ReturnType<typeof ScrollAreaScrollbarAuto>;
|
||||
export default ScrollAreaScrollbarAuto;
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from "svelte-toolbelt";
|
||||
import {
|
||||
ScrollAreaScrollbarAutoState,
|
||||
ScrollAreaScrollbarHoverState,
|
||||
} from "../scroll-area.svelte.js";
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
import ScrollAreaScrollbarVisible from "./scroll-area-scrollbar-visible.svelte";
|
||||
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
||||
|
||||
let { forceMount = false, ...restProps }: _ScrollbarStubProps = $props();
|
||||
|
||||
const scrollbarHoverState = ScrollAreaScrollbarHoverState.create();
|
||||
const scrollbarAutoState = ScrollAreaScrollbarAutoState.create();
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, scrollbarHoverState.props, scrollbarAutoState.props, {
|
||||
"data-state": scrollbarHoverState.isVisible ? "visible" : "hidden",
|
||||
})
|
||||
);
|
||||
|
||||
const open = $derived(
|
||||
forceMount || (scrollbarHoverState.isVisible && scrollbarAutoState.isVisible)
|
||||
);
|
||||
</script>
|
||||
|
||||
<PresenceLayer {open} ref={scrollbarAutoState.scrollbar.opts.ref}>
|
||||
{#snippet presence()}
|
||||
<ScrollAreaScrollbarVisible {...mergedProps} />
|
||||
{/snippet}
|
||||
</PresenceLayer>
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
declare const ScrollAreaScrollbarHover: import("svelte").Component<_ScrollbarStubProps, {}, "">;
|
||||
type ScrollAreaScrollbarHover = ReturnType<typeof ScrollAreaScrollbarHover>;
|
||||
export default ScrollAreaScrollbarHover;
|
||||
Generated
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from "svelte-toolbelt";
|
||||
import { ScrollAreaScrollbarScrollState } from "../scroll-area.svelte.js";
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
import ScrollAreaScrollbarVisible from "./scroll-area-scrollbar-visible.svelte";
|
||||
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
||||
|
||||
let { forceMount = false, ...restProps }: _ScrollbarStubProps = $props();
|
||||
|
||||
const scrollbarScrollState = ScrollAreaScrollbarScrollState.create();
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, scrollbarScrollState.props));
|
||||
</script>
|
||||
|
||||
<PresenceLayer
|
||||
{...mergedProps}
|
||||
open={forceMount || !scrollbarScrollState.isHidden}
|
||||
ref={scrollbarScrollState.scrollbar.opts.ref}
|
||||
>
|
||||
{#snippet presence()}
|
||||
<ScrollAreaScrollbarVisible {...mergedProps} />
|
||||
{/snippet}
|
||||
</PresenceLayer>
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
declare const ScrollAreaScrollbarScroll: import("svelte").Component<_ScrollbarStubProps, {}, "">;
|
||||
type ScrollAreaScrollbarScroll = ReturnType<typeof ScrollAreaScrollbarScroll>;
|
||||
export default ScrollAreaScrollbarScroll;
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from "svelte-toolbelt";
|
||||
import { ScrollAreaScrollbarSharedState } from "../scroll-area.svelte.js";
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
|
||||
let { child, children, ...restProps }: _ScrollbarStubProps = $props();
|
||||
|
||||
const scrollbarSharedState = ScrollAreaScrollbarSharedState.create();
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, scrollbarSharedState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
declare const ScrollAreaScrollbarShared: import("svelte").Component<_ScrollbarStubProps, {}, "">;
|
||||
type ScrollAreaScrollbarShared = ReturnType<typeof ScrollAreaScrollbarShared>;
|
||||
export default ScrollAreaScrollbarShared;
|
||||
Generated
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { ScrollAreaScrollbarVisibleState } from "../scroll-area.svelte.js";
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
import ScrollAreaScrollbarX from "./scroll-area-scrollbar-x.svelte";
|
||||
import ScrollAreaScrollbarY from "./scroll-area-scrollbar-y.svelte";
|
||||
|
||||
let { ...restProps }: Omit<_ScrollbarStubProps, "forceMount"> = $props();
|
||||
|
||||
const scrollbarVisibleState = ScrollAreaScrollbarVisibleState.create();
|
||||
</script>
|
||||
|
||||
{#if scrollbarVisibleState.scrollbar.opts.orientation.current === "horizontal"}
|
||||
<ScrollAreaScrollbarX {...restProps} />
|
||||
{:else}
|
||||
<ScrollAreaScrollbarY {...restProps} />
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
declare const ScrollAreaScrollbarVisible: import("svelte").Component<Omit<_ScrollbarStubProps, "forceMount">, {}, "">;
|
||||
type ScrollAreaScrollbarVisible = ReturnType<typeof ScrollAreaScrollbarVisible>;
|
||||
export default ScrollAreaScrollbarVisible;
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { IsMounted } from "runed";
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { ScrollAreaScrollbarXState } from "../scroll-area.svelte.js";
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
import ScrollAreaScrollbarShared from "./scroll-area-scrollbar-shared.svelte";
|
||||
|
||||
let { ...restProps }: _ScrollbarStubProps = $props();
|
||||
|
||||
const isMounted = new IsMounted();
|
||||
|
||||
const scrollbarXState = ScrollAreaScrollbarXState.create({
|
||||
mounted: boxWith(() => isMounted.current),
|
||||
});
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
const mergedProps = $derived(mergeProps(restProps, scrollbarXState.props)) as any;
|
||||
</script>
|
||||
|
||||
<ScrollAreaScrollbarShared {...mergedProps} />
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
declare const ScrollAreaScrollbarX: import("svelte").Component<_ScrollbarStubProps, {}, "">;
|
||||
type ScrollAreaScrollbarX = ReturnType<typeof ScrollAreaScrollbarX>;
|
||||
export default ScrollAreaScrollbarX;
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { IsMounted } from "runed";
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { ScrollAreaScrollbarYState } from "../scroll-area.svelte.js";
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
import ScrollAreaScrollbarShared from "./scroll-area-scrollbar-shared.svelte";
|
||||
|
||||
let { ...restProps }: _ScrollbarStubProps = $props();
|
||||
|
||||
const isMounted = new IsMounted();
|
||||
|
||||
const scrollbarYState = ScrollAreaScrollbarYState.create({
|
||||
mounted: boxWith(() => isMounted.current),
|
||||
});
|
||||
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
const mergedProps = $derived(mergeProps(restProps, scrollbarYState.props)) as any;
|
||||
</script>
|
||||
|
||||
<ScrollAreaScrollbarShared {...mergedProps} />
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { _ScrollbarStubProps } from "../types.js";
|
||||
declare const ScrollAreaScrollbarY: import("svelte").Component<_ScrollbarStubProps, {}, "">;
|
||||
type ScrollAreaScrollbarY = ReturnType<typeof ScrollAreaScrollbarY>;
|
||||
export default ScrollAreaScrollbarY;
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { boxWith } from "svelte-toolbelt";
|
||||
import type { ScrollAreaScrollbarProps } from "../types.js";
|
||||
import { ScrollAreaScrollbarState } from "../scroll-area.svelte.js";
|
||||
import ScrollAreaScrollbarAuto from "./scroll-area-scrollbar-auto.svelte";
|
||||
import ScrollAreaScrollbarScroll from "./scroll-area-scrollbar-scroll.svelte";
|
||||
import ScrollAreaScrollbarHover from "./scroll-area-scrollbar-hover.svelte";
|
||||
import ScrollAreaScrollbarVisible from "./scroll-area-scrollbar-visible.svelte";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
orientation,
|
||||
...restProps
|
||||
}: ScrollAreaScrollbarProps = $props();
|
||||
|
||||
const scrollbarState = ScrollAreaScrollbarState.create({
|
||||
orientation: boxWith(() => orientation),
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const type = $derived(scrollbarState.root.opts.type.current);
|
||||
</script>
|
||||
|
||||
{#if type === "hover"}
|
||||
<ScrollAreaScrollbarHover {...restProps} {id} />
|
||||
{:else if type === "scroll"}
|
||||
<ScrollAreaScrollbarScroll {...restProps} {id} />
|
||||
{:else if type === "auto"}
|
||||
<ScrollAreaScrollbarAuto {...restProps} {id} />
|
||||
{:else if type === "always"}
|
||||
<ScrollAreaScrollbarVisible {...restProps} {id} />
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { ScrollAreaScrollbarProps } from "../types.js";
|
||||
declare const ScrollAreaScrollbar: import("svelte").Component<ScrollAreaScrollbarProps, {}, "ref">;
|
||||
type ScrollAreaScrollbar = ReturnType<typeof ScrollAreaScrollbar>;
|
||||
export default ScrollAreaScrollbar;
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { IsMounted } from "runed";
|
||||
import type { ScrollAreaThumbProps } from "../types.js";
|
||||
import { ScrollAreaThumbImplState } from "../scroll-area.svelte.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id,
|
||||
child,
|
||||
children,
|
||||
present,
|
||||
...restProps
|
||||
}: Omit<ScrollAreaThumbProps, "forceMount" | "id"> & {
|
||||
id: string;
|
||||
present: boolean;
|
||||
} = $props();
|
||||
|
||||
const isMounted = new IsMounted();
|
||||
|
||||
const thumbState = ScrollAreaThumbImplState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
mounted: boxWith(() => isMounted.current),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, thumbState.props, {
|
||||
style: {
|
||||
hidden: !present,
|
||||
},
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import type { ScrollAreaThumbProps } from "../types.js";
|
||||
type $$ComponentProps = Omit<ScrollAreaThumbProps, "forceMount" | "id"> & {
|
||||
id: string;
|
||||
present: boolean;
|
||||
};
|
||||
declare const ScrollAreaThumbImpl: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
||||
type ScrollAreaThumbImpl = ReturnType<typeof ScrollAreaThumbImpl>;
|
||||
export default ScrollAreaThumbImpl;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { ScrollAreaThumbProps } from "../types.js";
|
||||
import { ScrollAreaScrollbarVisibleContext } from "../scroll-area.svelte.js";
|
||||
import ScrollAreaThumbImpl from "./scroll-area-thumb-impl.svelte";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
forceMount = false,
|
||||
...restProps
|
||||
}: ScrollAreaThumbProps = $props();
|
||||
|
||||
const scrollbarState = ScrollAreaScrollbarVisibleContext.get();
|
||||
</script>
|
||||
|
||||
<PresenceLayer open={forceMount || scrollbarState.hasThumb} ref={scrollbarState.scrollbar.opts.ref}>
|
||||
{#snippet presence({ present })}
|
||||
<ScrollAreaThumbImpl {...restProps} {id} bind:ref {present} />
|
||||
{/snippet}
|
||||
</PresenceLayer>
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { ScrollAreaThumbProps } from "../types.js";
|
||||
declare const ScrollAreaThumb: import("svelte").Component<ScrollAreaThumbProps, {}, "ref">;
|
||||
type ScrollAreaThumb = ReturnType<typeof ScrollAreaThumb>;
|
||||
export default ScrollAreaThumb;
|
||||
Generated
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { ScrollAreaViewportProps } from "../types.js";
|
||||
import { ScrollAreaViewportState } from "../scroll-area.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
children,
|
||||
...restProps
|
||||
}: ScrollAreaViewportProps = $props();
|
||||
|
||||
const viewportState = ScrollAreaViewportState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, viewportState.props));
|
||||
const mergedContentProps = $derived(mergeProps({}, viewportState.contentProps));
|
||||
</script>
|
||||
|
||||
<div {...mergedProps}>
|
||||
<div {...mergedContentProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Hide scrollbars cross browser and enable momentum scroll for touch devices */
|
||||
:global([data-scroll-area-viewport]) {
|
||||
scrollbar-width: none !important;
|
||||
-ms-overflow-style: none !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
:global([data-scroll-area-viewport])::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:global(:where([data-scroll-area-viewport])) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
:global(:where([data-scroll-area-content])) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { ScrollAreaViewportProps } from "../types.js";
|
||||
declare const ScrollAreaViewport: import("svelte").Component<ScrollAreaViewportProps, {}, "ref">;
|
||||
type ScrollAreaViewport = ReturnType<typeof ScrollAreaViewport>;
|
||||
export default ScrollAreaViewport;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { ScrollAreaRootProps } from "../types.js";
|
||||
import { ScrollAreaRootState } from "../scroll-area.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
type = "hover",
|
||||
dir = "ltr",
|
||||
scrollHideDelay = 600,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: ScrollAreaRootProps = $props();
|
||||
|
||||
const rootState = ScrollAreaRootState.create({
|
||||
type: boxWith(() => type),
|
||||
dir: boxWith(() => dir),
|
||||
scrollHideDelay: boxWith(() => scrollHideDelay),
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, rootState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { ScrollAreaRootProps } from "../types.js";
|
||||
declare const ScrollArea: import("svelte").Component<ScrollAreaRootProps, {}, "ref">;
|
||||
type ScrollArea = ReturnType<typeof ScrollArea>;
|
||||
export default ScrollArea;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export { default as Root } from "./components/scroll-area.svelte";
|
||||
export { default as Viewport } from "./components/scroll-area-viewport.svelte";
|
||||
export { default as Scrollbar } from "./components/scroll-area-scrollbar.svelte";
|
||||
export { default as Thumb } from "./components/scroll-area-thumb.svelte";
|
||||
export { default as Corner } from "./components/scroll-area-corner.svelte";
|
||||
export type { ScrollAreaRootProps as RootProps, ScrollAreaViewportProps as ViewportProps, ScrollAreaScrollbarProps as ScrollbarProps, ScrollAreaThumbProps as ThumbProps, ScrollAreaCornerProps as CornerProps, } from "./types.js";
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export { default as Root } from "./components/scroll-area.svelte";
|
||||
export { default as Viewport } from "./components/scroll-area-viewport.svelte";
|
||||
export { default as Scrollbar } from "./components/scroll-area-scrollbar.svelte";
|
||||
export { default as Thumb } from "./components/scroll-area-thumb.svelte";
|
||||
export { default as Corner } from "./components/scroll-area-corner.svelte";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as ScrollArea from "./exports.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as ScrollArea from "./exports.js";
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* This logic is adapted from Radix UI ScrollArea component.
|
||||
* https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/src/ScrollArea.tsx
|
||||
* Credit to Jenna Smith (@jjenzz) for the original implementation.
|
||||
* Incredible thought must have went into solving all the intricacies of this component.
|
||||
*/
|
||||
import { Context } from "runed";
|
||||
import { DOMContext, type ReadableBoxedValues } from "svelte-toolbelt";
|
||||
import type { ScrollAreaType } from "./types.js";
|
||||
import type { BitsPointerEvent, RefAttachment, WithRefOpts } from "../../internal/types.js";
|
||||
import { type Direction, type Orientation } from "../../shared/index.js";
|
||||
import { StateMachine } from "../../internal/state-machine.js";
|
||||
export declare const ScrollAreaRootContext: Context<ScrollAreaRootState>;
|
||||
export declare const ScrollAreaScrollbarContext: Context<ScrollAreaScrollbarState>;
|
||||
export declare const ScrollAreaScrollbarVisibleContext: Context<ScrollAreaScrollbarVisibleState>;
|
||||
export declare const ScrollAreaScrollbarAxisContext: Context<ScrollbarAxis>;
|
||||
export declare const ScrollAreaScrollbarSharedContext: Context<ScrollAreaScrollbarSharedState>;
|
||||
interface Sizes {
|
||||
content: number;
|
||||
viewport: number;
|
||||
scrollbar: {
|
||||
size: number;
|
||||
paddingStart: number;
|
||||
paddingEnd: number;
|
||||
};
|
||||
}
|
||||
interface ScrollAreaRootStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
dir: Direction;
|
||||
type: ScrollAreaType;
|
||||
scrollHideDelay: number;
|
||||
}> {
|
||||
}
|
||||
export declare class ScrollAreaRootState {
|
||||
static create(opts: ScrollAreaRootStateOpts): ScrollAreaRootState;
|
||||
readonly opts: ScrollAreaRootStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
scrollAreaNode: HTMLElement | null;
|
||||
viewportNode: HTMLElement | null;
|
||||
contentNode: HTMLElement | null;
|
||||
scrollbarXNode: HTMLElement | null;
|
||||
scrollbarYNode: HTMLElement | null;
|
||||
cornerWidth: number;
|
||||
cornerHeight: number;
|
||||
scrollbarXEnabled: boolean;
|
||||
scrollbarYEnabled: boolean;
|
||||
domContext: DOMContext;
|
||||
constructor(opts: ScrollAreaRootStateOpts);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly dir: Direction;
|
||||
readonly style: {
|
||||
readonly position: "relative";
|
||||
readonly "--bits-scroll-area-corner-height": `${number}px`;
|
||||
readonly "--bits-scroll-area-corner-width": `${number}px`;
|
||||
};
|
||||
};
|
||||
}
|
||||
interface ScrollAreaViewportStateOpts extends WithRefOpts {
|
||||
}
|
||||
export declare class ScrollAreaViewportState {
|
||||
#private;
|
||||
static create(opts: ScrollAreaViewportStateOpts): ScrollAreaViewportState;
|
||||
readonly opts: ScrollAreaViewportStateOpts;
|
||||
readonly root: ScrollAreaRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly contentAttachment: RefAttachment;
|
||||
constructor(opts: ScrollAreaViewportStateOpts, root: ScrollAreaRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly style: {
|
||||
readonly overflowX: "scroll" | "hidden";
|
||||
readonly overflowY: "scroll" | "hidden";
|
||||
};
|
||||
};
|
||||
readonly contentProps: {
|
||||
readonly id: string;
|
||||
readonly "data-scroll-area-content": "";
|
||||
/**
|
||||
* When horizontal scrollbar is visible: this element should be at least
|
||||
* as wide as its children for size calculations to work correctly.
|
||||
*
|
||||
* When horizontal scrollbar is NOT visible: this element's width should
|
||||
* be constrained by the parent container to enable `text-overflow: ellipsis`
|
||||
*/
|
||||
readonly style: {
|
||||
readonly minWidth: "fit-content" | undefined;
|
||||
};
|
||||
};
|
||||
}
|
||||
interface ScrollAreaScrollbarStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
orientation: Orientation;
|
||||
}> {
|
||||
}
|
||||
export declare class ScrollAreaScrollbarState {
|
||||
static create(opts: ScrollAreaScrollbarStateOpts): ScrollAreaScrollbarState;
|
||||
readonly opts: ScrollAreaScrollbarStateOpts;
|
||||
readonly root: ScrollAreaRootState;
|
||||
readonly isHorizontal: boolean;
|
||||
hasThumb: boolean;
|
||||
constructor(opts: ScrollAreaScrollbarStateOpts, root: ScrollAreaRootState);
|
||||
}
|
||||
export declare class ScrollAreaScrollbarHoverState {
|
||||
static create(): ScrollAreaScrollbarHoverState;
|
||||
readonly scrollbar: ScrollAreaScrollbarState;
|
||||
root: ScrollAreaRootState;
|
||||
isVisible: boolean;
|
||||
constructor(scrollbar: ScrollAreaScrollbarState);
|
||||
readonly props: {
|
||||
readonly "data-state": "hidden" | "visible";
|
||||
};
|
||||
}
|
||||
export declare class ScrollAreaScrollbarScrollState {
|
||||
static create(): ScrollAreaScrollbarScrollState;
|
||||
readonly scrollbar: ScrollAreaScrollbarState;
|
||||
readonly root: ScrollAreaRootState;
|
||||
readonly machine: StateMachine<{
|
||||
hidden: {
|
||||
SCROLL: "scrolling";
|
||||
};
|
||||
scrolling: {
|
||||
SCROLL_END: "idle";
|
||||
POINTER_ENTER: "interacting";
|
||||
};
|
||||
interacting: {
|
||||
SCROLL: "interacting";
|
||||
POINTER_LEAVE: "idle";
|
||||
};
|
||||
idle: {
|
||||
HIDE: "hidden";
|
||||
SCROLL: "scrolling";
|
||||
POINTER_ENTER: "interacting";
|
||||
};
|
||||
}>;
|
||||
readonly isHidden: boolean;
|
||||
constructor(scrollbar: ScrollAreaScrollbarState);
|
||||
onpointerenter(_: BitsPointerEvent): void;
|
||||
onpointerleave(_: BitsPointerEvent): void;
|
||||
readonly props: {
|
||||
readonly "data-state": "hidden" | "visible";
|
||||
readonly onpointerenter: (_: BitsPointerEvent) => void;
|
||||
readonly onpointerleave: (_: BitsPointerEvent) => void;
|
||||
};
|
||||
}
|
||||
export declare class ScrollAreaScrollbarAutoState {
|
||||
static create(): ScrollAreaScrollbarAutoState;
|
||||
readonly scrollbar: ScrollAreaScrollbarState;
|
||||
readonly root: ScrollAreaRootState;
|
||||
isVisible: boolean;
|
||||
constructor(scrollbar: ScrollAreaScrollbarState);
|
||||
readonly props: {
|
||||
readonly "data-state": "hidden" | "visible";
|
||||
};
|
||||
}
|
||||
export declare class ScrollAreaScrollbarVisibleState {
|
||||
static create(): ScrollAreaScrollbarVisibleState;
|
||||
readonly scrollbar: ScrollAreaScrollbarState;
|
||||
readonly root: ScrollAreaRootState;
|
||||
thumbNode: HTMLElement | null;
|
||||
pointerOffset: number;
|
||||
sizes: Sizes;
|
||||
readonly thumbRatio: number;
|
||||
readonly hasThumb: boolean;
|
||||
prevTransformStyle: string;
|
||||
constructor(scrollbar: ScrollAreaScrollbarState);
|
||||
setSizes(sizes: Sizes): void;
|
||||
getScrollPosition(pointerPos: number, dir?: Direction): number;
|
||||
onThumbPointerUp(): void;
|
||||
onThumbPointerDown(pointerPos: number): void;
|
||||
xOnThumbPositionChange(): void;
|
||||
xOnWheelScroll(scrollPos: number): void;
|
||||
xOnDragScroll(pointerPos: number): void;
|
||||
yOnThumbPositionChange(): void;
|
||||
yOnWheelScroll(scrollPos: number): void;
|
||||
yOnDragScroll(pointerPos: number): void;
|
||||
}
|
||||
interface ScrollbarAxisStateOpts extends ReadableBoxedValues<{
|
||||
mounted: boolean;
|
||||
}> {
|
||||
}
|
||||
interface ScrollbarAxisState {
|
||||
onThumbPointerDown: (pointerPos: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => void;
|
||||
onDragScroll: (pointerPos: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => void;
|
||||
onWheelScroll: (e: WheelEvent, maxScrollPos: number) => void;
|
||||
onResize: () => void;
|
||||
onThumbPositionChange: () => void;
|
||||
onThumbPointerUp: () => void;
|
||||
props: {
|
||||
id: string;
|
||||
"data-orientation": "horizontal" | "vertical";
|
||||
style: Record<string, string | number | undefined>;
|
||||
};
|
||||
}
|
||||
export declare class ScrollAreaScrollbarXState implements ScrollbarAxisState {
|
||||
static create(opts: ScrollbarAxisStateOpts): ScrollbarAxis;
|
||||
readonly opts: ScrollbarAxisStateOpts;
|
||||
readonly scrollbarVis: ScrollAreaScrollbarVisibleState;
|
||||
readonly root: ScrollAreaRootState;
|
||||
readonly scrollbar: ScrollAreaScrollbarState;
|
||||
readonly attachment: RefAttachment;
|
||||
computedStyle: CSSStyleDeclaration | undefined;
|
||||
constructor(opts: ScrollbarAxisStateOpts, scrollbarVis: ScrollAreaScrollbarVisibleState);
|
||||
onThumbPointerDown: (pointerPos: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => void;
|
||||
onDragScroll: (pointerPos: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => void;
|
||||
onThumbPointerUp: () => void;
|
||||
onThumbPositionChange: () => void;
|
||||
onWheelScroll: (e: WheelEvent, maxScrollPos: number) => void;
|
||||
onResize: () => void;
|
||||
readonly thumbSize: number;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-orientation": "horizontal";
|
||||
readonly style: {
|
||||
readonly bottom: 0;
|
||||
readonly left: 0 | "var(--bits-scroll-area-corner-width)";
|
||||
readonly right: 0 | "var(--bits-scroll-area-corner-width)";
|
||||
readonly "--bits-scroll-area-thumb-width": `${number}px`;
|
||||
};
|
||||
};
|
||||
}
|
||||
export declare class ScrollAreaScrollbarYState implements ScrollbarAxisState {
|
||||
static create(opts: ScrollbarAxisStateOpts): ScrollbarAxis;
|
||||
readonly opts: ScrollbarAxisStateOpts;
|
||||
readonly scrollbarVis: ScrollAreaScrollbarVisibleState;
|
||||
readonly root: ScrollAreaRootState;
|
||||
readonly scrollbar: ScrollAreaScrollbarState;
|
||||
readonly attachment: RefAttachment;
|
||||
computedStyle: CSSStyleDeclaration | undefined;
|
||||
constructor(opts: ScrollbarAxisStateOpts, scrollbarVis: ScrollAreaScrollbarVisibleState);
|
||||
onThumbPointerDown(pointerPos: {
|
||||
x: number;
|
||||
y: number;
|
||||
}): void;
|
||||
onDragScroll(pointerPos: {
|
||||
x: number;
|
||||
y: number;
|
||||
}): void;
|
||||
onThumbPointerUp(): void;
|
||||
onThumbPositionChange(): void;
|
||||
onWheelScroll(e: WheelEvent, maxScrollPos: number): void;
|
||||
onResize(): void;
|
||||
readonly thumbSize: number;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-orientation": "vertical";
|
||||
readonly style: {
|
||||
readonly top: 0;
|
||||
readonly right: 0 | undefined;
|
||||
readonly left: 0 | undefined;
|
||||
readonly bottom: "var(--bits-scroll-area-corner-height)";
|
||||
readonly "--bits-scroll-area-thumb-height": `${number}px`;
|
||||
};
|
||||
};
|
||||
}
|
||||
type ScrollbarAxis = ScrollAreaScrollbarXState | ScrollAreaScrollbarYState;
|
||||
export declare class ScrollAreaScrollbarSharedState {
|
||||
#private;
|
||||
static create(): ScrollAreaScrollbarSharedState;
|
||||
readonly scrollbarState: ScrollbarAxis;
|
||||
readonly root: ScrollAreaRootState;
|
||||
readonly scrollbarVis: ScrollAreaScrollbarVisibleState;
|
||||
readonly scrollbar: ScrollAreaScrollbarState;
|
||||
rect: DOMRect | null;
|
||||
prevWebkitUserSelect: string;
|
||||
handleResize: () => void;
|
||||
handleThumbPositionChange: () => void;
|
||||
handleWheelScroll: (e: WheelEvent, maxScrollPos: number) => void;
|
||||
handleThumbPointerDown: (pointerPos: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => void;
|
||||
handleThumbPointerUp: () => void;
|
||||
readonly maxScrollPos: number;
|
||||
constructor(scrollbarState: ScrollbarAxis);
|
||||
handleDragScroll(e: PointerEvent): void;
|
||||
onpointerdown(e: BitsPointerEvent): void;
|
||||
onpointermove(e: BitsPointerEvent): void;
|
||||
onpointerup(e: BitsPointerEvent): void;
|
||||
onlostpointercapture(_: BitsPointerEvent): void;
|
||||
readonly props: never;
|
||||
}
|
||||
interface ScrollAreaThumbImplStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
mounted: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class ScrollAreaThumbImplState {
|
||||
#private;
|
||||
static create(opts: ScrollAreaThumbImplStateOpts): ScrollAreaThumbImplState;
|
||||
readonly opts: ScrollAreaThumbImplStateOpts;
|
||||
readonly scrollbarState: ScrollAreaScrollbarSharedState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: ScrollAreaThumbImplStateOpts, scrollbarState: ScrollAreaScrollbarSharedState);
|
||||
onpointerdowncapture(e: BitsPointerEvent): void;
|
||||
onpointerup(_: BitsPointerEvent): void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-state": "hidden" | "visible";
|
||||
readonly style: {
|
||||
readonly width: "var(--bits-scroll-area-thumb-width)";
|
||||
readonly height: "var(--bits-scroll-area-thumb-height)";
|
||||
readonly transform: string;
|
||||
};
|
||||
readonly onpointerdowncapture: (e: BitsPointerEvent) => void;
|
||||
readonly onpointerup: (_: BitsPointerEvent) => void;
|
||||
};
|
||||
}
|
||||
interface ScrollAreaCornerImplStateOpts extends WithRefOpts {
|
||||
}
|
||||
export declare class ScrollAreaCornerImplState {
|
||||
#private;
|
||||
static create(opts: ScrollAreaCornerImplStateOpts): ScrollAreaCornerImplState;
|
||||
readonly opts: ScrollAreaCornerImplStateOpts;
|
||||
readonly root: ScrollAreaRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly hasSize: boolean;
|
||||
constructor(opts: ScrollAreaCornerImplStateOpts, root: ScrollAreaRootState);
|
||||
readonly props: {
|
||||
id: string;
|
||||
style: {
|
||||
width: number;
|
||||
height: number;
|
||||
position: string;
|
||||
right: number | undefined;
|
||||
left: number | undefined;
|
||||
bottom: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
export {};
|
||||
+797
@@ -0,0 +1,797 @@
|
||||
/**
|
||||
* This logic is adapted from Radix UI ScrollArea component.
|
||||
* https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/src/ScrollArea.tsx
|
||||
* Credit to Jenna Smith (@jjenzz) for the original implementation.
|
||||
* Incredible thought must have went into solving all the intricacies of this component.
|
||||
*/
|
||||
import { Context, useDebounce, watch } from "runed";
|
||||
import { untrack } from "svelte";
|
||||
import { simpleBox, executeCallbacks, attachRef, DOMContext, getWindow, } from "svelte-toolbelt";
|
||||
import { mergeProps, useId } from "../../shared/index.js";
|
||||
import { clamp } from "../../internal/clamp.js";
|
||||
import { on } from "svelte/events";
|
||||
import { createBitsAttrs } from "../../internal/attrs.js";
|
||||
import { StateMachine } from "../../internal/state-machine.js";
|
||||
import { SvelteResizeObserver } from "../../internal/svelte-resize-observer.svelte.js";
|
||||
const scrollAreaAttrs = createBitsAttrs({
|
||||
component: "scroll-area",
|
||||
parts: ["root", "viewport", "corner", "thumb", "scrollbar"],
|
||||
});
|
||||
export const ScrollAreaRootContext = new Context("ScrollArea.Root");
|
||||
export const ScrollAreaScrollbarContext = new Context("ScrollArea.Scrollbar");
|
||||
export const ScrollAreaScrollbarVisibleContext = new Context("ScrollArea.ScrollbarVisible");
|
||||
export const ScrollAreaScrollbarAxisContext = new Context("ScrollArea.ScrollbarAxis");
|
||||
export const ScrollAreaScrollbarSharedContext = new Context("ScrollArea.ScrollbarShared");
|
||||
export class ScrollAreaRootState {
|
||||
static create(opts) {
|
||||
return ScrollAreaRootContext.set(new ScrollAreaRootState(opts));
|
||||
}
|
||||
opts;
|
||||
attachment;
|
||||
scrollAreaNode = $state(null);
|
||||
viewportNode = $state(null);
|
||||
contentNode = $state(null);
|
||||
scrollbarXNode = $state(null);
|
||||
scrollbarYNode = $state(null);
|
||||
cornerWidth = $state(0);
|
||||
cornerHeight = $state(0);
|
||||
scrollbarXEnabled = $state(false);
|
||||
scrollbarYEnabled = $state(false);
|
||||
domContext;
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(opts.ref, (v) => (this.scrollAreaNode = v));
|
||||
this.domContext = new DOMContext(opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
dir: this.opts.dir.current,
|
||||
style: {
|
||||
position: "relative",
|
||||
"--bits-scroll-area-corner-height": `${this.cornerHeight}px`,
|
||||
"--bits-scroll-area-corner-width": `${this.cornerWidth}px`,
|
||||
},
|
||||
[scrollAreaAttrs.root]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaViewportState {
|
||||
static create(opts) {
|
||||
return new ScrollAreaViewportState(opts, ScrollAreaRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
#contentId = simpleBox(useId());
|
||||
#contentRef = simpleBox(null);
|
||||
contentAttachment = attachRef(this.#contentRef, (v) => (this.root.contentNode = v));
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(opts.ref, (v) => (this.root.viewportNode = v));
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
style: {
|
||||
overflowX: this.root.scrollbarXEnabled ? "scroll" : "hidden",
|
||||
overflowY: this.root.scrollbarYEnabled ? "scroll" : "hidden",
|
||||
},
|
||||
[scrollAreaAttrs.viewport]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
contentProps = $derived.by(() => ({
|
||||
id: this.#contentId.current,
|
||||
"data-scroll-area-content": "",
|
||||
/**
|
||||
* When horizontal scrollbar is visible: this element should be at least
|
||||
* as wide as its children for size calculations to work correctly.
|
||||
*
|
||||
* When horizontal scrollbar is NOT visible: this element's width should
|
||||
* be constrained by the parent container to enable `text-overflow: ellipsis`
|
||||
*/
|
||||
style: { minWidth: this.root.scrollbarXEnabled ? "fit-content" : undefined },
|
||||
...this.contentAttachment,
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaScrollbarState {
|
||||
static create(opts) {
|
||||
return ScrollAreaScrollbarContext.set(new ScrollAreaScrollbarState(opts, ScrollAreaRootContext.get()));
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
isHorizontal = $derived.by(() => this.opts.orientation.current === "horizontal");
|
||||
hasThumb = $state(false);
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
watch(() => this.isHorizontal, (isHorizontal) => {
|
||||
if (isHorizontal) {
|
||||
this.root.scrollbarXEnabled = true;
|
||||
return () => {
|
||||
this.root.scrollbarXEnabled = false;
|
||||
};
|
||||
}
|
||||
else {
|
||||
this.root.scrollbarYEnabled = true;
|
||||
return () => {
|
||||
this.root.scrollbarYEnabled = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
export class ScrollAreaScrollbarHoverState {
|
||||
static create() {
|
||||
return new ScrollAreaScrollbarHoverState(ScrollAreaScrollbarContext.get());
|
||||
}
|
||||
scrollbar;
|
||||
root;
|
||||
isVisible = $state(false);
|
||||
constructor(scrollbar) {
|
||||
this.scrollbar = scrollbar;
|
||||
this.root = scrollbar.root;
|
||||
$effect(() => {
|
||||
const scrollAreaNode = this.root.scrollAreaNode;
|
||||
const hideDelay = this.root.opts.scrollHideDelay.current;
|
||||
let hideTimer = 0;
|
||||
if (!scrollAreaNode)
|
||||
return;
|
||||
const handlePointerEnter = () => {
|
||||
this.root.domContext.clearTimeout(hideTimer);
|
||||
untrack(() => (this.isVisible = true));
|
||||
};
|
||||
const handlePointerLeave = () => {
|
||||
if (hideTimer)
|
||||
this.root.domContext.clearTimeout(hideTimer);
|
||||
hideTimer = this.root.domContext.setTimeout(() => {
|
||||
untrack(() => {
|
||||
this.scrollbar.hasThumb = false;
|
||||
this.isVisible = false;
|
||||
});
|
||||
}, hideDelay);
|
||||
};
|
||||
const unsubListeners = executeCallbacks(on(scrollAreaNode, "pointerenter", handlePointerEnter), on(scrollAreaNode, "pointerleave", handlePointerLeave));
|
||||
return () => {
|
||||
this.root.domContext.getWindow().clearTimeout(hideTimer);
|
||||
unsubListeners();
|
||||
};
|
||||
});
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
"data-state": this.isVisible ? "visible" : "hidden",
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaScrollbarScrollState {
|
||||
static create() {
|
||||
return new ScrollAreaScrollbarScrollState(ScrollAreaScrollbarContext.get());
|
||||
}
|
||||
scrollbar;
|
||||
root;
|
||||
machine = new StateMachine("hidden", {
|
||||
hidden: {
|
||||
SCROLL: "scrolling",
|
||||
},
|
||||
scrolling: {
|
||||
SCROLL_END: "idle",
|
||||
POINTER_ENTER: "interacting",
|
||||
},
|
||||
interacting: {
|
||||
SCROLL: "interacting",
|
||||
POINTER_LEAVE: "idle",
|
||||
},
|
||||
idle: {
|
||||
HIDE: "hidden",
|
||||
SCROLL: "scrolling",
|
||||
POINTER_ENTER: "interacting",
|
||||
},
|
||||
});
|
||||
isHidden = $derived.by(() => this.machine.state.current === "hidden");
|
||||
constructor(scrollbar) {
|
||||
this.scrollbar = scrollbar;
|
||||
this.root = scrollbar.root;
|
||||
const debounceScrollend = useDebounce(() => this.machine.dispatch("SCROLL_END"), 100);
|
||||
$effect(() => {
|
||||
const _state = this.machine.state.current;
|
||||
const scrollHideDelay = this.root.opts.scrollHideDelay.current;
|
||||
if (_state === "idle") {
|
||||
const hideTimer = this.root.domContext.setTimeout(() => this.machine.dispatch("HIDE"), scrollHideDelay);
|
||||
return () => this.root.domContext.clearTimeout(hideTimer);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
const viewportNode = this.root.viewportNode;
|
||||
if (!viewportNode)
|
||||
return;
|
||||
const scrollDirection = this.scrollbar.isHorizontal ? "scrollLeft" : "scrollTop";
|
||||
let prevScrollPos = viewportNode[scrollDirection];
|
||||
const handleScroll = () => {
|
||||
const scrollPos = viewportNode[scrollDirection];
|
||||
const hasScrollInDirectionChanged = prevScrollPos !== scrollPos;
|
||||
if (hasScrollInDirectionChanged) {
|
||||
this.machine.dispatch("SCROLL");
|
||||
debounceScrollend();
|
||||
}
|
||||
prevScrollPos = scrollPos;
|
||||
};
|
||||
const unsubListener = on(viewportNode, "scroll", handleScroll);
|
||||
return unsubListener;
|
||||
});
|
||||
this.onpointerenter = this.onpointerenter.bind(this);
|
||||
this.onpointerleave = this.onpointerleave.bind(this);
|
||||
}
|
||||
onpointerenter(_) {
|
||||
this.machine.dispatch("POINTER_ENTER");
|
||||
}
|
||||
onpointerleave(_) {
|
||||
this.machine.dispatch("POINTER_LEAVE");
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
"data-state": this.machine.state.current === "hidden" ? "hidden" : "visible",
|
||||
onpointerenter: this.onpointerenter,
|
||||
onpointerleave: this.onpointerleave,
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaScrollbarAutoState {
|
||||
static create() {
|
||||
return new ScrollAreaScrollbarAutoState(ScrollAreaScrollbarContext.get());
|
||||
}
|
||||
scrollbar;
|
||||
root;
|
||||
isVisible = $state(false);
|
||||
constructor(scrollbar) {
|
||||
this.scrollbar = scrollbar;
|
||||
this.root = scrollbar.root;
|
||||
const handleResize = useDebounce(() => {
|
||||
const viewportNode = this.root.viewportNode;
|
||||
if (!viewportNode)
|
||||
return;
|
||||
const isOverflowX = viewportNode.offsetWidth < viewportNode.scrollWidth;
|
||||
const isOverflowY = viewportNode.offsetHeight < viewportNode.scrollHeight;
|
||||
this.isVisible = this.scrollbar.isHorizontal ? isOverflowX : isOverflowY;
|
||||
}, 10);
|
||||
new SvelteResizeObserver(() => this.root.viewportNode, handleResize);
|
||||
new SvelteResizeObserver(() => this.root.contentNode, handleResize);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
"data-state": this.isVisible ? "visible" : "hidden",
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaScrollbarVisibleState {
|
||||
static create() {
|
||||
return ScrollAreaScrollbarVisibleContext.set(new ScrollAreaScrollbarVisibleState(ScrollAreaScrollbarContext.get()));
|
||||
}
|
||||
scrollbar;
|
||||
root;
|
||||
thumbNode = $state(null);
|
||||
pointerOffset = $state(0);
|
||||
sizes = $state.raw({
|
||||
content: 0,
|
||||
viewport: 0,
|
||||
scrollbar: { size: 0, paddingStart: 0, paddingEnd: 0 },
|
||||
});
|
||||
thumbRatio = $derived.by(() => getThumbRatio(this.sizes.viewport, this.sizes.content));
|
||||
hasThumb = $derived.by(() => Boolean(this.thumbRatio > 0 && this.thumbRatio < 1));
|
||||
// this needs to be a $state to properly restore the transform style when the scrollbar
|
||||
// goes from a hidden to visible state, otherwise it will start at the beginning of the
|
||||
// scrollbar and flicker to the correct position after
|
||||
prevTransformStyle = $state("");
|
||||
constructor(scrollbar) {
|
||||
this.scrollbar = scrollbar;
|
||||
this.root = scrollbar.root;
|
||||
$effect(() => {
|
||||
this.scrollbar.hasThumb = this.hasThumb;
|
||||
});
|
||||
$effect(() => {
|
||||
if (!this.scrollbar.hasThumb && this.thumbNode) {
|
||||
this.prevTransformStyle = this.thumbNode.style.transform;
|
||||
}
|
||||
});
|
||||
}
|
||||
setSizes(sizes) {
|
||||
this.sizes = sizes;
|
||||
}
|
||||
getScrollPosition(pointerPos, dir) {
|
||||
return getScrollPositionFromPointer({
|
||||
pointerPos,
|
||||
pointerOffset: this.pointerOffset,
|
||||
sizes: this.sizes,
|
||||
dir,
|
||||
});
|
||||
}
|
||||
onThumbPointerUp() {
|
||||
this.pointerOffset = 0;
|
||||
}
|
||||
onThumbPointerDown(pointerPos) {
|
||||
this.pointerOffset = pointerPos;
|
||||
}
|
||||
xOnThumbPositionChange() {
|
||||
if (!(this.root.viewportNode && this.thumbNode))
|
||||
return;
|
||||
const scrollPos = this.root.viewportNode.scrollLeft;
|
||||
const offset = getThumbOffsetFromScroll({
|
||||
scrollPos,
|
||||
sizes: this.sizes,
|
||||
dir: this.root.opts.dir.current,
|
||||
});
|
||||
const transformStyle = `translate3d(${offset}px, 0, 0)`;
|
||||
this.thumbNode.style.transform = transformStyle;
|
||||
this.prevTransformStyle = transformStyle;
|
||||
}
|
||||
xOnWheelScroll(scrollPos) {
|
||||
if (!this.root.viewportNode)
|
||||
return;
|
||||
this.root.viewportNode.scrollLeft = scrollPos;
|
||||
}
|
||||
xOnDragScroll(pointerPos) {
|
||||
if (!this.root.viewportNode)
|
||||
return;
|
||||
this.root.viewportNode.scrollLeft = this.getScrollPosition(pointerPos, this.root.opts.dir.current);
|
||||
}
|
||||
yOnThumbPositionChange() {
|
||||
if (!(this.root.viewportNode && this.thumbNode))
|
||||
return;
|
||||
const scrollPos = this.root.viewportNode.scrollTop;
|
||||
const offset = getThumbOffsetFromScroll({ scrollPos, sizes: this.sizes });
|
||||
const transformStyle = `translate3d(0, ${offset}px, 0)`;
|
||||
this.thumbNode.style.transform = transformStyle;
|
||||
this.prevTransformStyle = transformStyle;
|
||||
}
|
||||
yOnWheelScroll(scrollPos) {
|
||||
if (!this.root.viewportNode)
|
||||
return;
|
||||
this.root.viewportNode.scrollTop = scrollPos;
|
||||
}
|
||||
yOnDragScroll(pointerPos) {
|
||||
if (!this.root.viewportNode)
|
||||
return;
|
||||
this.root.viewportNode.scrollTop = this.getScrollPosition(pointerPos, this.root.opts.dir.current);
|
||||
}
|
||||
}
|
||||
export class ScrollAreaScrollbarXState {
|
||||
static create(opts) {
|
||||
return ScrollAreaScrollbarAxisContext.set(new ScrollAreaScrollbarXState(opts, ScrollAreaScrollbarVisibleContext.get()));
|
||||
}
|
||||
opts;
|
||||
scrollbarVis;
|
||||
root;
|
||||
scrollbar;
|
||||
attachment;
|
||||
computedStyle = $state();
|
||||
constructor(opts, scrollbarVis) {
|
||||
this.opts = opts;
|
||||
this.scrollbarVis = scrollbarVis;
|
||||
this.root = scrollbarVis.root;
|
||||
this.scrollbar = scrollbarVis.scrollbar;
|
||||
this.attachment = attachRef(this.scrollbar.opts.ref, (v) => (this.root.scrollbarXNode = v));
|
||||
$effect(() => {
|
||||
if (!this.scrollbar.opts.ref.current)
|
||||
return;
|
||||
if (this.opts.mounted.current) {
|
||||
this.computedStyle = getComputedStyle(this.scrollbar.opts.ref.current);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
// Ensure when a user scrolls down and then the scrollbar is hidden
|
||||
// that when it shows again it will be positioned correctly.
|
||||
this.onResize();
|
||||
});
|
||||
}
|
||||
onThumbPointerDown = (pointerPos) => {
|
||||
this.scrollbarVis.onThumbPointerDown(pointerPos.x);
|
||||
};
|
||||
onDragScroll = (pointerPos) => {
|
||||
this.scrollbarVis.xOnDragScroll(pointerPos.x);
|
||||
};
|
||||
onThumbPointerUp = () => {
|
||||
this.scrollbarVis.onThumbPointerUp();
|
||||
};
|
||||
onThumbPositionChange = () => {
|
||||
this.scrollbarVis.xOnThumbPositionChange();
|
||||
};
|
||||
onWheelScroll = (e, maxScrollPos) => {
|
||||
if (!this.root.viewportNode)
|
||||
return;
|
||||
const scrollPos = this.root.viewportNode.scrollLeft + e.deltaX;
|
||||
this.scrollbarVis.xOnWheelScroll(scrollPos);
|
||||
// prevent window scroll when wheeling scrollbar
|
||||
if (isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
onResize = () => {
|
||||
if (!(this.scrollbar.opts.ref.current && this.root.viewportNode && this.computedStyle))
|
||||
return;
|
||||
this.scrollbarVis.setSizes({
|
||||
content: this.root.viewportNode.scrollWidth,
|
||||
viewport: this.root.viewportNode.offsetWidth,
|
||||
scrollbar: {
|
||||
size: this.scrollbar.opts.ref.current.clientWidth,
|
||||
paddingStart: toInt(this.computedStyle.paddingLeft),
|
||||
paddingEnd: toInt(this.computedStyle.paddingRight),
|
||||
},
|
||||
});
|
||||
};
|
||||
thumbSize = $derived.by(() => {
|
||||
return getThumbSize(this.scrollbarVis.sizes);
|
||||
});
|
||||
props = $derived.by(() => ({
|
||||
id: this.scrollbar.opts.id.current,
|
||||
"data-orientation": "horizontal",
|
||||
style: {
|
||||
bottom: 0,
|
||||
left: this.root.opts.dir.current === "rtl"
|
||||
? "var(--bits-scroll-area-corner-width)"
|
||||
: 0,
|
||||
right: this.root.opts.dir.current === "ltr"
|
||||
? "var(--bits-scroll-area-corner-width)"
|
||||
: 0,
|
||||
"--bits-scroll-area-thumb-width": `${this.thumbSize}px`,
|
||||
},
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaScrollbarYState {
|
||||
static create(opts) {
|
||||
return ScrollAreaScrollbarAxisContext.set(new ScrollAreaScrollbarYState(opts, ScrollAreaScrollbarVisibleContext.get()));
|
||||
}
|
||||
opts;
|
||||
scrollbarVis;
|
||||
root;
|
||||
scrollbar;
|
||||
attachment;
|
||||
computedStyle = $state();
|
||||
constructor(opts, scrollbarVis) {
|
||||
this.opts = opts;
|
||||
this.scrollbarVis = scrollbarVis;
|
||||
this.root = scrollbarVis.root;
|
||||
this.scrollbar = scrollbarVis.scrollbar;
|
||||
this.attachment = attachRef(this.scrollbar.opts.ref, (v) => (this.root.scrollbarYNode = v));
|
||||
$effect(() => {
|
||||
if (!this.scrollbar.opts.ref.current)
|
||||
return;
|
||||
if (this.opts.mounted.current) {
|
||||
this.computedStyle = getComputedStyle(this.scrollbar.opts.ref.current);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
// Ensure when a user scrolls down and then the scrollbar is hidden
|
||||
// that when it shows again it will be positioned correctly.
|
||||
this.onResize();
|
||||
});
|
||||
this.onThumbPointerDown = this.onThumbPointerDown.bind(this);
|
||||
this.onDragScroll = this.onDragScroll.bind(this);
|
||||
this.onThumbPointerUp = this.onThumbPointerUp.bind(this);
|
||||
this.onThumbPositionChange = this.onThumbPositionChange.bind(this);
|
||||
this.onWheelScroll = this.onWheelScroll.bind(this);
|
||||
this.onResize = this.onResize.bind(this);
|
||||
}
|
||||
onThumbPointerDown(pointerPos) {
|
||||
this.scrollbarVis.onThumbPointerDown(pointerPos.y);
|
||||
}
|
||||
onDragScroll(pointerPos) {
|
||||
this.scrollbarVis.yOnDragScroll(pointerPos.y);
|
||||
}
|
||||
onThumbPointerUp() {
|
||||
this.scrollbarVis.onThumbPointerUp();
|
||||
}
|
||||
onThumbPositionChange() {
|
||||
this.scrollbarVis.yOnThumbPositionChange();
|
||||
}
|
||||
onWheelScroll(e, maxScrollPos) {
|
||||
if (!this.root.viewportNode)
|
||||
return;
|
||||
const scrollPos = this.root.viewportNode.scrollTop + e.deltaY;
|
||||
this.scrollbarVis.yOnWheelScroll(scrollPos);
|
||||
// prevent window scroll when wheeling scrollbar
|
||||
if (isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
onResize() {
|
||||
if (!(this.scrollbar.opts.ref.current && this.root.viewportNode && this.computedStyle))
|
||||
return;
|
||||
this.scrollbarVis.setSizes({
|
||||
content: this.root.viewportNode.scrollHeight,
|
||||
viewport: this.root.viewportNode.offsetHeight,
|
||||
scrollbar: {
|
||||
size: this.scrollbar.opts.ref.current.clientHeight,
|
||||
paddingStart: toInt(this.computedStyle.paddingTop),
|
||||
paddingEnd: toInt(this.computedStyle.paddingBottom),
|
||||
},
|
||||
});
|
||||
}
|
||||
thumbSize = $derived.by(() => {
|
||||
return getThumbSize(this.scrollbarVis.sizes);
|
||||
});
|
||||
props = $derived.by(() => ({
|
||||
id: this.scrollbar.opts.id.current,
|
||||
"data-orientation": "vertical",
|
||||
style: {
|
||||
top: 0,
|
||||
right: this.root.opts.dir.current === "ltr" ? 0 : undefined,
|
||||
left: this.root.opts.dir.current === "rtl" ? 0 : undefined,
|
||||
bottom: "var(--bits-scroll-area-corner-height)",
|
||||
"--bits-scroll-area-thumb-height": `${this.thumbSize}px`,
|
||||
},
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaScrollbarSharedState {
|
||||
static create() {
|
||||
return ScrollAreaScrollbarSharedContext.set(new ScrollAreaScrollbarSharedState(ScrollAreaScrollbarAxisContext.get()));
|
||||
}
|
||||
scrollbarState;
|
||||
root;
|
||||
scrollbarVis;
|
||||
scrollbar;
|
||||
rect = $state.raw(null);
|
||||
prevWebkitUserSelect = $state("");
|
||||
handleResize;
|
||||
handleThumbPositionChange;
|
||||
handleWheelScroll;
|
||||
handleThumbPointerDown;
|
||||
handleThumbPointerUp;
|
||||
maxScrollPos = $derived.by(() => this.scrollbarVis.sizes.content - this.scrollbarVis.sizes.viewport);
|
||||
constructor(scrollbarState) {
|
||||
this.scrollbarState = scrollbarState;
|
||||
this.root = scrollbarState.root;
|
||||
this.scrollbarVis = scrollbarState.scrollbarVis;
|
||||
this.scrollbar = scrollbarState.scrollbarVis.scrollbar;
|
||||
this.handleResize = useDebounce(() => this.scrollbarState.onResize(), 10);
|
||||
this.handleThumbPositionChange = this.scrollbarState.onThumbPositionChange;
|
||||
this.handleWheelScroll = this.scrollbarState.onWheelScroll;
|
||||
this.handleThumbPointerDown = this.scrollbarState.onThumbPointerDown;
|
||||
this.handleThumbPointerUp = this.scrollbarState.onThumbPointerUp;
|
||||
$effect(() => {
|
||||
const maxScrollPos = this.maxScrollPos;
|
||||
const scrollbarNode = this.scrollbar.opts.ref.current;
|
||||
// we want to react to the viewport node changing so we leave this here
|
||||
this.root.viewportNode;
|
||||
const handleWheel = (e) => {
|
||||
const node = e.target;
|
||||
const isScrollbarWheel = scrollbarNode?.contains(node);
|
||||
if (isScrollbarWheel)
|
||||
this.handleWheelScroll(e, maxScrollPos);
|
||||
};
|
||||
const unsubListener = on(this.root.domContext.getDocument(), "wheel", handleWheel, {
|
||||
passive: false,
|
||||
});
|
||||
return unsubListener;
|
||||
});
|
||||
$effect.pre(() => {
|
||||
// react to changes to this:
|
||||
this.scrollbarVis.sizes;
|
||||
untrack(() => this.handleThumbPositionChange());
|
||||
});
|
||||
// $effect.pre(() => {
|
||||
// this.handleThumbPositionChange();
|
||||
// });
|
||||
new SvelteResizeObserver(() => this.scrollbar.opts.ref.current, this.handleResize);
|
||||
new SvelteResizeObserver(() => this.root.contentNode, this.handleResize);
|
||||
this.onpointerdown = this.onpointerdown.bind(this);
|
||||
this.onpointermove = this.onpointermove.bind(this);
|
||||
this.onpointerup = this.onpointerup.bind(this);
|
||||
this.onlostpointercapture = this.onlostpointercapture.bind(this);
|
||||
}
|
||||
handleDragScroll(e) {
|
||||
if (!this.rect)
|
||||
return;
|
||||
const x = e.clientX - this.rect.left;
|
||||
const y = e.clientY - this.rect.top;
|
||||
this.scrollbarState.onDragScroll({ x, y });
|
||||
}
|
||||
#cleanupPointerState() {
|
||||
if (this.rect === null)
|
||||
return;
|
||||
this.root.domContext.getDocument().body.style.webkitUserSelect = this.prevWebkitUserSelect;
|
||||
if (this.root.viewportNode)
|
||||
this.root.viewportNode.style.scrollBehavior = "";
|
||||
this.rect = null;
|
||||
}
|
||||
onpointerdown(e) {
|
||||
if (e.button !== 0)
|
||||
return;
|
||||
const target = e.target;
|
||||
target.setPointerCapture(e.pointerId);
|
||||
this.rect = this.scrollbar.opts.ref.current?.getBoundingClientRect() ?? null;
|
||||
// pointer capture doesn't prevent text selection in Safari
|
||||
// so we remove text selection manually when scrolling
|
||||
this.prevWebkitUserSelect = this.root.domContext.getDocument().body.style.webkitUserSelect;
|
||||
this.root.domContext.getDocument().body.style.webkitUserSelect = "none";
|
||||
if (this.root.viewportNode)
|
||||
this.root.viewportNode.style.scrollBehavior = "auto";
|
||||
this.handleDragScroll(e);
|
||||
}
|
||||
onpointermove(e) {
|
||||
this.handleDragScroll(e);
|
||||
}
|
||||
onpointerup(e) {
|
||||
const target = e.target;
|
||||
if (target.hasPointerCapture(e.pointerId)) {
|
||||
target.releasePointerCapture(e.pointerId);
|
||||
}
|
||||
this.#cleanupPointerState();
|
||||
}
|
||||
onlostpointercapture(_) {
|
||||
this.#cleanupPointerState();
|
||||
}
|
||||
props = $derived.by(() => mergeProps({
|
||||
...this.scrollbarState.props,
|
||||
style: {
|
||||
position: "absolute",
|
||||
...this.scrollbarState.props.style,
|
||||
},
|
||||
[scrollAreaAttrs.scrollbar]: "",
|
||||
onpointerdown: this.onpointerdown,
|
||||
onpointermove: this.onpointermove,
|
||||
onpointerup: this.onpointerup,
|
||||
onlostpointercapture: this.onlostpointercapture,
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaThumbImplState {
|
||||
static create(opts) {
|
||||
return new ScrollAreaThumbImplState(opts, ScrollAreaScrollbarSharedContext.get());
|
||||
}
|
||||
opts;
|
||||
scrollbarState;
|
||||
attachment;
|
||||
#root;
|
||||
#removeUnlinkedScrollListener = $state();
|
||||
#debounceScrollEnd = useDebounce(() => {
|
||||
if (this.#removeUnlinkedScrollListener) {
|
||||
this.#removeUnlinkedScrollListener();
|
||||
this.#removeUnlinkedScrollListener = undefined;
|
||||
}
|
||||
}, 100);
|
||||
constructor(opts, scrollbarState) {
|
||||
this.opts = opts;
|
||||
this.scrollbarState = scrollbarState;
|
||||
this.#root = scrollbarState.root;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.scrollbarState.scrollbarVis.thumbNode = v));
|
||||
$effect(() => {
|
||||
const viewportNode = this.#root.viewportNode;
|
||||
if (!viewportNode)
|
||||
return;
|
||||
const handleScroll = () => {
|
||||
this.#debounceScrollEnd();
|
||||
if (!this.#removeUnlinkedScrollListener) {
|
||||
const listener = addUnlinkedScrollListener(viewportNode, this.scrollbarState.handleThumbPositionChange);
|
||||
this.#removeUnlinkedScrollListener = listener;
|
||||
this.scrollbarState.handleThumbPositionChange();
|
||||
}
|
||||
};
|
||||
untrack(() => this.scrollbarState.handleThumbPositionChange());
|
||||
const unsubListener = on(viewportNode, "scroll", handleScroll);
|
||||
return unsubListener;
|
||||
});
|
||||
this.onpointerdowncapture = this.onpointerdowncapture.bind(this);
|
||||
this.onpointerup = this.onpointerup.bind(this);
|
||||
}
|
||||
onpointerdowncapture(e) {
|
||||
const thumb = e.target;
|
||||
if (!thumb)
|
||||
return;
|
||||
const thumbRect = thumb.getBoundingClientRect();
|
||||
const x = e.clientX - thumbRect.left;
|
||||
const y = e.clientY - thumbRect.top;
|
||||
this.scrollbarState.handleThumbPointerDown({ x, y });
|
||||
}
|
||||
onpointerup(_) {
|
||||
this.scrollbarState.handleThumbPointerUp();
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-state": this.scrollbarState.scrollbarVis.hasThumb ? "visible" : "hidden",
|
||||
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,
|
||||
[scrollAreaAttrs.thumb]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class ScrollAreaCornerImplState {
|
||||
static create(opts) {
|
||||
return new ScrollAreaCornerImplState(opts, ScrollAreaRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
#width = $state(0);
|
||||
#height = $state(0);
|
||||
hasSize = $derived(Boolean(this.#width && this.#height));
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
new SvelteResizeObserver(() => this.root.scrollbarXNode, () => {
|
||||
const height = this.root.scrollbarXNode?.offsetHeight || 0;
|
||||
this.root.cornerHeight = height;
|
||||
this.#height = height;
|
||||
});
|
||||
new SvelteResizeObserver(() => this.root.scrollbarYNode, () => {
|
||||
const width = this.root.scrollbarYNode?.offsetWidth || 0;
|
||||
this.root.cornerWidth = width;
|
||||
this.#width = width;
|
||||
});
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
style: {
|
||||
width: this.#width,
|
||||
height: this.#height,
|
||||
position: "absolute",
|
||||
right: this.root.opts.dir.current === "ltr" ? 0 : undefined,
|
||||
left: this.root.opts.dir.current === "rtl" ? 0 : undefined,
|
||||
bottom: 0,
|
||||
},
|
||||
[scrollAreaAttrs.corner]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
function toInt(value) {
|
||||
return value ? Number.parseInt(value, 10) : 0;
|
||||
}
|
||||
function getThumbRatio(viewportSize, contentSize) {
|
||||
const ratio = viewportSize / contentSize;
|
||||
return Number.isNaN(ratio) ? 0 : ratio;
|
||||
}
|
||||
function getThumbSize(sizes) {
|
||||
const ratio = getThumbRatio(sizes.viewport, sizes.content);
|
||||
const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd;
|
||||
const thumbSize = (sizes.scrollbar.size - scrollbarPadding) * ratio;
|
||||
return Math.max(thumbSize, 18);
|
||||
}
|
||||
function getScrollPositionFromPointer({ pointerPos, pointerOffset, sizes, dir = "ltr", }) {
|
||||
const thumbSizePx = getThumbSize(sizes);
|
||||
const thumbCenter = thumbSizePx / 2;
|
||||
const offset = pointerOffset || thumbCenter;
|
||||
const thumbOffsetFromEnd = thumbSizePx - offset;
|
||||
const minPointerPos = sizes.scrollbar.paddingStart + offset;
|
||||
const maxPointerPos = sizes.scrollbar.size - sizes.scrollbar.paddingEnd - thumbOffsetFromEnd;
|
||||
const maxScrollPos = sizes.content - sizes.viewport;
|
||||
const scrollRange = dir === "ltr" ? [0, maxScrollPos] : [maxScrollPos * -1, 0];
|
||||
const interpolate = linearScale([minPointerPos, maxPointerPos], scrollRange);
|
||||
return interpolate(pointerPos);
|
||||
}
|
||||
function getThumbOffsetFromScroll({ scrollPos, sizes, dir = "ltr", }) {
|
||||
const thumbSizePx = getThumbSize(sizes);
|
||||
const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd;
|
||||
const scrollbar = sizes.scrollbar.size - scrollbarPadding;
|
||||
const maxScrollPos = sizes.content - sizes.viewport;
|
||||
const maxThumbPos = scrollbar - thumbSizePx;
|
||||
const scrollClampRange = dir === "ltr" ? [0, maxScrollPos] : [maxScrollPos * -1, 0];
|
||||
const scrollWithoutMomentum = clamp(scrollPos, scrollClampRange[0], scrollClampRange[1]);
|
||||
const interpolate = linearScale([0, maxScrollPos], [0, maxThumbPos]);
|
||||
return interpolate(scrollWithoutMomentum);
|
||||
}
|
||||
// https://github.com/tmcw-up-for-adoption/simple-linear-scale/blob/master/index.js
|
||||
function linearScale(input, output) {
|
||||
return (value) => {
|
||||
if (input[0] === input[1] || output[0] === output[1])
|
||||
return output[0];
|
||||
const ratio = (output[1] - output[0]) / (input[1] - input[0]);
|
||||
return output[0] + ratio * (value - input[0]);
|
||||
};
|
||||
}
|
||||
function isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos) {
|
||||
return scrollPos > 0 && scrollPos < maxScrollPos;
|
||||
}
|
||||
function addUnlinkedScrollListener(node, handler) {
|
||||
let prevPosition = { left: node.scrollLeft, top: node.scrollTop };
|
||||
let rAF = 0;
|
||||
const win = getWindow(node);
|
||||
(function loop() {
|
||||
const position = { left: node.scrollLeft, top: node.scrollTop };
|
||||
const isHorizontalScroll = prevPosition.left !== position.left;
|
||||
const isVerticalScroll = prevPosition.top !== position.top;
|
||||
if (isHorizontalScroll || isVerticalScroll)
|
||||
handler();
|
||||
prevPosition = position;
|
||||
rAF = win.requestAnimationFrame(loop);
|
||||
})();
|
||||
return () => win.cancelAnimationFrame(rAF);
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import type { BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
|
||||
import type { Direction, Orientation, WithChild, Without } from "../../shared/index.js";
|
||||
export type ScrollAreaType = "hover" | "scroll" | "auto" | "always";
|
||||
export type ScrollAreaRootPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The type of scroll area to render.
|
||||
*
|
||||
* @defaultValue "hover"
|
||||
*/
|
||||
type?: ScrollAreaType;
|
||||
/**
|
||||
* The reading direction of the application.
|
||||
*/
|
||||
dir?: Direction;
|
||||
/**
|
||||
* The amount of time in milliseconds to delay before hiding the scrollbars
|
||||
* after leaving the scroll area or stopping scrolling.
|
||||
*
|
||||
* @defaultValue 600
|
||||
*/
|
||||
scrollHideDelay?: number;
|
||||
}>;
|
||||
export type ScrollAreaRootProps = ScrollAreaRootPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, ScrollAreaRootPropsWithoutHTML>;
|
||||
export type ScrollAreaViewportPropsWithoutHTML = Omit<WithChild, "child">;
|
||||
export type ScrollAreaViewportProps = ScrollAreaViewportPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, ScrollAreaViewportPropsWithoutHTML>;
|
||||
export type ScrollAreaScrollbarPropsWithoutHTML = WithChild<{
|
||||
orientation: Orientation;
|
||||
/**
|
||||
* Whether to forcefully mount the component. Useful when working with
|
||||
* external animation/transition libraries.
|
||||
*/
|
||||
forceMount?: boolean;
|
||||
}>;
|
||||
export type ScrollAreaScrollbarProps = ScrollAreaScrollbarPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, ScrollAreaScrollbarPropsWithoutHTML>;
|
||||
export type ScrollAreaThumbPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Whether to forcefully mount the component. Useful when working with
|
||||
* external animation/transition libraries.
|
||||
*/
|
||||
forceMount?: boolean;
|
||||
}>;
|
||||
export type ScrollAreaThumbProps = ScrollAreaThumbPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, ScrollAreaThumbPropsWithoutHTML>;
|
||||
export type ScrollAreaCornerPropsWithoutHTML = WithChild;
|
||||
export type ScrollAreaCornerProps = ScrollAreaCornerPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, ScrollAreaCornerPropsWithoutHTML>;
|
||||
export type _ScrollbarStubProps = Omit<ScrollAreaScrollbarProps, "orientation" | "ref" | "id"> & {
|
||||
id: string;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user