diff --git a/packages/table-core/src/features/column-faceting/createFacetedUniqueValues.ts b/packages/table-core/src/features/column-faceting/createFacetedUniqueValues.ts index 85e2bcc304..75e907dfbb 100644 --- a/packages/table-core/src/features/column-faceting/createFacetedUniqueValues.ts +++ b/packages/table-core/src/features/column-faceting/createFacetedUniqueValues.ts @@ -71,7 +71,7 @@ function _createFacetedUniqueValues< .map((column) => column.id) : [columnId] - const facetedUniqueValues = new Map() + const facetedUniqueValues = new ArrayKeyMap() for (let i = 0; i < flatRows.length; i++) { for (let c = 0; c < columnIds.length; c++) { @@ -94,3 +94,26 @@ function _createFacetedUniqueValues< return facetedUniqueValues } + +/** + * A Map subclass that normalises array keys so that: + * - Two distinct array instances with the same contents map to the same entry. + * - A serialised string like `'["a","b"]'` never collides with an array `['a','b']`. + * + * Internally arrays are stored under a null-byte-prefixed JSON key, which cannot + * appear in ordinary strings. + */ +class ArrayKeyMap extends Map { + static toKey(key: unknown): unknown { + return Array.isArray(key) ? '\0' + JSON.stringify(key) : key + } + override get(key: unknown): number | undefined { + return super.get(ArrayKeyMap.toKey(key)) + } + override set(key: unknown, value: number): this { + return super.set(ArrayKeyMap.toKey(key), value) + } + override has(key: unknown): boolean { + return super.has(ArrayKeyMap.toKey(key)) + } +} diff --git a/packages/table-core/tests/unit/features/column-faceting/columnFacetingFeature.test.ts b/packages/table-core/tests/unit/features/column-faceting/columnFacetingFeature.test.ts index ade1f20e91..daee5520d6 100644 --- a/packages/table-core/tests/unit/features/column-faceting/columnFacetingFeature.test.ts +++ b/packages/table-core/tests/unit/features/column-faceting/columnFacetingFeature.test.ts @@ -23,6 +23,7 @@ import type { ColumnDef } from '../../../../src' type Person = { firstName: string status: string | undefined + tags?: Array } const features = testFeatures({ @@ -45,12 +46,16 @@ const columns: Array> = [ filterFn: 'equalsString', id: 'status', }, + { + accessorKey: 'tags', + id: 'tags', + }, ] const data: Array = [ - { firstName: 'Alice', status: 'active' }, - { firstName: 'Bob', status: undefined }, - { firstName: 'Carol', status: 'active' }, + { firstName: 'Alice', status: 'active', tags: ['a', 'b'] }, + { firstName: 'Bob', status: undefined, tags: ['a', 'b'] }, + { firstName: 'Carol', status: 'active', tags: ['c'] }, ] function makeTable(columnFilters: Array<{ id: string; value: unknown }>) { @@ -81,8 +86,8 @@ describe('column faceting row model', () => { expect( firstNameColumn.getFacetedRowModel().rows.map((row) => row.original), ).toEqual([ - { firstName: 'Alice', status: 'active' }, - { firstName: 'Carol', status: 'active' }, + { firstName: 'Alice', status: 'active', tags: ['a', 'b'] }, + { firstName: 'Carol', status: 'active', tags: ['c'] }, ]) }) @@ -98,6 +103,20 @@ describe('column faceting row model', () => { ) }) + it('counts array cell values by content rather than reference', () => { + const table = makeTable([]) + const tagsColumn = table.getColumn('tags')! + + // ['a','b'] appears in two rows with different array references — + // without JSON.stringify as the map key both would get count 1 + expect(tagsColumn.getFacetedUniqueValues()).toEqual( + new Map([ + [JSON.stringify(['a', 'b']), 2], + [JSON.stringify(['c']), 1], + ]), + ) + }) + it('caches faceted factory functions per column and global context', () => { const facetedRowModel = vi.fn( (table: any, _columnId: string) => () => table.getPreFilteredRowModel(),