增加一个支持Scroll的布局组件。
This commit is contained in:
parent
76e9e81306
commit
322779effc
43
src/components/ScrollArea.module.css
Normal file
43
src/components/ScrollArea.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
188
src/components/ScrollArea.tsx
Normal file
188
src/components/ScrollArea.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user