diff --git a/examples/react/excel-sorting/index.html b/examples/react/excel-sorting/index.html new file mode 100644 index 0000000000..2e45988a52 --- /dev/null +++ b/examples/react/excel-sorting/index.html @@ -0,0 +1,12 @@ + + + + + + Excel-like Sorting with sortEmpty + + +
+ + + diff --git a/examples/react/excel-sorting/package.json b/examples/react/excel-sorting/package.json new file mode 100644 index 0000000000..c997435788 --- /dev/null +++ b/examples/react/excel-sorting/package.json @@ -0,0 +1,25 @@ +{ + "name": "tanstack-table-example-excel-sorting", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@faker-js/faker": "^8.4.1", + "@tanstack/react-table": "^8.21.3", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@rollup/plugin-replace": "^5.0.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "5.4.5", + "vite": "^5.3.2" + } +} diff --git a/examples/react/excel-sorting/src/index.css b/examples/react/excel-sorting/src/index.css new file mode 100644 index 0000000000..27f9c18a84 --- /dev/null +++ b/examples/react/excel-sorting/src/index.css @@ -0,0 +1,184 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + margin: 0; + padding: 20px; + background-color: #f5f5f5; +} + +.app { + max-width: 1200px; + margin: 0 auto; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.header { + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.header h1 { + margin: 0 0 10px 0; + font-size: 24px; +} + +.header p { + margin: 0 0 15px 0; + opacity: 0.9; +} + +.header code { + background: rgba(255, 255, 255, 0.2); + padding: 2px 6px; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; +} + +.features { + margin-top: 15px; +} + +.features h3 { + margin: 0 0 10px 0; + font-size: 16px; +} + +.features ul { + margin: 0; + padding-left: 20px; +} + +.features li { + margin-bottom: 5px; +} + +.table-container { + padding: 20px; + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; + border: 1px solid #e0e0e0; + border-radius: 6px; + overflow: hidden; +} + +th { + background: #f8f9fa; + border-bottom: 2px solid #e0e0e0; + border-right: 1px solid #e0e0e0; + padding: 12px 8px; + text-align: left; + font-weight: 600; + font-size: 14px; + color: #495057; +} + +th:last-child { + border-right: none; +} + +th.sortable { + cursor: pointer; + user-select: none; + transition: background-color 0.2s; +} + +th.sortable:hover { + background: #e9ecef; +} + +.header-content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.sort-indicator { + margin-left: 4px; + font-size: 12px; +} + +td { + padding: 10px 8px; + border-bottom: 1px solid #f0f0f0; + border-right: 1px solid #f0f0f0; + font-size: 14px; +} + +td:last-child { + border-right: none; +} + +tr:hover { + background-color: #f8f9fa; +} + +tr:last-child td { + border-bottom: none; +} + +.info { + padding: 20px; + background: #f8f9fa; + border-top: 1px solid #e0e0e0; +} + +.info h3 { + margin: 0 0 10px 0; + font-size: 16px; + color: #495057; +} + +.info pre { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 10px; + font-size: 12px; + overflow-x: auto; + margin: 0 0 20px 0; +} + +.comparison { + margin-top: 20px; +} + +.comparison-table { + font-size: 13px; + margin-top: 10px; +} + +.comparison-table th { + background: #6c757d; + color: white; + font-size: 12px; + padding: 8px; +} + +.comparison-table td { + padding: 8px; + text-align: center; +} + +.comparison-table td:first-child { + text-align: left; + font-weight: 500; +} + +code { + background: #f1f3f4; + padding: 2px 4px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; +} diff --git a/examples/react/excel-sorting/src/main.tsx b/examples/react/excel-sorting/src/main.tsx new file mode 100644 index 0000000000..6534acae84 --- /dev/null +++ b/examples/react/excel-sorting/src/main.tsx @@ -0,0 +1,293 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table' +import './index.css' + +type Product = { + id: number + name: string + price: number | null | undefined + stock: number | null + category: string + description: string +} + +const defaultData: Product[] = [ + { + id: 1, + name: 'Laptop', + price: 999, + stock: 10, + category: 'Electronics', + description: 'High-performance laptop', + }, + { + id: 2, + name: 'Mouse', + price: 25, + stock: null, + category: 'Electronics', + description: '', + }, + { + id: 3, + name: 'Keyboard', + price: null, + stock: 5, + category: 'Electronics', + description: 'Mechanical keyboard', + }, + { + id: 4, + name: 'Monitor', + price: 399, + stock: undefined, + category: 'Electronics', + description: '4K display', + }, + { + id: 5, + name: 'Desk', + price: undefined, + stock: 2, + category: 'Furniture', + description: 'Standing desk', + }, + { + id: 6, + name: 'Chair', + price: 199, + stock: 0, + category: 'Furniture', + description: 'Ergonomic chair', + }, + { + id: 7, + name: 'Webcam', + price: 89, + stock: 15, + category: 'Electronics', + description: '', + }, + { + id: 8, + name: 'Headphones', + price: 150, + stock: null, + category: 'Electronics', + description: 'Noise-cancelling', + }, + { + id: 9, + name: 'Tablet', + price: 299, + stock: 8, + category: 'Electronics', + description: ' ', + }, + { + id: 10, + name: 'Bookshelf', + price: 75, + stock: undefined, + category: 'Furniture', + description: 'Wooden bookshelf', + }, +] + +const columnHelper = createColumnHelper() + +const columns = [ + columnHelper.accessor('id', { + header: 'ID', + size: 60, + }), + columnHelper.accessor('name', { + header: 'Product Name', + size: 150, + }), + columnHelper.accessor('price', { + header: 'Price ($)', + sortEmpty: 'last', // NEW: Empty values go to bottom + cell: info => { + const value = info.getValue() + return value != null ? `$${value}` : '-' + }, + size: 100, + }), + columnHelper.accessor('stock', { + header: 'Stock', + sortEmpty: 'last', + cell: info => { + const value = info.getValue() + return value != null ? value.toString() : 'N/A' + }, + size: 80, + }), + columnHelper.accessor('category', { + header: 'Category', + size: 120, + }), + columnHelper.accessor('description', { + header: 'Description', + sortEmpty: 'last', + isEmptyValue: value => + !value || (typeof value === 'string' && value.trim() === ''), + cell: info => { + const value = info.getValue() + return value?.trim() ? value : '(No description)' + }, + size: 200, + }), +] + +function App() { + const [data] = React.useState(() => [...defaultData]) + const [sorting, setSorting] = React.useState([]) + + const table = useReactTable({ + data, + columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }) + + return ( +
+
+

Excel-like Sorting with sortEmpty

+

+ This example shows the new sortEmpty option. Click column + headers to sort. null/undefined/empty values always appear at the + bottom. +

+
+

Key Features:

+
    +
  • + Price & Stock: null and undefined values are + sorted to the bottom +
  • +
  • + Description: empty strings and strings with only + whitespace are sorted to the bottom +
  • +
  • + Excel-like behavior: empty values always appear + at the bottom regardless of sort direction +
  • +
+
+
+ +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ))} + +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + header.column.getToggleSortingHandler()?.(e) + } + }} + tabIndex={header.column.getCanSort() ? 0 : -1} + className={header.column.getCanSort() ? 'sortable' : ''} + > +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} + +
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ +
+

Sorting State:

+
{JSON.stringify(sorting, null, 2)}
+ +
+

Comparison of sortUndefined vs sortEmpty:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeaturesortUndefinedsortEmpty (new)
Values processedundefined onlynull, undefined, empty strings
Custom empty values✅ isEmptyValue function
Optionsfalse, -1, 1, 'first', 'last'false, 'first', 'last'
Status⚠️ Deprecated✅ Recommended
+
+
+
+ ) +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +ReactDOM.createRoot(rootElement).render( + + + +) diff --git a/examples/react/excel-sorting/tsconfig.json b/examples/react/excel-sorting/tsconfig.json new file mode 100644 index 0000000000..a7fc6fbf23 --- /dev/null +++ b/examples/react/excel-sorting/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/react/excel-sorting/tsconfig.node.json b/examples/react/excel-sorting/tsconfig.node.json new file mode 100644 index 0000000000..42872c59f5 --- /dev/null +++ b/examples/react/excel-sorting/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/react/excel-sorting/vite.config.ts b/examples/react/excel-sorting/vite.config.ts new file mode 100644 index 0000000000..fcea1bef84 --- /dev/null +++ b/examples/react/excel-sorting/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@tanstack/react-table': resolve(__dirname, '../../../packages/react-table/src'), + '@tanstack/table-core': resolve(__dirname, '../../../packages/table-core/src'), + }, + }, +}) diff --git a/packages/table-core/src/features/RowSorting.ts b/packages/table-core/src/features/RowSorting.ts index c2e7c32d53..aecf5c709a 100644 --- a/packages/table-core/src/features/RowSorting.ts +++ b/packages/table-core/src/features/RowSorting.ts @@ -80,6 +80,7 @@ export interface SortingColumnDef { */ sortingFn?: SortingFnOption /** + * @deprecated Use `sortEmpty` for more comprehensive empty value handling * The priority of undefined values when sorting this column. * - `false` * - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) @@ -91,6 +92,26 @@ export interface SortingColumnDef { * @link [Guide](https://tanstack.com/table/v8/docs/guide/sorting) */ sortUndefined?: false | -1 | 1 | 'first' | 'last' + /** + * Determines how empty values (null, undefined, empty string) are sorted + * - `false` + * - Empty values are sorted normally + * - `'first'` + * - Empty values are sorted to the top + * - `'last'` + * - Empty values are sorted to the bottom + * @default false + * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/sorting#sortempty) + * @link [Guide](https://tanstack.com/table/v8/docs/guide/sorting) + */ + sortEmpty?: false | 'first' | 'last' + /** + * Custom function to determine if a value should be considered empty + * @default (value) => value == null || value === '' + * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/sorting#isemptyvalue) + * @link [Guide](https://tanstack.com/table/v8/docs/guide/sorting) + */ + isEmptyValue?: (value: unknown) => boolean } export interface SortingColumn { @@ -305,6 +326,14 @@ export const RowSorting: TableFeature = { column: Column, table: Table ): void => { + if (column.columnDef.sortUndefined && !column.columnDef.sortEmpty) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `[TanStack Table] sortUndefined is deprecated. Please use sortEmpty instead for more comprehensive empty value handling. Column: ${column.id}` + ) + } + } + column.getAutoSortingFn = () => { const firstRows = table.getFilteredRowModel().flatRows.slice(10) diff --git a/packages/table-core/src/utils/getSortedRowModel.ts b/packages/table-core/src/utils/getSortedRowModel.ts index eb5e9d5729..f27bfc46dd 100644 --- a/packages/table-core/src/utils/getSortedRowModel.ts +++ b/packages/table-core/src/utils/getSortedRowModel.ts @@ -26,6 +26,8 @@ export function getSortedRowModel(): ( string, { sortUndefined?: false | -1 | 1 | 'first' | 'last' + sortEmpty?: false | 'first' | 'last' + isEmptyValue?: (value: unknown) => boolean invertSorting?: boolean sortingFn: SortingFn } @@ -37,6 +39,8 @@ export function getSortedRowModel(): ( columnInfoById[sortEntry.id] = { sortUndefined: column.columnDef.sortUndefined, + sortEmpty: column.columnDef.sortEmpty, + isEmptyValue: column.columnDef.isEmptyValue ?? ((value: unknown) => value == null || value === ''), invertSorting: column.columnDef.invertSorting, sortingFn: column.getSortingFn(), } @@ -52,12 +56,31 @@ export function getSortedRowModel(): ( const sortEntry = availableSorting[i]! const columnInfo = columnInfoById[sortEntry.id]! const sortUndefined = columnInfo.sortUndefined + const sortEmpty = columnInfo.sortEmpty + const isEmptyValue = columnInfo.isEmptyValue! const isDesc = sortEntry?.desc ?? false let sortInt = 0 - // All sorting ints should always return in ascending order - if (sortUndefined) { + if (sortEmpty) { + const aValue = rowA.getValue(sortEntry.id) + const bValue = rowB.getValue(sortEntry.id) + + const aIsEmpty = isEmptyValue(aValue) + const bIsEmpty = isEmptyValue(bValue) + + if (aIsEmpty && bIsEmpty) { + continue // Both empty, check next sort column + } + + if (aIsEmpty || bIsEmpty) { + const emptyPosition = sortEmpty === 'last' ? 1 : -1 + sortInt = aIsEmpty ? emptyPosition : -emptyPosition + return sortInt + } + } + // Backward compatibility: handle sortUndefined if sortEmpty not set + else if (sortUndefined) { const aValue = rowA.getValue(sortEntry.id) const bValue = rowB.getValue(sortEntry.id) diff --git a/packages/table-core/tests/sortEmpty.test.ts b/packages/table-core/tests/sortEmpty.test.ts new file mode 100644 index 0000000000..a319d0ca9b --- /dev/null +++ b/packages/table-core/tests/sortEmpty.test.ts @@ -0,0 +1,290 @@ +import { describe, expect, it } from 'vitest' +import { + createColumnHelper, + getCoreRowModel, + getSortedRowModel, + createTable, +} from '../src' + +type Product = { + id: number + name: string + price: number | null | undefined + description: string +} + +const columnHelper = createColumnHelper() + +const testData: Product[] = [ + { id: 1, name: 'Product A', price: 100, description: 'Description A' }, + { id: 2, name: 'Product B', price: null, description: '' }, + { id: 3, name: 'Product C', price: 50, description: 'Description C' }, + { id: 4, name: 'Product D', price: undefined, description: ' ' }, + { id: 5, name: 'Product E', price: 200, description: 'Description E' }, +] + +describe('sortEmpty option', () => { + describe('sortEmpty: "last"', () => { + const columns = [ + columnHelper.accessor('id', { header: 'ID' }), + columnHelper.accessor('name', { header: 'Name' }), + columnHelper.accessor('price', { + header: 'Price', + sortEmpty: 'last', + }), + columnHelper.accessor('description', { + header: 'Description', + sortEmpty: 'last', + isEmptyValue: (value) => !value || (typeof value === 'string' && value.trim() === ''), + }), + ] + + it('should sort null and undefined to bottom when sortEmpty is "last"', () => { + const table = createTable({ + data: testData, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'price', desc: false }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const prices = sortedRows.map(row => row.original.price) + + expect(prices).toEqual([50, 100, 200, null, undefined]) + }) + + it('should respect custom isEmptyValue function', () => { + const table = createTable({ + data: testData, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'description', desc: false }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const descriptions = sortedRows.map(row => row.original.description) + + expect(descriptions).toEqual([ + 'Description A', + 'Description C', + 'Description E', + '', // empty string + ' ', // whitespace only + ]) + }) + + it('should handle descending sort with sortEmpty', () => { + const table = createTable({ + data: testData, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'price', desc: true }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const prices = sortedRows.map(row => row.original.price) + + // Empty values still at bottom even in desc sort + expect(prices).toEqual([200, 100, 50, null, undefined]) + }) + }) + + describe('sortEmpty: "first"', () => { + const columns = [ + columnHelper.accessor('id', { header: 'ID' }), + columnHelper.accessor('name', { header: 'Name' }), + columnHelper.accessor('price', { + header: 'Price', + sortEmpty: 'first', + }), + ] + + it('should sort null and undefined to top when sortEmpty is "first"', () => { + const table = createTable({ + data: testData, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'price', desc: false }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const prices = sortedRows.map(row => row.original.price) + + expect(prices).toEqual([null, undefined, 50, 100, 200]) + }) + + it('should handle descending sort with sortEmpty: "first"', () => { + const table = createTable({ + data: testData, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'price', desc: true }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const prices = sortedRows.map(row => row.original.price) + + // Empty values still at top even in desc sort + expect(prices).toEqual([null, undefined, 200, 100, 50]) + }) + }) + + describe('backward compatibility with sortUndefined', () => { + const legacyColumns = [ + columnHelper.accessor('id', { header: 'ID' }), + columnHelper.accessor('price', { + header: 'Price', + sortUndefined: 'last', + }), + ] + + it('should maintain backward compatibility with sortUndefined', () => { + const table = createTable({ + data: testData, + columns: legacyColumns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'price', desc: false }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const prices = sortedRows.map(row => row.original.price) + + // sortUndefined only affects undefined, not null + // null should be sorted normally, undefined at the end + expect(prices[prices.length - 1]).toBeUndefined() + expect(prices.includes(null)).toBe(true) + }) + }) + + describe('sortEmpty takes precedence over sortUndefined', () => { + const mixedColumns = [ + columnHelper.accessor('id', { header: 'ID' }), + columnHelper.accessor('price', { + header: 'Price', + sortEmpty: 'first', + sortUndefined: 'last', // This should be ignored + }), + ] + + it('should use sortEmpty when both sortEmpty and sortUndefined are specified', () => { + const table = createTable({ + data: testData, + columns: mixedColumns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'price', desc: false }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const prices = sortedRows.map(row => row.original.price) + + // sortEmpty: 'first' should take precedence + expect(prices).toEqual([null, undefined, 50, 100, 200]) + }) + }) + + describe('default isEmptyValue behavior', () => { + const defaultColumns = [ + columnHelper.accessor('id', { header: 'ID' }), + columnHelper.accessor('price', { + header: 'Price', + sortEmpty: 'last', + }), + ] + + it('should use default isEmptyValue function (null, undefined, empty string)', () => { + const dataWithEmptyString = [ + ...testData, + { id: 6, name: 'Product F', price: '' as unknown as number, description: 'Description F' }, + ] + + const table = createTable({ + data: dataWithEmptyString, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'price', desc: false }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const prices = sortedRows.map(row => row.original.price) + + // Default isEmptyValue should treat '', null, undefined as empty + expect(prices).toEqual([50, 100, 200, null, undefined, '']) + }) + }) + + describe('sortEmpty: false (disabled)', () => { + const disabledColumns = [ + columnHelper.accessor('id', { header: 'ID' }), + columnHelper.accessor('price', { + header: 'Price', + sortEmpty: false, + }), + ] + + it('should sort normally when sortEmpty is false', () => { + const table = createTable({ + data: testData, + columns: disabledColumns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + sorting: [{ id: 'price', desc: false }], + }, + onStateChange: () => {}, + renderFallbackValue: null, + }) + + const sortedRows = table.getSortedRowModel().rows + const prices = sortedRows.map(row => row.original.price) + + // Should use normal sorting behavior (null/undefined treated as values) + // The exact order depends on the sorting function implementation + expect(prices.length).toBe(5) + expect(prices.includes(50)).toBe(true) + expect(prices.includes(100)).toBe(true) + expect(prices.includes(200)).toBe(true) + expect(prices.includes(null)).toBe(true) + expect(prices.includes(undefined)).toBe(true) + }) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e57f437917..7756073df8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 11.1.2(size-limit@11.1.2) '@tanstack/config': specifier: ^0.6.0 - version: 0.6.0(@types/node@20.11.30)(esbuild@0.20.2)(rollup@4.13.0)(typescript@5.4.3)(vite@5.4.19) + version: 0.6.0(@types/node@20.11.30)(esbuild@0.25.4)(rollup@4.13.0)(typescript@5.4.3)(vite@5.4.19) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.4.2(vitest@1.4.0) @@ -82,7 +82,7 @@ importers: version: 4.0.0-alpha.8 prettier-plugin-svelte: specifier: ^3.2.2 - version: 3.2.2(prettier@4.0.0-alpha.8)(svelte@3.59.2) + version: 3.2.2(prettier@4.0.0-alpha.8)(svelte@4.2.20) rimraf: specifier: ^5.0.5 version: 5.0.5 @@ -94,7 +94,7 @@ importers: version: 0.3.1 rollup-plugin-svelte: specifier: ^7.2.0 - version: 7.2.0(rollup@4.13.0)(svelte@3.59.2) + version: 7.2.0(rollup@4.13.0)(svelte@4.2.20) rollup-plugin-visualizer: specifier: ^5.12.0 version: 5.12.0(rollup@4.13.0) @@ -1853,6 +1853,40 @@ importers: specifier: ^5.3.2 version: 5.4.19(@types/node@20.11.30)(less@4.2.0)(sass@1.71.1)(terser@5.29.1) + examples/react/excel-sorting: + dependencies: + '@faker-js/faker': + specifier: ^8.4.1 + version: 8.4.1 + '@tanstack/react-table': + specifier: ^8.21.3 + version: link:../../../packages/react-table + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@rollup/plugin-replace': + specifier: ^5.0.7 + version: 5.0.7(rollup@4.13.0) + '@types/react': + specifier: ^18.3.3 + version: 18.3.22 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.22) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.5.0(vite@5.4.19) + typescript: + specifier: 5.4.5 + version: 5.4.5 + vite: + specifier: ^5.3.2 + version: 5.4.19(@types/node@20.11.30)(less@4.2.0)(sass@1.71.1)(terser@5.29.1) + examples/react/expanding: dependencies: '@faker-js/faker': @@ -2160,7 +2194,7 @@ importers: version: 11.11.0 '@emotion/babel-plugin-jsx-pragmatic': specifier: ^0.2.1 - version: 0.2.1(@babel/core@7.24.3) + version: 0.2.1(@babel/core@7.27.1) '@faker-js/faker': specifier: ^8.4.1 version: 8.4.1 @@ -3728,7 +3762,7 @@ packages: typescript: 5.4.5 vite: 5.4.19(@types/node@20.11.30)(less@4.2.0)(sass@1.71.1)(terser@5.29.1) watchpack: 2.4.0 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) webpack-dev-middleware: 6.1.2(webpack@5.94.0) webpack-dev-server: 4.15.1(webpack@5.94.0) webpack-merge: 5.10.0 @@ -3764,7 +3798,7 @@ packages: dependencies: '@angular-devkit/architect': 0.1703.17 rxjs: 7.8.1 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) webpack-dev-server: 4.15.1(webpack@5.94.0) transitivePeerDependencies: - chokidar @@ -4882,6 +4916,16 @@ packages: '@babel/helper-plugin-utils': 7.24.0 dev: true + /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.27.1): + resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.24.0 + dev: true + /@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1): resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} @@ -6629,13 +6673,13 @@ packages: tslib: 2.6.2 dev: false - /@emotion/babel-plugin-jsx-pragmatic@0.2.1(@babel/core@7.24.3): + /@emotion/babel-plugin-jsx-pragmatic@0.2.1(@babel/core@7.27.1): resolution: {integrity: sha512-xy1SlgEJygAAIvIuC2idkGKJYa6v5iwoyILkvNKgk347bV+IImXrUat5Z86EmLGyWhEoTplVT9EHqTnHZG4HFw==} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.3) + '@babel/core': 7.27.1 + '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.27.1) dev: true /@emotion/babel-plugin@11.11.0: @@ -8174,7 +8218,7 @@ packages: dependencies: '@angular/compiler-cli': 17.3.12(@angular/compiler@17.3.12)(typescript@5.4.5) typescript: 5.4.5 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /@nodelib/fs.scandir@2.1.5: @@ -8819,7 +8863,7 @@ packages: rollup: 4.13.0 dev: true - /@rollup/plugin-json@6.1.0(rollup@4.13.0): + /@rollup/plugin-json@6.1.0(rollup@4.41.1): resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -8828,8 +8872,8 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.13.0) - rollup: 4.13.0 + '@rollup/pluginutils': 5.1.0(rollup@4.41.1) + rollup: 4.41.1 dev: true /@rollup/plugin-node-resolve@15.2.3(rollup@4.13.0): @@ -8850,6 +8894,24 @@ packages: rollup: 4.13.0 dev: true + /@rollup/plugin-node-resolve@15.2.3(rollup@4.41.1): + resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.41.1) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.8 + rollup: 4.41.1 + dev: true + /@rollup/plugin-replace@5.0.5(rollup@4.13.0): resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} engines: {node: '>=14.0.0'} @@ -8916,6 +8978,21 @@ packages: rollup: 4.13.0 dev: true + /@rollup/pluginutils@5.1.0(rollup@4.41.1): + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.7 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 4.41.1 + dev: true + /@rollup/rollup-android-arm-eabi@4.13.0: resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} cpu: [arm] @@ -9388,7 +9465,7 @@ packages: tslib: 2.6.2 dev: false - /@tanstack/config@0.6.0(@types/node@20.11.30)(esbuild@0.20.2)(rollup@4.13.0)(typescript@5.4.3)(vite@5.4.19): + /@tanstack/config@0.6.0(@types/node@20.11.30)(esbuild@0.25.4)(rollup@4.13.0)(typescript@5.4.3)(vite@5.4.19): resolution: {integrity: sha512-ndVPsyXWZFz3RcpRF7q5L4Ol5zY+m1H2lAiufw+J4BrV09042PETU2OZAREYz88ZcLtu6p+LZAHKltmqrL8gDg==} engines: {node: '>=18'} hasBin: true @@ -9398,7 +9475,7 @@ packages: chalk: 5.3.0 commander: 12.0.0 current-git-branch: 1.1.0 - esbuild-register: 3.5.0(esbuild@0.20.2) + esbuild-register: 3.5.0(esbuild@0.25.4) git-log-parser: 1.2.0 interpret: 3.1.1 jsonfile: 6.1.0 @@ -9792,11 +9869,11 @@ packages: /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/parser': 7.27.2 + '@babel/types': 7.27.1 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.5 + '@types/babel__traverse': 7.20.7 dev: true /@types/babel__generator@7.6.8: @@ -9812,12 +9889,6 @@ packages: '@babel/types': 7.24.0 dev: true - /@types/babel__traverse@7.20.5: - resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} - dependencies: - '@babel/types': 7.24.0 - dev: true - /@types/babel__traverse@7.20.7: resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} dependencies: @@ -10137,7 +10208,7 @@ packages: /@vitest/snapshot@1.4.0: resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} dependencies: - magic-string: 0.30.8 + magic-string: 0.30.17 pathe: 1.1.2 pretty-format: 29.7.0 dev: true @@ -10145,7 +10216,7 @@ packages: /@vitest/snapshot@1.6.1: resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} dependencies: - magic-string: 0.30.8 + magic-string: 0.30.17 pathe: 1.1.2 pretty-format: 29.7.0 dev: true @@ -10939,7 +11010,7 @@ packages: '@babel/core': 7.26.10 find-cache-dir: 4.0.0 schema-utils: 4.3.2 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /babel-plugin-add-module-exports@0.2.1: @@ -11754,7 +11825,7 @@ packages: normalize-path: 3.0.0 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /core-js-compat@3.36.1: @@ -11865,7 +11936,7 @@ packages: postcss-modules-values: 4.0.0(postcss@8.4.38) postcss-value-parser: 4.2.0 semver: 7.6.0 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /css-select@5.1.0: @@ -12426,13 +12497,13 @@ packages: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} dev: true - /esbuild-register@3.5.0(esbuild@0.20.2): + /esbuild-register@3.5.0(esbuild@0.25.4): resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==} peerDependencies: esbuild: '>=0.12 <1' dependencies: debug: 4.3.4 - esbuild: 0.20.2 + esbuild: 0.25.4 transitivePeerDependencies: - supports-color dev: true @@ -14486,7 +14557,7 @@ packages: dependencies: klona: 2.0.6 less: 4.2.0 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /less@4.2.0: @@ -14535,7 +14606,7 @@ packages: webpack-sources: optional: true dependencies: - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) webpack-sources: 3.3.0 dev: true @@ -14978,7 +15049,7 @@ packages: dependencies: schema-utils: 4.3.2 tapable: 2.2.2 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /minimalistic-assert@1.0.1: @@ -15232,8 +15303,8 @@ packages: optional: true dependencies: '@angular/compiler-cli': 17.3.12(@angular/compiler@17.3.12)(typescript@5.4.5) - '@rollup/plugin-json': 6.1.0(rollup@4.13.0) - '@rollup/plugin-node-resolve': 15.2.3(rollup@4.13.0) + '@rollup/plugin-json': 6.1.0(rollup@4.41.1) + '@rollup/plugin-node-resolve': 15.2.3(rollup@4.41.1) '@rollup/wasm-node': 4.41.1 ajv: 8.17.1 ansi-colors: 4.1.3 @@ -15258,7 +15329,7 @@ packages: typescript: 5.4.5 optionalDependencies: esbuild: 0.20.2 - rollup: 4.13.0 + rollup: 4.41.1 dev: true /nice-napi@1.0.2: @@ -16053,7 +16124,7 @@ packages: jiti: 1.21.0 postcss: 8.4.35 semver: 7.6.0 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) transitivePeerDependencies: - typescript dev: true @@ -16141,14 +16212,14 @@ packages: picocolors: 1.1.1 source-map-js: 1.2.1 - /prettier-plugin-svelte@3.2.2(prettier@4.0.0-alpha.8)(svelte@3.59.2): + /prettier-plugin-svelte@3.2.2(prettier@4.0.0-alpha.8)(svelte@4.2.20): resolution: {integrity: sha512-ZzzE/wMuf48/1+Lf2Ffko0uDa6pyCfgHV6+uAhtg2U0AAXGrhCSW88vEJNAkAxW5qyrFY1y1zZ4J8TgHrjW++Q==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 dependencies: prettier: 4.0.0-alpha.8 - svelte: 3.59.2 + svelte: 4.2.20 dev: true /prettier@3.5.3: @@ -16747,7 +16818,7 @@ packages: rollup: 2.x || 3.x || 4.x dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.13.0) - magic-string: 0.30.8 + magic-string: 0.30.17 rollup: 4.13.0 dev: true @@ -16766,7 +16837,7 @@ packages: - debug dev: true - /rollup-plugin-svelte@7.2.0(rollup@4.13.0)(svelte@3.59.2): + /rollup-plugin-svelte@7.2.0(rollup@4.13.0)(svelte@4.2.20): resolution: {integrity: sha512-Qvo5VNFQZtaI+sHSjcCIFDP+olfKVyslAoJIkL3DxuhUpNY5Ys0+hhxUY3kuEKt9BXFgkFJiiic/XRb07zdSbg==} engines: {node: '>=10'} peerDependencies: @@ -16776,7 +16847,7 @@ packages: '@rollup/pluginutils': 4.2.1 resolve.exports: 2.0.2 rollup: 4.13.0 - svelte: 3.59.2 + svelte: 4.2.20 dev: true /rollup-plugin-visualizer@5.12.0(rollup@4.13.0): @@ -16935,7 +17006,7 @@ packages: dependencies: neo-async: 2.6.2 sass: 1.71.1 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /sass@1.71.1: @@ -17463,7 +17534,7 @@ packages: resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==} hasBin: true dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 buffer-crc32: 0.2.13 minimist: 1.2.8 sander: 0.5.1 @@ -17486,7 +17557,7 @@ packages: dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.0 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /source-map-support@0.5.21: @@ -17886,7 +17957,7 @@ packages: '@babel/core': 7.24.3 '@types/pug': 2.0.10 detect-indent: 6.1.0 - magic-string: 0.30.8 + magic-string: 0.30.17 sorcery: 0.11.0 strip-indent: 3.0.0 svelte: 4.2.20 @@ -17955,7 +18026,7 @@ packages: yallist: 4.0.0 dev: true - /terser-webpack-plugin@5.3.14(esbuild@0.20.2)(webpack@5.94.0): + /terser-webpack-plugin@5.3.14(esbuild@0.25.4)(webpack@5.94.0): resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -17972,12 +18043,12 @@ packages: optional: true dependencies: '@jridgewell/trace-mapping': 0.3.25 - esbuild: 0.20.2 + esbuild: 0.25.4 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.39.2 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /terser@5.29.1: @@ -19075,7 +19146,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.3.2 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /webpack-dev-middleware@6.1.2(webpack@5.94.0): @@ -19092,7 +19163,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.3.2 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /webpack-dev-server@4.15.1(webpack@5.94.0): @@ -19136,7 +19207,7 @@ packages: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) webpack-dev-middleware: 5.3.4(webpack@5.94.0) ws: 8.16.0 transitivePeerDependencies: @@ -19171,14 +19242,14 @@ packages: optional: true dependencies: typed-assert: 1.0.9 - webpack: 5.94.0(esbuild@0.20.2) + webpack: 5.94.0(esbuild@0.25.4) dev: true /webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} dev: true - /webpack@5.94.0(esbuild@0.20.2): + /webpack@5.94.0(esbuild@0.25.4): resolution: {integrity: sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==} engines: {node: '>=10.13.0'} hasBin: true @@ -19208,7 +19279,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(esbuild@0.20.2)(webpack@5.94.0) + terser-webpack-plugin: 5.3.14(esbuild@0.25.4)(webpack@5.94.0) watchpack: 2.4.4 webpack-sources: 3.3.0 transitivePeerDependencies: