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