Shadcn Table
componentdata-grid-view-menusource: ok · hit
Data Grid View Menu
A view menu component for the data grid to toggle column visibility
npx shadcn add "https://tablecn.com/r/data-grid-view-menu.json"free
Interaction
dropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
ui-primitives@base-ui/react
Registry dependencies
buttoncommandpopover
Files5 · source cached 10/9/2026, 11:28:18 AM
src/registry/bases/base/components/data-grid/data-grid-view-menu.tsx
"use client";
import { type RowData, Subscribe, type Table } from "@tanstack/react-table";
import { cn } from "cn";
import * as React from "react";
import type { DataGridFeatures } from "@/lib/data-grid-features";
import { Button } from "@/registry/bases/base/ui/button";
import { useDirection } from "@/registry/bases/base/ui/direction";
import {
Faceted,
FacetedContent,
FacetedEmpty,
FacetedGroup,
FacetedInput,
FacetedItem,
FacetedItemIndicator,
FacetedList,
FacetedTrigger,
} from "@/registry/bases/base/ui/faceted";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";
interface DataGridViewMenuProps<
TData extends RowData,
> extends React.ComponentProps<typeof FacetedContent> {
table: Table<DataGridFeatures, TData>;
disabled?: boolean;
}
export function DataGridViewMenu<TData extends RowData>({
table,
disabled,
className,
...props
}: DataGridViewMenuProps<TData>) {
const dir = useDirection();
const columns = React.useMemo(
() =>
table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide(),
),
[table],
);
return (
<Subscribe source={table.atoms.columnVisibility}>
{(columnVisibility) => (
<Faceted
multiple
value={columns
.filter((column) => columnVisibility[column.id] !== false)
.map((column) => column.id)}
>
<FacetedTrigger
render={
<Button
aria-label="Toggle columns"
role="combobox"
dir={dir}
variant="outline"
className="ms-auto hidden lg:flex"
disabled={disabled}
/>
}
>
<IconPlaceholder
lucide="Settings2"
tabler="IconSettings"
hugeicons="Settings05Icon"
phosphor="GearIcon"
remixicon="RiSettingsLine"
className="text-muted-foreground"
/>
View
</FacetedTrigger>
<FacetedContent
dir={dir}
align="center"
className={cn("w-44", className)}
{...props}
>
<FacetedInput placeholder="Search columns..." />
<FacetedList>
<FacetedEmpty>No columns found.</FacetedEmpty>
<FacetedGroup>
{columns.map((column) => (
<FacetedItem
key={column.id}
value={column.id}
keywords={[column.columnDef.meta?.label ?? column.id]}
onSelect={() => column.toggleVisibility()}
>
<span className="truncate">
{column.columnDef.meta?.label ?? column.id}
</span>
<FacetedItemIndicator />
</FacetedItem>
))}
</FacetedGroup>
</FacetedList>
</FacetedContent>
</Faceted>
)}
</Subscribe>
);
}
src/registry/bases/base/ui/direction.tsx
"use client";
export {
DirectionProvider,
useDirection,
} from "@base-ui/react/direction-provider";
src/registry/bases/base/ui/faceted.tsx
"use client";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "cn";
import * as React from "react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandList,
CommandSeparator,
} from "@/registry/bases/base/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/bases/base/ui/popover";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";
type FacetedSelectedValue<Multiple extends boolean> = Multiple extends true
? string[]
: string;
interface FacetedOption {
value: string;
label: string;
}
const NO_ITEMS: FacetedOption[] = [];
interface FacetedState {
value: string | string[] | undefined;
open: boolean;
}
interface FacetedStore {
subscribe: (callback: () => void) => () => void;
getState: () => FacetedState;
getProps: () => FacetedController;
notify: () => void;
setOpen: (open: boolean) => void;
selectItem: (value: string) => void;
clear: () => void;
}
interface FacetedController {
open?: boolean;
onOpenChange?: (open: boolean) => void;
value?: string | string[];
onValueChange?: (value: string | string[] | undefined) => void;
isControlled: boolean;
multiple: boolean;
items: FacetedOption[];
}
function getSelection(
state: FacetedState,
props: FacetedController,
): string | string[] | undefined {
return props.isControlled ? props.value : state.value;
}
function getHasSelection(value: string | string[] | undefined) {
if (Array.isArray(value)) return value.length > 0;
return !!value;
}
function getIsValueSelected(
value: string | string[] | undefined,
multiple: boolean,
itemValue: string,
) {
if (multiple) return Array.isArray(value) && value.includes(itemValue);
return value === itemValue;
}
function getSelectedItems(
value: string | string[] | undefined,
items: FacetedOption[],
) {
const values = Array.isArray(value) ? value : value ? [value] : [];
return values.map((itemValue) => ({
value: itemValue,
label: items.find((item) => item.value === itemValue)?.label ?? itemValue,
}));
}
function getIsFacetedSelectedValue<Multiple extends boolean>(
value: string | string[] | undefined,
multiple: Multiple,
): value is FacetedSelectedValue<Multiple> | undefined {
if (value === undefined) return true;
return multiple ? Array.isArray(value) : typeof value === "string";
}
function createFacetedStore(
propsRef: React.RefObject<FacetedController>,
state: FacetedState,
): FacetedStore {
const listeners = new Set<() => void>();
function commitValue(nextValue: string | string[] | undefined) {
const props = propsRef.current;
if (!props.isControlled && !Object.is(state.value, nextValue)) {
state.value = nextValue;
store.notify();
}
props.onValueChange?.(nextValue);
}
const store: FacetedStore = {
subscribe: (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
},
getState: () => state,
getProps: () => propsRef.current,
notify: () => {
for (const callback of listeners) {
callback();
}
},
setOpen: (open) => {
const { open: openProp, onOpenChange } = propsRef.current;
if (openProp === undefined && state.open !== open) {
state.open = open;
store.notify();
}
onOpenChange?.(open);
},
selectItem: (selectedValue) => {
const props = propsRef.current;
const currentValue = getSelection(state, props);
if (props.multiple) {
const selected = Array.isArray(currentValue) ? currentValue : [];
commitValue(
selected.includes(selectedValue)
? selected.filter((item) => item !== selectedValue)
: [...selected, selectedValue],
);
return;
}
commitValue(currentValue === selectedValue ? undefined : selectedValue);
store.setOpen(false);
},
clear: () => {
commitValue(propsRef.current.multiple ? [] : undefined);
},
};
return store;
}
function useFacetedStoreSelector<T>(
store: FacetedStore,
selector: (state: FacetedState) => T,
): T {
const getSnapshot = () => selector(store.getState());
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
}
const FacetedStoreContext = React.createContext<FacetedStore | null>(null);
function useFacetedStore(name: string) {
const store = React.useContext(FacetedStoreContext);
if (!store) {
throw new Error(`\`${name}\` must be used within \`Faceted\`.`);
}
return store;
}
interface FacetedProps<Multiple extends boolean = false> extends Omit<
React.ComponentProps<typeof Popover>,
"onOpenChange"
> {
value?: FacetedSelectedValue<Multiple>;
defaultValue?: FacetedSelectedValue<Multiple>;
onValueChange?: (value: FacetedSelectedValue<Multiple> | undefined) => void;
onOpenChange?: (open: boolean) => void;
items?: FacetedOption[];
multiple?: Multiple;
}
function Faceted<Multiple extends boolean = false>(
props: FacetedProps<Multiple>,
) {
const {
open: openProp,
defaultOpen = false,
onOpenChange,
value,
defaultValue,
onValueChange,
items = NO_ITEMS,
multiple = false as Multiple,
...popoverProps
} = props;
const controller: FacetedController = {
open: openProp,
onOpenChange,
value,
onValueChange(nextValue) {
if (!getIsFacetedSelectedValue(nextValue, multiple)) return;
onValueChange?.(nextValue);
},
isControlled: "value" in props,
multiple,
items,
};
const propsRef = React.useRef(controller);
propsRef.current = controller;
const storeRef = React.useRef<FacetedStore | null>(null);
storeRef.current ??= createFacetedStore(propsRef, {
value: defaultValue,
open: defaultOpen,
});
const store = storeRef.current;
const open = useFacetedStoreSelector(store, (state) => state.open);
return (
<FacetedStoreContext.Provider value={store}>
<Popover
data-slot="faceted"
open={openProp ?? open}
onOpenChange={store.setOpen}
{...popoverProps}
/>
</FacetedStoreContext.Provider>
);
}
function FacetedTrigger({
className,
...props
}: React.ComponentProps<typeof PopoverTrigger>) {
return (
<PopoverTrigger
data-slot="faceted-trigger"
className={cn("justify-between text-left", className)}
{...props}
/>
);
}
interface FacetedValueProps {
placeholder?: React.ReactNode;
children?: React.ReactNode | ((selected: FacetedOption[]) => React.ReactNode);
}
function FacetedValue({
placeholder = "Select options...",
children,
}: FacetedValueProps) {
const store = useFacetedStore("FacetedValue");
const value = useFacetedStoreSelector(store, (state) =>
getSelection(state, store.getProps()),
);
const selected = getSelectedItems(value, store.getProps().items);
if (typeof children === "function") return children(selected);
if (children != null) return children;
if (selected.length === 0) {
return (
<span
data-slot="faceted-value"
className="flex w-full items-center gap-1 text-muted-foreground"
>
{placeholder}
<IconPlaceholder
lucide="ChevronDown"
tabler="IconChevronDown"
hugeicons="ArrowDown01Icon"
phosphor="CaretDownIcon"
remixicon="RiArrowDownSLine"
className="ml-auto size-4 shrink-0 text-muted-foreground"
/>
</span>
);
}
if (selected.length > 2) {
return (
<FacetedBadgeList>
<FacetedBadge>{selected.length} selected</FacetedBadge>
</FacetedBadgeList>
);
}
return (
<FacetedBadgeList>
{selected.map((item) => (
<FacetedBadge key={item.value}>
<span className="truncate">{item.label}</span>
</FacetedBadge>
))}
</FacetedBadgeList>
);
}
function FacetedBadgeList({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="faceted-badge-list"
className={cn(
"flex w-max max-w-full flex-wrap items-center gap-1",
className,
)}
{...props}
/>
);
}
function FacetedBadge({
className,
render,
...props
}: useRender.ComponentProps<"span">) {
return useRender({
defaultTagName: "span",
render,
state: {
slot: "faceted-badge",
},
props: mergeProps<"span">(
{
className: cn(
"flex h-[calc(--spacing(5.25))] w-fit max-w-full min-w-0 items-center justify-center gap-1 rounded-sm bg-input/60 px-1.5 text-xs font-medium whitespace-nowrap text-foreground [&_svg]:pointer-events-none [&_svg]:size-3 [&_svg]:shrink-0",
className,
),
},
props,
),
});
}
function FacetedContent({
className,
children,
...props
}: React.ComponentProps<typeof PopoverContent>) {
return (
<PopoverContent
data-slot="faceted-content"
align="start"
className={cn("w-50 origin-(--transform-origin) p-0", className)}
{...props}
>
<Command className="p-0.5">{children}</Command>
</PopoverContent>
);
}
function FacetedInput({
ref,
...props
}: React.ComponentProps<typeof CommandInput>) {
return <CommandInput data-slot="faceted-input" ref={ref} {...props} />;
}
function FacetedList(props: React.ComponentProps<typeof CommandList>) {
return <CommandList data-slot="faceted-list" {...props} />;
}
function FacetedEmpty(props: React.ComponentProps<typeof CommandEmpty>) {
return <CommandEmpty data-slot="faceted-empty" {...props} />;
}
function FacetedGroup(props: React.ComponentProps<typeof CommandGroup>) {
return <CommandGroup data-slot="faceted-group" {...props} />;
}
function FacetedCommandItem({
className,
onPointerDownCapture,
onMouseDown,
onSelect,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
className={cn(
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
className,
)}
{...mergeProps<"div">(
{
onPointerDownCapture(event) {
// A focusable item steals the input on pointerdown.
event.preventDefault();
},
onMouseDown(event) {
// iOS Safari can emit a synthetic mousedown without a pointerdown.
event.preventDefault();
},
},
{ onPointerDownCapture, onMouseDown },
)}
onSelect={onSelect}
{...props}
/>
);
}
interface FacetedItemProps extends React.ComponentProps<
typeof CommandPrimitive.Item
> {
value: string;
}
function FacetedItem({ value, onSelect, ...props }: FacetedItemProps) {
const store = useFacetedStore("FacetedItem");
const isSelected = useFacetedStoreSelector(store, (state) => {
const props = store.getProps();
return getIsValueSelected(
getSelection(state, props),
props.multiple,
value,
);
});
return (
<FacetedCommandItem
data-slot="faceted-item"
data-checked={isSelected || undefined}
aria-checked={isSelected}
onSelect={() => {
if (onSelect) {
onSelect(value);
return;
}
store.selectItem(value);
}}
{...props}
/>
);
}
function FacetedItemIndicator({
className,
children,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="faceted-item-indicator"
className={cn(
"ml-auto grid place-items-center *:col-start-1 *:row-start-1",
className,
)}
{...props}
>
{children != null && (
<span className="font-mono text-xs group-data-checked/command-item:invisible">
{children}
</span>
)}
<IconPlaceholder
lucide="Check"
tabler="IconCheck"
hugeicons="Tick02Icon"
phosphor="CheckIcon"
remixicon="RiCheckLine"
className="invisible group-data-checked/command-item:visible"
/>
</span>
);
}
function FacetedClear({
className,
children = "Clear",
onSelect,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
const store = useFacetedStore("FacetedClear");
const hasSelection = useFacetedStoreSelector(store, (state) =>
getHasSelection(getSelection(state, store.getProps())),
);
if (!hasSelection) return null;
return (
<FacetedCommandItem
data-slot="faceted-clear"
className={cn("justify-center text-center", className)}
onSelect={(value) => {
onSelect?.(value);
store.clear();
}}
{...props}
>
{children}
</FacetedCommandItem>
);
}
function FacetedSeparator(
props: React.ComponentProps<typeof CommandSeparator>,
) {
return (
<CommandSeparator
data-slot="faceted-separator"
className="mx-0 my-0.5 w-full"
{...props}
/>
);
}
export {
Faceted,
FacetedBadge,
FacetedBadgeList,
FacetedClear,
FacetedContent,
FacetedEmpty,
FacetedGroup,
FacetedInput,
FacetedItem,
FacetedItemIndicator,
FacetedList,
FacetedSeparator,
FacetedTrigger,
FacetedValue,
};
src/lib/data-grid-features.ts
import {
assignPrototypeAPIs,
assignTableAPIs,
type CellData,
columnFilteringFeature,
columnOrderingFeature,
columnPinningFeature,
columnResizingFeature,
columnSizingFeature,
columnVisibilityFeature,
createFilteredRowModel,
createSortedRowModel,
functionalUpdate,
makeStateUpdater,
metaHelper,
type OnChangeFn,
type RowData,
rowSelectionFeature,
rowSortingFeature,
type Table,
type TableFeature,
type TableFeatures,
tableFeatures,
type Updater,
} from "@tanstack/react-table";
import type {
CellPosition,
DataGridColumnMeta,
DataGridTableMeta,
RowHeightValue,
} from "@/lib/data-grid-types";
import { getLineCount, getRowHeightValue } from "@/lib/data-grid-utils";
const DEFAULT_ROW_HEIGHT: RowHeightValue = "short";
interface TableState_DataGridRowHeight {
rowHeight: RowHeightValue;
}
interface TableOptions_DataGridRowHeight {
onRowHeightChange?: OnChangeFn<RowHeightValue>;
}
interface Table_DataGridRowHeight {
getRowHeight: () => RowHeightValue;
getRowSize: () => number;
getRowLineCount: () => number;
setRowHeight: (updater: Updater<RowHeightValue>) => void;
resetRowHeight: (defaultState?: boolean) => void;
}
interface TableState_DataGridCellEditing {
editingCell: CellPosition | null;
}
interface TableOptions_DataGridCellEditing {
enableCellEditing?: boolean;
onEditingCellChange?: OnChangeFn<CellPosition | null>;
}
interface ColumnDef_DataGridCellEditing {
enableCellEditing?: boolean;
}
interface Table_DataGridCellEditing {
getEditingCell: () => CellPosition | null;
setEditingCell: (updater: Updater<CellPosition | null>) => void;
resetEditingCell: (defaultState?: boolean) => void;
}
interface Column_DataGridCellEditing {
getCanEdit: () => boolean;
}
interface Cell_DataGridCellEditing {
getCanEdit: () => boolean;
getIsEditing: () => boolean;
}
declare module "@tanstack/react-table" {
interface Plugins {
dataGridRowHeightFeature: TableFeature;
dataGridCellEditingFeature: TableFeature;
}
interface TableState_FeatureMap {
dataGridRowHeightFeature: TableState_DataGridRowHeight;
dataGridCellEditingFeature: TableState_DataGridCellEditing;
}
interface TableOptions_FeatureMap<
in out TFeatures extends TableFeatures,
in out TData extends RowData,
> {
dataGridRowHeightFeature: TableOptions_DataGridRowHeight;
dataGridCellEditingFeature: TableOptions_DataGridCellEditing;
}
interface ColumnDef_FeatureMap<
in out TFeatures extends TableFeatures,
in out TData extends RowData,
TValue extends CellData,
> {
dataGridCellEditingFeature: ColumnDef_DataGridCellEditing;
}
interface Table_FeatureMap<
in out TFeatures extends TableFeatures,
in out TData extends RowData,
> {
dataGridRowHeightFeature: Table_DataGridRowHeight;
dataGridCellEditingFeature: Table_DataGridCellEditing;
}
interface Column_FeatureMap<
in out TFeatures extends TableFeatures,
in out TData extends RowData,
> {
dataGridCellEditingFeature: Column_DataGridCellEditing;
}
interface Cell_FeatureMap {
dataGridCellEditingFeature: Cell_DataGridCellEditing;
}
}
type DataGridInstance = Table<DataGridFeatures, RowData>;
function asDataGrid(table: object) {
return table as DataGridInstance;
}
const dataGridRowHeightFeature: TableFeature = {
getInitialState: (initialState) => ({
rowHeight: DEFAULT_ROW_HEIGHT,
...initialState,
}),
getDefaultTableOptions: (table) => {
const options: TableOptions_DataGridRowHeight = {
onRowHeightChange: makeStateUpdater("rowHeight", table),
};
return options;
},
constructTableAPIs: (table) => {
const instance = asDataGrid(table);
const setRowHeight = (updater: Updater<RowHeightValue>) =>
instance.options.onRowHeightChange?.((old) =>
functionalUpdate(updater, old),
);
assignTableAPIs("dataGridRowHeightFeature", table, {
table_getRowHeight: {
fn: () => instance.atoms.rowHeight.get(),
},
table_getRowSize: {
fn: () => getRowHeightValue(instance.atoms.rowHeight.get()),
},
table_getRowLineCount: {
fn: () => getLineCount(instance.atoms.rowHeight.get()),
},
table_setRowHeight: { fn: setRowHeight },
table_resetRowHeight: {
fn: (defaultState?: boolean) =>
setRowHeight(
defaultState
? DEFAULT_ROW_HEIGHT
: (instance.initialState.rowHeight ?? DEFAULT_ROW_HEIGHT),
),
},
});
},
};
function getCanEditColumn(column: {
table: object;
columnDef: ColumnDef_DataGridCellEditing;
}) {
return (
asDataGrid(column.table).options.enableCellEditing !== false &&
column.columnDef.enableCellEditing !== false
);
}
const dataGridCellEditingFeature: TableFeature = {
getInitialState: (initialState) => ({
editingCell: null,
...initialState,
}),
getDefaultTableOptions: (table) => {
const options: TableOptions_DataGridCellEditing = {
onEditingCellChange: makeStateUpdater("editingCell", table),
};
return options;
},
constructTableAPIs: (table) => {
const instance = asDataGrid(table);
const setEditingCell = (updater: Updater<CellPosition | null>) =>
instance.options.onEditingCellChange?.((old) =>
functionalUpdate(updater, old),
);
assignTableAPIs("dataGridCellEditingFeature", table, {
table_getEditingCell: {
fn: () => instance.atoms.editingCell.get(),
},
table_setEditingCell: { fn: setEditingCell },
table_resetEditingCell: {
fn: (defaultState?: boolean) =>
setEditingCell(
defaultState ? null : (instance.initialState.editingCell ?? null),
),
},
});
},
assignColumnPrototype: (prototype, table) => {
assignPrototypeAPIs("dataGridCellEditingFeature", prototype, table, {
column_getCanEdit: { fn: getCanEditColumn },
});
},
assignCellPrototype: (prototype, table) => {
assignPrototypeAPIs("dataGridCellEditingFeature", prototype, table, {
cell_getCanEdit: {
fn: (cell: { column: Parameters<typeof getCanEditColumn>[0] }) =>
getCanEditColumn(cell.column),
},
cell_getIsEditing: {
fn: (cell: {
table: object;
row: { id: string };
column: { id: string };
}) => {
const editingCell = asDataGrid(cell.table).atoms.editingCell.get();
return (
editingCell?.rowId === cell.row.id &&
editingCell.columnId === cell.column.id
);
},
},
});
},
};
export const dataGridFeatures = tableFeatures({
columnFilteringFeature,
columnOrderingFeature,
columnPinningFeature,
columnResizingFeature,
columnSizingFeature,
columnVisibilityFeature,
rowSelectionFeature,
rowSortingFeature,
dataGridRowHeightFeature,
dataGridCellEditingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
tableMeta: metaHelper<DataGridTableMeta>(),
columnMeta: metaHelper<DataGridColumnMeta>(),
});
export type DataGridFeatures = typeof dataGridFeatures;
src/lib/data-grid-types.ts
import type { Cell, RowData } from "@tanstack/react-table";
import type { DataGridFeatures } from "@/lib/data-grid-features";
export type Direction = "ltr" | "rtl";
export type RowHeightValue = "short" | "medium" | "tall" | "extra-tall";
export interface CellSelectOption {
label: string;
value: string;
icon?: React.ComponentType<React.ComponentProps<"svg">>;
count?: number;
}
export type CellOpts =
| {
variant: "short-text";
}
| {
variant: "long-text";
}
| {
variant: "number";
min?: number;
max?: number;
step?: number;
}
| {
variant: "select";
options: CellSelectOption[];
}
| {
variant: "multi-select";
options: CellSelectOption[];
}
| {
variant: "checkbox";
}
| {
variant: "date";
}
| {
variant: "url";
}
| {
variant: "file";
maxFileSize?: number;
maxFiles?: number;
accept?: string;
multiple?: boolean;
};
export interface CellUpdate {
rowId: string;
columnId: string;
value: unknown;
}
export interface DataGridColumnMeta {
label?: string;
cell?: CellOpts;
}
export interface DataGridTableMeta {
dataGridRef?: React.RefObject<HTMLElement | null>;
cellMapRef?: React.RefObject<Map<string, HTMLDivElement>>;
focusedCell?: CellPosition | null;
editingCell?: CellPosition | null;
selectionState?: SelectionState;
searchOpen?: boolean;
getIsCellSelected?: (rowId: string, columnId: string) => boolean;
getIsSearchMatch?: (rowId: string, columnId: string) => boolean;
getIsActiveSearchMatch?: (rowId: string, columnId: string) => boolean;
scrollToCell?: (
rowId: string,
columnId: string,
align?: "auto" | "start" | "center" | "end",
) => void;
onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;
onDataUpdate?: (params: CellUpdate | Array<CellUpdate>) => void;
onRowsDelete?: (rowIds: string[]) => void | Promise<void>;
onColumnClick?: (columnId: string) => void;
onCellClick?: (
rowId: string,
columnId: string,
event?: React.MouseEvent,
) => void;
onCellDoubleClick?: (rowId: string, columnId: string) => void;
onCellMouseDown?: (
rowId: string,
columnId: string,
event: React.MouseEvent,
) => void;
onCellMouseEnter?: (rowId: string, columnId: string) => void;
onCellMouseUp?: () => void;
onCellContextMenu?: (
rowId: string,
columnId: string,
event: React.MouseEvent,
) => void;
onCellEditingStart?: (rowId: string, columnId: string) => void;
onCellEditingStop?: (opts?: {
direction?: NavigationDirection;
moveToNextRow?: boolean;
}) => void;
onCellsCopy?: () => void;
onCellsCut?: () => void;
onCellsPaste?: (expand?: boolean) => void;
onSelectionClear?: () => void;
onFilesUpload?: (params: {
files: File[];
rowId: string;
columnId: string;
}) => Promise<FileCellData[]>;
onFilesDelete?: (params: {
fileIds: string[];
rowId: string;
columnId: string;
}) => void | Promise<void>;
contextMenu?: ContextMenuState;
onContextMenuOpenChange?: (open: boolean) => void;
pasteDialog?: PasteDialogState;
onPasteDialogOpenChange?: (open: boolean) => void;
readOnly?: boolean;
}
export interface CellPosition {
rowId: string;
columnId: string;
}
export interface CellRange {
start: CellPosition;
end: CellPosition;
}
export interface SelectionState {
selectedCells: Set<string>;
selectionRange: CellRange | null;
isSelecting: boolean;
}
export interface ContextMenuState {
open: boolean;
x: number;
y: number;
}
export interface PasteDialogState {
open: boolean;
rowsNeeded: number;
clipboardText: string;
}
export type NavigationDirection =
| "up"
| "down"
| "left"
| "right"
| "home"
| "end"
| "ctrl+up"
| "ctrl+down"
| "ctrl+home"
| "ctrl+end"
| "pageup"
| "pagedown"
| "pageleft"
| "pageright"
| "tab"
| "shift+tab";
export interface SearchState {
searchMatches: CellPosition[];
matchIndex: number;
searchOpen: boolean;
onSearchOpenChange: (open: boolean) => void;
searchQuery: string;
onSearchQueryChange: (query: string) => void;
onSearch: (query: string) => void;
onNavigateToNextMatch: () => void;
onNavigateToPrevMatch: () => void;
}
export interface DataGridCellProps<TData extends RowData> {
cell: Cell<DataGridFeatures, TData>;
tableMeta: DataGridTableMeta;
rowId: string;
columnId: string;
rowHeight: RowHeightValue;
isEditing: boolean;
isFocused: boolean;
isSelected: boolean;
isSearchMatch: boolean;
isActiveSearchMatch: boolean;
readOnly: boolean;
}
export interface FileCellData {
id: string;
name: string;
size: number;
type: string;
url?: string;
}
export type TextFilterOperator =
| "contains"
| "notContains"
| "equals"
| "notEquals"
| "startsWith"
| "endsWith"
| "isEmpty"
| "isNotEmpty";
export type NumberFilterOperator =
| "equals"
| "notEquals"
| "lessThan"
| "lessThanOrEqual"
| "greaterThan"
| "greaterThanOrEqual"
| "isBetween"
| "isEmpty"
| "isNotEmpty";
export type DateFilterOperator =
| "equals"
| "notEquals"
| "before"
| "after"
| "onOrBefore"
| "onOrAfter"
| "isBetween"
| "isEmpty"
| "isNotEmpty";
export type SelectFilterOperator =
| "is"
| "isNot"
| "isAnyOf"
| "isNoneOf"
| "isEmpty"
| "isNotEmpty";
export type BooleanFilterOperator = "isTrue" | "isFalse";
export type FilterOperator =
| TextFilterOperator
| NumberFilterOperator
| DateFilterOperator
| SelectFilterOperator
| BooleanFilterOperator;
export interface FilterValue {
operator: FilterOperator;
value?: string | number | string[];
endValue?: string | number;
}