Skip to content

Commit 09658d1

Browse files
authored
feat: Cherry-pick DH-18779: Auto-resize column header groups (deephaven#2566) (deephaven#2574)
- Calculate adjusted paddings for columns based on the width of column header groups. Pre-requisite for DH-18780 - Add optional `getMetricCalculator` prop in `IrisGrid` and `IrisGridPanel`, needed to override metric calculator for Pivots Fix duplicated `mouseHanders` prop in the handlers array in `IrisGrid` - Add unit and e2e tests for column header separators
1 parent b525cda commit 09658d1

15 files changed

+831
-16
lines changed

packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
PartitionConfig,
4848
IrisGridRenderer,
4949
MouseHandlersProp,
50+
GetMetricCalculatorType,
5051
} from '@deephaven/iris-grid';
5152
import {
5253
type RowDataMap,
@@ -156,7 +157,10 @@ export interface OwnProps extends DashboardPanelProps {
156157
theme?: Partial<IrisGridThemeType> & Record<string, unknown>;
157158

158159
mouseHandlers?: MouseHandlersProp;
160+
159161
renderer?: IrisGridRenderer;
162+
163+
getMetricCalculator?: GetMetricCalculatorType;
160164
}
161165

162166
interface StateProps {
@@ -1115,6 +1119,7 @@ export class IrisGridPanel extends PureComponent<
11151119
user,
11161120
renderer,
11171121
settings,
1122+
getMetricCalculator,
11181123
theme,
11191124
} = this.props;
11201125
const {
@@ -1250,6 +1255,7 @@ export class IrisGridPanel extends PureComponent<
12501255
frozenColumns={frozenColumns}
12511256
theme={theme}
12521257
columnHeaderGroups={columnHeaderGroups}
1258+
getMetricCalculator={getMetricCalculator}
12531259
>
12541260
{childrenContent}
12551261
</IrisGrid>

packages/grid/src/Grid.test.tsx

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,25 @@ function mouseMove(
175175
);
176176
}
177177

178+
function mouseHover(
179+
column: VisibleIndex,
180+
row: VisibleIndex,
181+
component: Grid,
182+
extraMouseArgs?: MouseEventInit,
183+
clientX?: number,
184+
clientY?: number
185+
) {
186+
mouseEvent(
187+
column,
188+
row,
189+
component.handleMouseMove,
190+
'mousemove',
191+
extraMouseArgs,
192+
clientX,
193+
clientY
194+
);
195+
}
196+
178197
function mouseUp(
179198
column: VisibleIndex,
180199
row: VisibleIndex,
@@ -1038,3 +1057,163 @@ describe('paste tests', () => {
10381057
});
10391058
});
10401059
});
1060+
1061+
describe('column separators', () => {
1062+
const resizableTheme = { ...defaultTheme, allowColumnResize: true };
1063+
1064+
function getColumnSeparatorX(columnIndex: VisibleIndex): number {
1065+
// Position mouse at the right edge of the column (separator position)
1066+
const { rowHeaderWidth, columnWidth } = resizableTheme;
1067+
return rowHeaderWidth + columnWidth * (columnIndex + 1) - 2;
1068+
}
1069+
1070+
function getColumnHeaderY(): number {
1071+
// Position mouse in the column header area
1072+
return Math.floor(resizableTheme.columnHeaderHeight / 2);
1073+
}
1074+
1075+
describe('column separator detection', () => {
1076+
it('should detect column separator on mouse move and update cursor', () => {
1077+
const component = makeGridComponent(new MockGridModel(), resizableTheme);
1078+
const { columnWidth } = resizableTheme;
1079+
const separatorX = getColumnSeparatorX(0);
1080+
const headerY = getColumnHeaderY();
1081+
1082+
// Move mouse to the middle of the column header
1083+
mouseHover(0, 0, component, {}, separatorX - columnWidth / 2, headerY);
1084+
expect(component.state.cursor).toBeNull();
1085+
1086+
// Move mouse over the column separator
1087+
mouseHover(0, 0, component, {}, separatorX, headerY);
1088+
1089+
// Check that the grid recognizes we're over a separator
1090+
// The cursor should change to indicate resize capability
1091+
expect(component.state.cursor).toBe('col-resize');
1092+
});
1093+
1094+
it('should not detect separator when column resize is disabled', () => {
1095+
const noResizeTheme = { ...defaultTheme, allowColumnResize: false };
1096+
const component = makeGridComponent(new MockGridModel(), noResizeTheme);
1097+
const separatorX = getColumnSeparatorX(0);
1098+
const headerY = getColumnHeaderY();
1099+
1100+
// Move mouse to where separator would be
1101+
mouseHover(0, 0, component, {}, separatorX, headerY);
1102+
1103+
// Should not detect separator when resize is disabled
1104+
expect(component.state.draggingColumnSeparator).toBeNull();
1105+
expect(component.state.cursor).toBeNull();
1106+
});
1107+
});
1108+
1109+
describe('column separator drag interactions', () => {
1110+
it('should initiate column resize on mouse down at separator', () => {
1111+
const component = makeGridComponent(new MockGridModel(), resizableTheme);
1112+
const separatorX = getColumnSeparatorX(0);
1113+
const headerY = getColumnHeaderY();
1114+
1115+
// Click down on the separator
1116+
mouseDown(0, 0, component, {}, separatorX, headerY);
1117+
1118+
// Should initiate drag state
1119+
expect(component.state.isDragging).toBe(true);
1120+
expect(component.state.draggingColumnSeparator).toBeDefined();
1121+
});
1122+
1123+
it('should update column width during drag', () => {
1124+
const component = makeGridComponent(new MockGridModel(), resizableTheme);
1125+
const separatorX = getColumnSeparatorX(0);
1126+
const headerY = getColumnHeaderY();
1127+
1128+
// Start drag
1129+
mouseDown(0, 0, component, {}, separatorX, headerY);
1130+
1131+
// Drag to resize
1132+
const newX = separatorX + 50; // Drag 50px to the right
1133+
mouseMove(0, 0, component, {}, newX, headerY);
1134+
1135+
// Should be in drag state
1136+
expect(component.state.isDragging).toBe(true);
1137+
expect(component.state.draggingColumnSeparator).toBeDefined();
1138+
});
1139+
1140+
it('should complete resize on mouse up', () => {
1141+
const component = makeGridComponent(new MockGridModel(), resizableTheme);
1142+
const separatorX = getColumnSeparatorX(0);
1143+
const headerY = getColumnHeaderY();
1144+
1145+
// Start drag
1146+
mouseDown(0, 0, component, {}, separatorX, headerY);
1147+
1148+
// Drag to resize
1149+
const newX = separatorX + 50;
1150+
mouseMove(0, 0, component, {}, newX, headerY);
1151+
1152+
// End drag
1153+
mouseUp(0, 0, component, {}, newX, headerY);
1154+
1155+
// Should complete the resize
1156+
expect(component.state.isDragging).toBe(false);
1157+
expect(component.state.draggingColumnSeparator).toBeNull();
1158+
});
1159+
1160+
it('should not initiate resize when not over separator', () => {
1161+
const component = makeGridComponent(new MockGridModel(), resizableTheme);
1162+
const centerX = getClientX(0); // Center of column, not separator
1163+
const headerY = getColumnHeaderY();
1164+
1165+
// Click in center of column header
1166+
mouseDown(0, 0, component, {}, centerX, headerY);
1167+
1168+
// Should not initiate column resize
1169+
expect(component.state.isDragging).toBe(false);
1170+
expect(component.state.draggingColumnSeparator).toBeNull();
1171+
});
1172+
});
1173+
1174+
describe('column separator double-click interactions', () => {
1175+
it('should auto-size column on double-click on separator', () => {
1176+
const component = makeGridComponent(new MockGridModel(), resizableTheme);
1177+
const separatorX = getColumnSeparatorX(0);
1178+
const headerY = getColumnHeaderY();
1179+
1180+
// Double-click on separator using the correct function name
1181+
mouseDoubleClick(0, 0, component, {}, separatorX, headerY);
1182+
1183+
// Should trigger auto-sizing (exact behavior depends on implementation)
1184+
// The key is that the getSeparator path was exercised
1185+
expect(component.state).toBeDefined();
1186+
});
1187+
1188+
it('should not auto-size when double-clicking away from separator', () => {
1189+
const component = makeGridComponent(new MockGridModel(), resizableTheme);
1190+
const centerX = getClientX(0); // Center of column
1191+
const headerY = getColumnHeaderY();
1192+
1193+
// Double-click in center of column header
1194+
mouseDoubleClick(0, 0, component, {}, centerX, headerY);
1195+
1196+
// Should not trigger auto-sizing behavior
1197+
expect(component.state.isDragging).toBe(false);
1198+
});
1199+
});
1200+
1201+
describe('column separator with different grid configurations', () => {
1202+
it('should handle separator detection with floating columns', () => {
1203+
const model = new MockGridModel({
1204+
floatingLeftColumnCount: 1,
1205+
columnCount: 5,
1206+
});
1207+
const component = makeGridComponent(model, resizableTheme);
1208+
1209+
// Test separator for floating column
1210+
const separatorX = getColumnSeparatorX(0);
1211+
const headerY = getColumnHeaderY();
1212+
1213+
mouseMove(0, 0, component, {}, separatorX, headerY);
1214+
1215+
// Should handle floating column separators
1216+
expect(component.state.cursor).toBeDefined();
1217+
});
1218+
});
1219+
});

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ export type MouseHandlersProp = readonly (
277277
| ((irisGrid: IrisGrid) => GridMouseHandler)
278278
)[];
279279

280+
export type GetMetricCalculatorType = (
281+
...args: ConstructorParameters<typeof IrisGridMetricCalculator>
282+
) => IrisGridMetricCalculator;
283+
280284
export interface IrisGridProps {
281285
children: React.ReactNode;
282286
advancedFilters: ReadonlyAdvancedFilterMap;
@@ -365,6 +369,8 @@ export interface IrisGridProps {
365369
renderer?: IrisGridRenderer;
366370

367371
density?: 'compact' | 'regular' | 'spacious';
372+
373+
getMetricCalculator: GetMetricCalculatorType;
368374
}
369375

370376
export interface IrisGridState {
@@ -550,6 +556,9 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
550556
canToggleSearch: true,
551557
mouseHandlers: EMPTY_ARRAY,
552558
keyHandlers: EMPTY_ARRAY,
559+
getMetricCalculator: (
560+
...args: ConstructorParameters<typeof IrisGridMetricCalculator>
561+
): IrisGridMetricCalculator => new IrisGridMetricCalculator(...args),
553562
};
554563

555564
constructor(props: IrisGridProps) {
@@ -735,20 +744,16 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
735744
canCopy,
736745
frozenColumns,
737746
columnHeaderGroups,
747+
getMetricCalculator,
738748
} = props;
739749

740-
const { mouseHandlers: mouseHandlersProp } = props;
741-
742750
const { dh } = model;
743751
const keyHandlers: KeyHandler[] = [
744752
new CopyCellKeyHandler(this),
745753
new ReverseKeyHandler(this),
746754
new ClearFilterKeyHandler(this),
747755
];
748-
const mouseHandlers: (
749-
| GridMouseHandler
750-
| ((irisGrid: IrisGrid) => GridMouseHandler)
751-
)[] = [
756+
const mouseHandlers: MouseHandlersProp = [
752757
new IrisGridCellOverflowMouseHandler(this),
753758
new IrisGridRowTreeMouseHandler(this),
754759
new IrisGridTokenMouseHandler(this),
@@ -760,11 +765,10 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
760765
new IrisGridDataSelectMouseHandler(this),
761766
new PendingMouseHandler(this),
762767
new IrisGridPartitionedTableMouseHandler(this),
763-
...mouseHandlersProp,
768+
...(canCopy ? [new IrisGridCopyCellMouseHandler(this)] : []),
764769
];
765770
if (canCopy) {
766771
keyHandlers.push(new CopyKeyHandler(this));
767-
mouseHandlers.push(new IrisGridCopyCellMouseHandler(this));
768772
}
769773
const movedColumns =
770774
movedColumnsProp.length > 0
@@ -773,7 +777,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
773777
const movedRows =
774778
movedRowsProp.length > 0 ? movedRowsProp : model.initialMovedRows;
775779

776-
const metricCalculator = new IrisGridMetricCalculator({
780+
const metricCalculator = getMetricCalculator({
777781
userColumnWidths: new Map(userColumnWidths),
778782
userRowHeights: new Map(userRowHeights),
779783
movedColumns,

packages/iris-grid/src/IrisGridMetricCalculator.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ describe('IrisGridMetricCalculator', () => {
4747
const index = columns.findIndex(col => col.name === name);
4848
return index !== -1 ? index : undefined;
4949
},
50+
get columnHeaderGroups() {
51+
return [];
52+
},
5053
});
5154
calculator = new IrisGridMetricCalculator();
5255
state = makeGridMetricState(model);

0 commit comments

Comments
 (0)