Skip to content

Commit

Permalink
Handle column sorting events directly on the header cell
Browse files Browse the repository at this point in the history
  • Loading branch information
nstepien committed Aug 22, 2023
1 parent 7b3e0d2 commit ebb4a67
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 52 deletions.
48 changes: 39 additions & 9 deletions src/HeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRef } from 'react';
import { css } from '@linaria/core';

import { useRovingTabIndex } from './hooks';
Expand All @@ -6,6 +7,12 @@ 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;
Expand Down Expand Up @@ -51,6 +58,8 @@ export default function HeaderCell<R, SR>({
shouldFocusGrid,
direction
}: HeaderCellProps<R, SR>) {
const ignoreClickRef = useRef(false);

const isRtl = direction === 'rtl';
const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellSelected);
const sortIndex = sortColumns?.findIndex((sort) => sort.columnKey === column.key);
Expand All @@ -62,28 +71,36 @@ 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
});

const renderHeaderCell = column.renderHeaderCell ?? defaultRenderHeaderCell;

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

function onPointerDown(event: React.PointerEvent<HTMLDivElement>) {
if (event.pointerType === 'mouse' && event.buttons !== 1) {
return;
}

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

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

// prevent selecting/sorting if we started resizing the column, including double-clicking
ignoreClickRef.current = true;

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 width = isRtl ? right + offset - event.clientX : event.clientX + offset - left;
if (width > 0) {
Expand All @@ -92,10 +109,12 @@ export default function HeaderCell<R, SR>({
}

function onLostPointerCapture() {
ignoreClickRef.current = false;
currentTarget.removeEventListener('pointermove', onPointerMove);
currentTarget.removeEventListener('lostpointercapture', onLostPointerCapture);
}

const { currentTarget, pointerId } = event;
currentTarget.setPointerCapture(pointerId);
currentTarget.addEventListener('pointermove', onPointerMove);
currentTarget.addEventListener('lostpointercapture', onLostPointerCapture);
Expand Down Expand Up @@ -138,15 +157,18 @@ export default function HeaderCell<R, SR>({
}
}

function onClick() {
function onClick(event: React.MouseEvent<HTMLSpanElement>) {
if (ignoreClickRef.current) return;

selectCell(column.idx);

if (column.sortable) {
onSort(event.ctrlKey || event.metaKey);
}
}

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

if (offset > 11) {
if (getPointerOffset(event) > 11) {
// +1px to account for the border size
return;
}
Expand All @@ -162,6 +184,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 @@ -177,12 +207,12 @@ export default function HeaderCell<R, SR>({
onClick={onClick}
onDoubleClick={column.resizable ? onDoubleClick : undefined}
onPointerDown={column.resizable ? onPointerDown : undefined}
onKeyDown={column.sortable ? onKeyDown : undefined}
>
{renderHeaderCell({
column,
sortDirection,
priority,
onSort,
tabIndex: childTabIndex
})}
</div>
Expand Down
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
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;
overflow: clip;
`;

function Root() {
Expand Down

0 comments on commit ebb4a67

Please sign in to comment.