Skip to content

Commit f2efc0a

Browse files
authored
feat: Expandable rows shows tooltip (#1068)
Closes #1061 Upon hovering an expandable row, display a tooltip with instructions on how to expand or expand all children. The tooltip closes on any action but shows again if you move on the expandable row.
1 parent 60c955a commit f2efc0a

4 files changed

Lines changed: 219 additions & 0 deletions

File tree

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
PopperOptions,
2525
ReferenceObject,
2626
Button,
27+
ContextActionUtils,
2728
} from '@deephaven/components';
2829
import {
2930
Grid,
@@ -119,6 +120,7 @@ import {
119120
IrisGridContextMenuHandler,
120121
IrisGridDataSelectMouseHandler,
121122
IrisGridFilterMouseHandler,
123+
IrisGridRowTreeMouseHandler,
122124
IrisGridSortMouseHandler,
123125
PendingMouseHandler,
124126
} from './mousehandlers';
@@ -409,6 +411,8 @@ export interface IrisGridState {
409411
showOverflowModal: boolean;
410412
overflowText: string;
411413
overflowButtonTooltipProps: CSSProperties | null;
414+
expandCellTooltipProps: CSSProperties | null;
415+
expandTooltipDisplayValue: string;
412416

413417
gotoRow: string;
414418
gotoRowError: string;
@@ -716,6 +720,7 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
716720
}
717721
const mouseHandlers = [
718722
new IrisGridCellOverflowMouseHandler(this),
723+
new IrisGridRowTreeMouseHandler(this),
719724
new IrisGridColumnSelectMouseHandler(this),
720725
new IrisGridColumnTooltipMouseHandler(this),
721726
new IrisGridSortMouseHandler(this),
@@ -832,6 +837,8 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
832837
showOverflowModal: false,
833838
overflowText: '',
834839
overflowButtonTooltipProps: null,
840+
expandCellTooltipProps: null,
841+
expandTooltipDisplayValue: 'expand',
835842
isGotoShown: false,
836843
gotoRow: '',
837844
gotoRowError: '',
@@ -3552,6 +3559,43 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
35523559
}
35533560
);
35543561

3562+
getExpandCellTooltip = memoize(
3563+
(expandCellTooltipProps: CSSProperties): ReactNode => {
3564+
if (expandCellTooltipProps == null) {
3565+
return null;
3566+
}
3567+
3568+
const { expandTooltipDisplayValue } = this.state;
3569+
3570+
const wrapperStyle: CSSProperties = {
3571+
position: 'absolute',
3572+
...expandCellTooltipProps,
3573+
pointerEvents: 'none',
3574+
};
3575+
3576+
const popperOptions: PopperOptions = {
3577+
placement: 'bottom-start',
3578+
};
3579+
3580+
return (
3581+
<div style={wrapperStyle}>
3582+
<Tooltip
3583+
key={Date.now()}
3584+
options={popperOptions}
3585+
ref={this.handleTooltipRef}
3586+
>
3587+
<div style={{ textAlign: 'left' }}>
3588+
Click to {expandTooltipDisplayValue} row
3589+
<br />
3590+
{ContextActionUtils.isMacPlatform() ? '⌘' : 'Ctrl+'}Click to
3591+
expand row and all children
3592+
</div>
3593+
</Tooltip>
3594+
</div>
3595+
);
3596+
}
3597+
);
3598+
35553599
handleGotoRowSelectedRowNumberSubmit(): void {
35563600
const { gotoRow: rowNumber } = this.state;
35573601
this.focusRowInGrid(rowNumber);
@@ -3793,6 +3837,7 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
37933837
showOverflowModal,
37943838
overflowText,
37953839
overflowButtonTooltipProps,
3840+
expandCellTooltipProps,
37963841
isGotoShown,
37973842
gotoRow,
37983843
gotoRowError,
@@ -4403,6 +4448,8 @@ export class IrisGrid extends Component<IrisGridProps, IrisGridState> {
44034448
{advancedFilterMenus}
44044449
{overflowButtonTooltipProps &&
44054450
this.getOverflowButtonTooltip(overflowButtonTooltipProps)}
4451+
{expandCellTooltipProps &&
4452+
this.getExpandCellTooltip(expandCellTooltipProps)}
44064453
</div>
44074454
<GotoRow
44084455
model={model}

packages/iris-grid/src/IrisGridRenderer.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const ICON_NAMES = Object.freeze({
4242
CELL_OVERFLOW: 'cellOverflow',
4343
});
4444

45+
const EXPAND_ICON_SIZE = 10;
4546
const ICON_SIZE = 16;
4647

4748
export type IrisGridRenderState = GridRenderState & {
@@ -1076,6 +1077,53 @@ class IrisGridRenderer extends GridRenderer {
10761077

10771078
context.restore();
10781079
}
1080+
1081+
getExpandButtonPosition(
1082+
{
1083+
mouseX,
1084+
mouseY,
1085+
metrics,
1086+
theme,
1087+
}: {
1088+
mouseX: Coordinate | null;
1089+
mouseY: Coordinate | null;
1090+
metrics: GridMetrics | undefined;
1091+
theme: GridThemeType;
1092+
},
1093+
depth: number | null
1094+
): {
1095+
left: Coordinate | null;
1096+
top: Coordinate | null;
1097+
width: number | null;
1098+
height: number | null;
1099+
} {
1100+
const NULL_POSITION = { left: null, top: null, width: null, height: null };
1101+
if (mouseX == null || mouseY == null || depth == null || !metrics) {
1102+
return NULL_POSITION;
1103+
}
1104+
const { rowHeight, left, top } = GridUtils.getCellInfoFromXY(
1105+
mouseX,
1106+
mouseY,
1107+
metrics
1108+
);
1109+
1110+
assertNotNull(left);
1111+
assertNotNull(rowHeight);
1112+
assertNotNull(top);
1113+
const { cellHorizontalPadding } = theme;
1114+
1115+
const width = EXPAND_ICON_SIZE + 2 * cellHorizontalPadding;
1116+
1117+
const buttonLeft = Math.max(left + EXPAND_ICON_SIZE * depth, metrics.gridX);
1118+
const buttonTop = metrics.gridY + top;
1119+
1120+
return {
1121+
left: buttonLeft,
1122+
top: buttonTop,
1123+
width,
1124+
height: rowHeight,
1125+
};
1126+
}
10791127
}
10801128

10811129
export default IrisGridRenderer;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import {
2+
EventHandlerResult,
3+
Grid,
4+
GridMouseHandler,
5+
GridPoint,
6+
GridRowTreeMouseHandler,
7+
isExpandableGridModel,
8+
} from '@deephaven/grid';
9+
import { assertNotNull } from '@deephaven/utils';
10+
import deepEqual from 'deep-equal';
11+
import type IrisGrid from '../IrisGrid';
12+
13+
class IrisGridRowTreeMouseHandler extends GridMouseHandler {
14+
private irisGrid: IrisGrid;
15+
16+
constructor(irisGrid: IrisGrid) {
17+
super(750); // Needs to be before GridRowTreeMouseHandler
18+
19+
this.irisGrid = irisGrid;
20+
}
21+
22+
private destroyTooltip(): void {
23+
this.irisGrid.setState({ expandCellTooltipProps: null });
24+
}
25+
26+
private setCursor(gridPoint: GridPoint, grid: Grid): EventHandlerResult {
27+
if (GridRowTreeMouseHandler.isInTreeBox(gridPoint, grid)) {
28+
this.cursor = 'pointer';
29+
return { stopPropagation: false, preventDefault: false };
30+
}
31+
32+
this.cursor = null;
33+
return false;
34+
}
35+
36+
private getButtonPosition({
37+
x,
38+
y,
39+
column,
40+
row,
41+
}: GridPoint): {
42+
left: number;
43+
top: number;
44+
width: number;
45+
height: number;
46+
} | null {
47+
if (column == null || row == null) {
48+
return null;
49+
}
50+
const { renderer, grid, state, props } = this.irisGrid;
51+
if (!grid) {
52+
return null;
53+
}
54+
const { metrics } = state;
55+
const { model } = props;
56+
57+
const { canvasContext: context } = grid;
58+
const theme = grid.getTheme();
59+
const rendererState = {
60+
context,
61+
mouseX: x,
62+
mouseY: y,
63+
metrics,
64+
model,
65+
theme,
66+
};
67+
68+
const depth = isExpandableGridModel(model) ? model.depthForRow(row) : null;
69+
70+
const { left, top, width, height } = renderer.getExpandButtonPosition(
71+
rendererState,
72+
depth
73+
);
74+
if (left == null || width == null || top == null || height == null) {
75+
return null;
76+
}
77+
78+
return { left, top, width, height };
79+
}
80+
81+
onMove(gridPoint: GridPoint, grid: Grid): EventHandlerResult {
82+
if (GridRowTreeMouseHandler.isInTreeBox(gridPoint, grid)) {
83+
const { expandCellTooltipProps } = this.irisGrid.state;
84+
const { model } = this.irisGrid.props;
85+
const newProps = this.getButtonPosition(gridPoint);
86+
const { row } = gridPoint;
87+
assertNotNull(row);
88+
const isRowExpanded =
89+
isExpandableGridModel(model) && model.isRowExpanded(row);
90+
if (!deepEqual(expandCellTooltipProps, newProps)) {
91+
this.irisGrid.setState({
92+
expandCellTooltipProps: newProps,
93+
expandTooltipDisplayValue: isRowExpanded ? 'collapse' : 'expand',
94+
});
95+
}
96+
} else {
97+
this.destroyTooltip();
98+
}
99+
return this.setCursor(gridPoint, grid);
100+
}
101+
102+
onDown(): EventHandlerResult {
103+
this.destroyTooltip();
104+
return false;
105+
}
106+
107+
onContextMenu(): EventHandlerResult {
108+
this.destroyTooltip();
109+
return false;
110+
}
111+
112+
onWheel(): EventHandlerResult {
113+
this.destroyTooltip();
114+
return false;
115+
}
116+
117+
onLeave(): EventHandlerResult {
118+
this.destroyTooltip();
119+
return false;
120+
}
121+
}
122+
123+
export default IrisGridRowTreeMouseHandler;

packages/iris-grid/src/mousehandlers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export { default as IrisGridColumnTooltipMouseHandler } from './IrisGridColumnTo
44
export { default as IrisGridContextMenuHandler } from './IrisGridContextMenuHandler';
55
export { default as IrisGridDataSelectMouseHandler } from './IrisGridDataSelectMouseHandler';
66
export { default as IrisGridFilterMouseHandler } from './IrisGridFilterMouseHandler';
7+
export { default as IrisGridRowTreeMouseHandler } from './IrisGridRowTreeMouseHandler';
78
export { default as IrisGridSortMouseHandler } from './IrisGridSortMouseHandler';
89
export { default as PendingMouseHandler } from './PendingMouseHandler';

0 commit comments

Comments
 (0)