@ehfuse/overlay-scrollbar 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -139
- package/dist/index.d.ts +16 -3
- package/dist/index.esm.js +261 -236
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +260 -235
- package/dist/index.js.map +1 -1
- package/dist/src/OverlayScrollbar.d.ts +18 -4
- package/dist/src/OverlayScrollbar.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,52 +1,26 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import { forwardRef, useRef,
|
|
2
|
+
import { forwardRef, useRef, useState, useImperativeHandle, useCallback, useEffect, useMemo } from 'react';
|
|
3
3
|
|
|
4
|
-
const OverlayScrollbar = forwardRef(({ children, onScroll,
|
|
4
|
+
const OverlayScrollbar = forwardRef(({ className = "", style = {}, children, onScroll, scrollbarWidth = 8, // deprecated
|
|
5
|
+
thumbRadius, showArrows = false, arrowStep = 50, trackWidth = 16, thumbWidth = 8, thumbMinHeight = 50, trackColor = "rgba(128, 128, 128, 0.1)", thumbColor = "rgba(128, 128, 128, 0.6)", thumbActiveColor = "rgba(128, 128, 128, 0.9)", arrowColor = "rgba(128, 128, 128, 0.6)", arrowActiveColor = "rgba(64, 64, 64, 1.0)", hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
5
6
|
const containerRef = useRef(null);
|
|
6
7
|
const contentRef = useRef(null);
|
|
7
8
|
const scrollbarRef = useRef(null);
|
|
8
9
|
const thumbRef = useRef(null);
|
|
9
|
-
//
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
const style = document.createElement("style");
|
|
12
|
-
style.textContent = `
|
|
13
|
-
.overlay-scrollbar-container::-webkit-scrollbar {
|
|
14
|
-
display: none !important;
|
|
15
|
-
width: 0 !important;
|
|
16
|
-
height: 0 !important;
|
|
17
|
-
background: transparent !important;
|
|
18
|
-
}
|
|
19
|
-
.overlay-scrollbar-container::-webkit-scrollbar-track {
|
|
20
|
-
display: none !important;
|
|
21
|
-
}
|
|
22
|
-
.overlay-scrollbar-container::-webkit-scrollbar-thumb {
|
|
23
|
-
display: none !important;
|
|
24
|
-
}
|
|
25
|
-
.overlay-scrollbar-container::-webkit-scrollbar-corner {
|
|
26
|
-
display: none !important;
|
|
27
|
-
}
|
|
28
|
-
.overlay-scrollbar-container * {
|
|
29
|
-
scrollbar-width: none;
|
|
30
|
-
-ms-overflow-style: none;
|
|
31
|
-
}
|
|
32
|
-
.overlay-scrollbar-container *::-webkit-scrollbar {
|
|
33
|
-
display: none !important;
|
|
34
|
-
width: 0 !important;
|
|
35
|
-
height: 0 !important;
|
|
36
|
-
}
|
|
37
|
-
`;
|
|
38
|
-
document.head.appendChild(style);
|
|
39
|
-
return () => {
|
|
40
|
-
document.head.removeChild(style);
|
|
41
|
-
};
|
|
42
|
-
}, []);
|
|
10
|
+
// 기본 상태들
|
|
43
11
|
const [scrollbarVisible, setScrollbarVisible] = useState(false);
|
|
44
|
-
const [trackVisible, setTrackVisible] = useState(false); // 트랙 표시 상태 추가
|
|
45
12
|
const [isDragging, setIsDragging] = useState(false);
|
|
46
13
|
const [dragStart, setDragStart] = useState({ y: 0, scrollTop: 0 });
|
|
47
14
|
const [thumbHeight, setThumbHeight] = useState(0);
|
|
48
15
|
const [thumbTop, setThumbTop] = useState(0);
|
|
49
|
-
|
|
16
|
+
const [activeArrow, setActiveArrow] = useState(null);
|
|
17
|
+
const [hoveredArrow, setHoveredArrow] = useState(null);
|
|
18
|
+
// 초기 마운트 시 hover 방지용
|
|
19
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
20
|
+
// 휠 스크롤 감지용
|
|
21
|
+
const wheelTimeoutRef = useRef(null);
|
|
22
|
+
const [isWheelScrolling, setIsWheelScrolling] = useState(false);
|
|
23
|
+
// 숨김 타이머
|
|
50
24
|
const hideTimeoutRef = useRef(null);
|
|
51
25
|
// ref를 통해 외부에서 스크롤 컨테이너에 접근할 수 있도록 함
|
|
52
26
|
useImperativeHandle(ref, () => ({
|
|
@@ -69,72 +43,60 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
|
|
|
69
43
|
return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0;
|
|
70
44
|
},
|
|
71
45
|
}), []);
|
|
72
|
-
//
|
|
46
|
+
// 스크롤 가능 여부 체크
|
|
47
|
+
const isScrollable = useCallback(() => {
|
|
48
|
+
if (!containerRef.current || !contentRef.current)
|
|
49
|
+
return false;
|
|
50
|
+
return (contentRef.current.scrollHeight >
|
|
51
|
+
containerRef.current.clientHeight + 2);
|
|
52
|
+
}, []);
|
|
53
|
+
// 타이머 정리
|
|
73
54
|
const clearHideTimer = useCallback(() => {
|
|
74
55
|
if (hideTimeoutRef.current) {
|
|
75
56
|
clearTimeout(hideTimeoutRef.current);
|
|
76
57
|
hideTimeoutRef.current = null;
|
|
77
58
|
}
|
|
78
59
|
}, []);
|
|
79
|
-
// 스크롤바
|
|
60
|
+
// 스크롤바 숨기기 타이머
|
|
80
61
|
const setHideTimer = useCallback((delay) => {
|
|
81
|
-
clearHideTimer();
|
|
62
|
+
clearHideTimer();
|
|
82
63
|
hideTimeoutRef.current = setTimeout(() => {
|
|
83
|
-
|
|
84
|
-
setScrollbarVisible(false);
|
|
85
|
-
setTrackVisible(false);
|
|
86
|
-
}
|
|
64
|
+
setScrollbarVisible(false);
|
|
87
65
|
hideTimeoutRef.current = null;
|
|
88
66
|
}, delay);
|
|
89
|
-
}, [
|
|
90
|
-
//
|
|
91
|
-
const isScrollable = useCallback(() => {
|
|
92
|
-
if (!containerRef.current || !contentRef.current)
|
|
93
|
-
return false;
|
|
94
|
-
const container = containerRef.current;
|
|
95
|
-
const content = contentRef.current;
|
|
96
|
-
return content.scrollHeight > container.clientHeight + 2;
|
|
97
|
-
}, []);
|
|
98
|
-
// 스크롤바 크기 및 위치 계산
|
|
67
|
+
}, [clearHideTimer, isDragging]);
|
|
68
|
+
// 스크롤바 위치 및 크기 업데이트
|
|
99
69
|
const updateScrollbar = useCallback(() => {
|
|
100
|
-
if (!containerRef.current ||
|
|
70
|
+
if (!containerRef.current ||
|
|
71
|
+
!contentRef.current ||
|
|
72
|
+
!scrollbarRef.current)
|
|
101
73
|
return;
|
|
102
74
|
const container = containerRef.current;
|
|
103
75
|
const content = contentRef.current;
|
|
104
76
|
const containerHeight = container.clientHeight;
|
|
105
77
|
const contentHeight = content.scrollHeight;
|
|
106
78
|
const scrollTop = container.scrollTop;
|
|
107
|
-
//
|
|
108
|
-
// containerHeight,
|
|
109
|
-
// contentHeight,
|
|
110
|
-
// scrollTop,
|
|
111
|
-
// hasScrollableContent: contentHeight > containerHeight,
|
|
112
|
-
// });
|
|
113
|
-
// 스크롤 가능한 콘텐츠가 있는지 확인 (여유분 2px 추가로 더 정확한 판단)
|
|
79
|
+
// 스크롤 불가능하면 숨김
|
|
114
80
|
if (contentHeight <= containerHeight + 2) {
|
|
115
|
-
// console.log("스크롤 불가능한 콘텐츠, 스크롤바 숨김");
|
|
116
81
|
setScrollbarVisible(false);
|
|
117
|
-
|
|
118
|
-
clearHideTimer(); // 타이머도 정리
|
|
82
|
+
clearHideTimer();
|
|
119
83
|
return;
|
|
120
84
|
}
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const scrollRatio =
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// maxThumbTop,
|
|
134
|
-
// });
|
|
85
|
+
// 화살표와 간격 공간 계산 (화살표 + 위아래여백 4px + 화살표간격 4px씩, 화살표 없어도 위아래 4px씩 여백)
|
|
86
|
+
const arrowSpace = showArrows ? scrollbarWidth * 2 + 16 : 8;
|
|
87
|
+
// 썸 높이 계산 (사용자 설정 최소 높이 사용, 화살표 공간 제외)
|
|
88
|
+
const availableHeight = containerHeight - arrowSpace;
|
|
89
|
+
const scrollRatio = containerHeight / contentHeight;
|
|
90
|
+
const calculatedThumbHeight = Math.max(availableHeight * scrollRatio, thumbMinHeight);
|
|
91
|
+
// 썸 위치 계산 (화살표와 간격 공간 제외)
|
|
92
|
+
const scrollableHeight = contentHeight - containerHeight;
|
|
93
|
+
const thumbScrollableHeight = availableHeight - calculatedThumbHeight;
|
|
94
|
+
const calculatedThumbTop = scrollableHeight > 0
|
|
95
|
+
? (scrollTop / scrollableHeight) * thumbScrollableHeight
|
|
96
|
+
: 0;
|
|
135
97
|
setThumbHeight(calculatedThumbHeight);
|
|
136
98
|
setThumbTop(calculatedThumbTop);
|
|
137
|
-
}, []);
|
|
99
|
+
}, [clearHideTimer, showArrows, scrollbarWidth, thumbMinHeight]);
|
|
138
100
|
// 썸 드래그 시작
|
|
139
101
|
const handleThumbMouseDown = useCallback((event) => {
|
|
140
102
|
event.preventDefault();
|
|
@@ -146,24 +108,21 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
|
|
|
146
108
|
y: event.clientY,
|
|
147
109
|
scrollTop: containerRef.current.scrollTop,
|
|
148
110
|
});
|
|
111
|
+
clearHideTimer();
|
|
149
112
|
setScrollbarVisible(true);
|
|
150
|
-
setTrackVisible(true); // 드래그 시 트랙 표시
|
|
151
|
-
clearHideTimer(); // 드래그 중에는 타이머 취소
|
|
152
113
|
}, [clearHideTimer]);
|
|
153
114
|
// 썸 드래그 중
|
|
154
115
|
const handleMouseMove = useCallback((event) => {
|
|
155
116
|
if (!isDragging || !containerRef.current || !contentRef.current)
|
|
156
117
|
return;
|
|
157
|
-
event.preventDefault();
|
|
158
118
|
const container = containerRef.current;
|
|
159
119
|
const content = contentRef.current;
|
|
160
120
|
const containerHeight = container.clientHeight;
|
|
161
121
|
const contentHeight = content.scrollHeight;
|
|
162
|
-
const deltaY = event.clientY - dragStart.y;
|
|
163
122
|
const scrollableHeight = contentHeight - containerHeight;
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
const scrollDelta = (deltaY /
|
|
123
|
+
const deltaY = event.clientY - dragStart.y;
|
|
124
|
+
const thumbScrollableHeight = containerHeight - thumbHeight;
|
|
125
|
+
const scrollDelta = (deltaY / thumbScrollableHeight) * scrollableHeight;
|
|
167
126
|
const newScrollTop = Math.max(0, Math.min(scrollableHeight, dragStart.scrollTop + scrollDelta));
|
|
168
127
|
container.scrollTop = newScrollTop;
|
|
169
128
|
updateScrollbar();
|
|
@@ -171,10 +130,11 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
|
|
|
171
130
|
// 썸 드래그 종료
|
|
172
131
|
const handleMouseUp = useCallback(() => {
|
|
173
132
|
setIsDragging(false);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
133
|
+
if (isScrollable()) {
|
|
134
|
+
setHideTimer(hideDelay); // 기본 숨김 시간 적용
|
|
135
|
+
}
|
|
136
|
+
}, [isScrollable, setHideTimer, hideDelay]);
|
|
137
|
+
// 트랙 클릭으로 스크롤 점프
|
|
178
138
|
const handleTrackClick = useCallback((event) => {
|
|
179
139
|
if (!containerRef.current ||
|
|
180
140
|
!contentRef.current ||
|
|
@@ -192,43 +152,89 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
|
|
|
192
152
|
container.scrollTop = Math.max(0, Math.min(contentHeight - containerHeight, newScrollTop));
|
|
193
153
|
updateScrollbar();
|
|
194
154
|
setScrollbarVisible(true);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
155
|
+
setHideTimer(hideDelay);
|
|
156
|
+
}, [updateScrollbar, setHideTimer, hideDelay]);
|
|
157
|
+
// 위쪽 화살표 클릭 핸들러
|
|
158
|
+
const handleUpArrowClick = useCallback((event) => {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
event.stopPropagation();
|
|
161
|
+
if (!containerRef.current)
|
|
162
|
+
return;
|
|
163
|
+
const newScrollTop = Math.max(0, containerRef.current.scrollTop - arrowStep);
|
|
164
|
+
containerRef.current.scrollTop = newScrollTop;
|
|
165
|
+
updateScrollbar();
|
|
166
|
+
setScrollbarVisible(true);
|
|
167
|
+
setHideTimer(hideDelay);
|
|
168
|
+
}, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
|
|
169
|
+
// 아래쪽 화살표 클릭 핸들러
|
|
170
|
+
const handleDownArrowClick = useCallback((event) => {
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
event.stopPropagation();
|
|
173
|
+
if (!containerRef.current || !contentRef.current)
|
|
174
|
+
return;
|
|
175
|
+
const container = containerRef.current;
|
|
176
|
+
const content = contentRef.current;
|
|
177
|
+
const maxScrollTop = content.scrollHeight - container.clientHeight;
|
|
178
|
+
const newScrollTop = Math.min(maxScrollTop, container.scrollTop + arrowStep);
|
|
179
|
+
container.scrollTop = newScrollTop;
|
|
180
|
+
updateScrollbar();
|
|
181
|
+
setScrollbarVisible(true);
|
|
182
|
+
setHideTimer(hideDelay);
|
|
183
|
+
}, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
|
|
184
|
+
// 스크롤 이벤트 리스너
|
|
199
185
|
useEffect(() => {
|
|
200
186
|
const container = containerRef.current;
|
|
201
187
|
if (!container)
|
|
202
188
|
return;
|
|
203
|
-
|
|
204
|
-
const handleWheel = () => {
|
|
205
|
-
clearHideTimer(); // 먼저 기존 타이머 취소
|
|
206
|
-
setScrollbarVisible(true);
|
|
207
|
-
// 휠 스크롤 시에는 트랙 숨김 (thumb만 표시)
|
|
189
|
+
const handleScroll = (event) => {
|
|
208
190
|
updateScrollbar();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// 스크롤 이벤트 디바운스
|
|
212
|
-
const debouncedScroll = (event) => {
|
|
213
|
-
clearHideTimer(); // 먼저 기존 타이머 취소
|
|
191
|
+
// 스크롤 중에는 스크롤바 표시
|
|
192
|
+
clearHideTimer();
|
|
214
193
|
setScrollbarVisible(true);
|
|
215
|
-
// 스크롤
|
|
216
|
-
|
|
217
|
-
setHideTimer(
|
|
194
|
+
// 휠 스크롤 중이면 빠른 숨김, 아니면 기본 숨김 시간 적용
|
|
195
|
+
const delay = isWheelScrolling ? hideDelayOnWheel : hideDelay;
|
|
196
|
+
setHideTimer(delay);
|
|
218
197
|
if (onScroll) {
|
|
219
198
|
onScroll(event);
|
|
220
199
|
}
|
|
221
200
|
};
|
|
222
|
-
|
|
201
|
+
const handleWheel = () => {
|
|
202
|
+
// 휠 스크롤 상태 표시
|
|
203
|
+
setIsWheelScrolling(true);
|
|
204
|
+
// 기존 휠 타이머 제거
|
|
205
|
+
if (wheelTimeoutRef.current) {
|
|
206
|
+
clearTimeout(wheelTimeoutRef.current);
|
|
207
|
+
}
|
|
208
|
+
// 300ms 후 휠 스크롤 상태 해제 (휠 스크롤이 끝났다고 간주)
|
|
209
|
+
wheelTimeoutRef.current = setTimeout(() => {
|
|
210
|
+
setIsWheelScrolling(false);
|
|
211
|
+
}, 300);
|
|
212
|
+
clearHideTimer();
|
|
213
|
+
setScrollbarVisible(true);
|
|
214
|
+
};
|
|
215
|
+
container.addEventListener("scroll", handleScroll, {
|
|
216
|
+
passive: true,
|
|
217
|
+
});
|
|
218
|
+
container.addEventListener("wheel", handleWheel, {
|
|
223
219
|
passive: true,
|
|
224
220
|
});
|
|
225
|
-
container.addEventListener("wheel", handleWheel, { passive: true });
|
|
226
221
|
return () => {
|
|
227
|
-
container.removeEventListener("scroll",
|
|
222
|
+
container.removeEventListener("scroll", handleScroll);
|
|
228
223
|
container.removeEventListener("wheel", handleWheel);
|
|
224
|
+
if (wheelTimeoutRef.current) {
|
|
225
|
+
clearTimeout(wheelTimeoutRef.current);
|
|
226
|
+
}
|
|
229
227
|
};
|
|
230
|
-
}, [
|
|
231
|
-
|
|
228
|
+
}, [
|
|
229
|
+
updateScrollbar,
|
|
230
|
+
onScroll,
|
|
231
|
+
clearHideTimer,
|
|
232
|
+
setHideTimer,
|
|
233
|
+
hideDelay,
|
|
234
|
+
hideDelayOnWheel,
|
|
235
|
+
isWheelScrolling,
|
|
236
|
+
]);
|
|
237
|
+
// 전역 마우스 이벤트 리스너
|
|
232
238
|
useEffect(() => {
|
|
233
239
|
if (isDragging) {
|
|
234
240
|
document.addEventListener("mousemove", handleMouseMove);
|
|
@@ -239,153 +245,172 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
|
|
|
239
245
|
};
|
|
240
246
|
}
|
|
241
247
|
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
242
|
-
// 초기 스크롤바
|
|
248
|
+
// 초기 스크롤바 업데이트
|
|
243
249
|
useEffect(() => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const container = containerRef.current;
|
|
248
|
-
const content = contentRef.current;
|
|
249
|
-
if (container && content) {
|
|
250
|
-
const hasScrollableContent = content.scrollHeight > container.clientHeight + 2; // 여유분 2px 추가
|
|
251
|
-
// console.log("초기 스크롤바 체크:", {
|
|
252
|
-
// hasScrollableContent,
|
|
253
|
-
// contentScrollHeight: content.scrollHeight,
|
|
254
|
-
// containerClientHeight: container.clientHeight,
|
|
255
|
-
// });
|
|
256
|
-
if (hasScrollableContent) {
|
|
257
|
-
// 초기에는 스크롤바를 숨김 상태로 유지 (스크롤이나 hover 시에만 표시)
|
|
258
|
-
setScrollbarVisible(false);
|
|
259
|
-
setTrackVisible(false);
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
// 스크롤이 필요 없으면 확실히 숨김
|
|
263
|
-
setScrollbarVisible(false);
|
|
264
|
-
setTrackVisible(false);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
// 차트 렌더링을 고려하여 더 긴 지연시간 적용
|
|
269
|
-
const timeoutId = setTimeout(checkAndUpdateScrollbar, 200);
|
|
270
|
-
return () => clearTimeout(timeoutId);
|
|
271
|
-
}, [updateScrollbar, children, isDragging]);
|
|
272
|
-
// 리사이즈 옵저버
|
|
250
|
+
updateScrollbar();
|
|
251
|
+
}, [updateScrollbar]);
|
|
252
|
+
// 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
|
|
273
253
|
useEffect(() => {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
254
|
+
const timer = setTimeout(() => {
|
|
255
|
+
setIsInitialized(true);
|
|
256
|
+
}, 100); // 100ms 후 초기화 완료
|
|
257
|
+
return () => clearTimeout(timer);
|
|
258
|
+
}, []);
|
|
259
|
+
// Resize observer로 크기 변경 감지
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
if (!containerRef.current || !contentRef.current)
|
|
277
262
|
return;
|
|
278
263
|
const resizeObserver = new ResizeObserver(() => {
|
|
279
|
-
|
|
280
|
-
setTimeout(() => {
|
|
281
|
-
updateScrollbar();
|
|
282
|
-
}, 100);
|
|
264
|
+
updateScrollbar();
|
|
283
265
|
});
|
|
284
|
-
resizeObserver.observe(
|
|
285
|
-
resizeObserver.observe(
|
|
286
|
-
return () =>
|
|
287
|
-
resizeObserver.disconnect();
|
|
288
|
-
};
|
|
266
|
+
resizeObserver.observe(containerRef.current);
|
|
267
|
+
resizeObserver.observe(contentRef.current);
|
|
268
|
+
return () => resizeObserver.disconnect();
|
|
289
269
|
}, [updateScrollbar]);
|
|
290
|
-
//
|
|
270
|
+
// 계산된 값들을 메모이제이션하여 안정화
|
|
271
|
+
const { finalThumbWidth, finalTrackWidth } = useMemo(() => {
|
|
272
|
+
const computedThumbWidth = thumbWidth !== undefined ? thumbWidth : scrollbarWidth;
|
|
273
|
+
let computedTrackWidth = trackWidth !== undefined ? trackWidth : scrollbarWidth * 2;
|
|
274
|
+
// thumbWidth가 trackWidth보다 크거나 같으면 trackWidth를 thumbWidth와 같게 설정
|
|
275
|
+
if (computedThumbWidth >= computedTrackWidth) {
|
|
276
|
+
computedTrackWidth = computedThumbWidth;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
finalThumbWidth: computedThumbWidth,
|
|
280
|
+
finalTrackWidth: computedTrackWidth,
|
|
281
|
+
};
|
|
282
|
+
}, [thumbWidth, trackWidth, scrollbarWidth]);
|
|
283
|
+
// 썸 radius 계산 (기본값: thumbWidth / 2)
|
|
284
|
+
const calculatedThumbRadius = thumbRadius !== undefined ? thumbRadius : finalThumbWidth / 2;
|
|
285
|
+
// 화살표 색상 계산 (기본값: 독립적인 색상)
|
|
286
|
+
const finalArrowColor = arrowColor || "rgba(128, 128, 128, 0.8)";
|
|
287
|
+
const finalArrowActiveColor = arrowActiveColor || "rgba(128, 128, 128, 1.0)";
|
|
288
|
+
// 웹킷 스크롤바 숨기기용 CSS 동적 주입
|
|
291
289
|
useEffect(() => {
|
|
290
|
+
const styleId = "overlay-scrollbar-webkit-hide";
|
|
291
|
+
// 이미 스타일이 있으면 제거
|
|
292
|
+
const existingStyle = document.getElementById(styleId);
|
|
293
|
+
if (existingStyle) {
|
|
294
|
+
existingStyle.remove();
|
|
295
|
+
}
|
|
296
|
+
const style = document.createElement("style");
|
|
297
|
+
style.id = styleId;
|
|
298
|
+
style.textContent = `
|
|
299
|
+
.overlay-scrollbar-container::-webkit-scrollbar {
|
|
300
|
+
display: none !important;
|
|
301
|
+
width: 0 !important;
|
|
302
|
+
height: 0 !important;
|
|
303
|
+
}
|
|
304
|
+
.overlay-scrollbar-container::-webkit-scrollbar-track {
|
|
305
|
+
display: none !important;
|
|
306
|
+
}
|
|
307
|
+
.overlay-scrollbar-container::-webkit-scrollbar-thumb {
|
|
308
|
+
display: none !important;
|
|
309
|
+
}
|
|
310
|
+
`;
|
|
311
|
+
document.head.appendChild(style);
|
|
292
312
|
return () => {
|
|
293
|
-
|
|
294
|
-
|
|
313
|
+
const styleToRemove = document.getElementById(styleId);
|
|
314
|
+
if (styleToRemove) {
|
|
315
|
+
styleToRemove.remove();
|
|
295
316
|
}
|
|
296
317
|
};
|
|
297
318
|
}, []);
|
|
298
|
-
return (jsxs("div", { className: `overlay-scrollbar ${className
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
319
|
+
return (jsxs("div", { className: `overlay-scrollbar-wrapper ${className}`, style: Object.assign({ display: "flex", flexDirection: "column", position: "relative", minHeight: 0, height: "100%", flex: "1 1 0%" }, style), children: [jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", style: {
|
|
320
|
+
width: "100%", // 명시적 너비 설정
|
|
321
|
+
height: "100%", // 상위 컨테이너의 전체 높이 사용
|
|
322
|
+
minHeight: 0, // 최소 높이 보장
|
|
323
|
+
overflow: "auto", // 네이티브 스크롤 기능 유지
|
|
324
|
+
// 브라우저 기본 스크롤바만 숨기기
|
|
302
325
|
scrollbarWidth: "none", // Firefox
|
|
303
326
|
msOverflowStyle: "none", // IE/Edge
|
|
304
|
-
}, children: jsx("div", { ref: contentRef,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
right: 0,
|
|
308
|
-
width: 20, // 20px 넓은 hover 영역
|
|
309
|
-
height: "100%",
|
|
310
|
-
zIndex: 5,
|
|
311
|
-
pointerEvents: "auto",
|
|
312
|
-
}, onMouseEnter: () => {
|
|
313
|
-
// 스크롤 가능한 경우에만 스크롤바 표시
|
|
327
|
+
}, children: jsx("div", { ref: contentRef, className: "overlay-scrollbar-content", style: {
|
|
328
|
+
minHeight: "100%",
|
|
329
|
+
}, children: children }) }), jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
|
|
314
330
|
if (isScrollable()) {
|
|
315
331
|
clearHideTimer();
|
|
316
332
|
setScrollbarVisible(true);
|
|
317
|
-
setTrackVisible(true); // hover 시 트랙까지 표시
|
|
318
333
|
}
|
|
319
334
|
}, onMouseLeave: () => {
|
|
320
|
-
// 스크롤바 hover 영역에서 벗어남 시
|
|
321
335
|
if (!isDragging && isScrollable()) {
|
|
322
|
-
|
|
323
|
-
setHideTimer(1000); // 1초 후 숨김
|
|
336
|
+
setHideTimer(hideDelay);
|
|
324
337
|
}
|
|
325
|
-
}
|
|
326
|
-
// 스크롤 가능한 경우에만 스크롤바 영역에 hover 시 타이머 취소하고 표시 유지
|
|
327
|
-
if (isScrollable()) {
|
|
328
|
-
clearHideTimer();
|
|
329
|
-
setScrollbarVisible(true);
|
|
330
|
-
setTrackVisible(true);
|
|
331
|
-
}
|
|
332
|
-
}, onMouseLeave: () => {
|
|
333
|
-
// 스크롤바 영역에서 벗어나면 일정 시간 후 숨김
|
|
334
|
-
if (!isDragging && isScrollable()) {
|
|
335
|
-
setHideTimer(1000);
|
|
336
|
-
}
|
|
337
|
-
}, className: `overlay-scrollbar-track ${scrollbarVisible ? "visible" : ""}`, style: {
|
|
338
|
+
}, style: {
|
|
338
339
|
position: "absolute",
|
|
339
340
|
top: 0,
|
|
340
|
-
right:
|
|
341
|
-
width:
|
|
341
|
+
right: 0, // 완전히 오른쪽에 붙임
|
|
342
|
+
width: `${finalTrackWidth}px`, // hover 영역 너비
|
|
342
343
|
height: "100%",
|
|
343
344
|
opacity: scrollbarVisible ? 1 : 0,
|
|
344
|
-
transition: "opacity 0.
|
|
345
|
-
pointerEvents: scrollbarVisible ? "auto" : "none",
|
|
346
|
-
zIndex: 10,
|
|
345
|
+
transition: "opacity 0.2s ease-in-out",
|
|
347
346
|
cursor: "pointer",
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
347
|
+
zIndex: 1000,
|
|
348
|
+
pointerEvents: "auto", // 항상 이벤트 활성화 (hover 감지용)
|
|
349
|
+
}, children: [jsx("div", { className: "overlay-scrollbar-track-background", onClick: handleTrackClick, style: {
|
|
350
|
+
position: "absolute",
|
|
351
|
+
top: showArrows
|
|
352
|
+
? `${finalThumbWidth + 8}px`
|
|
353
|
+
: "4px",
|
|
354
|
+
right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
|
|
355
|
+
width: `${finalThumbWidth}px`,
|
|
356
|
+
height: showArrows
|
|
357
|
+
? `calc(100% - ${finalThumbWidth * 2 + 16}px)`
|
|
358
|
+
: "calc(100% - 8px)",
|
|
359
|
+
backgroundColor: trackColor,
|
|
360
|
+
borderRadius: `${calculatedThumbRadius}px`,
|
|
361
|
+
cursor: "pointer",
|
|
362
|
+
} }), jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, style: {
|
|
363
|
+
position: "absolute",
|
|
364
|
+
top: `${(showArrows ? finalThumbWidth + 8 : 4) +
|
|
365
|
+
thumbTop}px`,
|
|
366
|
+
right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
|
|
367
|
+
width: `${finalThumbWidth}px`,
|
|
368
|
+
height: `${Math.max(thumbHeight, thumbMinHeight)}px`,
|
|
369
|
+
backgroundColor: isDragging
|
|
370
|
+
? thumbActiveColor
|
|
371
|
+
: thumbColor,
|
|
372
|
+
borderRadius: `${calculatedThumbRadius}px`,
|
|
373
|
+
cursor: "pointer",
|
|
374
|
+
transition: isDragging
|
|
375
|
+
? "none"
|
|
376
|
+
: "background-color 0.2s ease-in-out",
|
|
377
|
+
} })] }), showArrows && (jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
|
|
378
|
+
position: "absolute",
|
|
379
|
+
top: "4px",
|
|
380
|
+
right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
|
|
381
|
+
width: `${finalThumbWidth}px`,
|
|
382
|
+
height: `${finalThumbWidth}px`,
|
|
383
|
+
cursor: "pointer",
|
|
384
|
+
display: "flex",
|
|
385
|
+
alignItems: "center",
|
|
386
|
+
justifyContent: "center",
|
|
387
|
+
fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
|
|
388
|
+
color: hoveredArrow === "up"
|
|
389
|
+
? finalArrowActiveColor
|
|
390
|
+
: finalArrowColor,
|
|
391
|
+
userSelect: "none",
|
|
392
|
+
zIndex: 1001,
|
|
393
|
+
opacity: scrollbarVisible ? 1 : 0,
|
|
394
|
+
transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
|
|
395
|
+
}, children: "\u25B2" })), showArrows && (jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
|
|
396
|
+
position: "absolute",
|
|
397
|
+
bottom: "4px",
|
|
398
|
+
right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
|
|
399
|
+
width: `${finalThumbWidth}px`,
|
|
400
|
+
height: `${finalThumbWidth}px`,
|
|
401
|
+
cursor: "pointer",
|
|
402
|
+
display: "flex",
|
|
403
|
+
alignItems: "center",
|
|
404
|
+
justifyContent: "center",
|
|
405
|
+
fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
|
|
406
|
+
color: hoveredArrow === "down"
|
|
407
|
+
? finalArrowActiveColor
|
|
408
|
+
: finalArrowColor,
|
|
409
|
+
userSelect: "none",
|
|
410
|
+
zIndex: 1001,
|
|
411
|
+
opacity: scrollbarVisible ? 1 : 0,
|
|
412
|
+
transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
|
|
413
|
+
}, children: "\u25BC" }))] }));
|
|
389
414
|
});
|
|
390
415
|
|
|
391
416
|
export { OverlayScrollbar, OverlayScrollbar as default };
|