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
1,941 changes: 1,037 additions & 904 deletions apps/docs/package-lock.json

Large diffs are not rendered by default.

984 changes: 470 additions & 514 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions src/DataTable.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
color-scheme: var(--rdt-color-scheme, light);
/* Sort icon scales with the theme's icon size */
--rdt-sort-icon-size: calc(var(--rdt-icon-size, 18px) * 0.8);
/* Expander open/close animation length. Mirrors EXPAND_DURATION in
constants.ts, which keeps the closing row mounted for exactly this long
before unmounting it — keep the two in sync. */
--rdt-expand-duration: 220ms;
}

.rdt_tableDisabled {
Expand Down Expand Up @@ -1619,11 +1623,11 @@
/* Expander row — grid-row trick gives a true height animation without JS measurement */
.rdt_expanderRowAnimated {
display: grid;
animation: rdt_expandIn 0.22s cubic-bezier(0.2, 0, 0, 1) forwards;
animation: rdt_expandIn var(--rdt-expand-duration, 220ms) cubic-bezier(0.2, 0, 0, 1) forwards;
}

.rdt_expanderRowClosing {
animation: rdt_expandOut 0.22s cubic-bezier(0.2, 0, 0, 1) forwards;
animation: rdt_expandOut var(--rdt-expand-duration, 220ms) cubic-bezier(0.2, 0, 0, 1) forwards;
}

@keyframes rdt_expandIn {
Expand Down Expand Up @@ -1671,6 +1675,4 @@
.rdt_spinner {
animation: none !important;
}

/* Column reorder FLIP animations are skipped via JS matchMedia check */
}
37 changes: 18 additions & 19 deletions src/components/ColumnFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { FilterState, FilterCondition, FilterOperator, FilterType, Localiza

type ColumnFilterOptions = NonNullable<Localization['filter']>;
import { emptyFilterState, isFilterActive } from '../hooks/useColumnFilter';
import FilterIcon from '../icons/FilterIcon';

type OperatorOption = { value: FilterOperator; label: string; noInput?: boolean; twoInputs?: boolean };

Expand Down Expand Up @@ -39,13 +40,14 @@ const DEFAULT_DATE_OPERATORS: OperatorOption[] = [
{ value: 'notBlank', label: 'Not blank', noInput: true },
];

function baseOperators(filterType: FilterType): OperatorOption[] {
if (filterType === 'number') return DEFAULT_NUMBER_OPERATORS;
if (filterType === 'date' || filterType === 'datetime' || filterType === 'time') return DEFAULT_DATE_OPERATORS;
return DEFAULT_TEXT_OPERATORS;
}

function operatorsFor(filterType: FilterType, overrides?: ColumnFilterOptions['operators']): OperatorOption[] {
const base =
filterType === 'number'
? DEFAULT_NUMBER_OPERATORS
: filterType === 'date' || filterType === 'datetime' || filterType === 'time'
? DEFAULT_DATE_OPERATORS
: DEFAULT_TEXT_OPERATORS;
const base = baseOperators(filterType);
if (!overrides) return base;
return base.map(op => (overrides[op.value] ? { ...op, label: overrides[op.value]! } : op));
}
Expand All @@ -54,6 +56,14 @@ function defaultOperator(filterType: FilterType): FilterOperator {
return filterType === 'text' ? 'contains' : 'equals';
}

function inputTypeFor(filterType: FilterType): string {
if (filterType === 'number') return 'number';
if (filterType === 'date') return 'date';
if (filterType === 'datetime') return 'datetime-local';
if (filterType === 'time') return 'time';
return 'text';
}

function emptyCondition(filterType: FilterType): FilterCondition {
return { operator: defaultOperator(filterType) };
}
Expand All @@ -75,16 +85,7 @@ type ConditionRowProps = {
function ConditionRow({ condition, filterType, options, onChange, onRemove }: ConditionRowProps): JSX.Element {
const operators = operatorsFor(filterType, options.operators);
const selected = operators.find(o => o.value === condition.operator) ?? operators[0];
const inputType =
filterType === 'number'
? 'number'
: filterType === 'date'
? 'date'
: filterType === 'datetime'
? 'datetime-local'
: filterType === 'time'
? 'time'
: 'text';
const inputType = inputTypeFor(filterType);
// Time inputs default to minute precision; step=1 exposes a seconds field so
// logs can be filtered to the second.
const inputStep = filterType === 'time' ? 1 : undefined;
Expand Down Expand Up @@ -301,9 +302,7 @@ export default function ColumnFilter({
toggleOpen();
}}
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
</svg>
<FilterIcon />
{isActive && <span className="rdt_filterDot" />}
</button>

Expand Down
13 changes: 7 additions & 6 deletions src/components/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ import useSortFlipAnimation from '../hooks/useSortFlipAnimation';
import useContextMenu from '../hooks/useContextMenu';
import ContextMenu from './ContextMenu';

function getHeadSepClass(headerSeparator: boolean | 'subtle' | 'full'): string | undefined {
if (headerSeparator === false) return undefined;
if (headerSeparator === 'full') return 'rdt_headSeparatorFull';
return 'rdt_headSeparator';
}

function DataTableInner<T>(props: TableProps<T>, ref: React.ForwardedRef<DataTableHandle>): JSX.Element {
const {
data = defaultProps.data,
Expand Down Expand Up @@ -516,12 +522,7 @@ function DataTableInner<T>(props: TableProps<T>, ref: React.ForwardedRef<DataTab
const effectiveHeaderSep = headerSeparator !== undefined ? headerSeparator : (themeObj.headerSeparator ?? true);
const sepClass =
effectiveColumnSep === 'full' ? 'rdt_colSeparatorFull' : effectiveColumnSep ? 'rdt_colSeparator' : undefined;
const headSepClass =
effectiveHeaderSep === false
? undefined
: effectiveHeaderSep === 'full'
? 'rdt_headSeparatorFull'
: 'rdt_headSeparator';
const headSepClass = getHeadSepClass(effectiveHeaderSep);

return (
<StylesContext.Provider value={tableStyles}>
Expand Down
16 changes: 9 additions & 7 deletions src/components/ResponsiveWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ type ResponsiveWrapperProps = React.HTMLAttributes<HTMLDivElement> & {
$animateRows?: boolean;
};

// fixedHeader needs a scroll container even when responsive is off — maxHeight
// without overflow lets rows spill over whatever renders below the table.
function getScrollClass($fixedHeader?: boolean, $responsive?: boolean): string | undefined {
if ($fixedHeader) return 'rdt_responsiveWrapperFixed';
if ($responsive) return 'rdt_responsiveWrapperScroll';
return undefined;
}

const ResponsiveWrapper = React.forwardRef<HTMLDivElement, ResponsiveWrapperProps>(function ResponsiveWrapper(
{
$responsive,
Expand All @@ -24,13 +32,7 @@ const ResponsiveWrapper = React.forwardRef<HTMLDivElement, ResponsiveWrapperProp
ref,
) {
const customStyles = useStyles();
// fixedHeader needs a scroll container even when responsive is off — maxHeight
// without overflow lets rows spill over whatever renders below the table.
const scrollClass = $fixedHeader
? 'rdt_responsiveWrapperFixed'
: $responsive
? 'rdt_responsiveWrapperScroll'
: undefined;
const scrollClass = getScrollClass($fixedHeader, $responsive);
return (
<div
ref={ref}
Expand Down
26 changes: 14 additions & 12 deletions src/components/TableCol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import '../DataTable.css';
import { useStyles } from '../context/StylesContext';
import { CellExtended } from './Cell';
import NativeSortIcon from '../icons/NativeSortIcon';
import MenuIcon from '../icons/MenuIcon';
import ColumnFilter from './ColumnFilter';
import { equalizeId, getPinnedCellMeta } from '../util';
import type { PinnedOffsets } from '../util';
Expand All @@ -16,6 +17,16 @@ import type { SortingSlice } from '../hooks/useSorting';

type FilterLocalization = NonNullable<Localization['filter']>;

function getAriaSort(
disableSort: boolean,
sortActive: boolean,
columnSortDirection: SortOrder,
): React.AriaAttributes['aria-sort'] {
if (disableSort) return undefined;
if (!sortActive) return 'none';
return columnSortDirection === SortOrder.ASC ? 'ascending' : 'descending';
}

type TableColProps<T> = {
column: TableColumn<T>;
disabled: boolean;
Expand Down Expand Up @@ -146,6 +157,7 @@ function TableCol<T>({

const sortActive = !!(column.sortable && sortIndex !== -1);
const disableSort = !column.sortable || disabled;
const ariaSort = getAriaSort(disableSort, sortActive, columnSortDirection);
const isNavActive = !!cellNavigation && activeCell?.row === -1 && activeCell?.col === navCol;
// With cellNavigation the whole grid is one Tab stop (roving tabindex); otherwise
// only sortable headers are tabbable.
Expand Down Expand Up @@ -228,15 +240,7 @@ function TableCol<T>({
.join(' ')}
onClick={!disableSort ? handleClick : undefined}
onKeyDown={!disableSort ? handleKeyDown : undefined}
aria-sort={
!disableSort
? sortActive
? columnSortDirection === SortOrder.ASC
? 'ascending'
: 'descending'
: 'none'
: undefined
}
aria-sort={ariaSort}
>
{!disableSort && customSortIconRight && renderCustomSortIcon()}
{!disableSort && nativeSortIconRight && renderNativeSortIcon(sortActive)}
Expand Down Expand Up @@ -283,9 +287,7 @@ function TableCol<T>({
}}
onPointerDown={e => e.stopPropagation()}
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</svg>
<MenuIcon />
</button>
)}
{onResizeStart && column.id != null && (
Expand Down
5 changes: 2 additions & 3 deletions src/components/TableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import TableCellCheckbox from './TableCellCheckbox';
import TableCellExpander from './TableCellExpander';
import ExpanderRow from './ExpanderRow';
import RightPinSpacer from './RightPinSpacer';
import MenuIcon from '../icons/MenuIcon';
import { prop, equalizeId, getConditionalStyle, getFirstRightPinnedId, getPrefixColCount, isEven } from '../util';
import { STOP_PROP_TAG } from '../constants';
import useRowExpander from '../hooks/useRowExpander';
Expand Down Expand Up @@ -279,9 +280,7 @@ function Row<T>({
rowMenu.onMenuButtonClick(row, rowIndex, e);
}}
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</svg>
<MenuIcon />
</button>
</div>
)}
Expand Down
8 changes: 8 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ export const STOP_PROP_TAG = 'allowRowEvents';
*/
export const SYSTEM_COL_WIDTH = 48;

/**
* Duration (in ms) of the expander row open/close animation. The close handler
* keeps the row mounted for this long so the exit animation can finish before
* unmount. Mirrored in DataTable.css as --rdt-expand-duration, which drives the
* rdt_expandIn / rdt_expandOut animations — keep the two in sync.
*/
export const EXPAND_DURATION = 220;

export enum Direction {
LTR = 'ltr',
RTL = 'rtl',
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/useRowExpander.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';

const EXPAND_DURATION = 220;
import { EXPAND_DURATION } from '../constants';

type ExpanderState = { expanded: boolean; mounted: boolean; closing: boolean };
type ExpanderAction = { type: 'open' } | { type: 'close' } | { type: 'unmount' } | { type: 'sync'; value: boolean };
Expand Down
21 changes: 21 additions & 0 deletions src/icons/FilterIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

type FilterIconProps = {
size?: number;
};

const FilterIcon: React.FC<FilterIconProps> = ({ size = 14 }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
role="presentation"
>
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
</svg>
);

export default FilterIcon;
21 changes: 21 additions & 0 deletions src/icons/MenuIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

type MenuIconProps = {
size?: number;
};

const MenuIcon: React.FC<MenuIconProps> = ({ size = 14 }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
aria-hidden="true"
role="presentation"
>
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</svg>
);

export default MenuIcon;
22 changes: 15 additions & 7 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,20 @@ export type PinnedCellMeta = {
className: string;
};

// Logical insets so pin bands mirror under RTL: 'left' pins stick to the
// inline-start edge (physical right in RTL), 'right' pins to inline-end.
function getPinnedStyle(
pinnedLeft: boolean,
pinnedRight: boolean,
offsets: PinnedOffsets,
id: string | number | undefined,
zIndex?: number,
): React.CSSProperties {
if (pinnedLeft) return { position: 'sticky', insetInlineStart: offsets.left[id!], ...(zIndex != null && { zIndex }) };
if (pinnedRight) return { position: 'sticky', insetInlineEnd: offsets.right[id!], ...(zIndex != null && { zIndex }) };
return {};
}

export function getPinnedCellMeta<T>(
column: TableColumn<T>,
pinnedOffsets: PinnedOffsets | undefined,
Expand All @@ -426,13 +440,7 @@ export function getPinnedCellMeta<T>(
const isLastLeftPin = pinnedLeft && id != null && offsets.left[id] === maxLeft;
const isFirstRightPin = pinnedRight && id != null && offsets.right[id] === maxRight;

// Logical insets so pin bands mirror under RTL: 'left' pins stick to the
// inline-start edge (physical right in RTL), 'right' pins to inline-end.
const style: React.CSSProperties = pinnedLeft
? { position: 'sticky', insetInlineStart: offsets.left[id!], ...(zIndex != null && { zIndex }) }
: pinnedRight
? { position: 'sticky', insetInlineEnd: offsets.right[id!], ...(zIndex != null && { zIndex }) }
: {};
const style = getPinnedStyle(pinnedLeft, pinnedRight, offsets, id, zIndex);

const className = [
pinnedLeft && 'rdt_pinLeft',
Expand Down