@ehfuse/mui-virtual-data-table 1.0.1 → 1.0.2

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
@@ -533,6 +533,21 @@ showScrollbar = true, }, ref) => {
533
533
  const thumbMinHeight = finalThumbConfig.minHeight;
534
534
  const showArrows = finalArrowsConfig.visible;
535
535
  const arrowStep = finalArrowsConfig.step;
536
+ // 포커스 유지 함수 (키보드 입력이 계속 작동하도록)
537
+ const maintainFocus = react.useCallback(() => {
538
+ if (!containerRef.current)
539
+ return;
540
+ // 현재 포커스된 요소 확인
541
+ const activeElement = document.activeElement;
542
+ // 오버레이 스크롤바 내부에 이미 포커스된 요소가 있으면 스킵
543
+ if (activeElement &&
544
+ containerRef.current.contains(activeElement) &&
545
+ activeElement !== containerRef.current) {
546
+ return;
547
+ }
548
+ // 포커스된 요소가 없거나 외부에 있으면 컨테이너에 포커스
549
+ containerRef.current.focus();
550
+ }, []);
536
551
  // ref를 통해 외부에서 스크롤 컨테이너에 접근할 수 있도록 함
537
552
  react.useImperativeHandle(ref, () => ({
538
553
  getScrollContainer: () => containerRef.current,
@@ -656,7 +671,6 @@ showScrollbar = true, }, ref) => {
656
671
  ]);
657
672
  // 썸 드래그 시작
658
673
  const handleThumbMouseDown = react.useCallback((event) => {
659
- var _a;
660
674
  event.preventDefault();
661
675
  event.stopPropagation();
662
676
  const actualScrollContainer = findScrollableElement();
@@ -671,8 +685,8 @@ showScrollbar = true, }, ref) => {
671
685
  clearHideTimer();
672
686
  setScrollbarVisible(true);
673
687
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
674
- (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
675
- }, [findScrollableElement, clearHideTimer]);
688
+ maintainFocus();
689
+ }, [findScrollableElement, clearHideTimer, maintainFocus]);
676
690
  // 썸 드래그 중
677
691
  const handleMouseMove = react.useCallback((event) => {
678
692
  if (!isDragging)
@@ -706,7 +720,6 @@ showScrollbar = true, }, ref) => {
706
720
  }, [isScrollable, setHideTimer, finalAutoHideConfig.delay]);
707
721
  // 트랙 클릭으로 스크롤 점프
708
722
  const handleTrackClick = react.useCallback((event) => {
709
- var _a;
710
723
  if (!scrollbarRef.current) {
711
724
  return;
712
725
  }
@@ -726,16 +739,16 @@ showScrollbar = true, }, ref) => {
726
739
  setScrollbarVisible(true);
727
740
  setHideTimer(finalAutoHideConfig.delay);
728
741
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
729
- (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
742
+ maintainFocus();
730
743
  }, [
731
744
  updateScrollbar,
732
745
  setHideTimer,
733
746
  finalAutoHideConfig.delay,
734
747
  findScrollableElement,
748
+ maintainFocus,
735
749
  ]);
736
750
  // 위쪽 화살표 클릭 핸들러
737
751
  const handleUpArrowClick = react.useCallback((event) => {
738
- var _a;
739
752
  event.preventDefault();
740
753
  event.stopPropagation();
741
754
  if (!containerRef.current)
@@ -746,16 +759,16 @@ showScrollbar = true, }, ref) => {
746
759
  setScrollbarVisible(true);
747
760
  setHideTimer(finalAutoHideConfig.delay);
748
761
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
749
- (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
762
+ maintainFocus();
750
763
  }, [
751
764
  updateScrollbar,
752
765
  setHideTimer,
753
766
  arrowStep,
754
767
  finalAutoHideConfig.delay,
768
+ maintainFocus,
755
769
  ]);
756
770
  // 아래쪽 화살표 클릭 핸들러
757
771
  const handleDownArrowClick = react.useCallback((event) => {
758
- var _a;
759
772
  event.preventDefault();
760
773
  event.stopPropagation();
761
774
  if (!containerRef.current || !contentRef.current)
@@ -769,12 +782,13 @@ showScrollbar = true, }, ref) => {
769
782
  setScrollbarVisible(true);
770
783
  setHideTimer(finalAutoHideConfig.delay);
771
784
  // 포커스 유지 (키보드 입력이 계속 작동하도록)
772
- (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
785
+ maintainFocus();
773
786
  }, [
774
787
  updateScrollbar,
775
788
  setHideTimer,
776
789
  arrowStep,
777
790
  finalAutoHideConfig.delay,
791
+ maintainFocus,
778
792
  ]);
779
793
  // 드래그 스크롤 시작
780
794
  const handleDragScrollStart = react.useCallback((event) => {
@@ -1022,7 +1036,7 @@ showScrollbar = true, }, ref) => {
1022
1036
  return () => clearTimeout(timer);
1023
1037
  }, [updateScrollbar]);
1024
1038
  // 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
1025
- react.useEffect(() => {
1039
+ react.useLayoutEffect(() => {
1026
1040
  const timer = setTimeout(() => {
1027
1041
  setIsInitialized(true);
1028
1042
  // 초기화 후 스크롤바 업데이트 (썸 높이 정확하게 계산)
@@ -1031,11 +1045,7 @@ showScrollbar = true, }, ref) => {
1031
1045
  if (!finalAutoHideConfig.enabled && isScrollable()) {
1032
1046
  setScrollbarVisible(true);
1033
1047
  }
1034
- // 스크롤 컨테이너에 자동 포커스 (키보드 네비게이션 활성화)
1035
- if (containerRef.current) {
1036
- containerRef.current.focus();
1037
- }
1038
- }, 100);
1048
+ }, 0);
1039
1049
  return () => clearTimeout(timer);
1040
1050
  }, [isScrollable, updateScrollbar, finalAutoHideConfig.enabled]);
1041
1051
  // Resize observer로 크기 변경 감지
@@ -1265,7 +1275,7 @@ const OVERLAY_SCROLLBAR_TRACK_CONFIG = {
1265
1275
  /**
1266
1276
  * 데이터 기반 무한 스크롤 및 가상화를 지원하는 테이블 컴포넌트
1267
1277
  */
1268
- function VirtualDataTableComponent({ data, loading = false, columns, onRowClick, rowHeight = 50, columnHeight = 56, striped, rowDivider = true, onSort, onLoadMore, sortBy, sortDirection, showPaper = true, paddingX = "1rem", scrollbars, emptyMessage = "NO DATA", LoadingComponent, }) {
1278
+ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick, rowHeight = 50, columnHeight = 56, striped, rowDivider = true, onSort, onLoadMore, sortBy, sortDirection, showPaper = true, paddingX = "1rem", paddingTop = 0, paddingBottom = 0, rowHoverColor, rowHoverOpacity, scrollbars, emptyMessage = "NO DATA", LoadingComponent, }) {
1269
1279
  // console.log("=== VirtualDataTable 렌더링 ===", {
1270
1280
  // dataLength: data.length,
1271
1281
  // loading,
@@ -1305,11 +1315,13 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1305
1315
  },
1306
1316
  "& .MuiTable-root": {
1307
1317
  paddingRight: paddingX,
1318
+ paddingTop: paddingTop,
1319
+ paddingBottom: paddingBottom,
1308
1320
  },
1309
1321
  } }) }));
1310
1322
  }),
1311
1323
  // eslint-disable-next-line react-hooks/exhaustive-deps
1312
- [] // 빈 배열: 최초 마운트 시에만 생성, scrollbars paddingX 클로저로 고정
1324
+ [] // 빈 배열: 최초 마운트 시에만 생성, scrollbars, paddingX, paddingTop, paddingBottom은 클로저로 고정
1313
1325
  );
1314
1326
  // Striped row 배경색 계산
1315
1327
  const stripedRowColor = react.useMemo(() => {
@@ -1352,6 +1364,8 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1352
1364
  const isMouseDownRef = react.useRef(false);
1353
1365
  const initialScrollTopRef = react.useRef(0);
1354
1366
  const totalDragDistanceRef = react.useRef(0);
1367
+ const isScrollDraggingRef = react.useRef(false); // OverlayScrollbar 드래그 스크롤 감지용
1368
+ react.useRef(null);
1355
1369
  /**
1356
1370
  * 마우스 버튼 누름 이벤트 핸들러
1357
1371
  * OverlayScrollbar 사용시에는 기본 드래그 스크롤을 비활성화
@@ -1376,7 +1390,6 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1376
1390
  // DOM 스타일 직접 변경 (리렌더링 방지)
1377
1391
  if (scrollContainerRef.current) {
1378
1392
  scrollContainerRef.current.style.userSelect = "none";
1379
- scrollContainerRef.current.style.webkitUserSelect = "none";
1380
1393
  }
1381
1394
  }
1382
1395
  if (isDraggingRef.current) {
@@ -1420,7 +1433,6 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1420
1433
  // DOM 스타일 초기화
1421
1434
  if (scrollContainerRef.current) {
1422
1435
  scrollContainerRef.current.style.userSelect = "auto";
1423
- scrollContainerRef.current.style.webkitUserSelect = "auto";
1424
1436
  }
1425
1437
  }, []);
1426
1438
  // 정렬이 변경될 때 모든 TableSortLabel의 hover 상태 초기화
@@ -1540,7 +1552,6 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1540
1552
  width: col.width,
1541
1553
  minWidth: col.width,
1542
1554
  ...col.style,
1543
- backgroundColor: "#ffffff",
1544
1555
  fontWeight: "bold",
1545
1556
  position: "sticky",
1546
1557
  top: 0,
@@ -1601,7 +1612,6 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1601
1612
  width: col.width,
1602
1613
  minWidth: col.width,
1603
1614
  ...col.style,
1604
- backgroundColor: "#ffffff",
1605
1615
  fontWeight: "bold",
1606
1616
  position: "sticky",
1607
1617
  top: 0,
@@ -1663,7 +1673,6 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1663
1673
  },
1664
1674
  } })] }) })) : (col.text) }, String(col.id)))),
1665
1675
  ...Object.entries(groupMap).map(([group, cols]) => (jsxRuntime.jsx(material.TableCell, { align: "center", colSpan: cols.length, style: {
1666
- backgroundColor: "#ffffff",
1667
1676
  fontWeight: "bold",
1668
1677
  position: "sticky",
1669
1678
  top: 0,
@@ -1678,7 +1687,6 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1678
1687
  width: col.width,
1679
1688
  minWidth: col.width,
1680
1689
  ...col.style,
1681
- backgroundColor: "#ffffff",
1682
1690
  fontWeight: "bold",
1683
1691
  position: "sticky",
1684
1692
  top: 0,
@@ -1778,7 +1786,6 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1778
1786
  height: columnHeight,
1779
1787
  "& th": {
1780
1788
  padding: "16px",
1781
- backgroundColor: "#ffffff",
1782
1789
  position: "sticky",
1783
1790
  top: 0,
1784
1791
  zIndex: 2,
@@ -1793,10 +1800,22 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1793
1800
  // react-virtuoso는 'data-index' 속성으로 index를 전달합니다
1794
1801
  const rowIndex = rest["data-index"] ?? 0;
1795
1802
  const isOddRow = rowIndex % 2 === 1;
1796
- return (jsxRuntime.jsx(material.TableRow, { ...rest, onClick: () => {
1797
- if (!isDraggingRef.current && item && onRowClick) {
1803
+ return (jsxRuntime.jsx(material.TableRow, { ...rest, onMouseDown: () => {
1804
+ // 마우스 다운 드래그 플래그 초기화
1805
+ isScrollDraggingRef.current = false;
1806
+ }, onMouseMove: () => {
1807
+ // 마우스가 눌린 상태로 움직이면 드래그로 간주
1808
+ isScrollDraggingRef.current = true;
1809
+ }, onClick: () => {
1810
+ // 드래그 스크롤이 아니고, 아이템이 있고, onRowClick이 있을 때만 실행
1811
+ if (!isScrollDraggingRef.current &&
1812
+ !isDraggingRef.current &&
1813
+ item &&
1814
+ onRowClick) {
1798
1815
  onRowClick(item, rowIndex);
1799
1816
  }
1817
+ // 클릭 후 플래그 리셋
1818
+ isScrollDraggingRef.current = false;
1800
1819
  }, sx: {
1801
1820
  userSelect: "none",
1802
1821
  height: rowHeight,
@@ -1815,7 +1834,85 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1815
1834
  },
1816
1835
  "&:hover": onRowClick
1817
1836
  ? {
1818
- backgroundColor: "#E3EEFA",
1837
+ backgroundColor: (theme) => {
1838
+ const isDark = theme.palette.mode === "dark";
1839
+ const defaultColor = "#000000";
1840
+ const color = rowHoverColor ?? defaultColor;
1841
+ const opacity = rowHoverOpacity ?? 0.06;
1842
+ // hex를 rgb로 변환
1843
+ const hex = color.replace("#", "");
1844
+ let r = parseInt(hex.substring(0, 2), 16) / 255;
1845
+ let g = parseInt(hex.substring(2, 4), 16) / 255;
1846
+ let b = parseInt(hex.substring(4, 6), 16) / 255;
1847
+ // 다크 모드일 때 밝기만 반전 (HSL 변환)
1848
+ if (isDark) {
1849
+ // RGB to HSL
1850
+ const max = Math.max(r, g, b);
1851
+ const min = Math.min(r, g, b);
1852
+ let h = 0, s = 0, l = (max + min) / 2;
1853
+ if (max !== min) {
1854
+ const d = max - min;
1855
+ s =
1856
+ l > 0.5
1857
+ ? d / (2 - max - min)
1858
+ : d / (max + min);
1859
+ switch (max) {
1860
+ case r:
1861
+ h =
1862
+ ((g - b) / d +
1863
+ (g < b
1864
+ ? 6
1865
+ : 0)) /
1866
+ 6;
1867
+ break;
1868
+ case g:
1869
+ h =
1870
+ ((b - r) / d +
1871
+ 2) /
1872
+ 6;
1873
+ break;
1874
+ case b:
1875
+ h =
1876
+ ((r - g) / d +
1877
+ 4) /
1878
+ 6;
1879
+ break;
1880
+ }
1881
+ }
1882
+ // 밝기만 반전 (0.0 <-> 1.0)
1883
+ l = 1 - l;
1884
+ // HSL to RGB
1885
+ const hue2rgb = (p, q, t) => {
1886
+ if (t < 0)
1887
+ t += 1;
1888
+ if (t > 1)
1889
+ t -= 1;
1890
+ if (t < 1 / 6)
1891
+ return (p + (q - p) * 6 * t);
1892
+ if (t < 1 / 2)
1893
+ return q;
1894
+ if (t < 2 / 3)
1895
+ return (p +
1896
+ (q - p) *
1897
+ (2 / 3 - t) *
1898
+ 6);
1899
+ return p;
1900
+ };
1901
+ if (s === 0) {
1902
+ r = g = b = l;
1903
+ }
1904
+ else {
1905
+ const q = l < 0.5
1906
+ ? l * (1 + s)
1907
+ : l + s - l * s;
1908
+ const p = 2 * l - q;
1909
+ r = hue2rgb(p, q, h + 1 / 3);
1910
+ g = hue2rgb(p, q, h);
1911
+ b = hue2rgb(p, q, h - 1 / 3);
1912
+ }
1913
+ }
1914
+ return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${opacity})`;
1915
+ },
1819
1916
  transition: "background-color 0.2s ease",
1820
1917
  }
1821
1918
  : {},
@@ -1830,6 +1927,8 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1830
1927
  stripedRowColor,
1831
1928
  rowDivider,
1832
1929
  columnHeight,
1930
+ rowHoverColor,
1931
+ rowHoverOpacity,
1833
1932
  VirtuosoScroller,
1834
1933
  ]);
1835
1934
  // 공통 테이블 내용
@@ -1837,6 +1936,11 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1837
1936
  position: "relative",
1838
1937
  height: "100%",
1839
1938
  width: "100%",
1939
+ "& .MuiTableHead-root": {
1940
+ backgroundColor: (theme) => theme.palette.mode === "dark"
1941
+ ? "#1e1e1e !important"
1942
+ : "#ffffff !important",
1943
+ },
1840
1944
  }, children: [jsxRuntime.jsx(reactVirtuoso.TableVirtuoso, { ref: virtuosoRef, data: data, totalCount: onLoadMore ? data.length + 1 : data.length, fixedHeaderContent: fixedHeaderContent, itemContent: rowContent, rangeChanged: handleRangeChange, components: VirtuosoTableComponents, style: { height: "100%" }, increaseViewportBy: { top: 100, bottom: 300 }, overscan: 5, followOutput: false }, tableKey), data.length === 0 && !loading && (jsxRuntime.jsx(material.Box, { sx: {
1841
1945
  position: "absolute",
1842
1946
  top: 0,
@@ -1865,7 +1969,7 @@ function VirtualDataTableComponent({ data, loading = false, columns, onRowClick,
1865
1969
  show: data.length === 0, // 최초 로딩에만 배경 표시
1866
1970
  opacity: 0.8,
1867
1971
  } })) }))] }));
1868
- return showPaper ? (jsxRuntime.jsx(material.Paper, { className: "grow", style: {
1972
+ return showPaper ? (jsxRuntime.jsx(material.Paper, { className: "grow", elevation: 1, sx: {
1869
1973
  padding: 0,
1870
1974
  paddingLeft: paddingX,
1871
1975
  height: "100%",