增加一个支持Scroll的布局组件。
This commit is contained in:
		
							
								
								
									
										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> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user