mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-09 10:18:57 +00:00
@@ -1,18 +1,17 @@
|
||||
import { createSignal, onCleanup, onMount, splitProps, type ComponentProps, Show } from "solid-js"
|
||||
import { animate, type AnimationPlaybackControls } from "motion"
|
||||
import { createSignal, onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { FAST_SPRING } from "./motion"
|
||||
|
||||
export interface ScrollViewProps extends ComponentProps<"div"> {
|
||||
viewportRef?: (el: HTMLDivElement) => void
|
||||
reverse?: boolean
|
||||
orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb
|
||||
}
|
||||
|
||||
export function ScrollView(props: ScrollViewProps) {
|
||||
const i18n = useI18n()
|
||||
const merged = mergeProps({ orientation: "vertical" }, props)
|
||||
const [local, events, rest] = splitProps(
|
||||
props,
|
||||
["class", "children", "viewportRef", "style", "reverse"],
|
||||
merged,
|
||||
["class", "children", "viewportRef", "orientation", "style"],
|
||||
[
|
||||
"onScroll",
|
||||
"onWheel",
|
||||
@@ -26,9 +25,9 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
],
|
||||
)
|
||||
|
||||
let rootRef!: HTMLDivElement
|
||||
let viewportRef!: HTMLDivElement
|
||||
let thumbRef!: HTMLDivElement
|
||||
let anim: AnimationPlaybackControls | undefined
|
||||
|
||||
const [isHovered, setIsHovered] = createSignal(false)
|
||||
const [isDragging, setIsDragging] = createSignal(false)
|
||||
@@ -37,8 +36,6 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
const [thumbTop, setThumbTop] = createSignal(0)
|
||||
const [showThumb, setShowThumb] = createSignal(false)
|
||||
|
||||
const reverse = () => local.reverse === true
|
||||
|
||||
const updateThumb = () => {
|
||||
if (!viewportRef) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = viewportRef
|
||||
@@ -60,13 +57,9 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
const maxScrollTop = scrollHeight - clientHeight
|
||||
const maxThumbTop = trackHeight - height
|
||||
|
||||
const top = (() => {
|
||||
if (maxScrollTop <= 0) return 0
|
||||
if (!reverse()) return (scrollTop / maxScrollTop) * maxThumbTop
|
||||
return ((maxScrollTop + scrollTop) / maxScrollTop) * maxThumbTop
|
||||
})()
|
||||
const top = maxScrollTop > 0 ? (scrollTop / maxScrollTop) * maxThumbTop : 0
|
||||
|
||||
// Ensure thumb stays within bounds
|
||||
// Ensure thumb stays within bounds (shouldn't be necessary due to math above, but good for safety)
|
||||
const boundedTop = trackPadding + Math.max(0, Math.min(top, maxThumbTop))
|
||||
|
||||
setThumbHeight(height)
|
||||
@@ -89,7 +82,6 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
stop()
|
||||
observer.disconnect()
|
||||
})
|
||||
|
||||
@@ -131,31 +123,6 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
thumbRef.addEventListener("pointerup", onPointerUp)
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
if (!anim) return
|
||||
anim.stop()
|
||||
anim = undefined
|
||||
}
|
||||
|
||||
const limit = (top: number) => {
|
||||
const max = viewportRef.scrollHeight - viewportRef.clientHeight
|
||||
if (reverse()) return Math.max(-max, Math.min(0, top))
|
||||
return Math.max(0, Math.min(max, top))
|
||||
}
|
||||
|
||||
const glide = (top: number) => {
|
||||
stop()
|
||||
anim = animate(viewportRef.scrollTop, limit(top), {
|
||||
...FAST_SPRING,
|
||||
onUpdate: (v) => {
|
||||
viewportRef.scrollTop = v
|
||||
},
|
||||
onComplete: () => {
|
||||
anim = undefined
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Keybinds implementation
|
||||
// We ensure the viewport has a tabindex so it can receive focus
|
||||
// We can also explicitly catch PageUp/Down if we want smooth scroll or specific behavior,
|
||||
@@ -180,11 +147,11 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
break
|
||||
case "Home":
|
||||
e.preventDefault()
|
||||
glide(reverse() ? -(viewportRef.scrollHeight - viewportRef.clientHeight) : 0)
|
||||
viewportRef.scrollTo({ top: 0, behavior: "smooth" })
|
||||
break
|
||||
case "End":
|
||||
e.preventDefault()
|
||||
glide(reverse() ? 0 : viewportRef.scrollHeight - viewportRef.clientHeight)
|
||||
viewportRef.scrollTo({ top: viewportRef.scrollHeight, behavior: "smooth" })
|
||||
break
|
||||
case "ArrowUp":
|
||||
e.preventDefault()
|
||||
@@ -199,6 +166,7 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
class={`scroll-view ${local.class || ""}`}
|
||||
style={local.style}
|
||||
onPointerEnter={() => setIsHovered(true)}
|
||||
@@ -209,26 +177,16 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
<div
|
||||
ref={viewportRef}
|
||||
class="scroll-view__viewport"
|
||||
data-reverse={reverse() ? "true" : undefined}
|
||||
onScroll={(e) => {
|
||||
updateThumb()
|
||||
if (typeof events.onScroll === "function") events.onScroll(e as any)
|
||||
}}
|
||||
onWheel={(e) => {
|
||||
if (e.deltaY) stop()
|
||||
if (typeof events.onWheel === "function") events.onWheel(e as any)
|
||||
}}
|
||||
onTouchStart={(e) => {
|
||||
stop()
|
||||
if (typeof events.onTouchStart === "function") events.onTouchStart(e as any)
|
||||
}}
|
||||
onWheel={events.onWheel as any}
|
||||
onTouchStart={events.onTouchStart as any}
|
||||
onTouchMove={events.onTouchMove as any}
|
||||
onTouchEnd={events.onTouchEnd as any}
|
||||
onTouchCancel={events.onTouchCancel as any}
|
||||
onPointerDown={(e) => {
|
||||
stop()
|
||||
if (typeof events.onPointerDown === "function") events.onPointerDown(e as any)
|
||||
}}
|
||||
onPointerDown={events.onPointerDown as any}
|
||||
onClick={events.onClick as any}
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
|
||||
Reference in New Issue
Block a user