Skip to content

Commit 7b8a205

Browse files
authored
Use mouse down event to select cell (#3774)
I checked a few popular grids and all select the cell on pointer down but they do not scroll the cell into view which we do. Not sure which behavior is better 🆕 onCellMouseDown event
1 parent 1cff0fc commit 7b8a205

17 files changed

Lines changed: 184 additions & 135 deletions

README.md

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,26 @@ function MyGrid() {
317317

318318
###### `onFill?: Maybe<(event: FillEvent<R>) => R>`
319319

320-
###### `onCellClick?: Maybe<(args: CellClickArgs<R, SR>, event: CellMouseEvent) => void>`
320+
###### `onCellMouseDown: Maybe<(args: CellMouseArgs<R, SR>, event: CellMouseEvent) => void>`
321321

322-
Callback triggered when a cell is clicked. The default behavior is to select the cell. Call `preventGridDefault` to prevent the default behavior
322+
Callback triggered when a pointer becomes active in a cell. The default behavior is to select the cell. Call `preventGridDefault` to prevent the default behavior.
323323

324324
```tsx
325-
function onCellClick(args: CellClickArgs<R, SR>, event: CellMouseEvent) {
325+
function onCellMouseDown(args: CellMouseDownArgs<R, SR>, event: CellMouseEvent) {
326+
if (args.column.key === 'id') {
327+
event.preventGridDefault();
328+
}
329+
}
330+
331+
<DataGrid rows={rows} columns={columns} onCellMouseDown={onCellMouseDown} />;
332+
```
333+
334+
###### `onCellClick?: Maybe<(args: CellMouseArgs<R, SR>, event: CellMouseEvent) => void>`
335+
336+
Callback triggered when a cell is clicked.
337+
338+
```tsx
339+
function onCellClick(args: CellMouseArgs<R, SR>, event: CellMouseEvent) {
326340
if (args.column.key === 'id') {
327341
event.preventGridDefault();
328342
}
@@ -334,17 +348,16 @@ function onCellClick(args: CellClickArgs<R, SR>, event: CellMouseEvent) {
334348
This event can be used to open cell editor on single click
335349

336350
```tsx
337-
function onCellClick(args: CellClickArgs<R, SR>, event: CellMouseEvent) {
351+
function onCellClick(args: CellMouseArgs<R, SR>, event: CellMouseEvent) {
338352
if (args.column.key === 'id') {
339-
event.preventGridDefault();
340353
args.selectCell(true);
341354
}
342355
}
343356
```
344357

345358
Arguments:
346359

347-
`args: CellClickArgs<R, SR>`
360+
`args: CellMouseArgs<R, SR>`
348361

349362
- `args.rowIdx`: `number` - row index of the currently selected cell
350363
- `args.row`: `R` - row object of the currently selected cell
@@ -356,12 +369,12 @@ Arguments:
356369
- `event.preventGridDefault:`: `() => void`
357370
- `event.isGridDefaultPrevented`: `boolean`
358371

359-
###### `onCellDoubleClick?: Maybe<(args: CellClickArgs<R, SR>, event: CellMouseEvent) => void>`
372+
###### `onCellDoubleClick?: Maybe<(args: CellMouseArgs<R, SR>, event: CellMouseEvent) => void>`
360373

361-
Callback triggered when a cell is double-clicked. The default behavior is to open the editor if the cell is editable. Call `preventGridDefault` to prevent the default behavior
374+
Callback triggered when a cell is double-clicked. The default behavior is to open the editor if the cell is editable. Call `preventGridDefault` to prevent the default behavior.
362375

363376
```tsx
364-
function onCellDoubleClick(args: CellClickArgs<R, SR>, event: CellMouseEvent) {
377+
function onCellDoubleClick(args: CellMouseArgs<R, SR>, event: CellMouseEvent) {
365378
if (args.column.key === 'id') {
366379
event.preventGridDefault();
367380
}
@@ -370,14 +383,15 @@ function onCellDoubleClick(args: CellClickArgs<R, SR>, event: CellMouseEvent) {
370383
<DataGrid rows={rows} columns={columns} onCellDoubleClick={onCellDoubleClick} />;
371384
```
372385

373-
###### `onCellContextMenu?: Maybe<(args: CellClickArgs<R, SR>, event: CellMouseEvent) => void>`
386+
###### `onCellContextMenu?: Maybe<(args: CellMouseArgs<R, SR>, event: CellMouseEvent) => void>`
374387

375-
Callback triggered when a cell is right-clicked. The default behavior is to select the cell. Call `preventGridDefault` to prevent the default behavior
388+
Callback triggered when a cell is right-clicked.
376389

377390
```tsx
378-
function onCellContextMenu(args: CellClickArgs<R, SR>, event: CellMouseEvent) {
391+
function onCellContextMenu(args: CellMouseArgs<R, SR>, event: CellMouseEvent) {
379392
if (args.column.key === 'id') {
380-
event.preventGridDefault();
393+
event.preventDefault();
394+
// open custom context menu
381395
}
382396
}
383397

@@ -412,11 +426,11 @@ function onCellKeyDown(args: CellKeyDownArgs<R, SR>, event: CellKeyboardEvent) {
412426

413427
Check [more examples](website/routes/CellNavigation.tsx)
414428

415-
###### `onCellCopy?: Maybe<(args: CellCopyEvent<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => void>`
429+
###### `onCellCopy?: Maybe<(args: CellCopyArgs<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => void>`
416430

417431
Callback triggered when a cell's content is copied.
418432

419-
###### `onCellPaste?: Maybe<(args: CellPasteEvent<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => void>`
433+
###### `onCellPaste?: Maybe<(args: CellPasteArgs<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => void>`
420434

421435
Callback triggered when content is pasted into a cell.
422436

src/Cell.tsx

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { memo } from 'react';
1+
import { memo, type MouseEvent } from 'react';
22
import { css } from '@linaria/core';
33

44
import { useRovingTabIndex } from './hooks';
55
import { createCellEvent, getCellClassname, getCellStyle, isCellEditableUtil } from './utils';
6-
import type { CellRendererProps } from './types';
6+
import type { CellMouseEventHandler, CellRendererProps } from './types';
77

88
const cellDraggedOver = css`
99
@layer rdg.Cell {
@@ -21,9 +21,14 @@ function Cell<R, SR>({
2121
row,
2222
rowIdx,
2323
className,
24+
onMouseDown,
25+
onCellMouseDown,
2426
onClick,
27+
onCellClick,
2528
onDoubleClick,
29+
onCellDoubleClick,
2630
onContextMenu,
31+
onCellContextMenu,
2732
onRowChange,
2833
selectCell,
2934
style,
@@ -46,31 +51,43 @@ function Cell<R, SR>({
4651
selectCell({ rowIdx, idx: column.idx }, openEditor);
4752
}
4853

49-
function handleClick(event: React.MouseEvent<HTMLDivElement>) {
50-
if (onClick) {
54+
function handleMouseEvent(
55+
event: React.MouseEvent<HTMLDivElement>,
56+
eventHandler?: CellMouseEventHandler<R, SR>
57+
) {
58+
let eventHandled = false;
59+
if (eventHandler) {
5160
const cellEvent = createCellEvent(event);
52-
onClick({ rowIdx, row, column, selectCell: selectCellWrapper }, cellEvent);
53-
if (cellEvent.isGridDefaultPrevented()) return;
61+
eventHandler({ rowIdx, row, column, selectCell: selectCellWrapper }, cellEvent);
62+
eventHandled = cellEvent.isGridDefaultPrevented();
5463
}
55-
selectCellWrapper();
64+
return eventHandled;
5665
}
5766

58-
function handleContextMenu(event: React.MouseEvent<HTMLDivElement>) {
59-
if (onContextMenu) {
60-
const cellEvent = createCellEvent(event);
61-
onContextMenu({ rowIdx, row, column, selectCell: selectCellWrapper }, cellEvent);
62-
if (cellEvent.isGridDefaultPrevented()) return;
67+
function handleMouseDown(event: MouseEvent<HTMLDivElement>) {
68+
onMouseDown?.(event);
69+
if (!handleMouseEvent(event, onCellMouseDown)) {
70+
// select cell if the event is not prevented
71+
selectCellWrapper();
6372
}
64-
selectCellWrapper();
6573
}
6674

67-
function handleDoubleClick(event: React.MouseEvent<HTMLDivElement>) {
68-
if (onDoubleClick) {
69-
const cellEvent = createCellEvent(event);
70-
onDoubleClick({ rowIdx, row, column, selectCell: selectCellWrapper }, cellEvent);
71-
if (cellEvent.isGridDefaultPrevented()) return;
75+
function handleClick(event: MouseEvent<HTMLDivElement>) {
76+
onClick?.(event);
77+
handleMouseEvent(event, onCellClick);
78+
}
79+
80+
function handleDoubleClick(event: MouseEvent<HTMLDivElement>) {
81+
onDoubleClick?.(event);
82+
if (!handleMouseEvent(event, onCellDoubleClick)) {
83+
// go into edit mode if the event is not prevented
84+
selectCellWrapper(true);
7285
}
73-
selectCellWrapper(true);
86+
}
87+
88+
function handleContextMenu(event: MouseEvent<HTMLDivElement>) {
89+
onContextMenu?.(event);
90+
handleMouseEvent(event, onCellContextMenu);
7491
}
7592

7693
function handleRowChange(newRow: R) {
@@ -91,6 +108,7 @@ function Cell<R, SR>({
91108
...style
92109
}}
93110
onClick={handleClick}
111+
onMouseDown={handleMouseDown}
94112
onDoubleClick={handleDoubleClick}
95113
onContextMenu={handleContextMenu}
96114
onFocus={onFocus}

src/DataGrid.tsx

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,13 @@ import {
4040
} from './utils';
4141
import type {
4242
CalculatedColumn,
43-
CellClickArgs,
4443
CellClipboardEvent,
45-
CellCopyEvent,
44+
CellCopyArgs,
4645
CellKeyboardEvent,
4746
CellKeyDownArgs,
48-
CellMouseEvent,
47+
CellMouseEventHandler,
4948
CellNavigationMode,
50-
CellPasteEvent,
49+
CellPasteArgs,
5150
CellSelectArgs,
5251
Column,
5352
ColumnOrColumnGroup,
@@ -186,29 +185,25 @@ export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends Sha
186185
/**
187186
* Event props
188187
*/
188+
/** Callback triggered when a pointer becomes active in a cell */
189+
onCellMouseDown?: CellMouseEventHandler<R, SR>;
189190
/** Callback triggered when a cell is clicked */
190-
onCellClick?: Maybe<
191-
(args: CellClickArgs<NoInfer<R>, NoInfer<SR>>, event: CellMouseEvent) => void
192-
>;
191+
onCellClick?: CellMouseEventHandler<R, SR>;
193192
/** Callback triggered when a cell is double-clicked */
194-
onCellDoubleClick?: Maybe<
195-
(args: CellClickArgs<NoInfer<R>, NoInfer<SR>>, event: CellMouseEvent) => void
196-
>;
193+
onCellDoubleClick?: CellMouseEventHandler<R, SR>;
197194
/** Callback triggered when a cell is right-clicked */
198-
onCellContextMenu?: Maybe<
199-
(args: CellClickArgs<NoInfer<R>, NoInfer<SR>>, event: CellMouseEvent) => void
200-
>;
195+
onCellContextMenu?: CellMouseEventHandler<R, SR>;
201196
/** Callback triggered when a key is pressed in a cell */
202197
onCellKeyDown?: Maybe<
203198
(args: CellKeyDownArgs<NoInfer<R>, NoInfer<SR>>, event: CellKeyboardEvent) => void
204199
>;
205200
/** Callback triggered when a cell's content is copied */
206201
onCellCopy?: Maybe<
207-
(args: CellCopyEvent<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => void
202+
(args: CellCopyArgs<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => void
208203
>;
209204
/** Callback triggered when content is pasted into a cell */
210205
onCellPaste?: Maybe<
211-
(args: CellPasteEvent<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => NoInfer<R>
206+
(args: CellPasteArgs<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => NoInfer<R>
212207
>;
213208
/** Function called whenever cell selection is changed */
214209
onSelectedCellChange?: Maybe<(args: CellSelectArgs<NoInfer<R>, NoInfer<SR>>) => void>;
@@ -274,6 +269,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
274269
onSortColumnsChange,
275270
defaultColumnOptions,
276271
// Event props
272+
onCellMouseDown,
277273
onCellClick,
278274
onCellDoubleClick,
279275
onCellContextMenu,
@@ -493,6 +489,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
493489
const handleColumnResizeEndLatest = useLatestFunc(handleColumnResizeEnd);
494490
const onColumnsReorderLastest = useLatestFunc(onColumnsReorder);
495491
const onSortColumnsChangeLatest = useLatestFunc(onSortColumnsChange);
492+
const onCellMouseDownLatest = useLatestFunc(onCellMouseDown);
496493
const onCellClickLatest = useLatestFunc(onCellClick);
497494
const onCellDoubleClickLatest = useLatestFunc(onCellDoubleClick);
498495
const onCellContextMenuLatest = useLatestFunc(onCellContextMenu);
@@ -525,23 +522,17 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
525522
/**
526523
* effects
527524
*/
528-
useLayoutEffect(() => {
529-
if (
530-
focusSinkRef.current !== null &&
531-
selectedCellIsWithinSelectionBounds &&
532-
selectedPosition.idx === -1
533-
) {
534-
focusSinkRef.current.focus({ preventScroll: true });
535-
scrollIntoView(focusSinkRef.current);
536-
}
537-
}, [selectedCellIsWithinSelectionBounds, selectedPosition]);
538-
539525
useLayoutEffect(() => {
540526
if (shouldFocusCell) {
527+
if (focusSinkRef.current !== null && selectedPosition.idx === -1) {
528+
focusSinkRef.current.focus({ preventScroll: true });
529+
scrollIntoView(focusSinkRef.current);
530+
} else {
531+
focusCellOrCellContent();
532+
}
541533
setShouldFocusCell(false);
542-
focusCellOrCellContent();
543534
}
544-
}, [shouldFocusCell, focusCellOrCellContent]);
535+
}, [shouldFocusCell, focusCellOrCellContent, selectedPosition.idx]);
545536

546537
useImperativeHandle(ref, () => ({
547538
element: gridRef.current,
@@ -1141,6 +1132,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
11411132
viewportColumns: rowColumns,
11421133
isRowSelectionDisabled: isRowSelectionDisabled?.(row) ?? false,
11431134
isRowSelected,
1135+
onCellMouseDown: onCellMouseDownLatest,
11441136
onCellClick: onCellClickLatest,
11451137
onCellDoubleClick: onCellDoubleClickLatest,
11461138
onCellContextMenu: onCellContextMenuLatest,

src/GroupCell.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ function GroupCell<R, SR>({
5050
...getCellStyle(column),
5151
cursor: isLevelMatching ? 'pointer' : 'default'
5252
}}
53+
onMouseDown={(event) => {
54+
// prevents clicking on the cell from stealing focus from focusSink
55+
event.preventDefault();
56+
}}
5357
onClick={isLevelMatching ? toggleGroup : undefined}
5458
onFocus={onFocus}
5559
>

src/GroupRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function GroupedRow<R, SR>({
7272
selectedCellIdx === -1 && rowSelectedClassname,
7373
className
7474
)}
75-
onClick={handleSelectGroup}
75+
onMouseDown={handleSelectGroup}
7676
style={getRowStyle(gridRowStart)}
7777
{...props}
7878
>

src/GroupedColumnHeaderCell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function GroupedColumnHeaderCell<R, SR>({
2727
const rowSpan = getHeaderCellRowSpan(column, rowIdx);
2828
const index = column.idx + 1;
2929

30-
function onClick() {
30+
function onMouseDown() {
3131
selectCell({ idx: column.idx, rowIdx });
3232
}
3333

@@ -46,7 +46,7 @@ export default function GroupedColumnHeaderCell<R, SR>({
4646
gridColumnEnd: index + colSpan
4747
}}
4848
onFocus={onFocus}
49-
onClick={onClick}
49+
onMouseDown={onMouseDown}
5050
>
5151
{column.name}
5252
</div>

src/HeaderCell.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,11 @@ export default function HeaderCell<R, SR>({
145145
}
146146
}
147147

148-
function onClick(event: React.MouseEvent<HTMLSpanElement>) {
148+
function onMouseDown() {
149149
selectCell({ idx: column.idx, rowIdx });
150+
}
150151

152+
function onClick(event: React.MouseEvent<HTMLSpanElement>) {
151153
if (sortable) {
152154
onSort(event.ctrlKey || event.metaKey);
153155
}
@@ -250,6 +252,7 @@ export default function HeaderCell<R, SR>({
250252
...getHeaderCellStyle(column, rowIdx, rowSpan),
251253
...getCellStyle(column, colSpan)
252254
}}
255+
onMouseDown={onMouseDown}
253256
onFocus={onFocus}
254257
onClick={onClick}
255258
onKeyDown={onKeyDown}

src/Row.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ function Row<R, SR>({
1919
row,
2020
viewportColumns,
2121
selectedCellEditor,
22+
onCellMouseDown,
2223
onCellClick,
2324
onCellDoubleClick,
2425
onCellContextMenu,
@@ -67,9 +68,10 @@ function Row<R, SR>({
6768
rowIdx,
6869
isDraggedOver: draggedOverCellIdx === idx,
6970
isCellSelected,
70-
onClick: onCellClick,
71-
onDoubleClick: onCellDoubleClick,
72-
onContextMenu: onCellContextMenu,
71+
onCellMouseDown,
72+
onCellClick,
73+
onCellDoubleClick,
74+
onCellContextMenu,
7375
onRowChange: handleRowChange,
7476
selectCell
7577
})

0 commit comments

Comments
 (0)