@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.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { forwardRef, useRef, useState, useMemo, useImperativeHandle, useCallback, useEffect } from 'react';
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
- const OverlayScrollbar = forwardRef(({ className = "", style = {}, children, onScroll, scrollContainer: externalScrollContainer,
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 = {}, track = {}, arrows = {}, dragScroll = {},
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, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
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
- return ({
328
- width: (_a = thumb.width) !== null && _a !== void 0 ? _a : 8,
329
- minHeight: (_b = thumb.minHeight) !== null && _b !== void 0 ? _b : 50,
330
- radius: (_c = thumb.radius) !== null && _c !== void 0 ? _c : ((_d = thumb.width) !== null && _d !== void 0 ? _d : 8) / 2,
331
- color: (_e = thumb.color) !== null && _e !== void 0 ? _e : "rgba(128, 128, 128, 0.6)",
332
- activeColor: (_f = thumb.activeColor) !== null && _f !== void 0 ? _f : "rgba(128, 128, 128, 0.9)",
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
- return ({
346
- visible: (_a = arrows.visible) !== null && _a !== void 0 ? _a : false,
347
- step: (_b = arrows.step) !== null && _b !== void 0 ? _b : 50,
348
- color: (_c = arrows.color) !== null && _c !== void 0 ? _c : "rgba(128, 128, 128, 0.8)",
349
- activeColor: (_d = arrows.activeColor) !== null && _d !== void 0 ? _d : "rgba(64, 64, 64, 1.0)",
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
- // externalScrollContainer가 있으면 우선 사용
390
- if (externalScrollContainer) {
391
- console.log("OverlayScrollbar: checking external container", {
392
- scrollHeight: externalScrollContainer.scrollHeight,
393
- clientHeight: externalScrollContainer.clientHeight,
394
- });
395
- // virtuoso의 내부 스크롤러를 찾기
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
- console.log("OverlayScrollbar: found scrollable child element", {
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
- }, [externalScrollContainer]);
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, isDragging]);
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
- console.log("OverlayScrollbar: updating scrollbar", {
472
- containerHeight,
473
- contentHeight,
474
- scrollTop,
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(hideDelay); // 기본 숨김 시간 적용
576
+ setHideTimer(finalAutoHideConfig.delay); // 기본 숨김 시간 적용
548
577
  }
549
- }, [isScrollable, setHideTimer, hideDelay]);
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(hideDelay);
599
+ setHideTimer(finalAutoHideConfig.delay);
583
600
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
584
601
  (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
585
- }, [updateScrollbar, setHideTimer, hideDelay, findScrollableElement]);
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(hideDelay);
619
+ setHideTimer(finalAutoHideConfig.delay);
598
620
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
599
621
  (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
600
- }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
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(hideDelay);
642
+ setHideTimer(finalAutoHideConfig.delay);
616
643
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
617
644
  (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
618
- }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
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(hideDelay);
712
+ setHideTimer(finalAutoHideConfig.delay);
681
713
  }
682
- }, [isScrollable, setHideTimer, hideDelay]);
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 ? hideDelayOnWheel : hideDelay;
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
- hideDelay,
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
- // externalScrollContainer가 있으면 우선 관찰
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, externalScrollContainer]);
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(hideDelay);
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 + 8}px`
926
- : "4px",
927
- right: `${(adjustedTrackWidth -
928
- finalThumbConfig.width) /
929
- 2}px`, // 트랙 가운데 정렬
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 + 16}px)`
933
- : "calc(100% - 8px)",
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: `${finalThumbConfig.radius}px`,
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 ? finalThumbWidth + 8 : 4) +
940
- thumbTop}px`,
941
- right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
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.activeColor
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: isDragging
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: "4px",
955
- right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
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.activeColor
1092
+ ? finalArrowsConfig.hoverColor
965
1093
  : finalArrowsConfig.color,
966
1094
  userSelect: "none",
967
1095
  zIndex: 1001,
968
- opacity: scrollbarVisible ? 1 : 0,
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: "4px",
973
- right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
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.activeColor
1118
+ ? finalArrowsConfig.hoverColor
983
1119
  : finalArrowsConfig.color,
984
1120
  userSelect: "none",
985
1121
  zIndex: 1001,
986
- opacity: scrollbarVisible ? 1 : 0,
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
  });