Skip to content

Commit

Permalink
Handle column sorting events directly on the header cell (#3316)
Browse files Browse the repository at this point in the history
* Handle column sorting events directly on the header cell

* fix main styles

* add resize handler element

* rm newline
  • Loading branch information
nstepien authored Aug 22, 2023
1 parent 7b3e0d2 commit c7a8e82
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 85 deletions.
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

0 comments on commit c7a8e82

Please sign in to comment.