Skip to content

Commit 8b2562c

Browse files
authored
Hover over cell (#6420)
## 📝 Summary <!-- Provide a concise summary of what this pull request is addressing. If this PR fixes any issues, list them here by number (e.g., Fixes #123). --> <img width="750" height="861" alt="image" src="https://github.com/user-attachments/assets/d902d0b9-4a49-491f-a033-507cb6369016" /> Similar to #4245, we would also like to have a function to provide a callback that can create hover text for the individual cells. ## 🔍 Description of Changes <!-- Detail the specific changes made in this pull request. Explain the problem addressed and how it was resolved. If applicable, provide before and after comparisons, screenshots, or any relevant details to help reviewers understand the changes easily. --> This is another TanStack table plugin. Needs some more polishing but you like to get a sense first if this would be accepted. ## 📋 Checklist - [X] I have read the [contributor guidelines](https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md). - [ ] For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on [Discord](https://marimo.io/discord?ref=pr), or the community [discussions](https://github.com/marimo-team/marimo/discussions) (Please provide a link if applicable). - [ ] I have added tests for the changes made. - [ ] I have run the code and verified that it works as expected.
1 parent 27b829a commit 8b2562c

File tree

8 files changed

+109
-4
lines changed

8 files changed

+109
-4
lines changed

examples/ui/table.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ def _(mo):
1919
{"first_name": "Michael", "last_name": "Scott"},
2020
{"first_name": "Jim", "last_name": "Halpert"},
2121
{"first_name": "Pam", "last_name": "Beesly"},
22-
]
22+
],
23+
# Show full name on hover for each row using column placeholders
24+
hover_template="{{first_name}} {{last_name}}",
2325
)
2426
table
2527
return (table,)

frontend/src/components/data-table/__tests__/data-table.test.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* Copyright 2024 Marimo. All rights reserved. */
22
import type { ColumnDef, RowSelectionState } from "@tanstack/react-table";
3-
import { render } from "@testing-library/react";
3+
import { render, screen } from "@testing-library/react";
44
import { describe, expect, it, vi } from "vitest";
55
import { TooltipProvider } from "@/components/ui/tooltip";
66
import { DataTable } from "../data-table";
@@ -57,4 +57,42 @@ describe("DataTable", () => {
5757
// Verify the rowSelection prop is maintained
5858
expect(commonProps.rowSelection).toEqual(initialRowSelection);
5959
});
60+
61+
it("applies hoverTemplate to the row title using row values", () => {
62+
interface RowData {
63+
id: number;
64+
first: string;
65+
last: string;
66+
}
67+
68+
const testData: RowData[] = [
69+
{ id: 1, first: "Michael", last: "Scott" },
70+
{ id: 2, first: "Jim", last: "Halpert" },
71+
];
72+
73+
const columns: Array<ColumnDef<RowData>> = [
74+
{ accessorKey: "first", header: "First" },
75+
{ accessorKey: "last", header: "Last" },
76+
];
77+
78+
render(
79+
<TooltipProvider>
80+
<DataTable
81+
data={testData}
82+
columns={columns}
83+
selection={null}
84+
totalRows={2}
85+
totalColumns={2}
86+
pagination={false}
87+
hoverTemplate={"{{first}} {{last}}"}
88+
/>
89+
</TooltipProvider>,
90+
);
91+
92+
// Grab all rows and assert title attribute computed from template
93+
const rows = screen.getAllByRole("row");
94+
// The first row is header; subsequent rows correspond to data
95+
expect(rows[1]).toHaveAttribute("title", "Michael Scott");
96+
expect(rows[2]).toHaveAttribute("title", "Jim Halpert");
97+
});
6098
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* Copyright 2025 Marimo. All rights reserved. */
2+
"use no memo";
3+
4+
import type { InitialTableState, TableFeature } from "@tanstack/react-table";
5+
import type { CellHoverTemplateTableState } from "./types";
6+
7+
export const CellHoverTemplateFeature: TableFeature = {
8+
getInitialState: (state?: InitialTableState): CellHoverTemplateTableState => {
9+
return {
10+
...state,
11+
cellHoverTemplate: null,
12+
};
13+
},
14+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* Copyright 2025 Marimo. All rights reserved. */
2+
/* eslint-disable @typescript-eslint/no-empty-interface */
3+
4+
export interface CellHoverTemplateTableState {
5+
cellHoverTemplate: string | null;
6+
}
7+
8+
// Use declaration merging to add our new feature APIs
9+
declare module "@tanstack/react-table" {
10+
interface TableState extends CellHoverTemplateTableState {}
11+
}

frontend/src/components/data-table/data-table.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { Table } from "@/components/ui/table";
2525
import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
2626
import { cn } from "@/utils/cn";
2727
import type { PanelType } from "../editor/chrome/panels/context-aware-panel/context-aware-panel";
28+
import { CellHoverTemplateFeature } from "./cell-hover-template/feature";
2829
import { CellSelectionFeature } from "./cell-selection/feature";
2930
import type { CellSelectionState } from "./cell-selection/types";
3031
import { CellStylingFeature } from "./cell-styling/feature";
@@ -64,6 +65,7 @@ interface DataTableProps<TData> extends Partial<DownloadActionProps> {
6465
rowSelection?: RowSelectionState;
6566
cellSelection?: CellSelectionState;
6667
cellStyling?: CellStyleState | null;
68+
hoverTemplate?: string | null;
6769
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
6870
onCellSelectionChange?: OnChangeFn<CellSelectionState>;
6971
getRowIds?: GetRowIds;
@@ -104,6 +106,7 @@ const DataTableInternal = <TData,>({
104106
rowSelection,
105107
cellSelection,
106108
cellStyling,
109+
hoverTemplate,
107110
paginationState,
108111
setPaginationState,
109112
downloadAs,
@@ -178,6 +181,7 @@ const DataTableInternal = <TData,>({
178181
ColumnFormattingFeature,
179182
CellSelectionFeature,
180183
CellStylingFeature,
184+
CellHoverTemplateFeature,
181185
CopyColumnFeature,
182186
FocusRowFeature,
183187
],
@@ -236,10 +240,11 @@ const DataTableInternal = <TData,>({
236240
? {}
237241
: // No pagination, show all rows
238242
{ pagination: { pageIndex: 0, pageSize: data.length } }),
239-
rowSelection,
240-
cellSelection,
243+
rowSelection: rowSelection ?? {},
244+
cellSelection: cellSelection ?? [],
241245
cellStyling,
242246
columnPinning: columnPinning,
247+
cellHoverTemplate: hoverTemplate,
243248
},
244249
});
245250

frontend/src/components/data-table/renderers.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,25 @@ export const DataTableBody = <TData,>({
9393
handleCellsKeyDown,
9494
} = useCellRangeSelection({ table });
9595

96+
function applyHoverTemplate(
97+
template: string,
98+
cells: Array<Cell<TData, unknown>>,
99+
): string {
100+
const variableRegex = /{{(\w+)}}/g;
101+
// Map column id -> stringified value
102+
const idToValue = new Map<string, string>();
103+
for (const c of cells) {
104+
const v = c.getValue();
105+
// Prefer empty string for nulls to keep tooltip clean
106+
const s = renderUnknownValue({ value: v, nullAsEmptyString: true });
107+
idToValue.set(c.column.id, s);
108+
}
109+
return template.replace(variableRegex, (_substr, varName: string) => {
110+
const val = idToValue.get(varName);
111+
return val !== undefined ? val : `{{${varName}}}`;
112+
});
113+
}
114+
96115
const renderCells = (cells: Array<Cell<TData, unknown>>) => {
97116
return cells.map((cell) => {
98117
const { className, style: pinningstyle } = getPinningStyles(cell.column);
@@ -101,6 +120,7 @@ export const DataTableBody = <TData,>({
101120
cell.getUserStyling?.() || {},
102121
pinningstyle,
103122
);
123+
104124
return (
105125
<TableCell
106126
tabIndex={0}
@@ -145,10 +165,18 @@ export const DataTableBody = <TData,>({
145165
const isRowViewedInPanel =
146166
rowViewerPanelOpen && viewedRowIdx === rowIndex;
147167

168+
// Compute hover title once per row using this row's cells (visible or hidden)
169+
const hoverTemplate = table.getState().cellHoverTemplate || null;
170+
const rowCells = row.getAllCells();
171+
const rowTitle = hoverTemplate
172+
? applyHoverTemplate(hoverTemplate, rowCells)
173+
: undefined;
174+
148175
return (
149176
<TableRow
150177
key={row.id}
151178
data-state={row.getIsSelected() && "selected"}
179+
title={rowTitle}
152180
// These classes ensure that empty rows (nulls) still render
153181
className={cn(
154182
"border-t h-6",

frontend/src/plugins/impl/DataTablePlugin.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
254254
maxColumns: z.union([z.number(), z.literal("all")]).default("all"),
255255
hasStableRowId: z.boolean().default(false),
256256
cellStyles: z.record(z.record(z.object({}).passthrough())).optional(),
257+
hoverTemplate: z.string().optional(),
257258
// Whether to load the data lazily.
258259
lazy: z.boolean().default(false),
259260
// If lazy, this will preload the first page of data
@@ -385,6 +386,7 @@ interface DataTableProps<T> extends Data<T>, DataTableFunctions {
385386
// Filters
386387
enableFilters?: boolean;
387388
cellStyles?: CellStyleState | null;
389+
hoverTemplate?: string | null;
388390
toggleDisplayHeader?: () => void;
389391
host: HTMLElement;
390392
cellId?: CellId | null;
@@ -707,6 +709,7 @@ const DataTableComponent = ({
707709
totalColumns,
708710
get_row_ids,
709711
cellStyles,
712+
hoverTemplate,
710713
toggleDisplayHeader,
711714
calculate_top_k_rows,
712715
preview_column,
@@ -904,6 +907,7 @@ const DataTableComponent = ({
904907
rowSelection={rowSelection}
905908
cellSelection={cellSelection}
906909
cellStyling={cellStyles}
910+
hoverTemplate={hoverTemplate}
907911
downloadAs={showDownload ? downloadAs : undefined}
908912
enableSearch={enableSearch}
909913
searchQuery={searchQuery}

marimo/_plugins/ui/_impl/table.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ def style_cell(_rowId, _columnName, value):
332332
on_change (Callable[[Union[List[JSONType], Dict[str, List[JSONType]], IntoDataFrame, List[TableCell]]], None], optional):
333333
Optional callback to run when this element's value changes.
334334
style_cell (Callable[[str, str, Any], Dict[str, Any]], optional): A function that takes the row id, column name and value and returns a dictionary of CSS styles.
335+
hover_template (str, optional): A template for the hover text of the table.
335336
max_columns (int, optional): Maximum number of columns to display. Defaults to the
336337
configured default_table_max_columns (50 by default). Set to None to show all columns.
337338
label (str, optional): A descriptive name for the table. Defaults to "".
@@ -439,6 +440,7 @@ def __init__(
439440
]
440441
] = None,
441442
style_cell: Optional[Callable[[str, str, Any], dict[str, Any]]] = None,
443+
hover_template: Optional[str] = None,
442444
# The _internal_* arguments are for overriding and unit tests
443445
# table should take the value unconditionally
444446
_internal_column_charts_row_limit: Optional[int] = None,
@@ -666,6 +668,7 @@ def __init__(
666668
"wrapped-columns": wrapped_columns,
667669
"has-stable-row-id": self._has_stable_row_id,
668670
"cell-styles": search_result_styles,
671+
"hover-template": hover_template,
669672
"lazy": _internal_lazy,
670673
"preload": _internal_preload,
671674
},

0 commit comments

Comments
 (0)