Skip to content

Commit 307546e

Browse files
committed
Fix invisible resize handles in column groups
1 parent fa61b3b commit 307546e

File tree

3 files changed

+220
-4
lines changed

3 files changed

+220
-4
lines changed

packages/grid/src/GridUtils.test.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type AxisRange, type BoundedAxisRange } from './GridAxisRange';
22
import { type ModelIndex, type MoveOperation } from './GridMetrics';
33
import type GridMetrics from './GridMetrics';
4+
import GridModel from './GridModel';
45
import GridRange, { type GridRangeIndex } from './GridRange';
56
import GridUtils, { type Token, type TokenBox } from './GridUtils';
67

@@ -1067,3 +1068,192 @@ describe('translateTokenBox', () => {
10671068
expect(GridUtils.translateTokenBox(input, metrics)).toEqual(expectedValue);
10681069
});
10691070
});
1071+
1072+
describe('getColumnSeparatorIndex', () => {
1073+
const mockTheme = {
1074+
...GridTheme,
1075+
allowColumnResize: true,
1076+
headerSeparatorHandleSize: 10,
1077+
columnHeaderHeight: 30,
1078+
};
1079+
1080+
const createMockMetrics = (
1081+
columnHeaderMaxDepth = 1
1082+
): Partial<GridMetrics> => ({
1083+
rowHeaderWidth: 50,
1084+
columnHeaderHeight: 30,
1085+
columnHeaderMaxDepth,
1086+
floatingColumns: [],
1087+
floatingLeftWidth: 0,
1088+
visibleColumns: [0, 1, 2, 3],
1089+
allColumnXs: new Map([
1090+
[0, 0],
1091+
[1, 100],
1092+
[2, 200],
1093+
[3, 300],
1094+
]),
1095+
allColumnWidths: new Map([
1096+
[0, 100],
1097+
[1, 100],
1098+
[2, 100],
1099+
[3, 100],
1100+
]),
1101+
modelColumns: new Map([
1102+
[0, 0],
1103+
[1, 1],
1104+
[2, 2],
1105+
[3, 3],
1106+
]),
1107+
// Additional properties needed for getRowAtY (called by getColumnHeaderDepthAtY)
1108+
gridY: 30,
1109+
floatingTopRowCount: 0,
1110+
floatingBottomRowCount: 0,
1111+
rowCount: 100,
1112+
visibleRows: [],
1113+
allRowYs: new Map(),
1114+
allRowHeights: new Map(),
1115+
});
1116+
1117+
class MockGroupedGridModel extends GridModel {
1118+
private headerGroups: Map<number, Map<number, string>>;
1119+
1120+
constructor(headerGroups: Map<number, Map<number, string>>) {
1121+
super();
1122+
this.headerGroups = headerGroups;
1123+
}
1124+
1125+
// eslint-disable-next-line class-methods-use-this
1126+
get rowCount(): number {
1127+
return 100;
1128+
}
1129+
1130+
// eslint-disable-next-line class-methods-use-this
1131+
get columnCount(): number {
1132+
return 4;
1133+
}
1134+
1135+
// eslint-disable-next-line class-methods-use-this
1136+
get columnHeaderMaxDepth(): number {
1137+
return 2;
1138+
}
1139+
1140+
// eslint-disable-next-line class-methods-use-this
1141+
textForCell(column: ModelIndex, row: ModelIndex): string {
1142+
return `${column},${row}`;
1143+
}
1144+
1145+
textForColumnHeader(column: ModelIndex, depth = 0): string | undefined {
1146+
return this.headerGroups.get(depth)?.get(column);
1147+
}
1148+
}
1149+
1150+
it('detects separator at column boundary', () => {
1151+
const metrics = createMockMetrics() as GridMetrics;
1152+
const x = 150; // At boundary between column 0 and 1 (100 + 50)
1153+
const y = 15; // In header area
1154+
1155+
const headerGroups = new Map([
1156+
[
1157+
0,
1158+
new Map([
1159+
[0, 'A'],
1160+
[1, 'B'],
1161+
[2, 'C'],
1162+
[3, 'D'],
1163+
]),
1164+
],
1165+
]);
1166+
const model = new MockGroupedGridModel(headerGroups);
1167+
1168+
const result = GridUtils.getColumnSeparatorIndex(
1169+
x,
1170+
y,
1171+
metrics,
1172+
mockTheme,
1173+
model
1174+
);
1175+
1176+
expect(result).toBe(0);
1177+
});
1178+
1179+
it('should return null at depth 1 when no separator exists (columns in same group)', () => {
1180+
// Depth 0 (base columns): A, B, C, D
1181+
// Depth 1 (groups): Group1, Group1, Group2, Group2
1182+
// This is the core bug fix: hovering at group level where separator doesn't exist
1183+
const headerGroups = new Map([
1184+
[
1185+
0,
1186+
new Map([
1187+
[0, 'A'],
1188+
[1, 'B'],
1189+
[2, 'C'],
1190+
[3, 'D'],
1191+
]),
1192+
],
1193+
[
1194+
1,
1195+
new Map([
1196+
[0, 'Group1'],
1197+
[1, 'Group1'],
1198+
[2, 'Group2'],
1199+
[3, 'Group2'],
1200+
]),
1201+
],
1202+
]);
1203+
const model = new MockGroupedGridModel(headerGroups);
1204+
const metrics = createMockMetrics(2) as GridMetrics;
1205+
1206+
const x = 150; // Between column 0 and 1
1207+
const y = 15; // At depth 1 (0 + 15)
1208+
1209+
const result = GridUtils.getColumnSeparatorIndex(
1210+
x,
1211+
y,
1212+
metrics,
1213+
mockTheme,
1214+
model
1215+
);
1216+
1217+
expect(result).toBeNull(); // No separator at depth 1 (both in Group1)
1218+
});
1219+
1220+
it('should detect separator at depth 1 when groups differ', () => {
1221+
// Depth 0 (base columns): A, B, C, D
1222+
// Depth 1 (groups): Group1, Group1, Group2, Group2
1223+
const headerGroups = new Map([
1224+
[
1225+
0,
1226+
new Map([
1227+
[0, 'A'],
1228+
[1, 'B'],
1229+
[2, 'C'],
1230+
[3, 'D'],
1231+
]),
1232+
],
1233+
[
1234+
1,
1235+
new Map([
1236+
[0, 'Group1'],
1237+
[1, 'Group1'],
1238+
[2, 'Group2'],
1239+
[3, 'Group2'],
1240+
]),
1241+
],
1242+
]);
1243+
const model = new MockGroupedGridModel(headerGroups);
1244+
const metrics = createMockMetrics(2) as GridMetrics;
1245+
1246+
const x = 250; // Between column 1 (Group1) and 2 (Group2)
1247+
const y = 15; // At depth 1
1248+
1249+
const result = GridUtils.getColumnSeparatorIndex(
1250+
x,
1251+
y,
1252+
metrics,
1253+
mockTheme,
1254+
model
1255+
);
1256+
1257+
expect(result).toBe(1); // Separator exists at depth 1 (Group1 vs Group2)
1258+
});
1259+
});

packages/grid/src/GridUtils.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from './GridAxisRange';
2626
import { isExpandableGridModel } from './ExpandableGridModel';
2727
import { type GridRenderState } from './GridRendererTypes';
28+
import type GridModel from './GridModel';
2829

2930
export type GridPoint = {
3031
x: Coordinate;
@@ -459,13 +460,15 @@ export class GridUtils {
459460
* @param y Mouse y coordinate
460461
* @param metrics The grid metrics
461462
* @param theme The grid theme with potential user overrides
463+
* @param model The grid model
462464
* @returns Index of the column separator at the coordinates provided, or null if none match
463465
*/
464466
static getColumnSeparatorIndex(
465467
x: Coordinate,
466468
y: Coordinate,
467469
metrics: GridMetrics,
468-
theme: GridTheme
470+
theme: GridTheme,
471+
model: GridModel
469472
): VisibleIndex | null {
470473
const {
471474
rowHeaderWidth,
@@ -476,6 +479,7 @@ export class GridUtils {
476479
allColumnXs,
477480
allColumnWidths,
478481
columnHeaderMaxDepth,
482+
modelColumns,
479483
} = metrics;
480484
const { allowColumnResize, headerSeparatorHandleSize } = theme;
481485

@@ -489,6 +493,27 @@ export class GridUtils {
489493

490494
const gridX = x - rowHeaderWidth;
491495
const halfSeparatorSize = headerSeparatorHandleSize * 0.5;
496+
const depth = GridUtils.getColumnHeaderDepthAtY(y, metrics);
497+
498+
// Helper function to check if a separator exists at the given column and depth
499+
const hasSeparatorAtDepth = (column: VisibleIndex): boolean => {
500+
if (depth == null) {
501+
return true; // Can't determine, allow by default
502+
}
503+
504+
const columnIndex = modelColumns.get(column);
505+
const nextColumnIndex = modelColumns.get(column + 1);
506+
507+
if (columnIndex == null || nextColumnIndex == null) {
508+
return false;
509+
}
510+
511+
// A separator exists if adjacent columns have different header text at this depth
512+
return (
513+
model.textForColumnHeader(columnIndex, depth) !==
514+
model.textForColumnHeader(nextColumnIndex, depth)
515+
);
516+
};
492517

493518
// Iterate through the floating columns first since they're on top
494519
let isPreviousColumnHidden = false;
@@ -507,7 +532,7 @@ export class GridUtils {
507532

508533
const minX = midX - halfSeparatorSize;
509534
const maxX = midX + halfSeparatorSize;
510-
if (minX <= gridX && gridX <= maxX) {
535+
if (minX <= gridX && gridX <= maxX && hasSeparatorAtDepth(column)) {
511536
return column;
512537
}
513538

@@ -538,7 +563,7 @@ export class GridUtils {
538563

539564
const minX = midX - halfSeparatorSize;
540565
const maxX = midX + halfSeparatorSize;
541-
if (minX <= gridX && gridX <= maxX) {
566+
if (minX <= gridX && gridX <= maxX && hasSeparatorAtDepth(column)) {
542567
return column;
543568
}
544569

packages/grid/src/mouse-handlers/GridColumnSeparatorMouseHandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ class GridColumnSeparatorMouseHandler extends GridSeparatorMouseHandler {
2828
x,
2929
y,
3030
metrics,
31-
theme
31+
theme,
32+
model
3233
);
3334

3435
if (separatorIndex == null || depth == null) {

0 commit comments

Comments
 (0)