Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 150 additions & 5 deletions packages/grid/src/GridUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { AxisRange } from './GridAxisRange';
import GridMetrics, { ModelIndex, MoveOperation } from './GridMetrics';
import GridRange, { GridRangeIndex } from './GridRange';
import GridUtils, { Token, TokenBox } from './GridUtils';
import type { BoundedAxisRange } from './GridAxisRange';
import { TestUtils } from '@deephaven/utils';
import { type AxisRange, type BoundedAxisRange } from './GridAxisRange';
import { type ModelIndex, type MoveOperation } from './GridMetrics';
import type GridMetrics from './GridMetrics';
import type GridModel from './GridModel';
import GridRange, { type GridRangeIndex } from './GridRange';
import GridTheme from './GridTheme';
import GridUtils, { type Token, type TokenBox } from './GridUtils';

function expectModelIndexes(
movedItems: MoveOperation[],
Expand Down Expand Up @@ -1065,3 +1068,145 @@ describe('translateTokenBox', () => {
expect(GridUtils.translateTokenBox(input, metrics)).toEqual(expectedValue);
});
});

describe('getColumnSeparatorIndex', () => {
const mockTheme = {
...GridTheme,
allowColumnResize: true,
headerSeparatorHandleSize: 10,
columnHeaderHeight: 30,
};

const createMockMetrics = (
columnHeaderMaxDepth = 1
): Partial<GridMetrics> => ({
rowHeaderWidth: 50,
columnHeaderHeight: 30,
columnHeaderMaxDepth,
floatingColumns: [],
floatingLeftWidth: 0,
visibleColumns: [0, 1, 2, 3],
allColumnXs: new Map([
[0, 0],
[1, 100],
[2, 200],
[3, 300],
]),
allColumnWidths: new Map([
[0, 100],
[1, 100],
[2, 100],
[3, 100],
]),
modelColumns: new Map([
[0, 0],
[1, 1],
[2, 2],
[3, 3],
]),
// Additional properties needed for getRowAtY (called by getColumnHeaderDepthAtY)
gridY: 30,
floatingTopRowCount: 0,
floatingBottomRowCount: 0,
rowCount: 100,
visibleRows: [],
allRowYs: new Map(),
allRowHeights: new Map(),
});

/**
* Creates a mock GridModel with grouped column headers for testing
*/
const createMockGroupedGridModel = (
headerGroups: Map<number, Map<number, string>>
): GridModel =>
TestUtils.createMockProxy<GridModel>({
columnCount: 4,
rowCount: 100,
columnHeaderMaxDepth: headerGroups.size,
textForColumnHeader: (column: ModelIndex, depth = 0) =>
headerGroups.get(depth)?.get(column) ?? '',
});

const singleLevelHeaderGroups = new Map([
[
0,
new Map([
[0, 'A'],
[1, 'B'],
[2, 'C'],
[3, 'D'],
]),
],
]);

const multiLevelHeaderGroups = new Map([
[
0,
new Map([
[0, 'A'],
[1, 'B'],
[2, 'C'],
[3, 'D'],
]),
],
[
1,
new Map([
[0, 'Group1'],
[1, 'Group1'],
[2, 'Group2'],
[3, 'Group2'],
]),
],
]);

it.each([
{
description: 'detects separator at column boundary',
x: 150, // At boundary between column 0 and 1 (100 + 50)
y: 15, // Middle of the top header (maxDepth - 1)
headerGroups: singleLevelHeaderGroups,
maxDepth: 1,
expected: 0,
},
{
description: 'detects there is no separator within the column',
x: 120, // Within column 1
y: 15,
headerGroups: singleLevelHeaderGroups,
maxDepth: 1,
expected: null,
},
{
description:
'should return null at depth 1 when no separator exists (columns in same group)',
x: 150, // Between column 0 and 1
y: 15, // Middle of the top header (maxDepth - 1)
headerGroups: multiLevelHeaderGroups,
maxDepth: 2,
expected: null,
},
{
description: 'should detect separator at depth 1 when groups differ',
x: 250, // Between Group1 and Group2
y: 15,
headerGroups: multiLevelHeaderGroups,
maxDepth: 2,
expected: 1,
},
])('$description', ({ x, y, headerGroups, maxDepth, expected }) => {
const metrics = createMockMetrics(maxDepth) as GridMetrics;
const model = createMockGroupedGridModel(headerGroups);

const result = GridUtils.getColumnSeparatorIndex(
x,
y,
metrics,
mockTheme,
model
);

expect(result).toBe(expected);
});
});
63 changes: 59 additions & 4 deletions packages/grid/src/GridUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
Range,
} from './GridAxisRange';
import { isExpandableGridModel } from './ExpandableGridModel';
import { GridRenderState } from './GridRendererTypes';
import { type GridRenderState } from './GridRendererTypes';
import type GridModel from './GridModel';

export type GridPoint = {
x: Coordinate;
Expand Down Expand Up @@ -453,19 +454,53 @@ export class GridUtils {
);
}

/**
* Check if a separator exists between a column and the next column at a given depth.
* A separator exists if adjacent columns have different header text at the specified depth.
*
* @param model The grid model
* @param depth The header depth to check at
* @param columnIndex The current model column index
* @param nextColumnIndex The next model column index (undefined for last column)
* @returns true if a separator should be shown, false otherwise
*/
static hasColumnSeparatorAtDepth(
model: GridModel,
depth: number | undefined,
columnIndex: ModelIndex | undefined,
nextColumnIndex: ModelIndex | undefined
): boolean {
if (depth == null || columnIndex == null) {
return false;
}

// Always show separator for the last column
if (nextColumnIndex == null) {
return true;
}

// A separator exists if adjacent columns have different header text at this depth
return (
model.textForColumnHeader(columnIndex, depth) !==
model.textForColumnHeader(nextColumnIndex, depth)
);
}

/**
* Gets the column index if the x/y coordinates provided are close enough to the separator, otherwise null
* @param x Mouse x coordinate
* @param y Mouse y coordinate
* @param metrics The grid metrics
* @param theme The grid theme with potential user overrides
* @param model The grid model
* @returns Index of the column separator at the coordinates provided, or null if none match
*/
static getColumnSeparatorIndex(
x: Coordinate,
y: Coordinate,
metrics: GridMetrics,
theme: GridTheme
theme: GridTheme,
model: GridModel
): VisibleIndex | null {
const {
rowHeaderWidth,
Expand All @@ -476,6 +511,7 @@ export class GridUtils {
allColumnXs,
allColumnWidths,
columnHeaderMaxDepth,
modelColumns,
} = metrics;
const { allowColumnResize, headerSeparatorHandleSize } = theme;

Expand All @@ -489,6 +525,7 @@ export class GridUtils {

const gridX = x - rowHeaderWidth;
const halfSeparatorSize = headerSeparatorHandleSize * 0.5;
const depth = GridUtils.getColumnHeaderDepthAtY(y, metrics);

// Iterate through the floating columns first since they're on top
let isPreviousColumnHidden = false;
Expand All @@ -507,7 +544,16 @@ export class GridUtils {

const minX = midX - halfSeparatorSize;
const maxX = midX + halfSeparatorSize;
if (minX <= gridX && gridX <= maxX) {
if (
minX <= gridX &&
gridX <= maxX &&
GridUtils.hasColumnSeparatorAtDepth(
model,
depth,
modelColumns.get(column),
modelColumns.get(column + 1)
)
) {
return column;
}

Expand Down Expand Up @@ -538,7 +584,16 @@ export class GridUtils {

const minX = midX - halfSeparatorSize;
const maxX = midX + halfSeparatorSize;
if (minX <= gridX && gridX <= maxX) {
if (
minX <= gridX &&
gridX <= maxX &&
GridUtils.hasColumnSeparatorAtDepth(
model,
depth,
modelColumns.get(column),
modelColumns.get(column + 1)
)
) {
return column;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class GridColumnSeparatorMouseHandler extends GridSeparatorMouseHandler {
x,
y,
metrics,
theme
theme,
model
);

if (separatorIndex == null || depth == null) {
Expand Down
Loading