beautiful-grid 1.0.13 → 1.0.15

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.
Files changed (50) hide show
  1. package/README.md +121 -6
  2. package/cjs/BGrid.js +388 -44
  3. package/cjs/components/CellNavigationDomSync.js +8 -0
  4. package/cjs/components/CellTextEditorGateway.js +13 -9
  5. package/cjs/components/ColResizer.js +13 -7
  6. package/cjs/components/MasterDetailCell.js +145 -0
  7. package/cjs/components/MasterDetailContext.js +42 -0
  8. package/cjs/components/Table.js +162 -52
  9. package/cjs/components/TableBody.js +7 -3
  10. package/cjs/components/TableBodyCell.js +2 -1
  11. package/cjs/components/TableSummary.js +25 -34
  12. package/cjs/components/TableSummaryFronzen.js +36 -34
  13. package/cjs/store/createAppStore.js +2 -0
  14. package/cjs/utils/cellSelectionGeometry.js +32 -6
  15. package/cjs/utils/columnVisibility.js +17 -14
  16. package/cjs/utils/index.js +1 -0
  17. package/cjs/utils/rowHeightMetrics.js +47 -6
  18. package/cjs/utils/summary.js +55 -0
  19. package/cjs/utils/useRowReorderController.js +5 -1
  20. package/esm/BGrid.js +387 -72
  21. package/esm/components/CellNavigationDomSync.js +8 -0
  22. package/esm/components/CellTextEditorGateway.js +12 -9
  23. package/esm/components/ColResizer.js +8 -2
  24. package/esm/components/MasterDetailCell.js +101 -0
  25. package/esm/components/MasterDetailContext.js +5 -0
  26. package/esm/components/Table.js +151 -42
  27. package/esm/components/TableBody.js +6 -2
  28. package/esm/components/TableBodyCell.js +2 -1
  29. package/esm/components/TableSummary.js +26 -29
  30. package/esm/components/TableSummaryFronzen.js +26 -29
  31. package/esm/store/createAppStore.js +2 -0
  32. package/esm/utils/cellSelectionGeometry.js +31 -5
  33. package/esm/utils/columnVisibility.js +21 -18
  34. package/esm/utils/index.js +1 -0
  35. package/esm/utils/rowHeightMetrics.js +46 -6
  36. package/esm/utils/summary.js +35 -0
  37. package/esm/utils/useRowReorderController.js +5 -1
  38. package/package.json +1 -1
  39. package/style.css +164 -14
  40. package/types/BGrid.d.ts +1 -1
  41. package/types/components/MasterDetailCell.d.ts +21 -0
  42. package/types/components/MasterDetailContext.d.ts +20 -0
  43. package/types/components/Table.d.ts +1 -0
  44. package/types/components/TableBody.d.ts +2 -1
  45. package/types/store/createAppStore.d.ts +1 -1
  46. package/types/types.d.ts +70 -4
  47. package/types/utils/cellSelectionGeometry.d.ts +3 -1
  48. package/types/utils/index.d.ts +1 -0
  49. package/types/utils/rowHeightMetrics.d.ts +15 -1
  50. package/types/utils/summary.d.ts +13 -0
package/esm/BGrid.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import Table from './components/Table';
3
- import { createPivotData, createRowHeightMetrics, buildHeaderMatrix, countVisibleCheckedIndexes, findDuplicateColumnIds, getCellValueByRowKey, getColumnId, getColumnKeyToken, getFrozenColumnsWidth, getTreeRowPath, processDataQuery, projectTreeData, projectColumnVisibility, resolveStatusOptions, resolvePaginationViewOptions, resolveScrollbarOptions, shouldRenderBottomBar, } from './utils';
3
+ import { createPivotData, createRowHeightMetrics, buildHeaderMatrix, countVisibleCheckedIndexes, findDuplicateColumnIds, getCellValueByRowKey, getColumnId, getColumnKeyToken, getFrozenColumnsWidth, getTreeRowPath, processDataQuery, projectTreeData, projectColumnVisibility, resolveStatusOptions, resolvePaginationViewOptions, resolveScrollbarOptions, computeSummaryHeight, DEFAULT_SUMMARY_ROW_HEIGHT, shouldRenderBottomBar, } from './utils';
4
4
  import { AppStoreProvider } from './store';
5
5
  import { TreeContext } from './components/TreeContext';
6
+ import { MasterDetailContext } from './components/MasterDetailContext';
6
7
  function computeModelColumns(columns, frozenColumnIndex, duplicateToolboxColumnIds) {
7
8
  let left = 0;
8
9
  let previousWidth = 0;
@@ -22,11 +23,19 @@ function computeModelColumns(columns, frozenColumnIndex, duplicateToolboxColumnI
22
23
  };
23
24
  });
24
25
  }
25
- export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, summaryHeight = 30, itemHeight = 15, itemPadding = 7, getRowHeight, columns, columnsGroup, columnGroups, onChangeColumns, frozenColumnIndex = 0, frozenRowCount = 0, data = [], page, scrollbar, status, pagination, bottomBarHeight, scrollTop = 0, scrollLeft = 0, className, style, rowChecked, sort, onClick, loading = false, spinning, disabled, rowKey, selectedRowKey, editable, onChangeData, showLineNumber, msg, getRowClassName, editTrigger = 'dblclick', cellMergeOptions, cellSelectionOptions, cellNavigationOptions, variant, summary, columnSortable, reorder, pivot, dataControl, icons, columnVisibility, searchOptions, contextMenuOptions, tree, }) {
26
+ function encodeMasterDetailRowKey(rowKey) {
27
+ const codePoints = Array.from(String(rowKey), character => character.codePointAt(0).toString(16));
28
+ return `${typeof rowKey}-${codePoints.join('-')}`;
29
+ }
30
+ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, summaryHeight: explicitSummaryHeight, summaryRowHeight = DEFAULT_SUMMARY_ROW_HEIGHT, itemHeight = 15, itemPadding = 7, getRowHeight, columns, columnsGroup, columnGroups, onChangeColumns, frozenColumnIndex = 0, frozenRowCount = 0, data = [], page, scrollbar, status, pagination, bottomBarHeight, scrollTop = 0, scrollLeft = 0, className, style, rowChecked, sort, onClick, loading = false, spinning, disabled, rowKey, selectedRowKey, editable, onChangeData, showLineNumber, msg, getRowClassName, editTrigger = 'dblclick', cellMergeOptions, cellSelectionOptions, cellNavigationOptions, variant, summary, columnSortable, reorder, pivot, dataControl, icons, columnVisibility, searchOptions, contextMenuOptions, tree, masterDetail, }) {
26
31
  const [uncontrolledExpandedRowKeys, setUncontrolledExpandedRowKeys] = React.useState(() => [
27
32
  ...(tree?.defaultExpandedRowKeys ?? []),
28
33
  ]);
29
34
  const expandedRowKeys = tree?.expandedRowKeys ?? uncontrolledExpandedRowKeys;
35
+ const [uncontrolledMasterDetailExpandedKeys, setUncontrolledMasterDetailExpandedKeys] = React.useState(() => masterDetail?.defaultExpandedRowKeys ?? []);
36
+ const masterDetailExpandedKeys = masterDetail?.expandedRowKeys !== undefined
37
+ ? masterDetail.expandedRowKeys
38
+ : uncontrolledMasterDetailExpandedKeys;
30
39
  const warnedSearchControlledRef = React.useRef({
31
40
  open: false,
32
41
  query: false,
@@ -92,7 +101,13 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
92
101
  const resolvedGetRowClassName = pivotEnabled ? undefined : getRowClassName;
93
102
  const requestedTreeEnabled = !!tree && tree.enabled !== false;
94
103
  const treeEnabled = requestedTreeEnabled && !pivotEnabled && resolvedRowKey !== undefined;
95
- const resolvedCellMergeOptions = treeEnabled ? undefined : visibilityProjection.cellMergeOptions;
104
+ const requestedMasterDetailEnabled = !!masterDetail && masterDetail.enabled !== false;
105
+ const masterDetailEnabled = requestedMasterDetailEnabled &&
106
+ !pivotEnabled &&
107
+ !treeEnabled &&
108
+ resolvedRowKey !== undefined &&
109
+ frozenRowCount <= 0;
110
+ const resolvedCellMergeOptions = treeEnabled || masterDetailEnabled ? undefined : visibilityProjection.cellMergeOptions;
96
111
  const resolvedCellSelectionOptions = resolvedDisabled
97
112
  ? { ...cellSelectionOptions, enabled: false }
98
113
  : cellSelectionOptions;
@@ -104,11 +119,11 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
104
119
  const resolvedContextMenuOptions = pivotEnabled || resolvedDisabled ? undefined : contextMenuOptions;
105
120
  const hasActiveClientQuery = resolvedDataControl?.mode === 'client' &&
106
121
  (resolvedDataControl.query.sortParams.length > 0 || resolvedDataControl.query.filterParams.length > 0);
107
- const resolvedReorder = React.useMemo(() => pivotEnabled || resolvedDisabled || treeEnabled
122
+ const resolvedReorder = React.useMemo(() => pivotEnabled || resolvedDisabled || treeEnabled || masterDetailEnabled
108
123
  ? undefined
109
124
  : (hasActiveClientQuery || frozenRowCount > 0) && reorder
110
125
  ? { ...reorder, enabled: false }
111
- : reorder, [frozenRowCount, hasActiveClientQuery, pivotEnabled, reorder, resolvedDisabled, treeEnabled]);
126
+ : reorder, [frozenRowCount, hasActiveClientQuery, masterDetailEnabled, pivotEnabled, reorder, resolvedDisabled, treeEnabled]);
112
127
  // Development warnings
113
128
  React.useEffect(() => {
114
129
  if (process.env.NODE_ENV !== 'production') {
@@ -149,6 +164,24 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
149
164
  if (treeEnabled && reorder?.enabled) {
150
165
  console.warn('[BGrid] Row reordering is disabled while tree mode is active.');
151
166
  }
167
+ if (requestedMasterDetailEnabled && rowKey === undefined) {
168
+ console.warn('[BGrid] masterDetail requires rowKey. Falling back to flat rows.');
169
+ }
170
+ if (pivotEnabled && requestedMasterDetailEnabled) {
171
+ console.warn('[BGrid] masterDetail is disabled while pivot mode is active.');
172
+ }
173
+ if (treeEnabled && requestedMasterDetailEnabled) {
174
+ console.warn('[BGrid] masterDetail is disabled while tree mode is active.');
175
+ }
176
+ if (masterDetailEnabled && cellMergeOptions) {
177
+ console.warn('[BGrid] cellMergeOptions is disabled while masterDetail mode is active.');
178
+ }
179
+ if (masterDetailEnabled && reorder?.enabled) {
180
+ console.warn('[BGrid] Row reordering is disabled while masterDetail mode is active.');
181
+ }
182
+ if (requestedMasterDetailEnabled && frozenRowCount > 0) {
183
+ console.warn('[BGrid] masterDetail is disabled when frozenRowCount > 0.');
184
+ }
152
185
  if (hasHiddenColumns && columnSortable) {
153
186
  console.warn('[BGrid] Column reordering is disabled while columns are hidden.');
154
187
  }
@@ -186,6 +219,8 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
186
219
  visibilityOptions?.hiddenColumnIds,
187
220
  visibilityOptions?.onChange,
188
221
  cellMergeOptions,
222
+ masterDetailEnabled,
223
+ requestedMasterDetailEnabled,
189
224
  requestedTreeEnabled,
190
225
  rowKey,
191
226
  treeEnabled,
@@ -250,12 +285,31 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
250
285
  ? requestedId
251
286
  : getColumnId(visibilityProjection.columns[0]);
252
287
  }, [tree?.treeColumnId, treeEnabled, visibilityProjection.columns]);
288
+ const resolvedExpandColumnId = React.useMemo(() => {
289
+ if (!masterDetailEnabled || visibilityProjection.columns.length === 0)
290
+ return undefined;
291
+ const requestedId = masterDetail?.expandColumnId;
292
+ return requestedId &&
293
+ visibilityProjection.columns.some(column => getColumnId(column) === requestedId)
294
+ ? requestedId
295
+ : getColumnId(visibilityProjection.columns[0]);
296
+ }, [masterDetail?.expandColumnId, masterDetailEnabled, visibilityProjection.columns]);
253
297
  const computedColumns = React.useMemo(() => {
254
- const columns = computeModelColumns(visibilityProjection.columns, resolvedFrozenColumnIndex, duplicateToolboxColumnIds);
255
- if (!resolvedTreeColumnId)
256
- return columns;
257
- return columns.map(column => (column.columnId === resolvedTreeColumnId ? { ...column, treeCell: true } : column));
258
- }, [duplicateToolboxColumnIds, resolvedFrozenColumnIndex, resolvedTreeColumnId, visibilityProjection.columns]);
298
+ let columns = computeModelColumns(visibilityProjection.columns, resolvedFrozenColumnIndex, duplicateToolboxColumnIds);
299
+ if (resolvedTreeColumnId) {
300
+ columns = columns.map(column => (column.columnId === resolvedTreeColumnId ? { ...column, treeCell: true } : column));
301
+ }
302
+ if (resolvedExpandColumnId) {
303
+ columns = columns.map(column => column.columnId === resolvedExpandColumnId ? { ...column, masterDetailCell: true } : column);
304
+ }
305
+ return columns;
306
+ }, [
307
+ duplicateToolboxColumnIds,
308
+ resolvedExpandColumnId,
309
+ resolvedFrozenColumnIndex,
310
+ resolvedTreeColumnId,
311
+ visibilityProjection.columns,
312
+ ]);
259
313
  const warnedTreeColumnIdRef = React.useRef(undefined);
260
314
  React.useEffect(() => {
261
315
  if (process.env.NODE_ENV === 'production' ||
@@ -268,6 +322,18 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
268
322
  warnedTreeColumnIdRef.current = tree.treeColumnId;
269
323
  console.warn(`[BGrid] tree.treeColumnId "${tree.treeColumnId}" is not visible. Falling back to the first visible column.`);
270
324
  }, [resolvedTreeColumnId, tree?.treeColumnId, treeEnabled]);
325
+ const warnedExpandColumnIdRef = React.useRef(undefined);
326
+ React.useEffect(() => {
327
+ if (process.env.NODE_ENV === 'production' ||
328
+ !masterDetailEnabled ||
329
+ !masterDetail?.expandColumnId ||
330
+ masterDetail.expandColumnId === resolvedExpandColumnId ||
331
+ warnedExpandColumnIdRef.current === masterDetail.expandColumnId) {
332
+ return;
333
+ }
334
+ warnedExpandColumnIdRef.current = masterDetail.expandColumnId;
335
+ console.warn(`[BGrid] masterDetail.expandColumnId "${masterDetail.expandColumnId}" is not visible. Falling back to the first visible column.`);
336
+ }, [masterDetail?.expandColumnId, masterDetailEnabled, resolvedExpandColumnId]);
271
337
  const queryColumns = React.useMemo(() => computeModelColumns(resolvedColumns, pivotEnabled ? 0 : frozenColumnIndex, duplicateToolboxColumnIds), [duplicateToolboxColumnIds, frozenColumnIndex, pivotEnabled, resolvedColumns]);
272
338
  const projectedOnChangeColumns = React.useCallback((visibleColumnIndex, info) => {
273
339
  if (!baseOnChangeColumns)
@@ -476,7 +542,247 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
476
542
  toggle: toggleTreeRow,
477
543
  };
478
544
  }, [resolvedDisabled, resolvedRowKey, resolvedTreeColumnId, resolvedTreeProjection, toggleTreeRow, tree]);
479
- const rowHeightMetrics = React.useMemo(() => createRowHeightMetrics(displayData, itemHeight + itemPadding * 2, getRowHeight), [displayData, getRowHeight, itemHeight, itemPadding]);
545
+ const rawGridId = React.useId();
546
+ const gridInstanceId = React.useMemo(() => rawGridId.replace(/[^a-zA-Z0-9_-]/g, '_'), [rawGridId]);
547
+ const masterDetailRowKeysMeta = React.useMemo(() => {
548
+ if (!masterDetailEnabled || resolvedRowKey === undefined)
549
+ return undefined;
550
+ const validKeys = new Set();
551
+ const rowsByKey = new Map();
552
+ const duplicateKeys = new Set();
553
+ const missingKeyIndexes = [];
554
+ for (let i = 0; i < resolvedData.length; i++) {
555
+ const key = getCellValueByRowKey(resolvedRowKey, resolvedData[i].values);
556
+ if (key === undefined || key === null) {
557
+ missingKeyIndexes.push(i);
558
+ }
559
+ else {
560
+ const k = key;
561
+ if (validKeys.has(k)) {
562
+ duplicateKeys.add(k);
563
+ }
564
+ else {
565
+ validKeys.add(k);
566
+ rowsByKey.set(k, { item: resolvedData[i], sourceIndex: i });
567
+ }
568
+ }
569
+ }
570
+ return { validKeys, rowsByKey, duplicateKeys, missingKeyIndexes };
571
+ }, [masterDetailEnabled, resolvedData, resolvedRowKey]);
572
+ React.useEffect(() => {
573
+ if (process.env.NODE_ENV === 'production' || !masterDetailRowKeysMeta)
574
+ return;
575
+ if (masterDetailRowKeysMeta.missingKeyIndexes.length > 0) {
576
+ console.warn(`[BGrid] masterDetail found ${masterDetailRowKeysMeta.missingKeyIndexes.length} rows with missing or null rowKey at indexes: [${masterDetailRowKeysMeta.missingKeyIndexes.slice(0, 5).join(', ')}${masterDetailRowKeysMeta.missingKeyIndexes.length > 5 ? ', ...' : ''}]. These rows cannot be expanded.`);
577
+ }
578
+ if (masterDetailRowKeysMeta.duplicateKeys.size > 0) {
579
+ console.warn(`[BGrid] masterDetail found duplicate rowKey values: [${Array.from(masterDetailRowKeysMeta.duplicateKeys).slice(0, 5).join(', ')}]. These rows cannot be expanded.`);
580
+ }
581
+ }, [masterDetailRowKeysMeta]);
582
+ const expandMode = masterDetail?.expandMode ?? 'multiple';
583
+ const masterDetailHasDetail = masterDetail?.hasDetail;
584
+ const effectiveMasterDetailExpandedKeysSet = React.useMemo(() => {
585
+ if (!masterDetailEnabled || !masterDetailRowKeysMeta)
586
+ return new Set();
587
+ const set = new Set();
588
+ for (const key of masterDetailExpandedKeys) {
589
+ if (masterDetailRowKeysMeta.duplicateKeys.has(key))
590
+ continue;
591
+ const row = masterDetailRowKeysMeta.rowsByKey.get(key);
592
+ if (!row)
593
+ continue;
594
+ if (masterDetailHasDetail) {
595
+ try {
596
+ if (!masterDetailHasDetail(row.item, row.sourceIndex))
597
+ continue;
598
+ }
599
+ catch {
600
+ continue;
601
+ }
602
+ }
603
+ set.add(key);
604
+ if (expandMode === 'single')
605
+ break;
606
+ }
607
+ return set;
608
+ }, [expandMode, masterDetailHasDetail, masterDetailEnabled, masterDetailExpandedKeys, masterDetailRowKeysMeta]);
609
+ const toggleMasterDetailRow = React.useCallback((rowKey, item, sourceIndex) => {
610
+ if (resolvedDisabled || !masterDetailEnabled || !masterDetail)
611
+ return;
612
+ const isExpanded = effectiveMasterDetailExpandedKeysSet.has(rowKey);
613
+ let nextKeys;
614
+ if (expandMode === 'single') {
615
+ nextKeys = isExpanded ? [] : [rowKey];
616
+ }
617
+ else {
618
+ if (isExpanded) {
619
+ nextKeys = masterDetailExpandedKeys.filter(k => !Object.is(k, rowKey));
620
+ }
621
+ else {
622
+ const exists = masterDetailExpandedKeys.some(k => Object.is(k, rowKey));
623
+ nextKeys = exists ? [...masterDetailExpandedKeys] : [...masterDetailExpandedKeys, rowKey];
624
+ }
625
+ }
626
+ if (masterDetail.expandedRowKeys === undefined) {
627
+ setUncontrolledMasterDetailExpandedKeys(nextKeys);
628
+ }
629
+ masterDetail.onExpandedRowKeysChange?.(nextKeys, {
630
+ rowKey,
631
+ expanded: !isExpanded,
632
+ item,
633
+ sourceIndex,
634
+ });
635
+ }, [
636
+ effectiveMasterDetailExpandedKeysSet,
637
+ expandMode,
638
+ masterDetail,
639
+ masterDetailEnabled,
640
+ masterDetailExpandedKeys,
641
+ resolvedDisabled,
642
+ ]);
643
+ const collapseMasterDetailRow = React.useCallback((rowKey) => {
644
+ if (resolvedDisabled || !masterDetailEnabled || !masterDetail)
645
+ return;
646
+ if (!effectiveMasterDetailExpandedKeysSet.has(rowKey))
647
+ return;
648
+ let nextKeys;
649
+ if (expandMode === 'single') {
650
+ nextKeys = [];
651
+ }
652
+ else {
653
+ nextKeys = masterDetailExpandedKeys.filter(k => !Object.is(k, rowKey));
654
+ }
655
+ if (masterDetail.expandedRowKeys === undefined) {
656
+ setUncontrolledMasterDetailExpandedKeys(nextKeys);
657
+ }
658
+ const sourceIndex = resolvedData.findIndex(d => Object.is(getCellValueByRowKey(resolvedRowKey, d.values), rowKey));
659
+ const item = resolvedData[sourceIndex];
660
+ if (item) {
661
+ masterDetail.onExpandedRowKeysChange?.(nextKeys, {
662
+ rowKey,
663
+ expanded: false,
664
+ item,
665
+ sourceIndex: sourceIndex >= 0 ? sourceIndex : 0,
666
+ });
667
+ }
668
+ }, [
669
+ effectiveMasterDetailExpandedKeysSet,
670
+ expandMode,
671
+ masterDetail,
672
+ masterDetailEnabled,
673
+ masterDetailExpandedKeys,
674
+ resolvedData,
675
+ resolvedDisabled,
676
+ resolvedRowKey,
677
+ ]);
678
+ const getToggleElementId = React.useCallback((rowKey) => `${gridInstanceId}-detail-toggle-${encodeMasterDetailRowKey(rowKey)}`, [gridInstanceId]);
679
+ const getDetailElementId = React.useCallback((rowKey) => `${gridInstanceId}-detail-region-${encodeMasterDetailRowKey(rowKey)}`, [gridInstanceId]);
680
+ const masterDetailContextValue = React.useMemo(() => {
681
+ if (!masterDetailEnabled || !resolvedExpandColumnId || resolvedRowKey === undefined || !masterDetail) {
682
+ return undefined;
683
+ }
684
+ return {
685
+ gridId: gridInstanceId,
686
+ expandColumnId: resolvedExpandColumnId,
687
+ icons: masterDetail.icons,
688
+ expandAriaLabel: masterDetail.expandAriaLabel ?? 'Expand row',
689
+ collapseAriaLabel: masterDetail.collapseAriaLabel ?? 'Collapse row',
690
+ disabled: resolvedDisabled,
691
+ expandedKeysSet: effectiveMasterDetailExpandedKeysSet,
692
+ getRowKey: values => {
693
+ const key = getCellValueByRowKey(resolvedRowKey, values);
694
+ return key === undefined || key === null ? undefined : key;
695
+ },
696
+ hasDetail: (item, sourceIndex) => {
697
+ const rowKey = getCellValueByRowKey(resolvedRowKey, item.values);
698
+ if (rowKey === undefined || rowKey === null)
699
+ return false;
700
+ if (masterDetailRowKeysMeta?.duplicateKeys.has(rowKey))
701
+ return false;
702
+ if (masterDetail.hasDetail) {
703
+ try {
704
+ return masterDetail.hasDetail(item, sourceIndex);
705
+ }
706
+ catch {
707
+ return false;
708
+ }
709
+ }
710
+ return true;
711
+ },
712
+ detailRender: masterDetail.detailRender,
713
+ toggle: toggleMasterDetailRow,
714
+ collapse: collapseMasterDetailRow,
715
+ getToggleElementId,
716
+ getDetailElementId,
717
+ };
718
+ }, [
719
+ collapseMasterDetailRow,
720
+ effectiveMasterDetailExpandedKeysSet,
721
+ getDetailElementId,
722
+ getToggleElementId,
723
+ gridInstanceId,
724
+ masterDetail,
725
+ masterDetailEnabled,
726
+ masterDetailRowKeysMeta?.duplicateKeys,
727
+ resolvedDisabled,
728
+ resolvedExpandColumnId,
729
+ resolvedRowKey,
730
+ toggleMasterDetailRow,
731
+ ]);
732
+ const rowHeightMetrics = React.useMemo(() => {
733
+ const fallbackHeight = itemHeight + itemPadding * 2;
734
+ if (!masterDetailEnabled || !masterDetail) {
735
+ return createRowHeightMetrics(displayData, fallbackHeight, getRowHeight);
736
+ }
737
+ return createRowHeightMetrics(displayData, fallbackHeight, getRowHeight, {
738
+ expandedKeysSet: effectiveMasterDetailExpandedKeysSet,
739
+ getRowKey: values => {
740
+ const key = getCellValueByRowKey(resolvedRowKey, values);
741
+ return key === undefined || key === null ? undefined : key;
742
+ },
743
+ getDetailHeight: (item, sourceIndex) => {
744
+ if (typeof masterDetail.detailRowHeight === 'function') {
745
+ try {
746
+ return masterDetail.detailRowHeight(item, sourceIndex);
747
+ }
748
+ catch {
749
+ return 200;
750
+ }
751
+ }
752
+ return typeof masterDetail.detailRowHeight === 'number' && masterDetail.detailRowHeight > 0
753
+ ? masterDetail.detailRowHeight
754
+ : 200;
755
+ },
756
+ hasDetail: (item, sourceIndex) => {
757
+ const rowKey = getCellValueByRowKey(resolvedRowKey, item.values);
758
+ if (rowKey === undefined || rowKey === null)
759
+ return false;
760
+ if (masterDetailRowKeysMeta?.duplicateKeys.has(rowKey))
761
+ return false;
762
+ if (masterDetail.hasDetail) {
763
+ try {
764
+ return masterDetail.hasDetail(item, sourceIndex);
765
+ }
766
+ catch {
767
+ return false;
768
+ }
769
+ }
770
+ return true;
771
+ },
772
+ sourceIndexByVisibleIndex: processedResult.sourceIndexByVisibleIndex,
773
+ });
774
+ }, [
775
+ displayData,
776
+ effectiveMasterDetailExpandedKeysSet,
777
+ getRowHeight,
778
+ itemHeight,
779
+ itemPadding,
780
+ masterDetail,
781
+ masterDetailEnabled,
782
+ masterDetailRowKeysMeta?.duplicateKeys,
783
+ processedResult.sourceIndexByVisibleIndex,
784
+ resolvedRowKey,
785
+ ]);
480
786
  const resolvedFrozenRowCount = pivotEnabled
481
787
  ? 0
482
788
  : Math.min(Math.max(Math.floor(frozenRowCount), 0), displayData.length);
@@ -535,6 +841,11 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
535
841
  console.warn('[BGrid] Both bottomBarHeight and footerHeight were provided. bottomBarHeight takes precedence.');
536
842
  }
537
843
  }, [bottomBarHeight, footerHeight]);
844
+ const resolvedSummaryHeight = React.useMemo(() => computeSummaryHeight({
845
+ summary: resolvedSummary,
846
+ summaryHeight: explicitSummaryHeight,
847
+ summaryRowHeight,
848
+ }), [resolvedSummary, explicitSummaryHeight, summaryRowHeight]);
538
849
  const initialStoreState = React.useMemo(() => {
539
850
  const initialWidth = width !== undefined ? Math.max(width, 100) : 0;
540
851
  const initialHeight = height !== undefined ? Math.max(height, 100) : 0;
@@ -543,7 +854,7 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
543
854
  ? Math.max(initialHeight -
544
855
  headerHeight -
545
856
  (initialShowBottomBar ? resolvedBottomBarHeight : 0) -
546
- (resolvedSummary ? summaryHeight : 0) -
857
+ (resolvedSummary ? resolvedSummaryHeight : 0) -
547
858
  containerBorderWidth * 2, 0)
548
859
  : 0;
549
860
  const displayItemCount = contentBodyHeight > 0 ? Math.ceil(contentBodyHeight / (itemHeight + itemPadding * 2)) : 0;
@@ -564,7 +875,8 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
564
875
  scrollbar: resolvedScrollbar,
565
876
  status: resolvedStatus,
566
877
  pagination: resolvedPagination,
567
- summaryHeight,
878
+ summaryHeight: resolvedSummaryHeight,
879
+ summaryRowHeight,
568
880
  itemHeight,
569
881
  itemPadding,
570
882
  frozenColumnIndex: resolvedFrozenColumnIndex,
@@ -635,7 +947,8 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
635
947
  resolvedScrollbar,
636
948
  resolvedStatus,
637
949
  resolvedPagination,
638
- summaryHeight,
950
+ resolvedSummaryHeight,
951
+ summaryRowHeight,
639
952
  itemHeight,
640
953
  itemPadding,
641
954
  rowHeightMetrics,
@@ -685,62 +998,64 @@ export function BGrid({ ref, width, height, headerHeight = 30, footerHeight, sum
685
998
  ]);
686
999
  return (React.createElement(AppStoreProvider, { initialState: initialStoreState },
687
1000
  React.createElement(TreeContext.Provider, { value: treeContextValue },
688
- React.createElement(Table, { ref: ref, sourceColumns: resolvedColumns,
689
- columns: computedColumns,
690
- columnsGroup: visibilityProjection.columnsGroup,
691
- columnGroups: visibilityProjection.columnGroups,
692
- onChangeColumns: resolvedOnChangeColumns,
693
- width,
694
- height,
695
- className,
696
- style,
697
- loading,
698
- disabled: resolvedDisabled,
699
- spinning,
700
- scrollLeft,
701
- scrollTop,
702
- headerHeight,
703
- footerHeight,
704
- bottomBarHeight: resolvedBottomBarHeight,
705
- scrollbar: resolvedScrollbar,
706
- status: resolvedStatus,
707
- pagination: resolvedPagination,
708
- summaryHeight,
709
- itemHeight,
710
- itemPadding,
711
- rowHeightMetrics,
712
- frozenColumnIndex: resolvedFrozenColumnIndex,
713
- frozenRowCount: resolvedFrozenRowCount,
714
- rowChecked: resolvedRowChecked,
715
- checkedIndexesMap,
716
- sort: resolvedSort,
717
- sortParams,
718
- dataQuery: resolvedDataQuery,
719
- dataControl: resolvedDataControl,
720
- icons,
721
- columnVisibilityState,
722
- searchOptions: resolvedSearchOptions,
723
- contextMenuOptions: resolvedContextMenuOptions,
724
- page: resolvedPage,
725
- data: displayData,
726
- sourceData: resolvedData,
727
- sourceIndexByVisibleIndex: processedResult.sourceIndexByVisibleIndex,
728
- visibleIndexBySourceIndex: processedResult.visibleIndexBySourceIndex,
729
- onClick: resolvedOnClick,
730
- rowKey: resolvedRowKey,
731
- selectedRowKey: resolvedSelectedRowKey,
732
- editable: resolvedEditable,
733
- editTrigger,
734
- onChangeData: resolvedOnChangeData,
735
- showLineNumber: resolvedShowLineNumber,
736
- msg,
737
- getRowClassName: resolvedGetRowClassName,
738
- cellMergeOptions: resolvedCellMergeOptions,
739
- cellSelectionOptions: resolvedCellSelectionOptions,
740
- cellNavigationOptions,
741
- variant,
742
- summary: resolvedSummary,
743
- columnSortable: resolvedColumnSortable,
744
- reorder: resolvedReorder,
745
- treeEnabled: !!treeContextValue }))));
1001
+ React.createElement(MasterDetailContext.Provider, { value: masterDetailContextValue },
1002
+ React.createElement(Table, { ref: ref, sourceColumns: resolvedColumns,
1003
+ columns: computedColumns,
1004
+ columnsGroup: visibilityProjection.columnsGroup,
1005
+ columnGroups: visibilityProjection.columnGroups,
1006
+ onChangeColumns: resolvedOnChangeColumns,
1007
+ width,
1008
+ height,
1009
+ className,
1010
+ style,
1011
+ loading,
1012
+ disabled: resolvedDisabled,
1013
+ spinning,
1014
+ scrollLeft,
1015
+ scrollTop,
1016
+ headerHeight,
1017
+ footerHeight,
1018
+ bottomBarHeight: resolvedBottomBarHeight,
1019
+ scrollbar: resolvedScrollbar,
1020
+ status: resolvedStatus,
1021
+ pagination: resolvedPagination,
1022
+ summaryHeight: resolvedSummaryHeight,
1023
+ summaryRowHeight,
1024
+ itemHeight,
1025
+ itemPadding,
1026
+ rowHeightMetrics,
1027
+ frozenColumnIndex: resolvedFrozenColumnIndex,
1028
+ frozenRowCount: resolvedFrozenRowCount,
1029
+ rowChecked: resolvedRowChecked,
1030
+ checkedIndexesMap,
1031
+ sort: resolvedSort,
1032
+ sortParams,
1033
+ dataQuery: resolvedDataQuery,
1034
+ dataControl: resolvedDataControl,
1035
+ icons,
1036
+ columnVisibilityState,
1037
+ searchOptions: resolvedSearchOptions,
1038
+ contextMenuOptions: resolvedContextMenuOptions,
1039
+ page: resolvedPage,
1040
+ data: displayData,
1041
+ sourceData: resolvedData,
1042
+ sourceIndexByVisibleIndex: processedResult.sourceIndexByVisibleIndex,
1043
+ visibleIndexBySourceIndex: processedResult.visibleIndexBySourceIndex,
1044
+ onClick: resolvedOnClick,
1045
+ rowKey: resolvedRowKey,
1046
+ selectedRowKey: resolvedSelectedRowKey,
1047
+ editable: resolvedEditable,
1048
+ editTrigger,
1049
+ onChangeData: resolvedOnChangeData,
1050
+ showLineNumber: resolvedShowLineNumber,
1051
+ msg,
1052
+ getRowClassName: resolvedGetRowClassName,
1053
+ cellMergeOptions: resolvedCellMergeOptions,
1054
+ cellSelectionOptions: resolvedCellSelectionOptions,
1055
+ cellNavigationOptions,
1056
+ variant,
1057
+ summary: resolvedSummary,
1058
+ columnSortable: resolvedColumnSortable,
1059
+ reorder: resolvedReorder,
1060
+ treeEnabled: !!treeContextValue })))));
746
1061
  }
@@ -12,6 +12,8 @@ export function CellNavigationDomSync({ containerRef, activeCell, cellSelectionR
12
12
  container
13
13
  .querySelectorAll('[data-bgrid-cell-active="true"], [data-bgrid-cell-active-multi-selection="true"]')
14
14
  .forEach(cell => {
15
+ if (cell.closest('[role="grid"], [role="treegrid"]') !== container)
16
+ return;
15
17
  cell.classList.remove('bgrid-cell-active', 'bgrid-cell-active-multi-selection');
16
18
  cell.removeAttribute('data-bgrid-cell-active');
17
19
  cell.removeAttribute('data-bgrid-cell-active-multi-selection');
@@ -20,6 +22,8 @@ export function CellNavigationDomSync({ containerRef, activeCell, cellSelectionR
20
22
  container
21
23
  .querySelectorAll(`td[data-bgrid-cell="true"][data-bgrid-logical-row-index="${activeCell.rowIndex}"][data-column-index="${activeCell.columnIndex}"]`)
22
24
  .forEach(cell => {
25
+ if (cell.closest('[role="grid"], [role="treegrid"]') !== container)
26
+ return;
23
27
  cell.classList.add('bgrid-cell-active');
24
28
  cell.setAttribute('data-bgrid-cell-active', 'true');
25
29
  if (hasMultiCellSelection) {
@@ -33,6 +37,8 @@ export function CellNavigationDomSync({ containerRef, activeCell, cellSelectionR
33
37
  const wholeRowSelection = coversEveryColumn && !coversEveryRow;
34
38
  const wholeColumnSelection = coversEveryRow && !coversEveryColumn;
35
39
  container.querySelectorAll('.bgrid-line-number-cell[data-row-index]').forEach(cell => {
40
+ if (cell.closest('[role="grid"], [role="treegrid"]') !== container)
41
+ return;
36
42
  const rowIndex = Number(cell.dataset.rowIndex);
37
43
  const active = !wholeColumnSelection &&
38
44
  Number.isFinite(rowIndex) &&
@@ -44,6 +50,8 @@ export function CellNavigationDomSync({ containerRef, activeCell, cellSelectionR
44
50
  cell.removeAttribute('data-bgrid-row-axis-active');
45
51
  });
46
52
  container.querySelectorAll('[data-header-cell-type][data-column-index]').forEach(cell => {
53
+ if (cell.closest('[role="grid"], [role="treegrid"]') !== container)
54
+ return;
47
55
  const columnIndex = Number(cell.dataset.columnIndex);
48
56
  const active = cell.dataset.headerCellType === 'column' &&
49
57
  !wholeRowSelection &&
@@ -98,14 +98,14 @@ export function CellTextEditorGateway({ containerRef }) {
98
98
  input.style.removeProperty('text-align');
99
99
  return;
100
100
  }
101
- const target = container.querySelector(`td[data-bgrid-cell="true"][data-row-index="${cellEditSession.hostCell.rowIndex}"][data-column-index="${cellEditSession.hostCell.columnIndex}"]`);
101
+ const target = Array.from(container.querySelectorAll(`td[data-bgrid-cell="true"][data-row-index="${cellEditSession.hostCell.rowIndex}"][data-column-index="${cellEditSession.hostCell.columnIndex}"]`)).find(el => el.closest('[role="grid"], [role="treegrid"]') === container);
102
102
  if (!(target instanceof HTMLElement)) {
103
103
  input.style.visibility = 'hidden';
104
104
  return;
105
105
  }
106
106
  const containerRect = container.getBoundingClientRect();
107
107
  const targetRect = target.getBoundingClientRect();
108
- const bodyViewport = container.querySelector('.bgrid-body-viewport');
108
+ const bodyViewport = Array.from(container.querySelectorAll('.bgrid-body-viewport')).find(el => el.closest('[role="grid"], [role="treegrid"]') === container);
109
109
  const bodyViewportRect = bodyViewport instanceof HTMLElement ? bodyViewport.getBoundingClientRect() : containerRect;
110
110
  const editorBox = resolveVisibleTextEditorVerticalBox({
111
111
  targetTop: targetRect.top,
@@ -118,9 +118,12 @@ export function CellTextEditorGateway({ containerRef }) {
118
118
  input.style.visibility = 'hidden';
119
119
  return;
120
120
  }
121
- input.style.transform = `translate(${targetRect.left - containerRect.left - container.clientLeft}px, ${editorBox.top - containerRect.top - container.clientTop}px)`;
122
- input.style.width = `${targetRect.width}px`;
123
- input.style.height = `${editorBox.height}px`;
121
+ // Convert viewport pixels back to local pixels during modal scale animations.
122
+ const scaleX = container.offsetWidth && containerRect.width ? containerRect.width / container.offsetWidth : 1;
123
+ const scaleY = container.offsetHeight && containerRect.height ? containerRect.height / container.offsetHeight : 1;
124
+ input.style.transform = `translate(${(targetRect.left - containerRect.left) / scaleX - container.clientLeft}px, ${(editorBox.top - containerRect.top) / scaleY - container.clientTop}px)`;
125
+ input.style.width = `${targetRect.width / scaleX}px`;
126
+ input.style.height = `${editorBox.height / scaleY}px`;
124
127
  if (column?.align) {
125
128
  input.style.textAlign = column.align;
126
129
  }
@@ -216,11 +219,11 @@ export function CellTextEditorGateway({ containerRef }) {
216
219
  return;
217
220
  endCellEdit(cellEditSession.id);
218
221
  }, [cellEditSession, endCellEdit]);
219
- const startReplace = React.useCallback(() => {
220
- if (!activeCell || !canStartText || cellEditSession)
222
+ const startReplace = React.useCallback((preserveInput = false) => {
223
+ if (!activeCell || !canStartText || cellEditSession || nativeStartRef.current)
221
224
  return;
222
225
  nativeStartRef.current = true;
223
- if (inputRef.current)
226
+ if (inputRef.current && !preserveInput)
224
227
  inputRef.current.value = '';
225
228
  beginCellEdit(activeCell, 'replace');
226
229
  }, [activeCell, beginCellEdit, canStartText, cellEditSession]);
@@ -233,7 +236,7 @@ export function CellTextEditorGateway({ containerRef }) {
233
236
  : '셀 텍스트 편집'
234
237
  : textEditor.ariaLabel ?? `행 ${cell.rowIndex + 1}, 열 ${cell.columnIndex + 1} 텍스트 편집`
235
238
  : 'DataGrid 키보드 탐색';
236
- return (React.createElement("input", { ref: inputRef, "data-bgrid-text-editor-gateway": 'true', className: isTextEditing ? 'bgrid-text-editor-gateway bgrid-text-editor-active' : 'bgrid-text-editor-gateway', "aria-label": ariaLabel, "aria-invalid": invalid || undefined, readOnly: !canStartText && !isTextEditing, ...inputProps, onBeforeInput: () => startReplace(), onInput: () => startReplace(), onCompositionStart: () => {
239
+ return (React.createElement("input", { ref: inputRef, "data-bgrid-text-editor-gateway": 'true', className: isTextEditing ? 'bgrid-text-editor-gateway bgrid-text-editor-active' : 'bgrid-text-editor-gateway', "aria-label": ariaLabel, "aria-invalid": invalid || undefined, readOnly: !canStartText && !isTextEditing, ...inputProps, onBeforeInput: () => startReplace(), onInput: () => startReplace(true), onCompositionStart: () => {
237
240
  composingRef.current = true;
238
241
  startReplace();
239
242
  }, onCompositionEnd: () => {