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