@youp-grid/react 0.2.1 → 0.2.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seungyoup Baek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -10,3 +10,18 @@ npm install @youp-grid/core @youp-grid/react
10
10
  import { YoupGrid } from "@youp-grid/react";
11
11
  import "@youp-grid/react/styles.css";
12
12
  ```
13
+
14
+ ## What It Provides
15
+
16
+ - virtualized React grid UI
17
+ - keyboard navigation and inline editing
18
+ - built-in text, number, checkbox, and select editors
19
+ - clipboard paste, fill handle, delete, undo, and redo write paths
20
+ - column menus, resizing, pinning, visibility, density, and row selection UI
21
+ - loading, empty, error, validation, pending, warning, and read-only states
22
+
23
+ The adapter emits changes through callbacks. Applications keep ownership of row data.
24
+
25
+ ## License
26
+
27
+ MIT. See the repository license.
package/dist/YoupGrid.js CHANGED
@@ -9,6 +9,9 @@ const DENSITY_ROW_HEIGHTS = {
9
9
  comfortable: 46,
10
10
  };
11
11
  const SELECTION_COLUMN_WIDTH = 44;
12
+ const AUTOSIZE_CELL_EXTRA_WIDTH = 8;
13
+ const AUTOSIZE_CELL_BORDER_THRESHOLD = 6;
14
+ let autosizeMeasureCanvas;
12
15
  export function YoupGrid(props) {
13
16
  const controller = useYoupGrid(props);
14
17
  const rowModel = controller.rowModel;
@@ -31,7 +34,10 @@ export function YoupGrid(props) {
31
34
  const [editingCell, setEditingCell] = useState();
32
35
  const [columnChooserOpen, setColumnChooserOpen] = useState(false);
33
36
  const [columnMenuOpenId, setColumnMenuOpenId] = useState();
37
+ const [activeTooltipCellKey, setActiveTooltipCellKey] = useState();
34
38
  const showRowSelectionColumn = props.showRowSelectionColumn ?? false;
39
+ const pinRowSelectionColumn = props.pinRowSelectionColumn ?? false;
40
+ const cellTooltipMode = props.cellTooltip?.mode ?? "native";
35
41
  const gridEditable = (props.editable ?? true) && !props.readOnly;
36
42
  const displayRows = rowModel.displayRows;
37
43
  const virtualRange = useMemo(() => {
@@ -75,9 +81,9 @@ export function YoupGrid(props) {
75
81
  const someVisibleRowsSelected = selectedVisibleRowCount > 0 && !allVisibleRowsSelected;
76
82
  const columnLayouts = useMemo(() => {
77
83
  return getColumnLayouts(rowModel.visibleColumns, {
78
- leftOffset: showRowSelectionColumn ? SELECTION_COLUMN_WIDTH : 0,
84
+ leftOffset: showRowSelectionColumn && pinRowSelectionColumn ? SELECTION_COLUMN_WIDTH : 0,
79
85
  });
80
- }, [rowModel.visibleColumns, showRowSelectionColumn]);
86
+ }, [pinRowSelectionColumn, rowModel.visibleColumns, showRowSelectionColumn]);
81
87
  const visibleColumns = useMemo(() => columnLayouts.map((layout) => layout.column), [columnLayouts]);
82
88
  const visibleRowIndexById = useMemo(() => {
83
89
  return new Map(rowModel.visibleRows.map((row, index) => [row.id, index]));
@@ -129,7 +135,7 @@ export function YoupGrid(props) {
129
135
  };
130
136
  const getGridCellMeta = (row, rowIndex, column) => {
131
137
  return (props.getCellMeta?.(getCellEditContext(row, rowIndex, column)) ??
132
- props.cellMeta?.[`${row.id}:${column.id}`]);
138
+ props.cellMeta?.[getCellKey(row.id, column.id)]);
133
139
  };
134
140
  const createGridCellValueChange = (cell, value) => {
135
141
  if (!cell.editable) {
@@ -163,6 +169,25 @@ export function YoupGrid(props) {
163
169
  setColumnMenuOpenId(undefined);
164
170
  }
165
171
  }, [columnMenuOpenId, visibleColumns]);
172
+ useEffect(() => {
173
+ if (cellTooltipMode !== "rich") {
174
+ setActiveTooltipCellKey(undefined);
175
+ return;
176
+ }
177
+ const cellKey = props.cellTooltip?.autoOpenCellKey;
178
+ if (!cellKey) {
179
+ return;
180
+ }
181
+ setActiveTooltipCellKey(cellKey);
182
+ const duration = props.cellTooltip?.autoOpenDurationMs ?? 2000;
183
+ if (duration <= 0) {
184
+ return;
185
+ }
186
+ const timeout = window.setTimeout(() => {
187
+ setActiveTooltipCellKey((current) => current === cellKey ? undefined : current);
188
+ }, duration);
189
+ return () => window.clearTimeout(timeout);
190
+ }, [cellTooltipMode, props.cellTooltip?.autoOpenCellKey, props.cellTooltip?.autoOpenDurationMs]);
166
191
  useEffect(() => {
167
192
  if (!props.infiniteScroll || !props.onRowsEndReached || !infiniteScrollTrigger.shouldLoadMore) {
168
193
  return;
@@ -313,6 +338,7 @@ export function YoupGrid(props) {
313
338
  "youp-grid",
314
339
  `youp-grid--density-${density}`,
315
340
  !gridEditable ? "youp-grid--read-only" : "",
341
+ pinRowSelectionColumn ? "youp-grid--selection-column-pinned" : "",
316
342
  props.className,
317
343
  ].filter(Boolean).join(" "),
318
344
  style: gridStyle,
@@ -403,6 +429,14 @@ export function YoupGrid(props) {
403
429
  }
404
430
  },
405
431
  resizeColumn: (width) => controller.setColumnWidth(layout.column.id, width),
432
+ autoSizeColumn: (event) => {
433
+ autoSizeColumnToFit({
434
+ event,
435
+ column: layout.column,
436
+ rows: rowModel.visibleRows,
437
+ resizeColumn: (width) => controller.setColumnWidth(layout.column.id, width),
438
+ });
439
+ },
406
440
  showMenu: props.showColumnMenu ?? true,
407
441
  menuOpen: columnMenuOpenId === layout.column.id,
408
442
  toggleMenu: () => {
@@ -462,10 +496,12 @@ export function YoupGrid(props) {
462
496
  editingCell,
463
497
  editable: gridEditable,
464
498
  disabledReason: props.disabledReason,
499
+ treeData: props.treeData ?? false,
465
500
  canEditCell: canEditGridCell,
466
501
  getCellMeta: getGridCellMeta,
467
502
  setRowSelected: (selected) => controller.setRowSelected(row.id, selected),
468
503
  setFocusedCell,
504
+ toggleTreeRowExpanded: controller.toggleTreeRowExpanded,
469
505
  startFillHandle: (event) => {
470
506
  if (!gridEditable) {
471
507
  return;
@@ -496,6 +532,14 @@ export function YoupGrid(props) {
496
532
  },
497
533
  });
498
534
  },
535
+ autoSizeColumn: (event, column) => {
536
+ autoSizeColumnToFit({
537
+ event,
538
+ column,
539
+ rows: rowModel.visibleRows,
540
+ resizeColumn: (width) => controller.setColumnWidth(column.id, width),
541
+ });
542
+ },
499
543
  applyCellValue,
500
544
  startEditing: startEditingCell,
501
545
  updateEditingDraft: (draftValue) => {
@@ -503,6 +547,12 @@ export function YoupGrid(props) {
503
547
  },
504
548
  cancelEditing: cancelEditingCell,
505
549
  commitEditing: commitEditingValue,
550
+ cellTooltipMode,
551
+ activeTooltipCellKey,
552
+ openCellTooltip: setActiveTooltipCellKey,
553
+ closeCellTooltip: (cellKey) => {
554
+ setActiveTooltipCellKey((current) => current === cellKey ? undefined : current);
555
+ },
506
556
  onRowClick: props.onRowClick,
507
557
  onRowDoubleClick: props.onRowDoubleClick,
508
558
  onCellKeyDown: (event, cell) => {
@@ -677,6 +727,7 @@ function renderHeaderCell(context) {
677
727
  role: "columnheader",
678
728
  style: getCellStyle(context.layout),
679
729
  "aria-sort": context.sorted === "desc" ? "descending" : context.sorted === "asc" ? "ascending" : "none",
730
+ "data-youp-column-id": context.layout.column.id,
680
731
  }, createElement("div", { className: "youp-grid__header-main" }, createElement("button", {
681
732
  className: "youp-grid__sort-button",
682
733
  type: "button",
@@ -736,6 +787,7 @@ function renderHeaderCell(context) {
736
787
  resizeColumn: context.resizeColumn,
737
788
  });
738
789
  },
790
+ onDoubleClick: context.autoSizeColumn,
739
791
  }));
740
792
  }
741
793
  function renderColumnMenu(context) {
@@ -883,15 +935,22 @@ function renderRow(context) {
883
935
  editingCell: context.editingCell,
884
936
  editable,
885
937
  disabledReason: context.disabledReason,
938
+ treeData: context.treeData,
886
939
  meta,
887
940
  setFocusedCell: context.setFocusedCell,
941
+ toggleTreeRowExpanded: context.toggleTreeRowExpanded,
888
942
  startFillHandle: context.startFillHandle,
943
+ autoSizeColumn: context.autoSizeColumn,
889
944
  applyCellValue: context.applyCellValue,
890
945
  startEditing: context.startEditing,
891
946
  updateEditingDraft: context.updateEditingDraft,
892
947
  cancelEditing: context.cancelEditing,
893
948
  commitEditing: context.commitEditing,
894
949
  onKeyDown: context.onCellKeyDown,
950
+ cellTooltipMode: context.cellTooltipMode,
951
+ activeTooltipCellKey: context.activeTooltipCellKey,
952
+ openCellTooltip: context.openCellTooltip,
953
+ closeCellTooltip: context.closeCellTooltip,
895
954
  renderCell: context.renderCell,
896
955
  });
897
956
  }));
@@ -935,6 +994,13 @@ function renderCell(context) {
935
994
  const column = context.layout.column;
936
995
  const value = column.accessor(context.row.original);
937
996
  const editable = context.editable;
997
+ const cellAlign = getColumnAlign(column);
998
+ const cellKey = getCellKey(context.row.id, column.id);
999
+ const showTreePrefix = context.treeData && context.columnIndex === 0;
1000
+ const hasRichTooltip = context.cellTooltipMode === "rich" && hasTooltipMessage(context.meta);
1001
+ const tooltipId = hasRichTooltip && context.activeTooltipCellKey === cellKey
1002
+ ? getCellTooltipId(cellKey)
1003
+ : undefined;
938
1004
  const cellContext = {
939
1005
  row: context.row,
940
1006
  column,
@@ -943,6 +1009,9 @@ function renderCell(context) {
943
1009
  focused: context.focused,
944
1010
  editable,
945
1011
  meta: context.meta,
1012
+ treeDepth: context.row.depth,
1013
+ hasChildren: context.row.hasChildren,
1014
+ expanded: context.row.expanded,
946
1015
  };
947
1016
  const cellState = {
948
1017
  row: context.row,
@@ -959,22 +1028,34 @@ function renderCell(context) {
959
1028
  cell: cellState,
960
1029
  row: context.row.original,
961
1030
  disabledReason: context.disabledReason,
1031
+ cellTooltipMode: context.cellTooltipMode,
962
1032
  applyCellValue: context.applyCellValue,
963
1033
  });
1034
+ const renderedContent = showTreePrefix && !context.editing
1035
+ ? renderTreeCellContent({
1036
+ row: context.row,
1037
+ content: cellContent,
1038
+ toggleExpanded: context.toggleTreeRowExpanded,
1039
+ })
1040
+ : cellContent;
964
1041
  return createElement("div", {
965
1042
  key: column.id,
966
1043
  className: getCellClassName([
967
1044
  "youp-grid__cell",
1045
+ `youp-grid__cell--align-${cellAlign}`,
968
1046
  context.focused ? "youp-grid__cell--focused" : "",
969
1047
  context.selected ? "youp-grid__cell--range-selected" : "",
970
1048
  context.fillTargeted ? "youp-grid__cell--fill-target" : "",
971
1049
  context.editing ? "youp-grid__cell--editing" : "",
972
1050
  !editable ? "youp-grid__cell--disabled" : "",
1051
+ showTreePrefix ? "youp-grid__cell--tree" : "",
1052
+ tooltipId ? "youp-grid__cell--tooltip-open" : "",
973
1053
  context.meta ? `youp-grid__cell--status-${context.meta.status}` : "",
974
1054
  ].filter(Boolean).join(" "), context.layout),
975
1055
  role: "gridcell",
976
1056
  tabIndex: context.focused && !context.editing ? 0 : -1,
977
- title: getCellTitle(context.meta, context.disabledReason, editable),
1057
+ title: getCellTitle(context.meta, context.disabledReason, editable, context.cellTooltipMode),
1058
+ "aria-describedby": tooltipId,
978
1059
  "aria-colindex": context.columnIndex + context.ariaColumnOffset + 1,
979
1060
  "aria-readonly": !editable || undefined,
980
1061
  "data-youp-row-index": context.rowIndex,
@@ -985,7 +1066,11 @@ function renderCell(context) {
985
1066
  event.currentTarget.focus({ preventScroll: true });
986
1067
  context.setFocusedCell({ rowIndex: context.rowIndex, columnIndex: context.columnIndex }, event.shiftKey);
987
1068
  },
988
- onDoubleClick: () => {
1069
+ onDoubleClick: (event) => {
1070
+ if (isCellRightBorderDoubleClick(event)) {
1071
+ context.autoSizeColumn(event, column);
1072
+ return;
1073
+ }
989
1074
  if (editable) {
990
1075
  if (column.editor === "checkbox") {
991
1076
  context.applyCellValue(cellState, !Boolean(value));
@@ -996,6 +1081,31 @@ function renderCell(context) {
996
1081
  }
997
1082
  },
998
1083
  onKeyDown: (event) => context.onKeyDown(event, cellState),
1084
+ onMouseEnter: () => {
1085
+ if (hasRichTooltip) {
1086
+ context.openCellTooltip(cellKey);
1087
+ }
1088
+ },
1089
+ onMouseLeave: () => {
1090
+ if (hasRichTooltip) {
1091
+ context.closeCellTooltip(cellKey);
1092
+ }
1093
+ },
1094
+ onFocus: () => {
1095
+ if (hasRichTooltip) {
1096
+ context.openCellTooltip(cellKey);
1097
+ }
1098
+ },
1099
+ onBlur: (event) => {
1100
+ if (!hasRichTooltip) {
1101
+ return;
1102
+ }
1103
+ const nextTarget = event.relatedTarget;
1104
+ if (nextTarget && event.currentTarget.contains(nextTarget)) {
1105
+ return;
1106
+ }
1107
+ context.closeCellTooltip(cellKey);
1108
+ },
999
1109
  }, context.editing
1000
1110
  ? renderCellEditor({
1001
1111
  cell: cellState,
@@ -1004,7 +1114,7 @@ function renderCell(context) {
1004
1114
  commitEditing: context.commitEditing,
1005
1115
  onKeyDown: context.onKeyDown,
1006
1116
  })
1007
- : cellContent, renderCellStatus(context.meta), !context.editing && context.showFillHandle
1117
+ : renderedContent, renderCellStatus(context.meta, context.cellTooltipMode), renderCellTooltip(context.meta, tooltipId), !context.editing && context.showFillHandle
1008
1118
  ? createElement("span", {
1009
1119
  className: "youp-grid__fill-handle",
1010
1120
  role: "button",
@@ -1013,6 +1123,18 @@ function renderCell(context) {
1013
1123
  })
1014
1124
  : undefined);
1015
1125
  }
1126
+ function getColumnAlign(column) {
1127
+ if (column.align) {
1128
+ return column.align;
1129
+ }
1130
+ if (column.editor === "number") {
1131
+ return "right";
1132
+ }
1133
+ if (column.editor === "checkbox") {
1134
+ return "center";
1135
+ }
1136
+ return "left";
1137
+ }
1016
1138
  function renderDefaultCellContent(context) {
1017
1139
  if (context.cell.column.editor === "checkbox") {
1018
1140
  return createElement("input", {
@@ -1020,7 +1142,7 @@ function renderDefaultCellContent(context) {
1020
1142
  type: "checkbox",
1021
1143
  checked: Boolean(context.cell.value),
1022
1144
  disabled: !context.cell.editable,
1023
- title: getCellTitle(context.cell.meta, context.disabledReason, context.cell.editable),
1145
+ title: getCellTitle(context.cell.meta, context.disabledReason, context.cell.editable, context.cellTooltipMode),
1024
1146
  "aria-label": context.cell.column.headerName,
1025
1147
  onChange: (event) => {
1026
1148
  context.applyCellValue(context.cell, event.currentTarget.checked);
@@ -1036,7 +1158,37 @@ function renderDefaultCellContent(context) {
1036
1158
  const text = formatCellValue(context.cell.column, context.row, context.cell.value);
1037
1159
  const placeholder = context.cell.column.placeholder;
1038
1160
  const hasPlaceholder = text.length === 0 && Boolean(placeholder);
1039
- return createElement("span", { className: hasPlaceholder ? "youp-grid__cell-placeholder" : undefined }, hasPlaceholder ? placeholder : text);
1161
+ return createElement("span", {
1162
+ className: [
1163
+ "youp-grid__cell-text",
1164
+ hasPlaceholder ? "youp-grid__cell-placeholder" : "",
1165
+ ].filter(Boolean).join(" "),
1166
+ }, hasPlaceholder ? placeholder : text);
1167
+ }
1168
+ function renderTreeCellContent(context) {
1169
+ const depth = Math.max(0, context.row.depth ?? 0);
1170
+ const toggle = context.row.hasChildren
1171
+ ? createElement("button", {
1172
+ className: "youp-grid__tree-toggle",
1173
+ type: "button",
1174
+ "aria-label": context.row.expanded ? "Collapse row" : "Expand row",
1175
+ "aria-expanded": context.row.expanded,
1176
+ onClick: (event) => {
1177
+ event.stopPropagation();
1178
+ context.toggleExpanded(context.row.id);
1179
+ },
1180
+ onDoubleClick: (event) => {
1181
+ event.stopPropagation();
1182
+ },
1183
+ onKeyDown: (event) => {
1184
+ event.stopPropagation();
1185
+ },
1186
+ }, createElement("span", { className: "youp-grid__tree-caret", "aria-hidden": true }, context.row.expanded ? "v" : ">"))
1187
+ : createElement("span", { className: "youp-grid__tree-toggle-spacer", "aria-hidden": true });
1188
+ return createElement("span", {
1189
+ className: "youp-grid__cell-tree-content",
1190
+ style: { paddingLeft: depth * 18 },
1191
+ }, toggle, createElement("span", { className: "youp-grid__cell-tree-value" }, context.content));
1040
1192
  }
1041
1193
  function renderCellEditor(context) {
1042
1194
  if (context.cell.column.editor === "select") {
@@ -1079,16 +1231,26 @@ function renderCellEditor(context) {
1079
1231
  },
1080
1232
  });
1081
1233
  }
1082
- function renderCellStatus(meta) {
1234
+ function renderCellStatus(meta, tooltipMode) {
1083
1235
  if (!meta) {
1084
1236
  return undefined;
1085
1237
  }
1086
1238
  return createElement("span", {
1087
1239
  className: `youp-grid__cell-status youp-grid__cell-status--${meta.status}`,
1088
- title: typeof meta.message === "string" ? meta.message : undefined,
1240
+ title: tooltipMode === "native" && typeof meta.message === "string" ? meta.message : undefined,
1089
1241
  "aria-hidden": true,
1090
1242
  });
1091
1243
  }
1244
+ function renderCellTooltip(meta, tooltipId) {
1245
+ if (!tooltipId || !meta?.message) {
1246
+ return undefined;
1247
+ }
1248
+ return createElement("span", {
1249
+ id: tooltipId,
1250
+ className: "youp-grid__cell-tooltip",
1251
+ role: "tooltip",
1252
+ }, meta.message);
1253
+ }
1092
1254
  function renderColumnToolbar(context) {
1093
1255
  if (!context.showColumnChooser && !context.showCsvExport && !context.showDensityControl) {
1094
1256
  return undefined;
@@ -1338,6 +1500,100 @@ function startColumnResize(context) {
1338
1500
  document.addEventListener("mousemove", handleMouseMove);
1339
1501
  document.addEventListener("mouseup", handleMouseUp);
1340
1502
  }
1503
+ function autoSizeColumnToFit(context) {
1504
+ context.event.preventDefault();
1505
+ context.event.stopPropagation();
1506
+ context.resizeColumn(getAutoSizeColumnWidth({
1507
+ anchor: context.event.currentTarget,
1508
+ column: context.column,
1509
+ rows: context.rows,
1510
+ }));
1511
+ }
1512
+ function getAutoSizeColumnWidth(context) {
1513
+ const metrics = getAutoSizeMetrics(context.anchor, context.column.id);
1514
+ const minWidth = context.column.minWidth ?? 64;
1515
+ const maxWidth = context.column.maxWidth ?? Number.MAX_SAFE_INTEGER;
1516
+ let width = measureAutoSizeText(context.column.headerName, metrics.headerFont) +
1517
+ metrics.headerHorizontalPadding +
1518
+ metrics.headerControlWidth +
1519
+ AUTOSIZE_CELL_EXTRA_WIDTH;
1520
+ for (const row of context.rows) {
1521
+ const text = getAutoSizeCellText(context.column, row.original);
1522
+ width = Math.max(width, measureAutoSizeText(text, metrics.cellFont) + metrics.cellHorizontalPadding + AUTOSIZE_CELL_EXTRA_WIDTH);
1523
+ }
1524
+ return Math.ceil(clamp(width, minWidth, maxWidth));
1525
+ }
1526
+ function getAutoSizeMetrics(anchor, columnId) {
1527
+ const anchorCell = anchor.closest(".youp-grid__cell");
1528
+ const root = anchor.closest(".youp-grid");
1529
+ const headerCell = anchorCell?.getAttribute("role") === "columnheader"
1530
+ ? anchorCell
1531
+ : findColumnElement(root, "columnheader", columnId);
1532
+ const bodyCell = anchorCell?.getAttribute("role") === "gridcell"
1533
+ ? anchorCell
1534
+ : findColumnElement(root, "gridcell", columnId);
1535
+ const headerCellStyle = headerCell ? window.getComputedStyle(headerCell) : undefined;
1536
+ const bodyCellStyle = bodyCell ? window.getComputedStyle(bodyCell) : headerCellStyle;
1537
+ const headerMain = headerCell?.querySelector(".youp-grid__header-main");
1538
+ const headerMainStyle = headerMain ? window.getComputedStyle(headerMain) : undefined;
1539
+ const menuButton = headerCell?.querySelector(".youp-grid__column-menu-button");
1540
+ const headerHorizontalPadding = headerCellStyle
1541
+ ? getPixelValue(headerCellStyle.paddingLeft) + getPixelValue(headerCellStyle.paddingRight)
1542
+ : 24;
1543
+ const cellHorizontalPadding = bodyCellStyle
1544
+ ? getPixelValue(bodyCellStyle.paddingLeft) + getPixelValue(bodyCellStyle.paddingRight)
1545
+ : headerHorizontalPadding;
1546
+ const headerControlWidth = menuButton
1547
+ ? menuButton.offsetWidth + getPixelValue(headerMainStyle?.columnGap ?? headerMainStyle?.gap ?? "0")
1548
+ : 0;
1549
+ return {
1550
+ headerFont: headerCellStyle ? getFontValue(headerCellStyle) : "13px sans-serif",
1551
+ cellFont: bodyCellStyle ? getFontValue(bodyCellStyle) : "13px sans-serif",
1552
+ headerHorizontalPadding,
1553
+ cellHorizontalPadding,
1554
+ headerControlWidth,
1555
+ };
1556
+ }
1557
+ function findColumnElement(root, role, columnId) {
1558
+ if (!root) {
1559
+ return undefined;
1560
+ }
1561
+ return Array.from(root.querySelectorAll(`[role='${role}'][data-youp-column-id]`))
1562
+ .find((element) => element.dataset.youpColumnId === columnId);
1563
+ }
1564
+ function isCellRightBorderDoubleClick(event) {
1565
+ const rect = event.currentTarget.getBoundingClientRect();
1566
+ return Math.abs(rect.right - event.clientX) <= AUTOSIZE_CELL_BORDER_THRESHOLD;
1567
+ }
1568
+ function getAutoSizeCellText(column, row) {
1569
+ const text = formatCellValue(column, row, column.accessor(row));
1570
+ return text.length === 0 && column.placeholder ? column.placeholder : text;
1571
+ }
1572
+ function measureAutoSizeText(text, font) {
1573
+ if (typeof document === "undefined") {
1574
+ return text.length * 8;
1575
+ }
1576
+ autosizeMeasureCanvas ?? (autosizeMeasureCanvas = document.createElement("canvas"));
1577
+ const context = autosizeMeasureCanvas.getContext("2d");
1578
+ if (!context) {
1579
+ return text.length * 8;
1580
+ }
1581
+ context.font = font;
1582
+ return context.measureText(text).width;
1583
+ }
1584
+ function getFontValue(style) {
1585
+ return style.font || [
1586
+ style.fontStyle,
1587
+ style.fontVariant,
1588
+ style.fontWeight,
1589
+ `${style.fontSize}/${style.lineHeight}`,
1590
+ style.fontFamily,
1591
+ ].join(" ");
1592
+ }
1593
+ function getPixelValue(value) {
1594
+ const parsed = Number.parseFloat(value);
1595
+ return Number.isFinite(parsed) ? parsed : 0;
1596
+ }
1341
1597
  function handleCellKeyDown(context) {
1342
1598
  if (context.rowCount === 0 || context.columnCount === 0) {
1343
1599
  return;
@@ -1884,8 +2140,8 @@ function normalizeEditorOptions(options) {
1884
2140
  function getEditorOptionValue(option) {
1885
2141
  return typeof option === "object" ? option.value : option;
1886
2142
  }
1887
- function getCellTitle(meta, disabledReason, editable) {
1888
- if (typeof meta?.message === "string") {
2143
+ function getCellTitle(meta, disabledReason, editable, tooltipMode) {
2144
+ if (tooltipMode === "native" && typeof meta?.message === "string") {
1889
2145
  return meta.message;
1890
2146
  }
1891
2147
  if (!editable && typeof disabledReason === "string") {
@@ -1893,6 +2149,15 @@ function getCellTitle(meta, disabledReason, editable) {
1893
2149
  }
1894
2150
  return undefined;
1895
2151
  }
2152
+ function hasTooltipMessage(meta) {
2153
+ return Boolean(meta?.message);
2154
+ }
2155
+ function getCellKey(rowId, columnId) {
2156
+ return `${String(rowId)}:${columnId}`;
2157
+ }
2158
+ function getCellTooltipId(cellKey) {
2159
+ return `youp-grid-cell-tooltip-${cellKey.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
2160
+ }
1896
2161
  function isTextEditingKey(event) {
1897
2162
  if (event.metaKey || event.ctrlKey || event.altKey) {
1898
2163
  return false;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { YoupGrid } from "./YoupGrid.ts";
2
2
  export { useYoupGrid } from "./useYoupGrid.ts";
3
- export type { YoupGridCanEditCellContext, YoupGridCellEditCommit, YoupGridCellEditCommitReason, YoupGridCellContext, YoupGridCellMeta, YoupGridCellMetaStatus, YoupGridCellsValueChange, YoupGridCellsValueChangeSource, YoupGridController, YoupGridDensity, YoupGridHeaderContext, YoupGridOptions, YoupGridProps, YoupGridRowEvent, YoupGridRowsEndReachedEvent, YoupGridStateChange, } from "./types.ts";
3
+ export type { YoupGridCanEditCellContext, YoupGridCellEditCommit, YoupGridCellEditCommitReason, YoupGridCellContext, YoupGridCellMeta, YoupGridCellMetaStatus, YoupGridCellTooltipMode, YoupGridCellTooltipOptions, YoupGridCellsValueChange, YoupGridCellsValueChangeSource, YoupGridController, YoupGridDensity, YoupGridHeaderContext, YoupGridOptions, YoupGridProps, YoupGridRowEvent, YoupGridRowsEndReachedEvent, YoupGridStateChange, } from "./types.ts";
package/dist/styles.css CHANGED
@@ -164,11 +164,15 @@
164
164
  --youp-grid-header-scroll-left: 0px;
165
165
  }
166
166
 
167
- .youp-grid__header .youp-grid__cell--header:not(.youp-grid__cell--pinned-left):not(.youp-grid__cell--pinned-right):not(.youp-grid__selection-cell),
168
- .youp-grid__header .youp-grid__cell--header-group:not(.youp-grid__cell--pinned-left):not(.youp-grid__cell--pinned-right):not(.youp-grid__selection-cell) {
167
+ .youp-grid__header .youp-grid__cell--header:not(.youp-grid__cell--pinned-left):not(.youp-grid__cell--pinned-right),
168
+ .youp-grid__header .youp-grid__cell--header-group:not(.youp-grid__cell--pinned-left):not(.youp-grid__cell--pinned-right) {
169
169
  transform: translateX(calc(-1 * var(--youp-grid-header-scroll-left)));
170
170
  }
171
171
 
172
+ .youp-grid--selection-column-pinned .youp-grid__header .youp-grid__selection-cell {
173
+ transform: none;
174
+ }
175
+
172
176
  .youp-grid__row--header {
173
177
  background: var(--youp-grid-header);
174
178
  }
@@ -212,9 +216,6 @@
212
216
  }
213
217
 
214
218
  .youp-grid__selection-cell {
215
- position: sticky;
216
- left: 0;
217
- z-index: 3;
218
219
  display: grid;
219
220
  place-items: center;
220
221
  min-width: 44px;
@@ -222,6 +223,12 @@
222
223
  background: #fff;
223
224
  }
224
225
 
226
+ .youp-grid--selection-column-pinned .youp-grid__selection-cell {
227
+ position: sticky;
228
+ left: 0;
229
+ z-index: 3;
230
+ }
231
+
225
232
  .youp-grid__cell.youp-grid__selection-cell {
226
233
  min-width: 44px;
227
234
  }
@@ -286,6 +293,18 @@
286
293
  white-space: nowrap;
287
294
  }
288
295
 
296
+ .youp-grid__cell--align-left {
297
+ text-align: left;
298
+ }
299
+
300
+ .youp-grid__cell--align-center {
301
+ text-align: center;
302
+ }
303
+
304
+ .youp-grid__cell--align-right {
305
+ text-align: right;
306
+ }
307
+
289
308
  .youp-grid--read-only .youp-grid__cell {
290
309
  cursor: default;
291
310
  }
@@ -336,6 +355,89 @@
336
355
  color: #94a3b8;
337
356
  }
338
357
 
358
+ .youp-grid__cell-text {
359
+ display: block;
360
+ min-width: 0;
361
+ overflow: hidden;
362
+ text-overflow: ellipsis;
363
+ }
364
+
365
+ .youp-grid__cell--tree {
366
+ display: flex;
367
+ align-items: center;
368
+ }
369
+
370
+ .youp-grid__cell-tree-content {
371
+ display: inline-flex;
372
+ min-width: 0;
373
+ align-items: center;
374
+ gap: 4px;
375
+ overflow: hidden;
376
+ text-overflow: ellipsis;
377
+ }
378
+
379
+ .youp-grid__cell-tree-value {
380
+ min-width: 0;
381
+ overflow: hidden;
382
+ text-overflow: ellipsis;
383
+ }
384
+
385
+ .youp-grid__tree-toggle,
386
+ .youp-grid__tree-toggle-spacer {
387
+ display: inline-flex;
388
+ flex: 0 0 auto;
389
+ align-items: center;
390
+ justify-content: center;
391
+ width: 18px;
392
+ height: 18px;
393
+ }
394
+
395
+ .youp-grid__tree-toggle {
396
+ padding: 0;
397
+ border: 0;
398
+ color: var(--youp-grid-muted);
399
+ font: inherit;
400
+ cursor: pointer;
401
+ background: transparent;
402
+ }
403
+
404
+ .youp-grid__tree-toggle:focus-visible {
405
+ outline: 2px solid #2563eb;
406
+ outline-offset: 1px;
407
+ }
408
+
409
+ .youp-grid__tree-caret {
410
+ width: 12px;
411
+ line-height: 1;
412
+ text-align: center;
413
+ }
414
+
415
+ .youp-grid__cell--tooltip-open {
416
+ z-index: 8;
417
+ overflow: visible;
418
+ }
419
+
420
+ .youp-grid__cell-tooltip {
421
+ position: absolute;
422
+ top: calc(100% + 6px);
423
+ left: var(--youp-grid-cell-inline-padding);
424
+ z-index: 20;
425
+ width: max-content;
426
+ min-width: 120px;
427
+ max-width: 260px;
428
+ padding: 6px 8px;
429
+ border: 1px solid #0f172a;
430
+ border-radius: 6px;
431
+ color: #fff;
432
+ font-size: 12px;
433
+ line-height: 1.4;
434
+ text-align: left;
435
+ white-space: normal;
436
+ pointer-events: none;
437
+ background: #0f172a;
438
+ box-shadow: 0 8px 24px rgb(15 23 42 / 18%);
439
+ }
440
+
339
441
  .youp-grid__cell-checkbox {
340
442
  width: 16px;
341
443
  height: 16px;
@@ -408,6 +510,18 @@
408
510
  outline: none;
409
511
  }
410
512
 
513
+ .youp-grid__cell--align-left .youp-grid__cell-editor {
514
+ text-align: left;
515
+ }
516
+
517
+ .youp-grid__cell--align-center .youp-grid__cell-editor {
518
+ text-align: center;
519
+ }
520
+
521
+ .youp-grid__cell--align-right .youp-grid__cell-editor {
522
+ text-align: right;
523
+ }
524
+
411
525
  .youp-grid__cell-editor--select {
412
526
  padding-right: 24px;
413
527
  }
package/dist/types.d.ts CHANGED
@@ -29,6 +29,12 @@ export type YoupGridCellMeta = {
29
29
  status: YoupGridCellMetaStatus;
30
30
  message?: ReactNode;
31
31
  };
32
+ export type YoupGridCellTooltipMode = "native" | "rich" | "none";
33
+ export type YoupGridCellTooltipOptions = {
34
+ mode?: YoupGridCellTooltipMode;
35
+ autoOpenCellKey?: string | null;
36
+ autoOpenDurationMs?: number;
37
+ };
32
38
  export type YoupGridCanEditCellContext<TRow> = {
33
39
  row: TRow;
34
40
  rowNode: RowNode<TRow>;
@@ -61,6 +67,8 @@ export type YoupGridOptions<TRow> = {
61
67
  defaultState?: GridState;
62
68
  onStateChange?: (change: YoupGridStateChange<TRow>) => void;
63
69
  getRowId?: (row: TRow, index: number) => GridRowId;
70
+ treeData?: boolean;
71
+ getParentRowId?: (row: TRow, index: number) => GridRowId | null | undefined;
64
72
  rowModelType?: GridRowModelType;
65
73
  serverRowCount?: number;
66
74
  serverFilteredRowCount?: number;
@@ -101,6 +109,8 @@ export type YoupGridController<TRow> = {
101
109
  setRowSelected: (rowId: GridRowId, selected: boolean) => void;
102
110
  setSelectedRows: (rowIds: readonly GridRowId[]) => void;
103
111
  toggleRowSelected: (rowId: GridRowId) => void;
112
+ setTreeExpandedRows: (rowIds: readonly GridRowId[]) => void;
113
+ toggleTreeRowExpanded: (rowId: GridRowId) => void;
104
114
  };
105
115
  export type YoupGridProps<TRow> = YoupGridOptions<TRow> & {
106
116
  className?: string;
@@ -118,6 +128,7 @@ export type YoupGridProps<TRow> = YoupGridOptions<TRow> & {
118
128
  showAggregationFooter?: boolean;
119
129
  showPagination?: boolean;
120
130
  showRowSelectionColumn?: boolean;
131
+ pinRowSelectionColumn?: boolean;
121
132
  csvFileName?: string;
122
133
  density?: YoupGridDensity;
123
134
  defaultDensity?: YoupGridDensity;
@@ -128,6 +139,7 @@ export type YoupGridProps<TRow> = YoupGridOptions<TRow> & {
128
139
  errorContent?: ReactNode;
129
140
  cellMeta?: Record<string, YoupGridCellMeta | undefined>;
130
141
  getCellMeta?: (context: YoupGridCanEditCellContext<TRow>) => YoupGridCellMeta | undefined;
142
+ cellTooltip?: YoupGridCellTooltipOptions;
131
143
  renderCell?: (context: YoupGridCellContext<TRow>) => ReactNode;
132
144
  renderHeader?: (context: YoupGridHeaderContext<TRow>) => ReactNode;
133
145
  onCellValueChange?: (change: YoupGridCellValueChange<TRow>) => void;
@@ -146,6 +158,9 @@ export type YoupGridCellContext<TRow> = {
146
158
  focused: boolean;
147
159
  editable: boolean;
148
160
  meta?: YoupGridCellMeta;
161
+ treeDepth?: number;
162
+ hasChildren?: boolean;
163
+ expanded?: boolean;
149
164
  };
150
165
  export type YoupGridHeaderContext<TRow> = {
151
166
  column: ResolvedColumnDef<TRow>;
@@ -1,4 +1,4 @@
1
- import { acknowledgeRemoteCache as acknowledgeCoreRemoteCache, buildRowModel, cancelRemoteRequest as cancelCoreRemoteRequest, clearFilter as clearCoreFilter, clearSort as clearCoreSort, createGridState, failRemoteRequest as failCoreRemoteRequest, finishRemoteRequest as finishCoreRemoteRequest, invalidateRemoteCache as invalidateCoreRemoteCache, setColumnHidden as setCoreColumnHidden, setColumnPinned as setCoreColumnPinned, setColumnWidth as setCoreColumnWidth, setCursorPage as setCoreCursorPage, setCursorPageSize as setCoreCursorPageSize, setCursorPagination as setCoreCursorPagination, setAggregation as setCoreAggregation, setFilter as setCoreFilter, setPagination, setRemoteCache as setCoreRemoteCache, setRowGrouping as setCoreRowGrouping, setRowSelected as setCoreRowSelected, setSelectedRows as setCoreSelectedRows, setSort as setCoreSort, startRemoteRequest as startCoreRemoteRequest, toggleRowGroupExpanded as toggleCoreRowGroupExpanded, toggleRowSelected as toggleCoreRowSelected, toggleSort as toggleCoreSort, } from "@youp-grid/core";
1
+ import { acknowledgeRemoteCache as acknowledgeCoreRemoteCache, buildRowModel, cancelRemoteRequest as cancelCoreRemoteRequest, clearFilter as clearCoreFilter, clearSort as clearCoreSort, createGridState, failRemoteRequest as failCoreRemoteRequest, finishRemoteRequest as finishCoreRemoteRequest, invalidateRemoteCache as invalidateCoreRemoteCache, setColumnHidden as setCoreColumnHidden, setColumnPinned as setCoreColumnPinned, setColumnWidth as setCoreColumnWidth, setCursorPage as setCoreCursorPage, setCursorPageSize as setCoreCursorPageSize, setCursorPagination as setCoreCursorPagination, setAggregation as setCoreAggregation, setFilter as setCoreFilter, setPagination, setRemoteCache as setCoreRemoteCache, setRowGrouping as setCoreRowGrouping, setRowSelected as setCoreRowSelected, setSelectedRows as setCoreSelectedRows, setSort as setCoreSort, setTreeExpandedRows as setCoreTreeExpandedRows, startRemoteRequest as startCoreRemoteRequest, toggleRowGroupExpanded as toggleCoreRowGroupExpanded, toggleRowSelected as toggleCoreRowSelected, toggleSort as toggleCoreSort, toggleTreeRowExpanded as toggleCoreTreeRowExpanded, } from "@youp-grid/core";
2
2
  import { useCallback, useMemo, useState } from "react";
3
3
  export function useYoupGrid(options) {
4
4
  const isControlled = options.state !== undefined;
@@ -12,6 +12,8 @@ export function useYoupGrid(options) {
12
12
  columns: options.columns,
13
13
  state,
14
14
  getRowId: options.getRowId,
15
+ treeData: options.treeData,
16
+ getParentRowId: options.getParentRowId,
15
17
  rowModelType: options.rowModelType,
16
18
  serverRowCount: options.serverRowCount,
17
19
  serverFilteredRowCount: options.serverFilteredRowCount,
@@ -20,6 +22,8 @@ export function useYoupGrid(options) {
20
22
  options.rows,
21
23
  options.columns,
22
24
  options.getRowId,
25
+ options.treeData,
26
+ options.getParentRowId,
23
27
  options.rowModelType,
24
28
  options.serverRowCount,
25
29
  options.serverFilteredRowCount,
@@ -31,6 +35,8 @@ export function useYoupGrid(options) {
31
35
  columns: options.columns,
32
36
  state: nextState,
33
37
  getRowId: options.getRowId,
38
+ treeData: options.treeData,
39
+ getParentRowId: options.getParentRowId,
34
40
  rowModelType: options.rowModelType,
35
41
  serverRowCount: options.serverRowCount,
36
42
  serverFilteredRowCount: options.serverFilteredRowCount,
@@ -93,5 +99,7 @@ export function useYoupGrid(options) {
93
99
  setRowSelected: (rowId, selected) => commitState(setCoreRowSelected(state, rowId, selected)),
94
100
  setSelectedRows: (rowIds) => commitState(setCoreSelectedRows(state, rowIds)),
95
101
  toggleRowSelected: (rowId) => commitState(toggleCoreRowSelected(state, rowId)),
102
+ setTreeExpandedRows: (rowIds) => commitState(setCoreTreeExpandedRows(state, rowIds)),
103
+ toggleTreeRowExpanded: (rowId) => commitState(toggleCoreTreeRowExpanded(state, rowId)),
96
104
  };
97
105
  }
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "@youp-grid/react",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "React adapter for Youp Grid.",
5
+ "license": "MIT",
6
+ "author": "SeungyoupBaek",
5
7
  "type": "module",
8
+ "keywords": [
9
+ "data-grid",
10
+ "grid",
11
+ "react",
12
+ "typescript",
13
+ "table"
14
+ ],
6
15
  "main": "./dist/index.js",
7
16
  "types": "./dist/index.d.ts",
8
17
  "exports": {
@@ -14,13 +23,18 @@
14
23
  },
15
24
  "files": [
16
25
  "dist",
17
- "README.md"
26
+ "README.md",
27
+ "LICENSE"
18
28
  ],
19
29
  "repository": {
20
30
  "type": "git",
21
31
  "url": "git+ssh://git@github.com/SeungyoupBaek/youp-grid.git",
22
32
  "directory": "packages/react"
23
33
  },
34
+ "bugs": {
35
+ "url": "https://github.com/SeungyoupBaek/youp-grid/issues"
36
+ },
37
+ "homepage": "https://github.com/SeungyoupBaek/youp-grid/tree/main/packages/react#readme",
24
38
  "publishConfig": {
25
39
  "access": "public"
26
40
  },
@@ -28,7 +42,7 @@
28
42
  "build": "tsc -p tsconfig.build.json && cp src/styles.css dist/styles.css"
29
43
  },
30
44
  "dependencies": {
31
- "@youp-grid/core": "0.2.1"
45
+ "@youp-grid/core": "0.2.3"
32
46
  },
33
47
  "peerDependencies": {
34
48
  "react": ">=18.2.0"