Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/components/DataTable/DataTableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cn } from '#utils';

import { DataTableEmptyState } from './DataTableEmptyState.tsx';
import { useDataTableHandle } from './hooks.ts';
import { flexRender } from './utils.tsx';
import { flexRender, getSizeVariableName } from './utils.tsx';

import type { DataTableContentProps } from './types.ts';

Expand Down Expand Up @@ -45,7 +45,7 @@ export const DataTableBody: FC<
>
{row.getVisibleCells().map((cell) => {
const style: CSSProperties = {
width: `calc(var(--col-${cell.column.id}-size) * 1px)`
width: `calc(var(${getSizeVariableName('col', cell.column.id)}) * 1px)`
};
if (cell.column.getIsPinned() === 'left') {
style.left = `${cell.column.getStart('left')}px`;
Expand Down
4 changes: 2 additions & 2 deletions src/components/DataTable/DataTableHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CSSProperties } from 'react';

import { ACTIONS_COLUMN_ID } from './constants.ts';
import { useDataTableHandle } from './hooks.ts';
import { flexRender } from './utils.tsx';
import { flexRender, getSizeVariableName } from './utils.tsx';

export const DataTableHead = () => {
const headerGroups = useDataTableHandle('headerGroups');
Expand All @@ -16,7 +16,7 @@ export const DataTableHead = () => {
const style: CSSProperties = {
// TODO - add more robust solution - should be able to block centering also - also set correct typing
justifyContent: header.column.columnDef.meta?.centered ? 'center' : 'start',
width: `calc(var(--header-${header?.id}-size) * 1px)`
width: `calc(var(${getSizeVariableName('header', header.id)}) * 1px)`
};
if (header.column.getIsPinned() === 'left') {
style.left = `${header.column.getStart('left')}px`;
Expand Down
17 changes: 17 additions & 0 deletions src/components/DataTable/__tests__/DataTable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ describe('DataTable', () => {
expect(root).toHaveStyle({ backgroundColor: 'blue' });
expect(root.style.getPropertyValue('--data-table-accent')).toBe('red');
});
it('should set valid CSS variables for a column whose ID contains spaces', () => {
render(
<DataTable
columns={[{ accessorFn: (row) => row.email, header: 'Email', id: 'Email Address (work)' }, ...columns.slice(2)]}
data={data}
/>
);
const style = screen.getByTestId('data-table-head').parentElement!.getAttribute('style')!;
const variableNames = style
.split(';')
.map((declaration) => declaration.split(':')[0]!.trim())
.filter((name) => name.endsWith('-size'));
expect(variableNames).toHaveLength(4);
for (const name of variableNames) {
expect(name).toMatch(/^--[a-zA-Z0-9_-]+$/);
}
});
it('should search', async () => {
render(<DataTable columns={columns} data={data} />);
const searchBar = screen.getByTestId('data-table-search-bar').querySelector('input')!;
Expand Down
17 changes: 17 additions & 0 deletions src/components/DataTable/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';

import { getSizeVariableName } from '../utils.tsx';

describe('getSizeVariableName', () => {
it('should not change an ID that contains only letters, digits, and hyphens', () => {
expect(getSizeVariableName('col', 'first-name1')).toBe('--col-first-name1-size');
expect(getSizeVariableName('header', 'email')).toBe('--header-email-size');
});
it('should return a valid CSS variable name for an ID that contains spaces', () => {
expect(getSizeVariableName('col', 'First Name')).toMatch(/^--[a-zA-Z0-9_-]+$/);
});
it('should return different names for IDs that differ only in special characters', () => {
const names = ['a b', 'a_b', 'a.b', 'a_20_b', 'ab'].map((id) => getSizeVariableName('col', id));
expect(new Set(names).size).toBe(names.length);
});
});
5 changes: 3 additions & 2 deletions src/components/DataTable/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
defineMemoizedHandle,
getColumnPinningWithActions,
getColumnsWithActions,
getSizeVariableName,
getTanstackTableState
} from './utils.tsx';

Expand Down Expand Up @@ -63,8 +64,8 @@ export function createDataTableStore<T>(params: DataTableStoreParams<T>) {
style.visibility = 'visible';
}
for (const header of headers) {
style[`--header-${header.id}-size`] = header.getSize();
style[`--col-${header.column.id}-size`] = header.column.getSize();
style[getSizeVariableName('header', header.id)] = header.getSize();
style[getSizeVariableName('col', header.column.id)] = header.column.getSize();
}
return { style };
});
Expand Down
15 changes: 15 additions & 0 deletions src/components/DataTable/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ function getTanstackTableState<T>({ initialState, rowActions }: DataTableStorePa
return state;
}

/**
* Returns the name of the CSS variable that holds the size of a header or column.
*
* An ID can contain characters that are not valid in a CSS variable name (e.g., a space). Each
* character other than an ASCII letter, a digit, or a hyphen is replaced by its code point between
* underscores. Because the underscore itself is replaced, two different IDs cannot give the same name.
*/
function getSizeVariableName(kind: 'col' | 'header', id: string): `--${string}` {
const encodedId = Array.from(id, (char) => {
return /[a-zA-Z0-9-]/.test(char) ? char : `_${char.codePointAt(0)!.toString(16)}_`;
}).join('');
return `--${kind}-${encodedId}-size`;
}

function isReactComponent<TProps>(component: unknown): component is ComponentType<TProps> {
return typeof component === 'function';
}
Expand All @@ -183,5 +197,6 @@ export {
flexRender,
getColumnPinningWithActions,
getColumnsWithActions,
getSizeVariableName,
getTanstackTableState
};
Loading