@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/dist/index.js CHANGED
@@ -293,18 +293,43 @@ const checkElementAndParents = (element, config) => {
293
293
  return false;
294
294
  };
295
295
 
296
- const OverlayScrollbar = react.forwardRef(({ className = "", style = {}, children, onScroll, scrollContainer: externalScrollContainer,
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 = {}, track = {}, arrows = {}, dragScroll = {},
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, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
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
- return ({
332
- width: (_a = thumb.width) !== null && _a !== void 0 ? _a : 8,
333
- minHeight: (_b = thumb.minHeight) !== null && _b !== void 0 ? _b : 50,
334
- radius: (_c = thumb.radius) !== null && _c !== void 0 ? _c : ((_d = thumb.width) !== null && _d !== void 0 ? _d : 8) / 2,
335
- color: (_e = thumb.color) !== null && _e !== void 0 ? _e : "rgba(128, 128, 128, 0.6)",
336
- activeColor: (_f = thumb.activeColor) !== null && _f !== void 0 ? _f : "rgba(128, 128, 128, 0.9)",
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
- return ({
350
- visible: (_a = arrows.visible) !== null && _a !== void 0 ? _a : false,
351
- step: (_b = arrows.step) !== null && _b !== void 0 ? _b : 50,
352
- color: (_c = arrows.color) !== null && _c !== void 0 ? _c : "rgba(128, 128, 128, 0.8)",
353
- activeColor: (_d = arrows.activeColor) !== null && _d !== void 0 ? _d : "rgba(64, 64, 64, 1.0)",
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
- // externalScrollContainer가 있으면 우선 사용
394
- if (externalScrollContainer) {
395
- console.log("OverlayScrollbar: checking external container", {
396
- scrollHeight: externalScrollContainer.scrollHeight,
397
- clientHeight: externalScrollContainer.clientHeight,
398
- });
399
- // virtuoso의 내부 스크롤러를 찾기
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
- console.log("OverlayScrollbar: found scrollable child element", {
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
- }, [externalScrollContainer]);
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, isDragging]);
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
- console.log("OverlayScrollbar: updating scrollbar", {
476
- containerHeight,
477
- contentHeight,
478
- scrollTop,
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(hideDelay); // 기본 숨김 시간 적용
580
+ setHideTimer(finalAutoHideConfig.delay); // 기본 숨김 시간 적용
552
581
  }
553
- }, [isScrollable, setHideTimer, hideDelay]);
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(hideDelay);
603
+ setHideTimer(finalAutoHideConfig.delay);
587
604
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
588
605
  (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
589
- }, [updateScrollbar, setHideTimer, hideDelay, findScrollableElement]);
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(hideDelay);
623
+ setHideTimer(finalAutoHideConfig.delay);
602
624
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
603
625
  (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
604
- }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
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(hideDelay);
646
+ setHideTimer(finalAutoHideConfig.delay);
620
647
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
621
648
  (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
622
- }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
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(hideDelay);
716
+ setHideTimer(finalAutoHideConfig.delay);
685
717
  }
686
- }, [isScrollable, setHideTimer, hideDelay]);
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 ? hideDelayOnWheel : hideDelay;
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
- hideDelay,
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
- // externalScrollContainer가 있으면 우선 관찰
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, externalScrollContainer]);
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(hideDelay);
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 + 8}px`
930
- : "4px",
931
- right: `${(adjustedTrackWidth -
932
- finalThumbConfig.width) /
933
- 2}px`, // 트랙 가운데 정렬
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 + 16}px)`
937
- : "calc(100% - 8px)",
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: `${finalThumbConfig.radius}px`,
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 ? finalThumbWidth + 8 : 4) +
944
- thumbTop}px`,
945
- right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
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.activeColor
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: isDragging
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: "4px",
959
- right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
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.activeColor
1096
+ ? finalArrowsConfig.hoverColor
969
1097
  : finalArrowsConfig.color,
970
1098
  userSelect: "none",
971
1099
  zIndex: 1001,
972
- opacity: scrollbarVisible ? 1 : 0,
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: "4px",
977
- right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
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.activeColor
1122
+ ? finalArrowsConfig.hoverColor
987
1123
  : finalArrowsConfig.color,
988
1124
  userSelect: "none",
989
1125
  zIndex: 1001,
990
- opacity: scrollbarVisible ? 1 : 0,
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
  });