From 322779effcc6e63292fb43afc577d98e8da104cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Mon, 6 Jan 2025 13:33:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=B8=AA=E6=94=AF?= =?UTF-8?q?=E6=8C=81Scroll=E7=9A=84=E5=B8=83=E5=B1=80=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ScrollArea.module.css | 43 ++++++ src/components/ScrollArea.tsx | 188 +++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 src/components/ScrollArea.module.css create mode 100644 src/components/ScrollArea.tsx diff --git a/src/components/ScrollArea.module.css b/src/components/ScrollArea.module.css new file mode 100644 index 0000000..dad17a4 --- /dev/null +++ b/src/components/ScrollArea.module.css @@ -0,0 +1,43 @@ +@layer components { + .scroll_area { + display: grid; + width: 100%; + height: 100%; + overflow: hidden; + grid-template-columns: auto calc(var(--spacing) * 6); + grid-template-rows: auto calc(var(--spacing) * 6); + } + .content { + grid-column: 1; + grid-row: 1; + overflow: hidden; + } + .v_scrollbar { + grid-column: 2; + grid-row: 1; + overflow: hidden; + position: relative; + .v_thumb { + width: 100%; + aspect-ratio: 1 / 3; + position: absolute; + border-radius: var(--border-radius-xxs); + background-color: oklch(from var(--color-primary) l c h / 70%); + cursor: pointer; + } + } + .h_scrollbar { + grid-column: 1; + grid-row: 2; + overflow: hidden; + position: relative; + .h_thumb { + height: 100%; + aspect-ratio: 3 / 1; + position: absolute; + border-radius: var(--border-radius-xxs); + background-color: oklch(from var(--color-primary) l c h / 70%); + cursor: pointer; + } + } +} diff --git a/src/components/ScrollArea.tsx b/src/components/ScrollArea.tsx new file mode 100644 index 0000000..d64d2a7 --- /dev/null +++ b/src/components/ScrollArea.tsx @@ -0,0 +1,188 @@ +import { clamp } from 'lodash-es'; +import { RefObject, useEffect, useRef, useState } from 'react'; +import styles from './ScrollArea.module.css'; + +type ScrollBarProps = { + containerRef: RefObject | null; +}; + +function VerticalScrollBar({ containerRef }: ScrollBarProps) { + const [thumbPos, setThumbPos] = useState(0); + const trackRef = useRef(null); + const thumbRef = useRef(null); + const handleMouseDown = (evt: MouseEvent) => { + evt.preventDefault(); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + const handleMouseMove = (evt: MouseEvent) => { + evt.preventDefault(); + const container = containerRef?.current; + const scrollbar = trackRef.current; + const thumb = thumbRef.current; + + if (container && scrollbar && thumb) { + const trackRect = scrollbar.getBoundingClientRect(); + const thumbRect = thumb.getBoundingClientRect(); + const offsetY = evt.clientY - trackRect.top - thumbRect.height / 2; + const thumbPosition = clamp(offsetY, 0, trackRect.height - thumbRect.height); + setThumbPos(thumbPosition); + + const scrollPercentage = thumbPosition / (trackRect.height - thumbRect.height); + container.scrollTop = scrollPercentage * (container.scrollHeight - container.clientHeight); + } + }; + const handleMouseUp = (evt: MouseEvent) => { + evt.preventDefault(); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + const updateThumbPosition = () => { + const container = containerRef?.current; + const scrollbar = trackRef.current; + const scrollPercentage = + container.scrollTop / (container.scrollHeight - container.clientHeight); + const thumbPosition = + scrollPercentage * (scrollbar.clientHeight - thumbRef.current!.clientHeight); + setThumbPos(thumbPosition); + }; + + useEffect(() => { + const container = containerRef?.current; + const scrollbar = trackRef.current; + if (container && scrollbar) { + container.addEventListener('scroll', updateThumbPosition); + return () => { + container.removeEventListener('scroll', updateThumbPosition); + }; + } + }, []); + + return ( +
+
+
+ ); +} + +function HorizontalScrollBar({ containerRef }: ScrollBarProps) { + const [thumbPos, setThumbPos] = useState(0); + const trackRef = useRef(null); + const thumbRef = useRef(null); + const handleMouseDown = (evt: MouseEvent) => { + evt.preventDefault(); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + const handleMouseMove = (evt: MouseEvent) => { + evt.preventDefault(); + const container = containerRef?.current; + const scrollbar = trackRef.current; + const thumb = thumbRef.current; + + if (container && scrollbar && thumb) { + const trackRect = scrollbar.getBoundingClientRect(); + const thumbRect = thumb.getBoundingClientRect(); + const offsetX = evt.clientX - trackRect.left - thumbRect.width / 2; + const thumbPosition = clamp(offsetX, 0, trackRect.width - thumbRect.width); + setThumbPos(thumbPosition); + + const scrollPercentage = thumbPosition / (trackRect.width - thumbRect.width); + container.scrollLeft = scrollPercentage * (container.scrollWidth - container.clientWidth); + } + }; + const handleMouseUp = (evt: MouseEvent) => { + evt.preventDefault(); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + const updateThumbPosition = () => { + const container = containerRef?.current; + const scrollbar = trackRef.current; + const scrollPercentage = container.scrollLeft / (container.scrollWidth - container.clientWidth); + const thumbPosition = + scrollPercentage * (scrollbar.clientWidth - thumbRef.current!.clientWidth); + setThumbPos(thumbPosition); + }; + + useEffect(() => { + const container = containerRef?.current; + const scrollbar = trackRef.current; + if (container && scrollbar) { + container.addEventListener('scroll', updateThumbPosition); + return () => { + container.removeEventListener('scroll', updateThumbPosition); + }; + } + }, []); + + return ( +
+
+
+ ); +} + +type ScrollAreaProps = { + children?: React.ReactNode; + enableX?: boolean; + enableY?: boolean; + normalizedScroll?: boolean; +}; + +export function ScrollArea({ + children, + enableX = false, + enableY = false, + normalizedScroll = false, +}: ScrollAreaProps) { + const scrollContainerRef = useRef(null); + const [xScrollNeeded, setXScrollNeeded] = useState(false); + const [yScrollNeeded, setYScrollNeeded] = useState(false); + const handleWheel = (evt: WheelEvent) => { + evt.preventDefault(); + const container = scrollContainerRef?.current; + if (enableY && container) { + const delta = evt.deltaY; + const normalizedDelta = normalizedScroll ? clamp(delta, -1, 1) * 30 : delta; + const newScrollTop = container.scrollTop + normalizedDelta; + container.scrollTop = clamp(newScrollTop, 0, container.scrollHeight - container.clientHeight); + } + if (enableX && container) { + const delta = evt.deltaX; + const normalizedDelta = normalizedScroll ? clamp(delta, -1, 1) * 30 : delta; + const newScrollLeft = container.scrollLeft + normalizedDelta; + container.scrollLeft = clamp(newScrollLeft, 0, container.scrollWidth - container.clientWidth); + } + }; + + useEffect(() => { + const container = scrollContainerRef.current; + if (container) { + setXScrollNeeded(container.scrollWidth > container.clientWidth); + setYScrollNeeded(container.scrollHeight > container.clientHeight); + } + }, [children]); + + return ( +
+
+ {children} +
+ {enableY && yScrollNeeded && } + {enableX && xScrollNeeded && } +
+ ); +}