Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
canExitGrid,
createCellEvent,
getColSpan,
getLeftRightKey,
getNextSelectedCellPosition,
isCtrlKeyHeldDown,
isDefaultCellInput,
Expand Down Expand Up @@ -377,9 +378,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
const summaryRowsHeight = summaryRowsCount * summaryRowHeight;
const clientHeight = gridHeight - headerRowsHeight - summaryRowsHeight;
const isSelectable = selectedRows != null && onSelectedRowsChange != null;
const isRtl = direction === 'rtl';
const leftKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
const rightKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
const { leftKey, rightKey } = getLeftRightKey(direction);
const ariaRowCount = rawAriaRowCount ?? headerRowsCount + rows.length + summaryRowsCount;

const defaultGridComponents = useMemo(
Expand Down Expand Up @@ -689,7 +688,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
assertIsValidKeyGetter<R, K>(rowKeyGetter);
const rowKey = rowKeyGetter(row);
selectRow({ row, checked: !selectedRows.has(rowKey), isShiftClick: false });
// do not scroll
// prevent scrolling
event.preventDefault();
return;
}
Expand Down Expand Up @@ -820,7 +819,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
cellNavigationMode = 'CHANGE_ROW';
}

// Do not allow focus to leave and prevent scrolling
// prevent scrolling and do not allow focus to leave
event.preventDefault();

const ctrlKey = isCtrlKeyHeldDown(event);
Expand Down
23 changes: 21 additions & 2 deletions src/HeaderCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
getCellStyle,
getHeaderCellRowSpan,
getHeaderCellStyle,
getLeftRightKey,
isCtrlKeyHeldDown,
stopPropagation
} from './utils';
import type { CalculatedColumn, SortColumn } from './types';
Expand Down Expand Up @@ -160,10 +162,26 @@ export default function HeaderCell<R, SR>({
}

function onKeyDown(event: React.KeyboardEvent<HTMLSpanElement>) {
if (event.key === ' ' || event.key === 'Enter') {
const { key } = event;
if (sortable && (key === ' ' || key === 'Enter')) {
// prevent scrolling
event.preventDefault();
onSort(event.ctrlKey || event.metaKey);
} else if (
resizable &&
isCtrlKeyHeldDown(event) &&
(key === 'ArrowLeft' || key === 'ArrowRight')
Copy link
Copy Markdown
Collaborator Author

@amanmahajan7 amanmahajan7 Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to suggestion on which shortcut to use. I did not find any good example other than this. But the example uses a different strategy where the resizer gets focused. I think we can keep our implementation simple

) {
// prevent navigation
// TODO: check if we can use `preventDefault` instead
Copy link
Copy Markdown
Collaborator Author

@amanmahajan7 amanmahajan7 Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to use preventDefault and handleCellKeyDown can ignore the event if it is prevented. I remember there was issue with editor though. I will check in a separate PR

event.stopPropagation();
const { width } = event.currentTarget.getBoundingClientRect();
const { leftKey } = getLeftRightKey(direction);
const offset = key === leftKey ? -10 : 10;
const newWidth = clampColumnWidth(width + offset, column);
if (newWidth !== width) {
onColumnResize(column, newWidth);
}
}
}

Expand Down Expand Up @@ -192,6 +210,7 @@ export default function HeaderCell<R, SR>({
if (event.dataTransfer.types.includes(dragDropKey.toLowerCase())) {
const sourceKey = event.dataTransfer.getData(dragDropKey.toLowerCase());
if (sourceKey !== column.key) {
// prevent the browser from redirecting in some cases
event.preventDefault();
onColumnsReorder?.(sourceKey, column.key);
}
Expand Down Expand Up @@ -242,7 +261,7 @@ export default function HeaderCell<R, SR>({
}}
onFocus={handleFocus}
onClick={onClick}
onKeyDown={sortable ? onKeyDown : undefined}
onKeyDown={onKeyDown}
{...draggableProps}
>
{column.renderHeaderCell({
Expand Down
9 changes: 4 additions & 5 deletions src/TreeDataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
import type { Key } from 'react';

import { useLatestFunc } from './hooks';
import { assertIsValidKeyGetter } from './utils';
import { assertIsValidKeyGetter, getLeftRightKey } from './utils';
import type {
CellClipboardEvent,
CellCopyEvent,
Expand Down Expand Up @@ -71,9 +71,7 @@ export function TreeDataGrid<R, SR = unknown, K extends Key = Key>({
const defaultRenderers = useDefaultRenderers<R, SR>();
const rawRenderRow = renderers?.renderRow ?? defaultRenderers?.renderRow ?? defaultRenderRow;
const headerAndTopSummaryRowsCount = 1 + (props.topSummaryRows?.length ?? 0);
const isRtl = props.direction === 'rtl';
const leftKey = isRtl ? 'ArrowRight' : 'ArrowLeft';
const rightKey = isRtl ? 'ArrowLeft' : 'ArrowRight';
const { leftKey, rightKey } = getLeftRightKey(props.direction);
const toggleGroupLatest = useLatestFunc(toggleGroup);

const { columns, groupBy } = useMemo(() => {
Expand Down Expand Up @@ -310,7 +308,8 @@ export function TreeDataGrid<R, SR = unknown, K extends Key = Key>({
// Expand the current group row if it is focused and is in collapsed state
(event.key === rightKey && !row.isExpanded))
) {
event.preventDefault(); // Prevents scrolling
// prevent scrolling
event.preventDefault();
event.preventGridDefault();
toggleGroup(row.id);
}
Expand Down
11 changes: 11 additions & 0 deletions src/utils/keyboardUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Direction, Maybe } from '../types';

// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
const nonInputKeys = new Set([
// Special keys
Expand Down Expand Up @@ -85,3 +87,12 @@ export function onEditorNavigation({ key, target }: React.KeyboardEvent<HTMLDivE
}
return false;
}

export function getLeftRightKey(direction: Maybe<Direction>) {
const isRtl = direction === 'rtl';

return {
leftKey: isRtl ? 'ArrowRight' : 'ArrowLeft',
rightKey: isRtl ? 'ArrowLeft' : 'ArrowRight'
} as const;
}
49 changes: 46 additions & 3 deletions test/browser/column/resizable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const columns: readonly Column<Row>[] = [
}
];

test('cannot not resize or auto resize column when resizable is not specified', () => {
test('cannot resize or auto resize column when resizable is not specified', () => {
setup<Row, unknown>({ columns, rows: [] });
const [col1] = getHeaderCells();
expect(queryResizeHandle(col1)).not.toBeInTheDocument();
Expand All @@ -75,7 +75,7 @@ test('should resize column when dragging the handle', async () => {
expect(onColumnResize).toHaveBeenCalledExactlyOnceWith(expect.objectContaining(columns[1]), 150);
});

test('should use the maxWidth if specified', async () => {
test('should use the maxWidth if specified when dragging the handle', async () => {
setup<Row, unknown>({ columns, rows: [] });
const grid = getGrid();
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px ' });
Expand All @@ -84,7 +84,7 @@ test('should use the maxWidth if specified', async () => {
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 400px' });
});

test('should use the minWidth if specified', async () => {
test('should use the minWidth if specified when dragging the handle', async () => {
setup<Row, unknown>({ columns, rows: [] });
const grid = getGrid();
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
Expand All @@ -93,6 +93,49 @@ test('should use the minWidth if specified', async () => {
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 100px' });
});

test('should resize column using keboard', async () => {
const onColumnResize = vi.fn();
setup<Row, unknown>({ columns, rows: [], onColumnResize });
const grid = getGrid();
expect(onColumnResize).not.toHaveBeenCalled();
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
const [, col2] = getHeaderCells();
await userEvent.click(col2);

await userEvent.keyboard('{Control>}{ArrowRight}{/Control}');
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 210px' });
expect(onColumnResize).toHaveBeenCalledWith(expect.objectContaining(columns[1]), 210);

await userEvent.keyboard('{Control>}{ArrowLeft}{/Control}');
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
expect(onColumnResize).toHaveBeenCalledWith(expect.objectContaining(columns[1]), 200);
expect(onColumnResize).toHaveBeenCalledTimes(2);
});

test('should use the maxWidth if specified when resizing using keyboard', async () => {
const onColumnResize = vi.fn();
setup<Row, unknown>({ columns, rows: [], onColumnResize });
const grid = getGrid();
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px ' });
const [, col2] = getHeaderCells();
await userEvent.click(col2);
await userEvent.keyboard(`{Control>}${'{ArrowRight}'.repeat(22)}{/Control}`);
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 400px' });
expect(onColumnResize).toHaveBeenCalledTimes(20);
});

test('should use the minWidth if specified resizing using keyboard', async () => {
const onColumnResize = vi.fn();
setup<Row, unknown>({ columns, rows: [], onColumnResize });
const grid = getGrid();
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 200px' });
const [, col2] = getHeaderCells();
await userEvent.click(col2);
await userEvent.keyboard(`{Control>}${'{ArrowLeft}'.repeat(12)}{/Control}`);
await expect.element(grid).toHaveStyle({ gridTemplateColumns: '100px 100px' });
expect(onColumnResize).toHaveBeenCalledTimes(10);
});

test('should auto resize column when resize handle is double clicked', async () => {
const onColumnResize = vi.fn();
setup<Row, unknown>({
Expand Down
1 change: 1 addition & 0 deletions website/components/CellExpanderFormatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function CellExpanderFormatter({
}: CellExpanderFormatterProps) {
function handleKeyDown(e: React.KeyboardEvent<HTMLSpanElement>) {
if (e.key === ' ' || e.key === 'Enter') {
// prevent scrolling
e.preventDefault();
onCellExpand();
}
Expand Down
1 change: 1 addition & 0 deletions website/components/ChildRowDeleteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function ChildRowDeleteButton({
}: ChildRowDeleteButtonProps) {
function handleKeyDown(e: React.KeyboardEvent<HTMLSpanElement>) {
if (e.key === 'Enter') {
// prevent scrolling
e.preventDefault();
onDeleteSubRow();
}
Expand Down
10 changes: 4 additions & 6 deletions website/routes/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,10 @@ function ContextMenuDemo() {
<menu
ref={menuRef}
className={contextMenuClassname}
style={
{
top: contextMenuProps.top,
left: contextMenuProps.left
} as unknown as React.CSSProperties
}
style={{
top: contextMenuProps.top,
left: contextMenuProps.left
}}
>
<li>
<button
Expand Down