/* Copyright 2026 Marimo. All rights reserved. */ import type { Cell } from "@tanstack/react-table"; import { useAtomValue } from "jotai"; import { CopyIcon, FilterIcon, SquareStack } from "lucide-react"; import type { RefObject } from "react"; import useEvent from "react-use-event-hook"; import { copyToClipboard } from "@/utils/copy"; import { Logger } from "@/utils/Logger"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuPortal, ContextMenuSeparator, ContextMenuTrigger, } from "../ui/context-menu"; import { DATA_CELL_ID } from "./cell-utils"; import { Filter, isMembershipFilterType, type MembershipFilterType, } from "./filters"; import { selectedCellsAtom } from "./range-focus/atoms"; import { getClipboardContent, getRawValue } from "./utils"; export const DataTableContextMenu = ({ contextMenuRef, tableBody, tableRef, copyAllCells, }: { contextMenuRef: RefObject | null>; tableBody: React.ReactNode; tableRef: RefObject; copyAllCells: () => void; }) => { const handleContextMenuChange = useEvent((open: boolean) => { const cell = contextMenuRef.current; if (!cell) { return; } // Add a background color to the cell when the context menu is open const cellElement = tableRef.current?.querySelector( `[${DATA_CELL_ID}="${cell.id}"]`, ); if (!cellElement) { Logger.error("Context menu cell not found in table"); return; } if (open) { cellElement.classList.add("bg-(--green-4)"); } else { cellElement.classList.remove("bg-(--green-4)"); } }); return ( {tableBody} ); }; export const CellContextMenu = ({ cellRef, copySelectedCells, }: { cellRef: RefObject | null>; copySelectedCells: () => void; }) => { const selectedCells = useAtomValue(selectedCellsAtom); const multipleSelectedCells = selectedCells.size > 1; const cell = cellRef.current; if (!cell) { Logger.error("No cell found in context menu"); return; } const table = cell.getContext().table; const displayedValue = cell.getValue(); const rawValue = getRawValue(table, cell.row.index, cell.column.id) ?? displayedValue; const handleCopyCell = () => { try { const { text, html } = getClipboardContent(rawValue, displayedValue); copyToClipboard(text, html); } catch (error) { Logger.error("Failed to copy context menu cell", error); } }; const column = cell.column; const filterType = column.columnDef.meta?.filterType; const membershipFilterType: MembershipFilterType | undefined = column.getCanFilter() && filterType && isMembershipFilterType(filterType) ? filterType : undefined; const handleFilterCell = ( type: MembershipFilterType, operator: "in" | "not_in", ) => { const filter = type === "number" ? Filter.number({ operator, values: [rawValue] }) : Filter.text({ operator, values: [rawValue] }); column.setFilterValue(filter); }; return ( Copy cell {multipleSelectedCells && ( Copy selected cells )} {membershipFilterType && ( <> handleFilterCell(membershipFilterType, "in")} > Filter by this value handleFilterCell(membershipFilterType, "not_in")} > Remove rows with this value )} ); };