@ehfuse/overlay-scrollbar 1.3.0 → 1.4.1
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 +16 -6
- package/dist/index.d.ts +15 -5
- package/dist/index.esm.js +282 -142
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +281 -141
- package/dist/index.js.map +1 -1
- package/dist/src/OverlayScrollbar.d.ts +15 -5
- package/dist/src/OverlayScrollbar.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import { forwardRef, useRef, useState, useMemo, useImperativeHandle, useCallback
|
|
2
|
+
import { forwardRef, useRef, useEffect, useState, useMemo, useImperativeHandle, useCallback } from 'react';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* MIT License
|
|
@@ -289,18 +289,43 @@ const checkElementAndParents = (element, config) => {
|
|
|
289
289
|
return false;
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
// 기본 설정 객체들을 컴포넌트 외부에 상수로 선언 (재렌더링 시 동일한 참조 유지)
|
|
293
|
+
const DEFAULT_THUMB_CONFIG = {};
|
|
294
|
+
const DEFAULT_TRACK_CONFIG = {};
|
|
295
|
+
const DEFAULT_ARROWS_CONFIG = {};
|
|
296
|
+
const DEFAULT_DRAG_SCROLL_CONFIG = {};
|
|
297
|
+
const DEFAULT_AUTO_HIDE_CONFIG = {};
|
|
298
|
+
const OverlayScrollbar = forwardRef(({ className = "", style = {}, children, onScroll,
|
|
293
299
|
// 그룹화된 설정 객체들
|
|
294
|
-
thumb =
|
|
300
|
+
thumb = DEFAULT_THUMB_CONFIG, track = DEFAULT_TRACK_CONFIG, arrows = DEFAULT_ARROWS_CONFIG, dragScroll = DEFAULT_DRAG_SCROLL_CONFIG, autoHide = DEFAULT_AUTO_HIDE_CONFIG,
|
|
295
301
|
// 기타 설정들
|
|
296
|
-
showScrollbar = true,
|
|
302
|
+
showScrollbar = true, }, ref) => {
|
|
303
|
+
// props 변경 추적용 ref
|
|
304
|
+
const prevPropsRef = useRef({});
|
|
305
|
+
// 렌더링 시 어떤 prop이 변경되었는지 체크
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
// 현재 props 저장
|
|
308
|
+
prevPropsRef.current = {
|
|
309
|
+
children,
|
|
310
|
+
onScroll,
|
|
311
|
+
showScrollbar,
|
|
312
|
+
thumb,
|
|
313
|
+
track,
|
|
314
|
+
arrows,
|
|
315
|
+
dragScroll,
|
|
316
|
+
autoHide,
|
|
317
|
+
};
|
|
318
|
+
});
|
|
297
319
|
const containerRef = useRef(null);
|
|
298
320
|
const contentRef = useRef(null);
|
|
299
321
|
const scrollbarRef = useRef(null);
|
|
300
322
|
const thumbRef = useRef(null);
|
|
323
|
+
// 스크롤 컨테이너 캐싱용 ref (성능 최적화)
|
|
324
|
+
const cachedScrollContainerRef = useRef(null);
|
|
301
325
|
// 기본 상태들
|
|
302
326
|
const [scrollbarVisible, setScrollbarVisible] = useState(false);
|
|
303
327
|
const [isDragging, setIsDragging] = useState(false);
|
|
328
|
+
const [isThumbHovered, setIsThumbHovered] = useState(false);
|
|
304
329
|
const [dragStart, setDragStart] = useState({ y: 0, scrollTop: 0 });
|
|
305
330
|
const [thumbHeight, setThumbHeight] = useState(0);
|
|
306
331
|
const [thumbTop, setThumbTop] = useState(0);
|
|
@@ -323,31 +348,40 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
323
348
|
const hideTimeoutRef = useRef(null);
|
|
324
349
|
// 그룹화된 설정 객체들에 기본값 설정
|
|
325
350
|
const finalThumbConfig = useMemo(() => {
|
|
326
|
-
var _a, _b, _c, _d, _e, _f;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
351
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
352
|
+
const baseColor = (_a = thumb.color) !== null && _a !== void 0 ? _a : "#606060";
|
|
353
|
+
return {
|
|
354
|
+
width: (_b = thumb.width) !== null && _b !== void 0 ? _b : 8,
|
|
355
|
+
minHeight: (_c = thumb.minHeight) !== null && _c !== void 0 ? _c : 50,
|
|
356
|
+
radius: (_d = thumb.radius) !== null && _d !== void 0 ? _d : ((_e = thumb.width) !== null && _e !== void 0 ? _e : 8) / 2,
|
|
357
|
+
color: baseColor,
|
|
358
|
+
opacity: (_f = thumb.opacity) !== null && _f !== void 0 ? _f : 0.6,
|
|
359
|
+
hoverColor: (_g = thumb.hoverColor) !== null && _g !== void 0 ? _g : baseColor,
|
|
360
|
+
hoverOpacity: (_h = thumb.hoverOpacity) !== null && _h !== void 0 ? _h : 1.0,
|
|
361
|
+
};
|
|
334
362
|
}, [thumb]);
|
|
335
363
|
const finalTrackConfig = useMemo(() => {
|
|
336
|
-
var _a, _b, _c;
|
|
364
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
337
365
|
return ({
|
|
338
366
|
width: (_a = track.width) !== null && _a !== void 0 ? _a : 16,
|
|
339
367
|
color: (_b = track.color) !== null && _b !== void 0 ? _b : "rgba(128, 128, 128, 0.1)",
|
|
340
368
|
visible: (_c = track.visible) !== null && _c !== void 0 ? _c : true,
|
|
369
|
+
alignment: (_d = track.alignment) !== null && _d !== void 0 ? _d : "center",
|
|
370
|
+
radius: (_f = (_e = track.radius) !== null && _e !== void 0 ? _e : finalThumbConfig.radius) !== null && _f !== void 0 ? _f : 4,
|
|
371
|
+
margin: (_g = track.margin) !== null && _g !== void 0 ? _g : 4,
|
|
341
372
|
});
|
|
342
|
-
}, [track]);
|
|
373
|
+
}, [track, finalThumbConfig.radius]);
|
|
343
374
|
const finalArrowsConfig = useMemo(() => {
|
|
344
|
-
var _a, _b, _c, _d;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
375
|
+
var _a, _b, _c, _d, _e, _f;
|
|
376
|
+
const baseColor = (_a = arrows.color) !== null && _a !== void 0 ? _a : "#808080";
|
|
377
|
+
return {
|
|
378
|
+
visible: (_b = arrows.visible) !== null && _b !== void 0 ? _b : false,
|
|
379
|
+
step: (_c = arrows.step) !== null && _c !== void 0 ? _c : 50,
|
|
380
|
+
color: baseColor,
|
|
381
|
+
opacity: (_d = arrows.opacity) !== null && _d !== void 0 ? _d : 0.6,
|
|
382
|
+
hoverColor: (_e = arrows.hoverColor) !== null && _e !== void 0 ? _e : baseColor,
|
|
383
|
+
hoverOpacity: (_f = arrows.hoverOpacity) !== null && _f !== void 0 ? _f : 1.0,
|
|
384
|
+
};
|
|
351
385
|
}, [arrows]);
|
|
352
386
|
const finalDragScrollConfig = useMemo(() => {
|
|
353
387
|
var _a, _b, _c;
|
|
@@ -357,6 +391,14 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
357
391
|
excludeSelectors: (_c = dragScroll.excludeSelectors) !== null && _c !== void 0 ? _c : [],
|
|
358
392
|
});
|
|
359
393
|
}, [dragScroll]);
|
|
394
|
+
const finalAutoHideConfig = useMemo(() => {
|
|
395
|
+
var _a, _b, _c;
|
|
396
|
+
return ({
|
|
397
|
+
enabled: (_a = autoHide.enabled) !== null && _a !== void 0 ? _a : true,
|
|
398
|
+
delay: (_b = autoHide.delay) !== null && _b !== void 0 ? _b : 1500,
|
|
399
|
+
delayOnWheel: (_c = autoHide.delayOnWheel) !== null && _c !== void 0 ? _c : 700,
|
|
400
|
+
});
|
|
401
|
+
}, [autoHide]);
|
|
360
402
|
// 호환성을 위한 변수들 (자주 사용되는 변수들만 유지)
|
|
361
403
|
const finalThumbWidth = finalThumbConfig.width;
|
|
362
404
|
const finalTrackWidth = finalTrackConfig.width;
|
|
@@ -384,32 +426,18 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
384
426
|
return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0;
|
|
385
427
|
},
|
|
386
428
|
}), []);
|
|
387
|
-
// 실제 스크롤 가능한 요소 찾기
|
|
429
|
+
// 실제 스크롤 가능한 요소 찾기 (캐싱 최적화)
|
|
388
430
|
const findScrollableElement = useCallback(() => {
|
|
389
|
-
//
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const virtuosoScroller = externalScrollContainer.querySelector('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller');
|
|
397
|
-
if (virtuosoScroller) {
|
|
398
|
-
const element = virtuosoScroller;
|
|
399
|
-
if (element.scrollHeight > element.clientHeight + 2) {
|
|
400
|
-
console.log("OverlayScrollbar: found virtuoso scroller", {
|
|
401
|
-
scrollHeight: element.scrollHeight,
|
|
402
|
-
clientHeight: element.clientHeight,
|
|
403
|
-
element,
|
|
404
|
-
});
|
|
405
|
-
return element;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
// externalScrollContainer 자체가 스크롤 가능한지 확인
|
|
409
|
-
if (externalScrollContainer.scrollHeight >
|
|
410
|
-
externalScrollContainer.clientHeight + 2) {
|
|
411
|
-
return externalScrollContainer;
|
|
431
|
+
// 캐시된 요소가 여전히 유효한지 확인
|
|
432
|
+
if (cachedScrollContainerRef.current) {
|
|
433
|
+
const cached = cachedScrollContainerRef.current;
|
|
434
|
+
// DOM에 연결되어 있고 여전히 스크롤 가능한지 확인
|
|
435
|
+
if (document.contains(cached) &&
|
|
436
|
+
cached.scrollHeight > cached.clientHeight + 2) {
|
|
437
|
+
return cached;
|
|
412
438
|
}
|
|
439
|
+
// 캐시 무효화
|
|
440
|
+
cachedScrollContainerRef.current = null;
|
|
413
441
|
}
|
|
414
442
|
if (!containerRef.current) {
|
|
415
443
|
return null;
|
|
@@ -418,6 +446,7 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
418
446
|
if (contentRef.current &&
|
|
419
447
|
contentRef.current.scrollHeight >
|
|
420
448
|
containerRef.current.clientHeight + 2) {
|
|
449
|
+
cachedScrollContainerRef.current = containerRef.current;
|
|
421
450
|
return containerRef.current;
|
|
422
451
|
}
|
|
423
452
|
// children 요소에서 스크롤 가능한 요소 찾기
|
|
@@ -425,16 +454,12 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
425
454
|
for (const child of childScrollableElements) {
|
|
426
455
|
const element = child;
|
|
427
456
|
if (element.scrollHeight > element.clientHeight + 2) {
|
|
428
|
-
|
|
429
|
-
scrollHeight: element.scrollHeight,
|
|
430
|
-
clientHeight: element.clientHeight,
|
|
431
|
-
element,
|
|
432
|
-
});
|
|
457
|
+
cachedScrollContainerRef.current = element;
|
|
433
458
|
return element;
|
|
434
459
|
}
|
|
435
460
|
}
|
|
436
461
|
return null;
|
|
437
|
-
}, [
|
|
462
|
+
}, []);
|
|
438
463
|
// 스크롤 가능 여부 체크
|
|
439
464
|
const isScrollable = useCallback(() => {
|
|
440
465
|
return findScrollableElement() !== null;
|
|
@@ -448,12 +473,16 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
448
473
|
}, []);
|
|
449
474
|
// 스크롤바 숨기기 타이머
|
|
450
475
|
const setHideTimer = useCallback((delay) => {
|
|
476
|
+
// 자동 숨김이 비활성화되어 있으면 타이머를 설정하지 않음
|
|
477
|
+
if (!finalAutoHideConfig.enabled) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
451
480
|
clearHideTimer();
|
|
452
481
|
hideTimeoutRef.current = setTimeout(() => {
|
|
453
482
|
setScrollbarVisible(false);
|
|
454
483
|
hideTimeoutRef.current = null;
|
|
455
484
|
}, delay);
|
|
456
|
-
}, [clearHideTimer,
|
|
485
|
+
}, [clearHideTimer, finalAutoHideConfig.enabled]);
|
|
457
486
|
// 스크롤바 위치 및 크기 업데이트
|
|
458
487
|
const updateScrollbar = useCallback(() => {
|
|
459
488
|
if (!scrollbarRef.current)
|
|
@@ -465,17 +494,18 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
465
494
|
clearHideTimer();
|
|
466
495
|
return;
|
|
467
496
|
}
|
|
497
|
+
// 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
|
|
498
|
+
if (!finalAutoHideConfig.enabled) {
|
|
499
|
+
setScrollbarVisible(true);
|
|
500
|
+
clearHideTimer();
|
|
501
|
+
}
|
|
468
502
|
const containerHeight = scrollableElement.clientHeight;
|
|
469
503
|
const contentHeight = scrollableElement.scrollHeight;
|
|
470
504
|
const scrollTop = scrollableElement.scrollTop;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
element: scrollableElement,
|
|
476
|
-
});
|
|
477
|
-
// 화살표와 간격 공간 계산 (화살표 + 위아래여백 4px + 화살표간격 4px씩, 화살표 없어도 위아래 4px씩 여백)
|
|
478
|
-
const arrowSpace = showArrows ? finalThumbWidth * 2 + 16 : 8;
|
|
505
|
+
// 화살표와 간격 공간 계산 (화살표 + 위아래 마진, 화살표 없어도 위아래 마진)
|
|
506
|
+
const arrowSpace = showArrows
|
|
507
|
+
? finalThumbWidth * 2 + finalTrackConfig.margin * 4
|
|
508
|
+
: finalTrackConfig.margin * 2;
|
|
479
509
|
// 썸 높이 계산 (사용자 설정 최소 높이 사용, 화살표 공간 제외)
|
|
480
510
|
const availableHeight = containerHeight - arrowSpace;
|
|
481
511
|
const scrollRatio = containerHeight / contentHeight;
|
|
@@ -494,6 +524,7 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
494
524
|
showArrows,
|
|
495
525
|
finalThumbWidth,
|
|
496
526
|
thumbMinHeight,
|
|
527
|
+
finalAutoHideConfig.enabled,
|
|
497
528
|
]);
|
|
498
529
|
// 썸 드래그 시작
|
|
499
530
|
const handleThumbMouseDown = useCallback((event) => {
|
|
@@ -502,7 +533,6 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
502
533
|
event.stopPropagation();
|
|
503
534
|
const actualScrollContainer = findScrollableElement();
|
|
504
535
|
if (!actualScrollContainer) {
|
|
505
|
-
console.log("Thumb drag - no scrollable element found");
|
|
506
536
|
return;
|
|
507
537
|
}
|
|
508
538
|
setIsDragging(true);
|
|
@@ -521,7 +551,6 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
521
551
|
return;
|
|
522
552
|
const actualScrollContainer = findScrollableElement();
|
|
523
553
|
if (!actualScrollContainer) {
|
|
524
|
-
console.log("Mouse move - no scrollable element found");
|
|
525
554
|
return;
|
|
526
555
|
}
|
|
527
556
|
const containerHeight = actualScrollContainer.clientHeight;
|
|
@@ -544,15 +573,13 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
544
573
|
const handleMouseUp = useCallback(() => {
|
|
545
574
|
setIsDragging(false);
|
|
546
575
|
if (isScrollable()) {
|
|
547
|
-
setHideTimer(
|
|
576
|
+
setHideTimer(finalAutoHideConfig.delay); // 기본 숨김 시간 적용
|
|
548
577
|
}
|
|
549
|
-
}, [isScrollable, setHideTimer,
|
|
578
|
+
}, [isScrollable, setHideTimer, finalAutoHideConfig.delay]);
|
|
550
579
|
// 트랙 클릭으로 스크롤 점프
|
|
551
580
|
const handleTrackClick = useCallback((event) => {
|
|
552
581
|
var _a;
|
|
553
|
-
console.log("handleTrackClick called", event);
|
|
554
582
|
if (!scrollbarRef.current) {
|
|
555
|
-
console.log("Track click - scrollbarRef not available");
|
|
556
583
|
return;
|
|
557
584
|
}
|
|
558
585
|
const scrollbar = scrollbarRef.current;
|
|
@@ -560,29 +587,24 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
560
587
|
const clickY = event.clientY - rect.top;
|
|
561
588
|
const actualScrollContainer = findScrollableElement();
|
|
562
589
|
if (!actualScrollContainer) {
|
|
563
|
-
console.log("Track click - no scrollable element found");
|
|
564
590
|
return;
|
|
565
591
|
}
|
|
566
|
-
console.log("Track click - using scrollable element", actualScrollContainer);
|
|
567
592
|
const containerHeight = actualScrollContainer.clientHeight;
|
|
568
593
|
const contentHeight = actualScrollContainer.scrollHeight;
|
|
569
594
|
const scrollRatio = clickY / containerHeight;
|
|
570
595
|
const newScrollTop = scrollRatio * (contentHeight - containerHeight);
|
|
571
|
-
console.log("Track click scroll calculation", {
|
|
572
|
-
clickY,
|
|
573
|
-
containerHeight,
|
|
574
|
-
contentHeight,
|
|
575
|
-
scrollRatio,
|
|
576
|
-
newScrollTop,
|
|
577
|
-
actualScrollContainer,
|
|
578
|
-
});
|
|
579
596
|
actualScrollContainer.scrollTop = Math.max(0, Math.min(contentHeight - containerHeight, newScrollTop));
|
|
580
597
|
updateScrollbar();
|
|
581
598
|
setScrollbarVisible(true);
|
|
582
|
-
setHideTimer(
|
|
599
|
+
setHideTimer(finalAutoHideConfig.delay);
|
|
583
600
|
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
584
601
|
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
585
|
-
}, [
|
|
602
|
+
}, [
|
|
603
|
+
updateScrollbar,
|
|
604
|
+
setHideTimer,
|
|
605
|
+
finalAutoHideConfig.delay,
|
|
606
|
+
findScrollableElement,
|
|
607
|
+
]);
|
|
586
608
|
// 위쪽 화살표 클릭 핸들러
|
|
587
609
|
const handleUpArrowClick = useCallback((event) => {
|
|
588
610
|
var _a;
|
|
@@ -594,10 +616,15 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
594
616
|
containerRef.current.scrollTop = newScrollTop;
|
|
595
617
|
updateScrollbar();
|
|
596
618
|
setScrollbarVisible(true);
|
|
597
|
-
setHideTimer(
|
|
619
|
+
setHideTimer(finalAutoHideConfig.delay);
|
|
598
620
|
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
599
621
|
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
600
|
-
}, [
|
|
622
|
+
}, [
|
|
623
|
+
updateScrollbar,
|
|
624
|
+
setHideTimer,
|
|
625
|
+
arrowStep,
|
|
626
|
+
finalAutoHideConfig.delay,
|
|
627
|
+
]);
|
|
601
628
|
// 아래쪽 화살표 클릭 핸들러
|
|
602
629
|
const handleDownArrowClick = useCallback((event) => {
|
|
603
630
|
var _a;
|
|
@@ -612,10 +639,15 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
612
639
|
container.scrollTop = newScrollTop;
|
|
613
640
|
updateScrollbar();
|
|
614
641
|
setScrollbarVisible(true);
|
|
615
|
-
setHideTimer(
|
|
642
|
+
setHideTimer(finalAutoHideConfig.delay);
|
|
616
643
|
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
617
644
|
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
618
|
-
}, [
|
|
645
|
+
}, [
|
|
646
|
+
updateScrollbar,
|
|
647
|
+
setHideTimer,
|
|
648
|
+
arrowStep,
|
|
649
|
+
finalAutoHideConfig.delay,
|
|
650
|
+
]);
|
|
619
651
|
// 드래그 스크롤 시작
|
|
620
652
|
const handleDragScrollStart = useCallback((event) => {
|
|
621
653
|
// 드래그 스크롤이 비활성화된 경우
|
|
@@ -677,9 +709,9 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
677
709
|
const handleDragScrollEnd = useCallback(() => {
|
|
678
710
|
setIsDragScrolling(false);
|
|
679
711
|
if (isScrollable()) {
|
|
680
|
-
setHideTimer(
|
|
712
|
+
setHideTimer(finalAutoHideConfig.delay);
|
|
681
713
|
}
|
|
682
|
-
}, [isScrollable, setHideTimer,
|
|
714
|
+
}, [isScrollable, setHideTimer, finalAutoHideConfig.delay]);
|
|
683
715
|
// 스크롤 이벤트 리스너 (externalScrollContainer 우선 사용)
|
|
684
716
|
useEffect(() => {
|
|
685
717
|
const handleScroll = (event) => {
|
|
@@ -688,7 +720,9 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
688
720
|
clearHideTimer();
|
|
689
721
|
setScrollbarVisible(true);
|
|
690
722
|
// 휠 스크롤 중이면 빠른 숨김, 아니면 기본 숨김 시간 적용
|
|
691
|
-
const delay = isWheelScrolling
|
|
723
|
+
const delay = isWheelScrolling
|
|
724
|
+
? finalAutoHideConfig.delayOnWheel
|
|
725
|
+
: finalAutoHideConfig.delay;
|
|
692
726
|
setHideTimer(delay);
|
|
693
727
|
if (onScroll) {
|
|
694
728
|
onScroll(event);
|
|
@@ -713,7 +747,6 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
713
747
|
const scrollableElement = findScrollableElement();
|
|
714
748
|
if (scrollableElement) {
|
|
715
749
|
elementsToWatch.push(scrollableElement);
|
|
716
|
-
console.log("OverlayScrollbar: watching scrollable element for events", scrollableElement);
|
|
717
750
|
}
|
|
718
751
|
// fallback: 내부 컨테이너와 children 요소도 감지
|
|
719
752
|
const container = containerRef.current;
|
|
@@ -750,10 +783,84 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
750
783
|
onScroll,
|
|
751
784
|
clearHideTimer,
|
|
752
785
|
setHideTimer,
|
|
753
|
-
|
|
754
|
-
hideDelayOnWheel,
|
|
786
|
+
finalAutoHideConfig,
|
|
755
787
|
isWheelScrolling,
|
|
756
788
|
]);
|
|
789
|
+
// 키보드 네비게이션 핸들러 (방향키, PageUp/PageDown/Home/End)
|
|
790
|
+
useEffect(() => {
|
|
791
|
+
const handleKeyDown = (event) => {
|
|
792
|
+
const scrollableElement = findScrollableElement();
|
|
793
|
+
if (!scrollableElement)
|
|
794
|
+
return;
|
|
795
|
+
const { key } = event;
|
|
796
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
|
|
797
|
+
const maxScrollTop = scrollHeight - clientHeight;
|
|
798
|
+
// 한 줄 스크롤 단위 (rowHeight 또는 기본값)
|
|
799
|
+
const lineScrollStep = 50;
|
|
800
|
+
let newScrollTop = null;
|
|
801
|
+
switch (key) {
|
|
802
|
+
case "ArrowUp":
|
|
803
|
+
event.preventDefault();
|
|
804
|
+
newScrollTop = Math.max(0, scrollTop - lineScrollStep);
|
|
805
|
+
break;
|
|
806
|
+
case "ArrowDown":
|
|
807
|
+
event.preventDefault();
|
|
808
|
+
newScrollTop = Math.min(maxScrollTop, scrollTop + lineScrollStep);
|
|
809
|
+
break;
|
|
810
|
+
case "PageUp":
|
|
811
|
+
event.preventDefault();
|
|
812
|
+
newScrollTop = Math.max(0, scrollTop - clientHeight);
|
|
813
|
+
break;
|
|
814
|
+
case "PageDown":
|
|
815
|
+
event.preventDefault();
|
|
816
|
+
newScrollTop = Math.min(maxScrollTop, scrollTop + clientHeight);
|
|
817
|
+
break;
|
|
818
|
+
case "Home":
|
|
819
|
+
event.preventDefault();
|
|
820
|
+
newScrollTop = 0;
|
|
821
|
+
break;
|
|
822
|
+
case "End":
|
|
823
|
+
event.preventDefault();
|
|
824
|
+
newScrollTop = maxScrollTop;
|
|
825
|
+
break;
|
|
826
|
+
default:
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (newScrollTop !== null) {
|
|
830
|
+
// 썸 위치를 먼저 업데이트
|
|
831
|
+
const scrollRatio = newScrollTop / maxScrollTop;
|
|
832
|
+
const arrowSpace = showArrows
|
|
833
|
+
? finalThumbWidth * 2 + finalTrackConfig.margin * 4
|
|
834
|
+
: finalTrackConfig.margin * 2;
|
|
835
|
+
const availableHeight = clientHeight - arrowSpace;
|
|
836
|
+
const scrollableThumbHeight = availableHeight - thumbHeight;
|
|
837
|
+
const newThumbTop = scrollableThumbHeight * scrollRatio;
|
|
838
|
+
setThumbTop(newThumbTop);
|
|
839
|
+
// 스크롤 위치를 즉시 변경 (애니메이션 없음)
|
|
840
|
+
scrollableElement.scrollTop = newScrollTop;
|
|
841
|
+
// 스크롤바 표시
|
|
842
|
+
clearHideTimer();
|
|
843
|
+
setScrollbarVisible(true);
|
|
844
|
+
setHideTimer(finalAutoHideConfig.delay);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
const container = containerRef.current;
|
|
848
|
+
if (container) {
|
|
849
|
+
container.addEventListener("keydown", handleKeyDown);
|
|
850
|
+
return () => {
|
|
851
|
+
container.removeEventListener("keydown", handleKeyDown);
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
}, [
|
|
855
|
+
findScrollableElement,
|
|
856
|
+
showArrows,
|
|
857
|
+
finalThumbWidth,
|
|
858
|
+
finalTrackConfig.margin,
|
|
859
|
+
thumbHeight,
|
|
860
|
+
clearHideTimer,
|
|
861
|
+
setHideTimer,
|
|
862
|
+
finalAutoHideConfig.delay,
|
|
863
|
+
]);
|
|
757
864
|
// 드래그 스크롤 전역 마우스 이벤트 리스너
|
|
758
865
|
useEffect(() => {
|
|
759
866
|
if (isDragScrolling) {
|
|
@@ -786,53 +893,65 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
786
893
|
}, 100);
|
|
787
894
|
return () => clearTimeout(timer);
|
|
788
895
|
}, [updateScrollbar]);
|
|
789
|
-
// externalScrollContainer가 변경될 때 스크롤바 업데이트
|
|
790
|
-
useEffect(() => {
|
|
791
|
-
if (externalScrollContainer) {
|
|
792
|
-
// externalScrollContainer가 설정된 후 스크롤바 업데이트
|
|
793
|
-
const timer = setTimeout(() => {
|
|
794
|
-
updateScrollbar();
|
|
795
|
-
}, 50);
|
|
796
|
-
return () => clearTimeout(timer);
|
|
797
|
-
}
|
|
798
|
-
}, [externalScrollContainer, updateScrollbar]);
|
|
799
896
|
// 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
|
|
800
897
|
useEffect(() => {
|
|
801
898
|
const timer = setTimeout(() => {
|
|
802
899
|
setIsInitialized(true);
|
|
803
|
-
console.log("OverlayScrollbar initialized", {
|
|
804
|
-
containerRef: !!containerRef.current,
|
|
805
|
-
contentRef: !!contentRef.current,
|
|
806
|
-
isScrollable: isScrollable(),
|
|
807
|
-
});
|
|
808
900
|
// 초기화 후 스크롤바 업데이트 (썸 높이 정확하게 계산)
|
|
809
901
|
updateScrollbar();
|
|
902
|
+
// 자동 숨김이 비활성화되어 있으면 스크롤바를 항상 표시
|
|
903
|
+
if (!finalAutoHideConfig.enabled && isScrollable()) {
|
|
904
|
+
setScrollbarVisible(true);
|
|
905
|
+
}
|
|
906
|
+
// 스크롤 컨테이너에 자동 포커스 (키보드 네비게이션 활성화)
|
|
907
|
+
if (containerRef.current) {
|
|
908
|
+
containerRef.current.focus();
|
|
909
|
+
}
|
|
810
910
|
}, 100);
|
|
811
911
|
return () => clearTimeout(timer);
|
|
812
|
-
}, [isScrollable]);
|
|
912
|
+
}, [isScrollable, updateScrollbar, finalAutoHideConfig.enabled]);
|
|
813
913
|
// Resize observer로 크기 변경 감지
|
|
814
914
|
useEffect(() => {
|
|
815
915
|
const resizeObserver = new ResizeObserver(() => {
|
|
816
916
|
updateScrollbar();
|
|
817
917
|
});
|
|
818
918
|
const elementsToObserve = [];
|
|
819
|
-
//
|
|
820
|
-
if (externalScrollContainer) {
|
|
821
|
-
elementsToObserve.push(externalScrollContainer);
|
|
822
|
-
}
|
|
823
|
-
// 내부 컨테이너들도 관찰
|
|
919
|
+
// 내부 컨테이너들 관찰
|
|
824
920
|
if (containerRef.current) {
|
|
825
921
|
elementsToObserve.push(containerRef.current);
|
|
826
922
|
}
|
|
827
923
|
if (contentRef.current) {
|
|
828
924
|
elementsToObserve.push(contentRef.current);
|
|
829
925
|
}
|
|
926
|
+
// 캐시된 스크롤 컨테이너도 관찰
|
|
927
|
+
if (cachedScrollContainerRef.current &&
|
|
928
|
+
document.contains(cachedScrollContainerRef.current)) {
|
|
929
|
+
elementsToObserve.push(cachedScrollContainerRef.current);
|
|
930
|
+
}
|
|
830
931
|
// 모든 요소들 관찰 시작
|
|
831
932
|
elementsToObserve.forEach((element) => {
|
|
832
933
|
resizeObserver.observe(element);
|
|
833
934
|
});
|
|
834
935
|
return () => resizeObserver.disconnect();
|
|
835
|
-
}, [updateScrollbar
|
|
936
|
+
}, [updateScrollbar]);
|
|
937
|
+
// MutationObserver로 DOM 변경 감지
|
|
938
|
+
useEffect(() => {
|
|
939
|
+
if (!containerRef.current) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const observer = new MutationObserver(() => {
|
|
943
|
+
// 캐시 초기화하여 새로운 스크롤 컨테이너 감지
|
|
944
|
+
cachedScrollContainerRef.current = null;
|
|
945
|
+
updateScrollbar();
|
|
946
|
+
});
|
|
947
|
+
observer.observe(containerRef.current, {
|
|
948
|
+
childList: true,
|
|
949
|
+
subtree: true,
|
|
950
|
+
attributes: true,
|
|
951
|
+
attributeFilter: ["style"],
|
|
952
|
+
});
|
|
953
|
+
return () => observer.disconnect();
|
|
954
|
+
}, [updateScrollbar]);
|
|
836
955
|
// trackWidth가 thumbWidth보다 작으면 thumbWidth와 같게 설정
|
|
837
956
|
const adjustedTrackWidth = Math.max(finalTrackWidth, finalThumbWidth);
|
|
838
957
|
// 웹킷 스크롤바 숨기기용 CSS 동적 주입
|
|
@@ -892,16 +1011,11 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
892
1011
|
display: "flex", // flex 컨테이너로 설정
|
|
893
1012
|
flexDirection: "column", // 세로 방향 정렬
|
|
894
1013
|
}, children: children }) }), showScrollbar && (jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
|
|
895
|
-
console.log("Track hover enter", {
|
|
896
|
-
isScrollable: isScrollable(),
|
|
897
|
-
scrollbarVisible,
|
|
898
|
-
});
|
|
899
1014
|
clearHideTimer();
|
|
900
1015
|
setScrollbarVisible(true);
|
|
901
1016
|
}, onMouseLeave: () => {
|
|
902
|
-
console.log("Track hover leave", { isDragging });
|
|
903
1017
|
if (!isDragging) {
|
|
904
|
-
setHideTimer(
|
|
1018
|
+
setHideTimer(finalAutoHideConfig.delay);
|
|
905
1019
|
}
|
|
906
1020
|
}, style: {
|
|
907
1021
|
position: "absolute",
|
|
@@ -915,44 +1029,58 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
915
1029
|
zIndex: 1000,
|
|
916
1030
|
pointerEvents: "auto",
|
|
917
1031
|
}, children: [finalTrackConfig.visible && (jsx("div", { className: "overlay-scrollbar-track-background", onClick: (e) => {
|
|
918
|
-
console.log("Track background clicked", e);
|
|
919
1032
|
e.preventDefault();
|
|
920
1033
|
e.stopPropagation();
|
|
921
1034
|
handleTrackClick(e);
|
|
922
1035
|
}, style: {
|
|
923
1036
|
position: "absolute",
|
|
924
1037
|
top: showArrows
|
|
925
|
-
? `${finalThumbConfig.width +
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1038
|
+
? `${finalThumbConfig.width +
|
|
1039
|
+
finalTrackConfig.margin * 2}px`
|
|
1040
|
+
: `${finalTrackConfig.margin}px`,
|
|
1041
|
+
right: finalTrackConfig.alignment === "right"
|
|
1042
|
+
? "0px"
|
|
1043
|
+
: `${(adjustedTrackWidth -
|
|
1044
|
+
finalThumbConfig.width) /
|
|
1045
|
+
2}px`, // 트랙 정렬
|
|
930
1046
|
width: `${finalThumbConfig.width}px`,
|
|
931
1047
|
height: showArrows
|
|
932
|
-
? `calc(100% - ${finalThumbConfig.width * 2 +
|
|
933
|
-
|
|
1048
|
+
? `calc(100% - ${finalThumbConfig.width * 2 +
|
|
1049
|
+
finalTrackConfig.margin * 4}px)`
|
|
1050
|
+
: `calc(100% - ${finalTrackConfig.margin * 2}px)`,
|
|
934
1051
|
backgroundColor: finalTrackConfig.color,
|
|
935
|
-
borderRadius: `${
|
|
1052
|
+
borderRadius: `${finalTrackConfig.radius}px`,
|
|
936
1053
|
cursor: "pointer",
|
|
937
|
-
} })), jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, style: {
|
|
1054
|
+
} })), jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, onMouseEnter: () => setIsThumbHovered(true), onMouseLeave: () => setIsThumbHovered(false), style: {
|
|
938
1055
|
position: "absolute",
|
|
939
|
-
top: `${(showArrows
|
|
940
|
-
|
|
941
|
-
|
|
1056
|
+
top: `${(showArrows
|
|
1057
|
+
? finalThumbWidth +
|
|
1058
|
+
finalTrackConfig.margin * 2
|
|
1059
|
+
: finalTrackConfig.margin) + thumbTop}px`,
|
|
1060
|
+
right: finalTrackConfig.alignment === "right"
|
|
1061
|
+
? "0px"
|
|
1062
|
+
: `${(adjustedTrackWidth -
|
|
1063
|
+
finalThumbWidth) /
|
|
1064
|
+
2}px`, // 트랙 정렬
|
|
942
1065
|
width: `${finalThumbWidth}px`,
|
|
943
1066
|
height: `${Math.max(thumbHeight, thumbMinHeight)}px`,
|
|
944
|
-
backgroundColor: isDragging
|
|
945
|
-
? finalThumbConfig.
|
|
1067
|
+
backgroundColor: isThumbHovered || isDragging
|
|
1068
|
+
? finalThumbConfig.hoverColor
|
|
946
1069
|
: finalThumbConfig.color,
|
|
1070
|
+
opacity: isThumbHovered || isDragging
|
|
1071
|
+
? finalThumbConfig.hoverOpacity
|
|
1072
|
+
: finalThumbConfig.opacity,
|
|
947
1073
|
borderRadius: `${finalThumbConfig.radius}px`,
|
|
948
1074
|
cursor: "pointer",
|
|
949
|
-
transition:
|
|
950
|
-
? "none"
|
|
951
|
-
: "background-color 0.2s ease-in-out",
|
|
1075
|
+
transition: "background-color 0.2s ease-in-out, opacity 0.2s ease-in-out",
|
|
952
1076
|
} })] })), showScrollbar && showArrows && (jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
|
|
953
1077
|
position: "absolute",
|
|
954
|
-
top:
|
|
955
|
-
right:
|
|
1078
|
+
top: `${finalTrackConfig.margin}px`,
|
|
1079
|
+
right: finalTrackConfig.alignment === "right"
|
|
1080
|
+
? "0px"
|
|
1081
|
+
: `${(adjustedTrackWidth -
|
|
1082
|
+
finalThumbWidth) /
|
|
1083
|
+
2}px`, // 트랙 정렬
|
|
956
1084
|
width: `${finalThumbWidth}px`,
|
|
957
1085
|
height: `${finalThumbWidth}px`,
|
|
958
1086
|
cursor: "pointer",
|
|
@@ -961,16 +1089,24 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
961
1089
|
justifyContent: "center",
|
|
962
1090
|
fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
|
|
963
1091
|
color: hoveredArrow === "up"
|
|
964
|
-
? finalArrowsConfig.
|
|
1092
|
+
? finalArrowsConfig.hoverColor
|
|
965
1093
|
: finalArrowsConfig.color,
|
|
966
1094
|
userSelect: "none",
|
|
967
1095
|
zIndex: 1001,
|
|
968
|
-
opacity: scrollbarVisible
|
|
1096
|
+
opacity: scrollbarVisible
|
|
1097
|
+
? hoveredArrow === "up"
|
|
1098
|
+
? finalArrowsConfig.hoverOpacity
|
|
1099
|
+
: finalArrowsConfig.opacity
|
|
1100
|
+
: 0,
|
|
969
1101
|
transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
|
|
970
1102
|
}, children: "\u25B2" })), showScrollbar && showArrows && (jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
|
|
971
1103
|
position: "absolute",
|
|
972
|
-
bottom:
|
|
973
|
-
right:
|
|
1104
|
+
bottom: `${finalTrackConfig.margin}px`,
|
|
1105
|
+
right: finalTrackConfig.alignment === "right"
|
|
1106
|
+
? "0px"
|
|
1107
|
+
: `${(adjustedTrackWidth -
|
|
1108
|
+
finalThumbWidth) /
|
|
1109
|
+
2}px`, // 트랙 정렬
|
|
974
1110
|
width: `${finalThumbWidth}px`,
|
|
975
1111
|
height: `${finalThumbWidth}px`,
|
|
976
1112
|
cursor: "pointer",
|
|
@@ -979,11 +1115,15 @@ showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
|
979
1115
|
justifyContent: "center",
|
|
980
1116
|
fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
|
|
981
1117
|
color: hoveredArrow === "down"
|
|
982
|
-
? finalArrowsConfig.
|
|
1118
|
+
? finalArrowsConfig.hoverColor
|
|
983
1119
|
: finalArrowsConfig.color,
|
|
984
1120
|
userSelect: "none",
|
|
985
1121
|
zIndex: 1001,
|
|
986
|
-
opacity: scrollbarVisible
|
|
1122
|
+
opacity: scrollbarVisible
|
|
1123
|
+
? hoveredArrow === "down"
|
|
1124
|
+
? finalArrowsConfig.hoverOpacity
|
|
1125
|
+
: finalArrowsConfig.opacity
|
|
1126
|
+
: 0,
|
|
987
1127
|
transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
|
|
988
1128
|
}, children: "\u25BC" }))] }));
|
|
989
1129
|
});
|