Skip to content

Handle column sorting events directly on the header cell #3316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 22, 2023
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
75 changes: 45 additions & 30 deletions src/HeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import { css } from '@linaria/core';

import { useRovingTabIndex } from './hooks';
import { clampColumnWidth, getCellClassname, getCellStyle } from './utils';
import { clampColumnWidth, getCellClassname, getCellStyle, stopPropagation } from './utils';
import type { CalculatedColumn, SortColumn } from './types';
import type { HeaderRowProps } from './HeaderRow';
import defaultRenderHeaderCell from './renderHeaderCell';

const cellSortableClassname = css`
@layer rdg.HeaderCell {
cursor: pointer;
}
`;

const cellResizable = css`
@layer rdg.HeaderCell {
touch-action: none;

&::after {
content: '';
cursor: col-resize;
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
inset-block-end: 0;
inline-size: 10px;
}
}
`;

const cellResizableClassname = `rdg-cell-resizable ${cellResizable}`;

export const resizeHandleClassname = css`
@layer rdg.HeaderCell {
cursor: col-resize;
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
inset-block-end: 0;
inline-size: 10px;
}
`;

type SharedHeaderRowProps<R, SR> = Pick<
HeaderRowProps<R, SR, React.Key>,
| 'sortColumns'
Expand Down Expand Up @@ -62,6 +69,7 @@ export default function HeaderCell<R, SR>({
sortDirection && !priority ? (sortDirection === 'ASC' ? 'ascending' : 'descending') : undefined;

const className = getCellClassname(column, column.headerCellClass, {
[cellSortableClassname]: column.sortable,
[cellResizableClassname]: column.resizable
});

Expand All @@ -73,18 +81,14 @@ export default function HeaderCell<R, SR>({
}

const { currentTarget, pointerId } = event;
const { right, left } = currentTarget.getBoundingClientRect();
const headerCell = currentTarget.parentElement!;
const { right, left } = headerCell.getBoundingClientRect();
const offset = isRtl ? event.clientX - left : right - event.clientX;

if (offset > 11) {
// +1px to account for the border size
return;
}

function onPointerMove(event: PointerEvent) {
// prevents text selection in Chrome, which fixes scrolling the grid while dragging, and fixes re-size on an autosized column
event.preventDefault();
const { right, left } = currentTarget.getBoundingClientRect();
const { right, left } = headerCell.getBoundingClientRect();
const width = isRtl ? right + offset - event.clientX : event.clientX + offset - left;
if (width > 0) {
onColumnResize(column, clampColumnWidth(width, column));
Expand Down Expand Up @@ -138,19 +142,15 @@ export default function HeaderCell<R, SR>({
}
}

function onClick() {
function onClick(event: React.MouseEvent<HTMLSpanElement>) {
selectCell(column.idx);
}

function onDoubleClick(event: React.MouseEvent<HTMLDivElement>) {
const { right, left } = event.currentTarget.getBoundingClientRect();
const offset = isRtl ? event.clientX - left : right - event.clientX;

if (offset > 11) {
// +1px to account for the border size
return;
if (column.sortable) {
onSort(event.ctrlKey || event.metaKey);
}
}

function onDoubleClick() {
onColumnResize(column, 'max-content');
}

Expand All @@ -162,6 +162,14 @@ export default function HeaderCell<R, SR>({
}
}

function onKeyDown(event: React.KeyboardEvent<HTMLSpanElement>) {
if (event.key === ' ' || event.key === 'Enter') {
// prevent scrolling
event.preventDefault();
onSort(event.ctrlKey || event.metaKey);
}
}

return (
<div
role="columnheader"
Expand All @@ -175,16 +183,23 @@ export default function HeaderCell<R, SR>({
style={getCellStyle(column, colSpan)}
onFocus={handleFocus}
onClick={onClick}
onDoubleClick={column.resizable ? onDoubleClick : undefined}
onPointerDown={column.resizable ? onPointerDown : undefined}
onKeyDown={column.sortable ? onKeyDown : undefined}
>
{renderHeaderCell({
column,
sortDirection,
priority,
onSort,
tabIndex: childTabIndex
})}

{column.resizable && (
<div
className={resizeHandleClassname}
onClick={stopPropagation}
onDoubleClick={onDoubleClick}
onPointerDown={onPointerDown}
/>
)}
</div>
);
}
46 changes: 6 additions & 40 deletions src/renderHeaderCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,15 @@ import { css } from '@linaria/core';
import type { RenderHeaderCellProps } from './types';
import { useDefaultRenderers } from './DataGridDefaultRenderersProvider';

const headerSortCell = css`
const headerSortCellClassname = css`
@layer rdg.SortableHeaderCell {
cursor: pointer;
display: flex;

&:focus {
outline: none;
}
}
`;

const headerSortCellClassname = `rdg-header-sort-cell ${headerSortCell}`;

const headerSortName = css`
@layer rdg.SortableHeaderCellName {
flex-grow: 1;
overflow: hidden;
overflow: clip;
text-overflow: ellipsis;
}
Expand All @@ -30,61 +22,35 @@ const headerSortNameClassname = `rdg-header-sort-name ${headerSortName}`;
export default function renderHeaderCell<R, SR>({
column,
sortDirection,
priority,
onSort,
tabIndex
priority
}: RenderHeaderCellProps<R, SR>) {
if (!column.sortable) return column.name;

return (
<SortableHeaderCell
onSort={onSort}
sortDirection={sortDirection}
priority={priority}
tabIndex={tabIndex}
>
<SortableHeaderCell sortDirection={sortDirection} priority={priority}>
{column.name}
</SortableHeaderCell>
);
}

type SharedHeaderCellProps<R, SR> = Pick<
RenderHeaderCellProps<R, SR>,
'sortDirection' | 'onSort' | 'priority' | 'tabIndex'
'sortDirection' | 'priority'
>;

interface SortableHeaderCellProps<R, SR> extends SharedHeaderCellProps<R, SR> {
children: React.ReactNode;
}

function SortableHeaderCell<R, SR>({
onSort,
sortDirection,
priority,
children,
tabIndex
children
}: SortableHeaderCellProps<R, SR>) {
const renderSortStatus = useDefaultRenderers<R, SR>()!.renderSortStatus!;

function handleKeyDown(event: React.KeyboardEvent<HTMLSpanElement>) {
if (event.key === ' ' || event.key === 'Enter') {
// stop propagation to prevent scrolling
event.preventDefault();
onSort(event.ctrlKey || event.metaKey);
}
}

function handleClick(event: React.MouseEvent<HTMLSpanElement>) {
onSort(event.ctrlKey || event.metaKey);
}

return (
<span
tabIndex={tabIndex}
className={headerSortCellClassname}
onClick={handleClick}
onKeyDown={handleKeyDown}
>
<span className={headerSortCellClassname}>
<span className={headerSortNameClassname}>{children}</span>
<span>{renderSortStatus({ sortDirection, priority })}</span>
</span>
Expand Down
1 change: 0 additions & 1 deletion src/style/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const cell = css`
background-color: inherit;

white-space: nowrap;
overflow: hidden;
overflow: clip;
text-overflow: ellipsis;
outline: none;
Expand Down
1 change: 0 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ export interface RenderHeaderCellProps<TRow, TSummaryRow = unknown> {
sortDirection: SortDirection | undefined;
priority: number | undefined;
tabIndex: number;
onSort: (ctrlClick: boolean) => void;
}

export interface CellRendererProps<TRow, TSummaryRow>
Expand Down
20 changes: 8 additions & 12 deletions test/column/resizable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fireEvent } from '@testing-library/react';

import type { Column } from '../../src';
import { resizeHandleClassname } from '../../src/HeaderCell';
import { getGrid, getHeaderCells, setup } from '../utils';

const pointerId = 1;
Expand Down Expand Up @@ -38,19 +39,22 @@ function resize<K extends keyof DOMRect>({
clientXEnd,
rect
}: ResizeEvent<K>) {
const resizeHandle = column.querySelector(`.${resizeHandleClassname}`);
if (resizeHandle === null) return;

const original = column.getBoundingClientRect.bind(column);
column.getBoundingClientRect = () => ({
...original(),
...rect
});
// eslint-disable-next-line testing-library/prefer-user-event
fireEvent.pointerDown(
column,
resizeHandle,
new PointerEvent('pointerdown', { pointerId, clientX: clientXStart })
);
// eslint-disable-next-line testing-library/prefer-user-event
fireEvent.pointerMove(column, new PointerEvent('pointermove', { clientX: clientXEnd }));
fireEvent.lostPointerCapture(column, new PointerEvent('lostpointercapture', {}));
fireEvent.pointerMove(resizeHandle, new PointerEvent('pointermove', { clientX: clientXEnd }));
fireEvent.lostPointerCapture(resizeHandle, new PointerEvent('lostpointercapture', {}));
}

const columns: readonly Column<Row>[] = [
Expand All @@ -77,15 +81,7 @@ test('should not resize column if resizable is not specified', () => {
expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' });
});

test('should not resize column if cursor offset is not within the allowed range', () => {
setup({ columns, rows: [] });
const [, col2] = getHeaderCells();
expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' });
resize({ column: col2, clientXStart: 288, clientXEnd: 250, rect: { right: 300, left: 100 } });
expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' });
});

test('should resize column if cursor offset is within the allowed range', () => {
test('should resize column when dragging the handle', () => {
setup({ columns, rows: [] });
const [, col2] = getHeaderCells();
expect(getGrid()).toHaveStyle({ gridTemplateColumns: '100px 200px' });
Expand Down
2 changes: 1 addition & 1 deletion website/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const mainClassname = css`
box-sizing: border-box;
block-size: 100vh;
padding: 8px;
overflow: hidden;
contain: inline-size;
`;

function Root() {
Expand Down