Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
c3b8162
feat: DH-21344: Dropdown for input table enums
dgodinez-dh Jun 9, 2026
a9eddaa
CellDropdownField
dgodinez-dh Jun 9, 2026
55585eb
render drop down
dgodinez-dh Jun 9, 2026
9691562
fix focus change behavior
dgodinez-dh Jun 9, 2026
ca75fe8
allowed values
dgodinez-dh Jun 9, 2026
395c778
clean up
dgodinez-dh Jun 9, 2026
bd85994
quiet picker
dgodinez-dh Jun 9, 2026
4a38561
insets
dgodinez-dh Jun 9, 2026
c999ac9
background
dgodinez-dh Jun 9, 2026
c044e58
defaultOpen
dgodinez-dh Jun 9, 2026
4ee9b22
fix height
dgodinez-dh Jun 9, 2026
26ca065
fix width
dgodinez-dh Jun 10, 2026
a5e0438
clean up
dgodinez-dh Jun 10, 2026
3c27203
box shadow
dgodinez-dh Jun 10, 2026
6d454e2
padding
dgodinez-dh Jun 10, 2026
3b8f0b6
multiple restrictions
dgodinez-dh Jun 10, 2026
bb689a8
More padding
dgodinez-dh Jun 10, 2026
2711f50
context
dgodinez-dh Jun 11, 2026
01162cd
move context provider
dgodinez-dh Jun 11, 2026
44d3a2d
more renaming
dgodinez-dh Jun 11, 2026
35a87f0
Separate contexts
dgodinez-dh Jun 11, 2026
2798ae1
separate theme provider
dgodinez-dh Jun 11, 2026
8ae03f0
render chevron
dgodinez-dh Jun 15, 2026
05b051b
better chevron
dgodinez-dh Jun 15, 2026
5375bc2
wider
dgodinez-dh Jun 15, 2026
4d315de
fix text shifting
dgodinez-dh Jun 17, 2026
a37e044
fix esc behavior
dgodinez-dh Jun 17, 2026
3c36809
better enter behavior
dgodinez-dh Jun 17, 2026
4821cb0
even better enter
dgodinez-dh Jun 17, 2026
c13b1a4
edit on enter
dgodinez-dh Jun 17, 2026
6e2c18d
better space behavior
dgodinez-dh Jun 17, 2026
38cd7b9
merge latest
dgodinez-dh Jun 17, 2026
b0dfff7
add comment
dgodinez-dh Jun 17, 2026
9f6058b
move types
dgodinez-dh Jun 17, 2026
18e6bd4
key handler should use model column index
dgodinez-dh Jun 22, 2026
ef0eb88
account for depth in cell background color
dgodinez-dh Jun 22, 2026
2afdc7b
update comment
dgodinez-dh Jun 22, 2026
53f7d9b
remove any
dgodinez-dh Jun 22, 2026
3449614
unit test
dgodinez-dh Jun 22, 2026
87aa247
merge latest
dgodinez-dh Jul 2, 2026
c225ced
batch suggestions
dgodinez-dh Jul 6, 2026
d485f4e
fix type
dgodinez-dh Jul 6, 2026
5989f85
merge latest
dgodinez-dh Jul 6, 2026
929b529
fix type
dgodinez-dh Jul 6, 2026
a7e2170
formatted backgrond color
dgodinez-dh Jul 6, 2026
1b66c47
account for foreground color
dgodinez-dh Jul 6, 2026
4ab56be
refactor type
dgodinez-dh Jul 6, 2026
91f4966
use model column
dgodinez-dh Jul 6, 2026
09c6e82
combine restriction key handler with edit key handler
dgodinez-dh Jul 6, 2026
5852f2f
exctend prop type
dgodinez-dh Jul 6, 2026
9eded27
plural
dgodinez-dh Jul 6, 2026
b8812f6
move registry to theme provider
dgodinez-dh Jul 6, 2026
8d49f57
rename theme to context
dgodinez-dh Jul 6, 2026
372d9da
remove CellInputRendereContext
dgodinez-dh Jul 6, 2026
eaa5037
deprecated exports
dgodinez-dh Jul 6, 2026
f8de705
add comment
dgodinez-dh Jul 7, 2026
e366aad
use grid-cell-input-field
dgodinez-dh Jul 7, 2026
05e7da6
fix compact font size
dgodinez-dh Jul 7, 2026
0a5070d
move css
dgodinez-dh Jul 8, 2026
c2ba6d8
compact font for cell input
dgodinez-dh Jul 8, 2026
6b3488e
fix scrollbar and center text vertically
dgodinez-dh Jul 8, 2026
3eebb5c
fix last commit
dgodinez-dh Jul 8, 2026
02dea2a
fix: align cell editor text with canvas across densities
mofojed Jul 8, 2026
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
6 changes: 3 additions & 3 deletions packages/app-utils/src/components/ThemeBootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ChartThemeProvider } from '@deephaven/chart';
import { MonacoThemeProvider } from '@deephaven/console';
import { isExternalThemeEnabled, ThemeProvider } from '@deephaven/components';
import { useAppSelector } from '@deephaven/dashboard';
import { IrisGridThemeProvider } from '@deephaven/iris-grid';
import { IrisGridContextProvider } from '@deephaven/iris-grid';
import { PluginsContext, useCustomThemes } from '@deephaven/plugin';
import { getSettings } from '@deephaven/redux';

Expand All @@ -27,9 +27,9 @@ export function ThemeBootstrap({
<ThemeProvider themes={themes} waitForActivation={waitForActivation}>
<ChartThemeProvider>
<MonacoThemeProvider>
<IrisGridThemeProvider density={settings.gridDensity}>
<IrisGridContextProvider density={settings.gridDensity}>
{children}
</IrisGridThemeProvider>
</IrisGridContextProvider>
</MonacoThemeProvider>
</ChartThemeProvider>
</ThemeProvider>
Expand Down
14 changes: 13 additions & 1 deletion packages/grid/src/CellInputField.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@
border: none;
width: 100%;
height: 100%;
padding: 2px 5px 3px 5px;
// The canvas draws cell text with `textBaseline: middle`, which sits ~1px
// higher than a `line-height: 1` textarea with symmetric vertical padding.
// Shift 1px from the top padding to the bottom so the edit text lines up
// with the canvas-rendered selection text (matches the legacy 2/3 split).
// Horizontal padding uses the theme's `cellHorizontalPadding` so the text
// aligns with the canvas across densities (5px regular, 7px spacious).
padding: calc(
(var(--grid-row-height, 19px) - var(--grid-font-size, 12px)) / 2 - 1px
)
var(--grid-cell-horizontal-padding, 5px)
calc((var(--grid-row-height, 19px) - var(--grid-font-size, 12px)) / 2 + 1px);
border-radius: 2px;
font-size: var(--grid-font-size, 12px);
line-height: 1;
resize: none;

&:focus {
Expand Down
10 changes: 0 additions & 10 deletions packages/grid/src/CellInputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export type CellInputFieldProps = {
fillRange?: boolean;
}
) => void;
onContextMenu?: React.MouseEventHandler<HTMLTextAreaElement>;
style?: React.CSSProperties;
};

Expand Down Expand Up @@ -48,7 +47,6 @@ export function CellInputField({
onChange = EMPTY_FUNCTION,
onCancel = EMPTY_FUNCTION,
onDone = EMPTY_FUNCTION,
onContextMenu = EMPTY_FUNCTION,
style,
}: CellInputFieldProps): JSX.Element {
const inputField = useRef<HTMLTextAreaElement>(null);
Expand Down Expand Up @@ -179,13 +177,6 @@ export function CellInputField({
]
);

const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLTextAreaElement, MouseEvent>) => {
onContextMenu(event);
},
[onContextMenu]
);

return (
<textarea
ref={inputField}
Expand All @@ -194,7 +185,6 @@ export function CellInputField({
onBlur={handleBlur}
onChange={handleChange}
onKeyDown={handleKeyDown}
onContextMenu={handleContextMenu}
onClick={handleClick}
autoComplete="off"
autoCorrect="off"
Expand Down
80 changes: 80 additions & 0 deletions packages/grid/src/Grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import MockGridModel from './MockGridModel';
import MockGridData from './MockGridData';
import { type VisibleIndex } from './GridMetrics';
import type GridModel from './GridModel';
import { type CellInputRendererRegistry } from './GridRendererTypes';

const defaultTheme = { ...GridTheme, autoSizeColumns: false } as GridThemeType;

Expand Down Expand Up @@ -1439,3 +1440,82 @@ describe('grid-block-events cleanup', () => {
expect(hasBlockEventsClass()).toBe(false);
});
});

describe('cellInputRendererRegistry', () => {
const RESTRICTION_TYPE = 'test-restriction';

it('calls the custom renderer and passes restrictions when the model returns a matching restriction type', () => {
const model = new MockGridModel({ isEditable: true });
jest
.spyOn(model, 'getColumnRestrictions')
.mockReturnValue([{ type: RESTRICTION_TYPE }]);

const customRenderer = jest.fn(() => (
<span data-testid="custom-cell-editor" />
));
const registry = new Map([
[RESTRICTION_TYPE, customRenderer],
]) as unknown as CellInputRendererRegistry;

let ref: React.RefObject<Grid>;
function GridWithRegistry() {
ref = useRef<Grid>(null);
return (
<div>
<Grid
model={model}
theme={defaultTheme}
cellInputRendererRegistry={registry}
onSelectionChanged={jest.fn()}
ref={ref}
/>
</div>
);
}
render(<GridWithRegistry />);
const component = ref!.current!;

mouseDoubleClick(3, 5, component);

expect(customRenderer).toHaveBeenCalled();
expect(customRenderer).toHaveBeenCalledWith(
expect.objectContaining({
restrictions: [{ type: RESTRICTION_TYPE }],
})
);
});

it('falls back to the default CellInputField when no registry entry matches the column restriction', () => {
const model = new MockGridModel({ isEditable: true });
// getColumnRestriction returns [] by default — no restrictions, no renderer lookup

const customRenderer = jest.fn(() => (
<span data-testid="custom-cell-editor" />
));
const registry = new Map([
[RESTRICTION_TYPE, customRenderer],
]) as unknown as CellInputRendererRegistry;

let ref: React.RefObject<Grid>;
function GridWithRegistry() {
ref = useRef<Grid>(null);
return (
<div>
<Grid
model={model}
theme={defaultTheme}
cellInputRendererRegistry={registry}
onSelectionChanged={jest.fn()}
ref={ref}
/>
</div>
);
}
render(<GridWithRegistry />);
const component = ref!.current!;

mouseDoubleClick(3, 5, component);

expect(customRenderer).not.toHaveBeenCalled();
});
});
107 changes: 83 additions & 24 deletions packages/grid/src/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ import {
type EditingCell,
type GridRenderState,
type EditingCellTextSelectionRange,
type CellInputRendererRegistry,
type CellInputProps,
} from './GridRendererTypes';

type LegacyCanvasRenderingContext2D = CanvasRenderingContext2D & {
Expand Down Expand Up @@ -142,6 +144,13 @@ export type GridProps = typeof Grid.defaultProps & {
// Renderer for the grid canvas
renderer?: GridRenderer;

/**
* Registry of cell input renderer functions keyed by column restriction type.
* Grid looks up columnRestrictions[0].type at render time and falls back to
* its built-in CellInputField when there is no match.
*/
cellInputRendererRegistry?: CellInputRendererRegistry;

// Optional state override to pass in to the metric and render state
// Can be used to add custom properties as well
stateOverride?: Record<string, unknown>;
Expand Down Expand Up @@ -264,6 +273,7 @@ class Grid extends PureComponent<GridProps, GridState> {
window.open(token.href, '_blank', 'noopener,noreferrer');
}
},
cellInputRendererRegistry: new Map() as CellInputRendererRegistry,
stateOverride: {} as Record<string, unknown>,
theme: {
autoSelectColumn: false,
Expand Down Expand Up @@ -2267,7 +2277,13 @@ class Grid extends PureComponent<GridProps, GridState> {
allRowHeights,
} = metrics;

const { activeCellSelectionBorderWidth } = this.getTheme();
const theme = this.getTheme();
const {
activeCellSelectionBorderWidth,
rowBackgroundColors,
maxDepth,
cellHorizontalPadding,
} = theme;

const x = allColumnXs.get(column);
const y = allRowYs.get(row);
Expand All @@ -2280,26 +2296,49 @@ class Grid extends PureComponent<GridProps, GridState> {
? activeCellSelectionBorderWidth
: 0;

let modelColumn;
let modelRow;
try {
modelColumn = this.getModelColumn(column);
modelRow = this.getModelRow(row);
} catch (e) {
return null;
}

// Per-cell formatting takes priority; only compute the row stripe if there's no cell-specific color
let cellBackgroundColor: string | undefined;
let cellForegroundColor: string | undefined;
if (modelColumn != null && modelRow != null) {
cellBackgroundColor =
model.backgroundColorForCell(modelColumn, modelRow, theme) ?? undefined;
cellForegroundColor = model.colorForCell(modelColumn, modelRow, theme);
}
if (cellBackgroundColor == null && rowBackgroundColors) {
const colorSets = GridRenderer.getCachedBackgroundColors(
rowBackgroundColors,
maxDepth
);
const depth = isExpandableGridModel(model) ? model.depthForRow(row) : 0;
const colorSet = colorSets[row % colorSets.length];
cellBackgroundColor = colorSet[Math.min(depth, colorSet.length - 1)];
}

// If the cell isn't visible, we still need to display an invisible cell for focus purposes
const wrapperStyle: CSSProperties =
x != null && y != null && w != null && h != null
? {
? ({
position: 'absolute',
left: gridX + x + leftBorderOffset,
top: gridY + y,
width: w - leftBorderOffset,
height: h,
}
'--grid-cell-bg': cellBackgroundColor,
'--grid-cell-fg': cellForegroundColor,
'--grid-row-height': `${h}px`,
'--grid-font-size': theme.font.split(' ')[0],
'--grid-cell-horizontal-padding': `${cellHorizontalPadding}px`,
} as CSSProperties)
: { opacity: 0 };

let modelColumn;
let modelRow;
try {
modelColumn = this.getModelColumn(column);
modelRow = this.getModelRow(row);
} catch (e) {
return null;
}
const inputStyle: CSSProperties | undefined =
modelColumn != null && modelRow != null
? {
Expand All @@ -2311,19 +2350,39 @@ class Grid extends PureComponent<GridProps, GridState> {
? model.isValidForCell(modelColumn, modelRow, value)
: false;

const { cellInputRendererRegistry } = this.props;
const restrictions =
modelColumn != null
? model.getColumnRestrictions(modelColumn)
: EMPTY_ARRAY;

const cellInputProps: CellInputProps = {
selectionRange,
className: classNames({ error: !isValid }),
onCancel: this.handleEditCellCancel,
onChange: this.handleEditCellChange,
onDone: this.handleEditCellCommit,
isQuickEdit,
style: inputStyle,
value,
restrictions,
};

// The server supports multiple restrictions on a single column, but the UI does not know how to render multiple restrictions simultaneously
// This uses a custom renderer if there is only one restriction and uses the default renderer if there are multiple restrictions
const renderer =
restrictions.length === 1
? cellInputRendererRegistry?.get(restrictions[0].type)
: undefined;
Comment on lines +2373 to +2376

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if column restrictions has more than one element, we just use undefined? Then what's the point of having an array for column restrictions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to discuss this. It is an array of restrictions because a user can add as many as they want. I should have a comment on this code, but I think this code is essentially saying, "The UI doesn't know how to render multiple column restrictions, so if you have more than one then we use the default cell renderer."

That being said, maybe there is a discussion to be had about how we render columns with multiple restrictions. But I feel like it would be difficult to create an algorithm or hierarchy that would handle all cases correctly given that a user can combine an restrictions even if they make no sense.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment.


return (
<div style={wrapperStyle}>
<CellInputField
key={`${column},${row}`}
selectionRange={selectionRange}
className={classNames({ error: !isValid })}
onCancel={this.handleEditCellCancel}
onChange={this.handleEditCellChange}
onDone={this.handleEditCellCommit}
isQuickEdit={isQuickEdit}
style={inputStyle}
value={value}
/>
<div key={`${column},${row}`} style={wrapperStyle}>
{renderer != null ? (
renderer(cellInputProps)
) : (
// eslint-disable-next-line react/jsx-props-no-spreading
<CellInputField {...cellInputProps} />
)}
</div>
);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/grid/src/GridModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,24 @@ abstract class GridModel<
renderTypeForCell(column: ModelIndex, row: ModelIndex): CellRenderType {
return 'text';
}

/**
* Get the column restrictions for a column, which can be used to determine
* what type of input component to render for cells in that column.
* @param column The model index of the column
* @returns The column restrictions, or an empty array if there are none
*/
getColumnRestrictions(column: ModelIndex): ColumnRestriction[] {
return [];
}
}

/**
* Base type for a column restriction. Carries a `type` discriminator that
* can be used to narrow to a more specific restriction shape.
*/
export type ColumnRestriction = {
type: string;
};

export default GridModel;
Loading
Loading