diff --git a/src/components/DataTable/DataTableBody.tsx b/src/components/DataTable/DataTableBody.tsx index c9ae761..3e513c7 100644 --- a/src/components/DataTable/DataTableBody.tsx +++ b/src/components/DataTable/DataTableBody.tsx @@ -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'; @@ -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`; diff --git a/src/components/DataTable/DataTableHead.tsx b/src/components/DataTable/DataTableHead.tsx index 38dcfcb..0c4c362 100644 --- a/src/components/DataTable/DataTableHead.tsx +++ b/src/components/DataTable/DataTableHead.tsx @@ -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'); @@ -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`; diff --git a/src/components/DataTable/__tests__/DataTable.spec.tsx b/src/components/DataTable/__tests__/DataTable.spec.tsx index b9d34cb..105325b 100644 --- a/src/components/DataTable/__tests__/DataTable.spec.tsx +++ b/src/components/DataTable/__tests__/DataTable.spec.tsx @@ -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( + 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(); const searchBar = screen.getByTestId('data-table-search-bar').querySelector('input')!; diff --git a/src/components/DataTable/__tests__/utils.spec.ts b/src/components/DataTable/__tests__/utils.spec.ts new file mode 100644 index 0000000..7706da2 --- /dev/null +++ b/src/components/DataTable/__tests__/utils.spec.ts @@ -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); + }); +}); diff --git a/src/components/DataTable/store.ts b/src/components/DataTable/store.ts index 6c84bcb..57aa124 100644 --- a/src/components/DataTable/store.ts +++ b/src/components/DataTable/store.ts @@ -17,6 +17,7 @@ import { defineMemoizedHandle, getColumnPinningWithActions, getColumnsWithActions, + getSizeVariableName, getTanstackTableState } from './utils.tsx'; @@ -63,8 +64,8 @@ export function createDataTableStore(params: DataTableStoreParams) { 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 }; }); diff --git a/src/components/DataTable/utils.tsx b/src/components/DataTable/utils.tsx index eb2d706..5a6a91b 100644 --- a/src/components/DataTable/utils.tsx +++ b/src/components/DataTable/utils.tsx @@ -172,6 +172,20 @@ function getTanstackTableState({ 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(component: unknown): component is ComponentType { return typeof component === 'function'; } @@ -183,5 +197,6 @@ export { flexRender, getColumnPinningWithActions, getColumnsWithActions, + getSizeVariableName, getTanstackTableState };