Skip to content

Events

All inter-subsystem communication in @witqq/spreadsheet goes through a typed EventBus. It implements a publish/subscribe pattern with type-safe event names and payloads.

Live Demo
Interact with the table — click cells, edit values, sort columns, scroll. Events appear in the log below.
Event Log (0)
No events yet. Click a cell, edit a value, or scroll the table.
View source code
EventBusDemo.tsx
import { useRef, useEffect, useState, useCallback } from 'react';
import { Spreadsheet } from '@witqq/spreadsheet-react';
import type { SpreadsheetRef } from '@witqq/spreadsheet-react';
import type {
CellEvent,
CellChangeEvent,
SelectionChangeEvent,
ScrollEvent,
SortChangeEvent,
} from '@witqq/spreadsheet';
import { DemoWrapper } from './DemoWrapper';
import { DemoButton } from './DemoButton';
import { generateEmployees, employeeColumns } from './generate-data';
import { useSiteTheme } from './useSiteTheme';
const data = generateEmployees(30);
const sortableColumns = employeeColumns.map((col) => ({ ...col, sortable: true }));
const EVENT_COLORS: Record<string, string> = {
cellClick: '#2563eb',
cellChange: '#16a34a',
selectionChange: '#9333ea',
scroll: '#64748b',
sortChange: '#ea580c',
};
interface EventEntry {
time: string;
name: string;
detail: string;
}
export function EventBusDemo() {
const { witTheme } = useSiteTheme();
const tableRef = useRef<SpreadsheetRef>(null);
const logRef = useRef<HTMLDivElement>(null);
const [events, setEvents] = useState<EventEntry[]>([]);
const clearLog = useCallback(() => setEvents([]), []);
useEffect(() => {
const engine = tableRef.current?.getInstance();
if (!engine) return;
const bus = engine.getEventBus();
const logEvent = (name: string, detail: string) => {
const time = new Date().toLocaleTimeString('en', { hour12: false });
setEvents((prev) => [...prev.slice(-49), { time, name, detail }]);
};
const unsubs = [
bus.on('cellClick', (e: CellEvent) => logEvent('cellClick', `row:${e.row} col:${e.col}`)),
bus.on('cellChange', (e: CellChangeEvent) =>
logEvent('cellChange', `[${e.row},${e.col}] "${e.oldValue}" → "${e.newValue}"`),
),
bus.on('selectionChange', (e: SelectionChangeEvent) =>
logEvent('selectionChange', `row:${e.selection.activeRow} col:${e.selection.activeCol}`),
),
bus.on('scroll', (e: ScrollEvent) =>
logEvent('scroll', `top:${Math.round(e.scrollTop)} left:${Math.round(e.scrollLeft)}`),
),
bus.on('sortChange', (e: SortChangeEvent) =>
logEvent('sortChange', `${e.sortColumns.length} column(s)`),
),
];
return () => unsubs.forEach((fn) => fn());
}, []);
useEffect(() => {
if (logRef.current) {
logRef.current.scrollTop = logRef.current.scrollHeight;
}
}, [events]);
return (
<DemoWrapper
height={500}
title="Live Demo"
description="Interact with the table — click cells, edit values, sort columns, scroll. Events appear in the log below."
>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: 1, minHeight: 0 }}>
<Spreadsheet
theme={witTheme}
ref={tableRef}
columns={sortableColumns}
data={data}
showRowNumbers
editable
style={{ width: '100%', height: '100%' }}
/>
</div>
<div style={{ borderTop: '1px solid var(--sl-color-gray-5)' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '4px 8px',
background: 'var(--sl-color-gray-6)',
fontSize: 11,
}}
>
<span style={{ fontWeight: 600, color: 'var(--sl-color-white)' }}>
Event Log ({events.length})
</span>
<DemoButton onClick={clearLog} style={{ padding: '2px 10px', fontSize: '0.72rem' }}>
Clear
</DemoButton>
</div>
<div
ref={logRef}
style={{
height: 120,
overflowY: 'auto',
background: 'var(--sl-color-gray-7, var(--sl-color-gray-6))',
padding: 8,
fontFamily: 'monospace',
fontSize: 11,
}}
>
{events.length === 0 && (
<div style={{ color: 'var(--sl-color-gray-3)', fontStyle: 'italic' }}>
No events yet. Click a cell, edit a value, or scroll the table.
</div>
)}
{events.map((evt, i) => (
<div
key={i}
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
lineHeight: '18px',
}}
>
<span style={{ color: 'var(--sl-color-gray-3)' }}>[{evt.time}]</span>{' '}
<span
style={{
color: EVENT_COLORS[evt.name] || 'var(--sl-color-white)',
fontWeight: 600,
}}
>
{evt.name}
</span>
{': '}
<span style={{ color: 'var(--sl-color-gray-2)' }}>{evt.detail}</span>
</div>
))}
</div>
</div>
</div>
</DemoWrapper>
);
}
const engine = tableRef.current?.getInstance();
// Subscribe to an event
engine.on('cellChange', (event) => {
console.log(`Cell [${event.row}, ${event.col}] changed`);
console.log(`Old: ${event.oldValue}, New: ${event.newValue}`);
});
// Unsubscribe when done
engine.off('cellChange', handler);
EventPayloadDescription
cellClick{ row, col, value, column, hitZone? }Single click on a cell
cellDoubleClick{ row, col, value, column, hitZone? }Double-click on a cell (opens editor)
cellHover{ row, col, value, column, hitZone? }Mouse moves over a cell (no buttons pressed)
cellChange{ row, col, value, column, oldValue, newValue, source }Cell value changed after edit commit
cellStatusChange{ row, col, oldStatus, newStatus, errorMessage? }Cell status lifecycle: changed → saving → saved, error
cellValidation{ row, col, result }Validation result after cell edit
EventPayloadDescription
selectionChange{ selection, previousSelection }Selection changed (click, keyboard, or programmatic)
EventPayloadDescription
scroll{ scrollTop, scrollLeft }Scroll position changed
EventPayloadDescription
ready(none)Engine initialization complete
destroy(none)Engine destroyed and resources cleaned up
EventPayloadDescription
commandExecute{ description }A command was executed
commandUndo{ description }A command was undone
commandRedo{ description }A command was redone
EventPayloadDescription
clipboardCopy{ rowCount, colCount }Cells copied to clipboard
clipboardCut{ rowCount, colCount }Cells cut to clipboard
clipboardPaste{ rowCount, colCount }Data pasted from clipboard
EventPayloadDescription
columnResizeStart{ colIndex }Column resize drag started
columnResize{ colIndex, oldWidth, newWidth }Column width changing during drag
columnResizeEnd{ colIndex, oldWidth, newWidth }Column resize drag completed
rowResizeStart{ rowIndex }Row resize drag started
rowResize{ rowIndex, oldHeight, newHeight }Row height changing during drag
rowResizeEnd{ rowIndex, oldHeight, newHeight }Row resize drag completed
EventPayloadDescription
autofillStart{ sourceRange }Fill handle drag started
autofillPreview{ sourceRange, fillRange, direction }Preview values during drag
autofillComplete{ sourceRange, fillRange, direction }Fill operation completed
EventPayloadDescription
sortChange{ sortColumns }Sort configuration changed
sortRejected{ reason }Sort request rejected
filterChange{ visibleRowCount, totalRowCount }Filter configuration changed
EventPayloadDescription
rowGroupToggle{ headerRow, expanded }Row group expanded/collapsed
rowGroupChange{ groupHeaders }Row grouping configuration changed
EventPayloadDescription
themeChange{ theme }Theme changed via setTheme()

TypeScript interfaces for all event payloads. Import from @witqq/spreadsheet.

Fired on cellClick, cellDoubleClick, and cellHover. When a cell type renderer declares hit zones via getHitZones(), the hitZone field contains the matched zone ID.

interface CellEvent {
row: number;
col: number;
value: CellValue;
column: ColumnDef;
hitZone?: string;
}

Fired on cellChange. Extends CellEvent with old/new value tracking.

interface CellChangeEvent extends CellEvent {
oldValue: CellValue;
newValue: CellValue;
source: string;
}

Fired on commandExecute, commandUndo, and commandRedo.

interface CommandEvent {
description: string;
}

Fired on selectionChange.

interface SelectionChangeEvent {
selection: Selection;
previousSelection: Selection;
}

Fired on clipboardCopy, clipboardCut, and clipboardPaste.

interface ClipboardDataEvent {
rowCount: number;
colCount: number;
}

Fired on columnResize and columnResizeEnd.

interface ColumnResizeEvent {
colIndex: number;
oldWidth: number;
newWidth: number;
}

Fired on rowResize and rowResizeEnd.

interface RowResizeEvent {
rowIndex: number;
oldHeight: number;
newHeight: number;
}

Fired on cellStatusChange when a cell’s tracking status transitions (e.g. changed → saving → saved).

interface CellStatusChangeEvent {
row: number;
col: number;
oldStatus: CellMetadata['status'] | undefined;
newStatus: CellMetadata['status'] | undefined;
errorMessage?: string;
}

Fired on cellValidation after a cell edit is validated against column/cell rules.

interface CellValidationEvent {
row: number;
col: number;
result: ValidationResult;
}

Fired on internal gridMouseDown, gridMouseMove, gridMouseUp, gridMouseHover, and gridContextMenu.

interface GridMouseEvent extends HitTestResult {
readonly originalEvent: MouseEvent;
readonly shiftKey: boolean;
readonly ctrlKey: boolean;
}

Fired on internal gridKeyDown.

interface GridKeyboardEvent {
readonly originalEvent: KeyboardEvent;
readonly key: string;
readonly shiftKey: boolean;
readonly ctrlKey: boolean;
}

Fired on rowGroupToggle when a row group is expanded or collapsed.

interface RowGroupToggleEvent {
readonly headerRow: number;
readonly expanded: boolean;
}

Fired on rowGroupChange when the set of group headers changes.

interface RowGroupChangeEvent {
readonly groupHeaders: readonly number[];
}

Fired on scroll when the viewport scroll position changes.

interface ScrollEvent {
scrollTop: number;
scrollLeft: number;
}

Fired on sortRejected when a sort request is blocked (e.g. merged regions exist).

interface SortRejectedEvent {
readonly reason: 'merged-regions-exist';
}

Fired on filterChange after filters are applied or cleared.

interface FilterChangeEvent {
readonly visibleRowCount: number;
readonly totalRowCount: number;
}

Fired on autofillStart when the user begins dragging the fill handle.

interface AutofillStartEvent {
sourceRange: CellRange;
}

Fired on autofillPreview during fill handle drag.

interface AutofillPreviewEvent {
sourceRange: CellRange;
fillRange: CellRange | null;
direction: FillDirection | null;
}

Fired on autofillComplete when the fill operation is committed.

interface AutofillCompleteEvent {
sourceRange: CellRange;
fillRange: CellRange;
direction: FillDirection;
}

Fired on sortChange when sort configuration changes.

interface SortChangeEvent {
readonly sortColumns: readonly { col: number; direction: 'asc' | 'desc' }[];
}

The EventTranslator converts raw DOM events (mouse clicks, touch, keyboard) into cell-level events. It performs hit-testing to determine which cell or region was interacted with.

Regions identified by EventTranslator:

RegionAreaActions
cellData cells in the grid bodyClick, edit, select
headerColumn header rowSort toggle, filter open, resize
row-numberRow number column on the leftRow selection
cornerTop-left corner (row numbers × header)Select all

On touch devices, the EventTranslator maps gestures:

GestureAction
TapSelect cell
Double-tapOpen inline editor
ScrollNative scroll (CSS touch-action: pan-x pan-y)

In the React wrapper, events are exposed as props:

<Spreadsheet<Row>
columns={columns}
data={data}
onCellChange={(event: CellChangeEvent) => {
// event.row, event.col, event.oldValue, event.newValue
}}
onSelectionChange={(event: SelectionChangeEvent) => {
// event.selection
}}
onSortChange={(event: SortChangeEvent) => {
// event.sortColumns
}}
onFilterChange={(event: FilterChangeEvent) => {
// event.visibleRowCount, event.totalRowCount
}}
onReady={() => {
// Table mounted and ready
}}
/>