增加一个支持Scroll的布局组件。

This commit is contained in:
徐涛 2025-01-06 13:33:12 +08:00
parent 76e9e81306
commit 322779effc
2 changed files with 231 additions and 0 deletions

View File

@ -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;
}
}
}

View File

@ -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<HTMLDivElement> | null;
};
function VerticalScrollBar({ containerRef }: ScrollBarProps) {
const [thumbPos, setThumbPos] = useState(0);
const trackRef = useRef<HTMLDivElement | null>(null);
const thumbRef = useRef<HTMLDivElement | null>(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 (
<div className={styles.v_scrollbar} ref={trackRef}>
<div
className={styles.v_thumb}
ref={thumbRef}
style={{ top: thumbPos }}
onMouseDown={handleMouseDown}
/>
</div>
);
}
function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
const [thumbPos, setThumbPos] = useState(0);
const trackRef = useRef<HTMLDivElement | null>(null);
const thumbRef = useRef<HTMLDivElement | null>(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 (
<div className={styles.h_scrollbar} ref={trackRef}>
<div
className={styles.h_thumb}
ref={thumbRef}
style={{ left: thumbPos }}
onMouseDown={handleMouseDown}
/>
</div>
);
}
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<HTMLDivElement>(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 (
<div className={styles.scroll_area}>
<div className={styles.content} ref={scrollContainerRef} onWheel={handleWheel}>
{children}
</div>
{enableY && yScrollNeeded && <VerticalScrollBar containerRef={scrollContainerRef} />}
{enableX && xScrollNeeded && <HorizontalScrollBar containerRef={scrollContainerRef} />}
</div>
);
}