diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 00000000..3132a4a1 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,8 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +node_modules +/www/dataserver/data +.next +.env.local +dist +out +.contentlayer \ No newline at end of file diff --git a/examples/src/pages/tests/datasource/sortinfo-controlled.spec.ts b/examples/src/pages/tests/datasource/sortinfo-controlled.spec.ts index 3ccc2520..702cc8d7 100644 --- a/examples/src/pages/tests/datasource/sortinfo-controlled.spec.ts +++ b/examples/src/pages/tests/datasource/sortinfo-controlled.spec.ts @@ -20,6 +20,7 @@ export default test.describe.parallel('DataSource', () => { id: persons[0].id, indexInAll: 0, rowSelected: false, + rowDisabled: false, isGroupRow: false, selfLoaded: true, dataSourceHasGrouping: false, @@ -29,6 +30,7 @@ export default test.describe.parallel('DataSource', () => { id: persons[1].id, indexInAll: 1, rowSelected: false, + rowDisabled: false, isGroupRow: false, selfLoaded: true, dataSourceHasGrouping: false, @@ -54,6 +56,7 @@ export default test.describe.parallel('DataSource', () => { dataSourceHasGrouping: false, indexInAll: 0, rowSelected: false, + rowDisabled: false, // indexInGroup: 0, isGroupRow: false, selfLoaded: true, @@ -63,6 +66,7 @@ export default test.describe.parallel('DataSource', () => { data: persons[1], indexInAll: 1, rowSelected: false, + rowDisabled: false, // indexInGroup: 1, id: persons[1].id, isGroupRow: false, diff --git a/examples/src/pages/tests/table/props/active-row-index/default.page.tsx b/examples/src/pages/tests/table/props/active-row-index/default.page.tsx index 06602587..6dbd6ffe 100644 --- a/examples/src/pages/tests/table/props/active-row-index/default.page.tsx +++ b/examples/src/pages/tests/table/props/active-row-index/default.page.tsx @@ -55,6 +55,7 @@ export default function KeyboardNavigationForRows() { defaultActiveRowIndex={99} keyboardNavigation="row" domProps={{ + autoFocus: true, style: { height: 800, }, diff --git a/examples/src/pages/tests/table/props/active-row-index/small-grid-one-col.page.tsx b/examples/src/pages/tests/table/props/active-row-index/small-grid-one-col.page.tsx new file mode 100644 index 00000000..d9d4c1b5 --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/small-grid-one-col.page.tsx @@ -0,0 +1,93 @@ +import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; + +import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; +import * as React from 'react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + country: string; + city: string; + currency: string; + + email: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + hobby: string; + salary: number; + age: number; +}; +const dataSource: Developer[] = [ + { + id: 1, + firstName: 'John', + lastName: 'Doe', + country: 'USA', + city: 'New York', + currency: '$', + email: 'John@doe.com', + preferredLanguage: 'JavaScript', + stack: 'frontend', + canDesign: 'yes', + hobby: 'Coding', + salary: 50000, + age: 30, + }, + { + id: 2, + firstName: 'Jane', + lastName: 'Doe', + country: 'USA', + city: 'San Francisco', + currency: '$', + email: 'Jane@doe.com', + preferredLanguage: 'Ruby', + stack: 'backend', + canDesign: 'no', + hobby: 'Photography', + salary: 60000, + age: 35, + }, + { + id: 3, + firstName: 'Bob', + lastName: 'Doe', + country: 'Canada', + city: 'Toronto', + currency: '$', + email: 'Bob@doe.com', + preferredLanguage: 'Ruby', + stack: 'frontend', + canDesign: 'no', + hobby: 'Photography', + salary: 40000, + age: 28, + }, +]; + +const columns: InfiniteTablePropColumns = { + firstName: { field: 'firstName', defaultFlex: 1 }, +}; + +export default function KeyboardNavigationForRows() { + return ( + primaryKey="id" data={dataSource}> + + columns={columns} + keyboardNavigation="row" + defaultActiveRowIndex={2} + rowHeight={40} + columnDefaultWidth={120} + header={false} + domProps={{ + style: { + width: '80vw', + minHeight: 120, + }, + }} + /> + + ); +} diff --git a/examples/src/pages/tests/table/props/active-row-index/small-grid.page.tsx b/examples/src/pages/tests/table/props/active-row-index/small-grid.page.tsx new file mode 100644 index 00000000..5e0eb470 --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/small-grid.page.tsx @@ -0,0 +1,96 @@ +import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; + +import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; +import * as React from 'react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + country: string; + city: string; + currency: string; + + email: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + hobby: string; + salary: number; + age: number; +}; +const dataSource: Developer[] = [ + { + id: 1, + firstName: 'John', + lastName: 'Doe', + country: 'USA', + city: 'New York', + currency: '$', + email: 'John@doe.com', + preferredLanguage: 'JavaScript', + stack: 'frontend', + canDesign: 'yes', + hobby: 'Coding', + salary: 50000, + age: 30, + }, + { + id: 2, + firstName: 'Jane', + lastName: 'Doe', + country: 'USA', + city: 'San Francisco', + currency: '$', + email: 'Jane@doe.com', + preferredLanguage: 'Ruby', + stack: 'backend', + canDesign: 'no', + hobby: 'Photography', + salary: 60000, + age: 35, + }, + { + id: 3, + firstName: 'Bob', + lastName: 'Doe', + country: 'Canada', + city: 'Toronto', + currency: '$', + email: 'Bob@doe.com', + preferredLanguage: 'Ruby', + stack: 'frontend', + canDesign: 'no', + hobby: 'Photography', + salary: 40000, + age: 28, + }, +]; + +const columns: InfiniteTablePropColumns = { + preferredLanguage: { field: 'preferredLanguage' }, + country: { field: 'country' }, + firstName: { field: 'firstName' }, + id: { field: 'id' }, +}; + +export default function KeyboardNavigationForRows() { + return ( + primaryKey="id" data={dataSource}> + + columns={columns} + keyboardNavigation="row" + defaultActiveRowIndex={2} + rowHeight={40} + columnDefaultWidth={120} + header={false} + domProps={{ + style: { + width: 800, + minHeight: 120, + }, + }} + /> + + ); +} diff --git a/examples/src/pages/tests/table/props/active-row-index/small-grid.spec.ts b/examples/src/pages/tests/table/props/active-row-index/small-grid.spec.ts new file mode 100644 index 00000000..977a1d4b --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/small-grid.spec.ts @@ -0,0 +1,26 @@ +import { getScrollerLocator } from '@examples/pages/tests/testUtils'; + +import { test, expect } from '@testing'; + +export default test.describe('No scrollbar grid', () => { + test('should not have scrollbars induced by the active row index selector', async ({ + page, + }) => { + await page.waitForInfinite(); + const scroller = await getScrollerLocator({ page }); + + const { scrollWidth, scrollHeight, offsetWidth } = await scroller.evaluate( + (node) => { + return { + scrollWidth: (node as HTMLElement).scrollWidth, + scrollHeight: (node as HTMLElement).scrollHeight, + offsetWidth: (node as HTMLElement).offsetWidth, + }; + }, + ); + + expect(scrollHeight).toEqual(120); + expect(scrollWidth).toEqual(800); + expect(offsetWidth).toEqual(800); + }); +}); diff --git a/examples/src/pages/tests/table/props/active-row-index/with-disabled-controlled.page.tsx b/examples/src/pages/tests/table/props/active-row-index/with-disabled-controlled.page.tsx new file mode 100644 index 00000000..f86584c9 --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/with-disabled-controlled.page.tsx @@ -0,0 +1,114 @@ +import { + InfiniteTable, + DataSource, + DataSourceData, + type InfiniteTablePropColumns, + DataSourceApi, + RowDisabledStateObject, +} from '@infinite-table/infinite-react'; + +import * as React from 'react'; +import { useState } from 'react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + country: string; + city: string; + currency: string; + + email: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + hobby: string; + salary: number; + age: number; +}; + +const dataSource: DataSourceData = ({}) => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers10-sql`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + preferredLanguage: { field: 'preferredLanguage' }, + id: { field: 'id' }, + country: { field: 'country' }, + salary: { + field: 'salary', + type: 'number', + }, + age: { field: 'age' }, + canDesign: { field: 'canDesign' }, + firstName: { field: 'firstName' }, + stack: { field: 'stack' }, + + hobby: { field: 'hobby' }, + city: { field: 'city' }, + currency: { field: 'currency' }, +}; + +export default function KeyboardNavigationForRows() { + const [activeRowIndex, setActiveRowIndex] = useState(0); + + const [rowDisabledState, setRowDisabledState] = + useState({ + enabledRows: true, + disabledRows: [3, 5, 6], + }); + + const [dataSourceApi, setDataSourceApi] = + useState>(); + + (globalThis as any).activeRowIndex = activeRowIndex; + return ( + <> +
+ + + +
+ + onReady={setDataSourceApi} + primaryKey="id" + data={dataSource} + selectionMode="multi-row" + rowDisabledState={rowDisabledState} + onRowDisabledStateChange={(s) => setRowDisabledState(s.getState())} + > + + columns={columns} + activeRowIndex={activeRowIndex} + onActiveRowIndexChange={setActiveRowIndex} + keyboardNavigation="row" + domProps={{ + autoFocus: true, + style: { + height: 800, + }, + }} + /> + + + ); +} diff --git a/examples/src/pages/tests/table/props/active-row-index/with-disabled-controlled.spec.ts b/examples/src/pages/tests/table/props/active-row-index/with-disabled-controlled.spec.ts new file mode 100644 index 00000000..e249cfcb --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/with-disabled-controlled.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@testing'; + +export default test.describe('Disabled rows controlled', () => { + test('should work fine', async ({ page, rowModel }) => { + await page.waitForInfinite(); + + expect(await rowModel.isRowDisabled(0)).toBe(false); + + await page.getByRole('button', { name: 'Disable All Rows' }).click(); + + expect(await rowModel.isRowDisabled(0)).toBe(true); + expect(await rowModel.isRowDisabled(1)).toBe(true); + expect(await rowModel.isRowDisabled(2)).toBe(true); + expect(await rowModel.isRowDisabled(3)).toBe(true); + + await page.getByRole('button', { name: 'Enable All Rows' }).click(); + + expect(await rowModel.isRowDisabled(0)).toBe(false); + expect(await rowModel.isRowDisabled(1)).toBe(false); + expect(await rowModel.isRowDisabled(2)).toBe(false); + expect(await rowModel.isRowDisabled(3)).toBe(false); + + const toggleButton = page.getByRole('button', { name: 'Toggle Row 1' }); + await toggleButton.click(); + expect(await rowModel.isRowDisabled(1)).toBe(true); + + await toggleButton.click(); + expect(await rowModel.isRowDisabled(1)).toBe(false); + }); +}); diff --git a/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows-as-fn.page.tsx b/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows-as-fn.page.tsx new file mode 100644 index 00000000..a12ecd13 --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows-as-fn.page.tsx @@ -0,0 +1,81 @@ +import { + InfiniteTable, + DataSource, + DataSourceData, + type InfiniteTablePropColumns, +} from '@infinite-table/infinite-react'; + +import * as React from 'react'; +import { useState } from 'react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + country: string; + city: string; + currency: string; + + email: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + hobby: string; + salary: number; + age: number; +}; + +const dataSource: DataSourceData = ({}) => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers10-sql`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + preferredLanguage: { field: 'preferredLanguage' }, + id: { field: 'id' }, + country: { field: 'country' }, + salary: { + field: 'salary', + type: 'number', + }, + age: { field: 'age' }, + canDesign: { field: 'canDesign' }, + firstName: { field: 'firstName' }, + stack: { field: 'stack' }, + + hobby: { field: 'hobby' }, + city: { field: 'city' }, + currency: { field: 'currency' }, +}; + +export default function KeyboardNavigationForRows() { + const [activeRowIndex, setActiveRowIndex] = useState(0); + + (globalThis as any).activeRowIndex = activeRowIndex; + return ( + + primaryKey="id" + data={dataSource} + selectionMode="multi-row" + isRowDisabled={(rowInfo) => + rowInfo.indexInAll === 3 || + rowInfo.indexInAll === 5 || + rowInfo.indexInAll === 6 + } + > + + columns={columns} + activeRowIndex={activeRowIndex} + onActiveRowIndexChange={setActiveRowIndex} + keyboardNavigation="row" + domProps={{ + autoFocus: true, + style: { + height: 800, + }, + }} + /> + + ); +} diff --git a/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows-as-fn.spec.ts b/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows-as-fn.spec.ts new file mode 100644 index 00000000..0d891477 --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows-as-fn.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@testing'; + +export default test.describe('Disabled rows using isRowDisabled fn', () => { + test('should work fine', async ({ page, rowModel }) => { + await page.waitForInfinite(); + + expect(await rowModel.isRowDisabled(0)).toBe(false); + expect(await rowModel.isRowDisabled(3)).toBe(true); + expect(await rowModel.isRowDisabled(5)).toBe(true); + expect(await rowModel.isRowDisabled(6)).toBe(true); + + await rowModel.clickRow(2); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 2, + ); + + // clicking a disabled row should not change the active row + await rowModel.clickRow(3); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 2, + ); + + // arrow down should skip over disabled row + await page.keyboard.press('ArrowDown'); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 4, + ); + + // again, arrow down should skip over disabled rows + await page.keyboard.press('ArrowDown'); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 7, + ); + + // arrow up should skip over disabled rows + await page.keyboard.press('ArrowUp'); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 4, + ); + + // again, arrow up should skip over disabled row + await page.keyboard.press('ArrowUp'); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 2, + ); + }); +}); diff --git a/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows.page.tsx b/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows.page.tsx new file mode 100644 index 00000000..40d49d9c --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows.page.tsx @@ -0,0 +1,84 @@ +import { + InfiniteTable, + DataSource, + DataSourceData, + type InfiniteTablePropColumns, +} from '@infinite-table/infinite-react'; + +import * as React from 'react'; +import { useState } from 'react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + country: string; + city: string; + currency: string; + + email: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + hobby: string; + salary: number; + age: number; +}; + +const dataSource: DataSourceData = ({}) => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers10-sql`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + preferredLanguage: { field: 'preferredLanguage' }, + id: { field: 'id' }, + country: { field: 'country' }, + salary: { + field: 'salary', + type: 'number', + }, + age: { field: 'age' }, + canDesign: { field: 'canDesign' }, + firstName: { field: 'firstName' }, + stack: { field: 'stack' }, + + hobby: { field: 'hobby' }, + city: { field: 'city' }, + currency: { field: 'currency' }, +}; + +export default function KeyboardNavigationForRows() { + const [activeRowIndex, setActiveRowIndex] = useState(0); + + (globalThis as any).activeRowIndex = activeRowIndex; + return ( + + primaryKey="id" + data={dataSource} + selectionMode="multi-row" + rowDisabledState={{ + enabledRows: true, + disabledRows: [3, 5, 6], + }} + defaultRowSelection={{ + selectedRows: [5, 6, 7, 8], + defaultSelection: false, + }} + > + + columns={columns} + activeRowIndex={activeRowIndex} + onActiveRowIndexChange={setActiveRowIndex} + keyboardNavigation="row" + domProps={{ + autoFocus: true, + style: { + height: 800, + }, + }} + /> + + ); +} diff --git a/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows.spec.ts b/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows.spec.ts new file mode 100644 index 00000000..9a718bbc --- /dev/null +++ b/examples/src/pages/tests/table/props/active-row-index/with-disabled-rows.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@testing'; + +export default test.describe('Disabled rows', () => { + test('should work fine', async ({ page, rowModel }) => { + await page.waitForInfinite(); + + expect(await rowModel.isRowDisabled(0)).toBe(false); + expect(await rowModel.isRowDisabled(3)).toBe(true); + + await rowModel.clickRow(2); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 2, + ); + + // clicking a disabled row should not change the active row + await rowModel.clickRow(3); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 2, + ); + + // arrow down should skip over disabled row + await page.keyboard.press('ArrowDown'); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 4, + ); + + // again, arrow down should skip over disabled rows + await page.keyboard.press('ArrowDown'); + expect(await page.evaluate(() => (globalThis as any).activeRowIndex)).toBe( + 7, + ); + }); +}); diff --git a/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.spec.ts b/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.spec.ts index a8dfc680..9966e5fd 100644 --- a/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.spec.ts +++ b/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.spec.ts @@ -41,6 +41,7 @@ export default test.describe.parallel('Lazy Load Grouped Data', () => { }); await cell.clickDetailIcon(); + await page.waitForTimeout(50); CALL_COUNT = 2; dataSourceCalls = await getCalls({ page }); diff --git a/examples/src/pages/tests/table/utils/MultiRowSelector.spec.ts b/examples/src/pages/tests/table/utils/MultiRowSelector.spec.ts index 7b3aa406..15166075 100644 --- a/examples/src/pages/tests/table/utils/MultiRowSelector.spec.ts +++ b/examples/src/pages/tests/table/utils/MultiRowSelector.spec.ts @@ -30,6 +30,7 @@ export default test.describe.parallel('MultiRowSelector', () => { const selector = new MultiRowSelector({ getIdForIndex: (index) => `${index}`, + isRowDisabledAt: (_index) => false, }); selector.rowSelectionState = rowSelection; @@ -91,6 +92,7 @@ export default test.describe.parallel('MultiRowSelector', () => { const selector = new MultiRowSelector({ getIdForIndex: (index) => `${index}`, + isRowDisabledAt: (_index) => false, }); selector.rowSelectionState = rowSelection; @@ -143,6 +145,7 @@ export default test.describe.parallel('MultiRowSelector', () => { const selector = new MultiRowSelector({ getIdForIndex: (index) => `${index}`, + isRowDisabledAt: (_index) => false, }); selector.rowSelectionState = rowSelection; @@ -211,6 +214,7 @@ export default test.describe.parallel('MultiRowSelector', () => { const selector = new MultiRowSelector({ getIdForIndex: (index) => `${index}`, + isRowDisabledAt: (_index) => false, }); selector.rowSelectionState = rowSelection; @@ -270,6 +274,7 @@ export default test.describe.parallel('MultiRowSelector', () => { const selector = new MultiRowSelector({ getIdForIndex: (index) => `${index}`, + isRowDisabledAt: (_index) => false, }); selector.rowSelectionState = rowSelection; @@ -343,6 +348,7 @@ export default test.describe.parallel('MultiRowSelector', () => { const selector = new MultiRowSelector({ getIdForIndex: (index) => `${index}`, + isRowDisabledAt: (_index) => false, }); selector.rowSelectionState = rowSelection; diff --git a/examples/src/pages/tests/testUtils/RowTestingModel.ts b/examples/src/pages/tests/testUtils/RowTestingModel.ts index 95244f63..f2d131a7 100644 --- a/examples/src/pages/tests/testUtils/RowTestingModel.ts +++ b/examples/src/pages/tests/testUtils/RowTestingModel.ts @@ -11,6 +11,7 @@ import { getSelectedRowIds, isNodeExpanded, isNodeGroupRow, + isRowDisabled, toggleGroupRow, } from '.'; @@ -87,6 +88,15 @@ export class RowTestingModel { return await isNodeGroupRow(node); } + async isRowDisabled(rowIndex: number) { + const node = this.getCellLocator({ + rowIndex, + colIndex: 0, + }); + + return await isRowDisabled(node); + } + async getCellComputedStylePropertyValue( cellLocation: CellLocation, propertyName: string, diff --git a/examples/src/pages/tests/testUtils/index.ts b/examples/src/pages/tests/testUtils/index.ts index 6522cec0..c25c38a5 100644 --- a/examples/src/pages/tests/testUtils/index.ts +++ b/examples/src/pages/tests/testUtils/index.ts @@ -112,12 +112,18 @@ export const getCellNodeLocator = ( export const isNodeExpanded = async (node: Locator) => { return await node.evaluate((n) => - n.classList.contains('.InfiniteColumnCell--group-row-expanded'), + n.classList.contains('InfiniteColumnCell--group-row-expanded'), ); }; export const isNodeGroupRow = async (node: Locator) => { return await node.evaluate((n) => - n.classList.contains('.InfiniteColumnCell--group-row'), + n.classList.contains('InfiniteColumnCell--group-row'), + ); +}; + +export const isRowDisabled = async (node: Locator) => { + return await node.evaluate((n) => + n.classList.contains('InfiniteColumnCell--disabled'), ); }; @@ -233,6 +239,10 @@ export const getScrollPosition = async ({ page }: { page: Page }) => { }); }; +export const getScrollerLocator = async ({ page }: { page: Page }) => { + return page.locator('.InfiniteBody > :first-child'); +}; + export const getMenuCellLocatorForKey = ( params: { rowKey: string | null; colName?: string }, { page }: { page: Page }, diff --git a/source/package-lock.json b/source/package-lock.json index 726fc750..e39349e0 100644 --- a/source/package-lock.json +++ b/source/package-lock.json @@ -1,6 +1,6 @@ { "name": "@infinite-table/infinite-react", - "version": "4.3.7", + "version": "4.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/source/package.json b/source/package.json index c2cf56fb..97eea965 100644 --- a/source/package.json +++ b/source/package.json @@ -25,7 +25,7 @@ "bugs": { "url": "https://github.com/infinite-table/infinite-react/issues" }, - "version": "4.3.7", + "version": "4.4.1", "main": "index.js", "module": "index.mjs", "typings": "index.d.ts", diff --git a/source/src/components/DataSource/RowDisabledState.ts b/source/src/components/DataSource/RowDisabledState.ts new file mode 100644 index 00000000..647154eb --- /dev/null +++ b/source/src/components/DataSource/RowDisabledState.ts @@ -0,0 +1,73 @@ +import { BooleanCollectionState } from './BooleanCollectionState'; +import { RowDisabledStateObject } from './types'; + +export class RowDisabledState extends BooleanCollectionState< + RowDisabledStateObject, + KeyType +> { + constructor( + state: RowDisabledStateObject | RowDisabledState, + ) { + //@ts-ignore + super(state); + } + public getState(): RowDisabledStateObject { + const enabledRows = this.allPositive + ? true + : [...(this.positiveMap?.keys() ?? [])]; + + const disabledRows = this.allNegative + ? true + : [...(this.negativeMap?.keys() ?? [])]; + + return { + enabledRows, + disabledRows, + } as RowDisabledStateObject; + } + + getPositiveFromState(state: RowDisabledStateObject) { + return state.enabledRows; + } + getNegativeFromState(state: RowDisabledStateObject) { + return state.disabledRows; + } + + public areAllDisabled() { + return this.areAllNegative(); + } + public areAllEnabled() { + return this.areAllPositive(); + } + + public disableAll() { + this.makeAllNegative(); + } + + public enableAll() { + this.makeAllPositive(); + } + + public isRowEnabled = (key: KeyType) => { + return !!this.isItemPositive(key); + }; + + public isRowDisabled(key: KeyType) { + return !this.isRowEnabled(key); + } + + public setRowEnabled(key: KeyType, enabled: boolean) { + return this.setItemValue(key, enabled); + } + + public disableRow(key: KeyType) { + this.setRowEnabled(key, false); + } + public enableRow(key: KeyType) { + this.setRowEnabled(key, true); + } + + public toggleRow(key: KeyType) { + this.toggleItem(key); + } +} diff --git a/source/src/components/DataSource/getDataSourceApi.ts b/source/src/components/DataSource/getDataSourceApi.ts index 0690c9cc..726c1cb0 100644 --- a/source/src/components/DataSource/getDataSourceApi.ts +++ b/source/src/components/DataSource/getDataSourceApi.ts @@ -9,7 +9,7 @@ import { raf } from '../../utils/raf'; import { InfiniteTableRowInfo } from '../InfiniteTable/types'; import { DataSourceCache } from './DataSourceCache'; import { getRowInfoAt, getRowInfoArray } from './dataSourceGetters'; - +import { RowDisabledState } from './RowDisabledState'; import { DataSourceInsertParam } from './types'; type GetDataSourceApiParam = { @@ -342,6 +342,86 @@ class DataSourceApiImpl implements DataSourceApi { this.actions.sortInfo = sortInfo; return; }; + + isRowDisabledAt = (rowIndex: number) => { + const rowInfo = this.getRowInfoByIndex(rowIndex); + + return rowInfo?.rowDisabled ?? false; + }; + + isRowDisabled = (primaryKey: any) => { + const rowInfo = this.getRowInfoByPrimaryKey(primaryKey); + + return rowInfo?.rowDisabled ?? false; + }; + + setRowEnabledAt = (rowIndex: number, enabled: boolean) => { + const currentRowDisabledState = this.getState().rowDisabledState; + + const rowDisabledState = currentRowDisabledState + ? new RowDisabledState(currentRowDisabledState) + : new RowDisabledState({ + enabledRows: true, + disabledRows: [], + }); + + const rowInfo = this.getRowInfoByIndex(rowIndex); + if (!rowInfo) { + return; + } + rowDisabledState.setRowEnabled(rowInfo.id, enabled); + + this.actions.rowDisabledState = rowDisabledState; + }; + + setRowEnabled = (primaryKey: any, enabled: boolean) => { + const rowInfo = this.getRowInfoByPrimaryKey(primaryKey); + if (!rowInfo) { + return; + } + this.setRowEnabledAt(rowInfo.indexInAll, enabled); + }; + + enableAllRows = () => { + const currentRowDisabledState = this.getState().rowDisabledState; + + if (!currentRowDisabledState) { + this.actions.rowDisabledState = new RowDisabledState({ + enabledRows: true, + disabledRows: [], + }); + return; + } + + const rowDisabledState = new RowDisabledState(currentRowDisabledState); + rowDisabledState.enableAll(); + this.actions.rowDisabledState = rowDisabledState; + }; + disableAllRows = () => { + const currentRowDisabledState = this.getState().rowDisabledState; + + if (!currentRowDisabledState) { + this.actions.rowDisabledState = new RowDisabledState({ + disabledRows: true, + enabledRows: [], + }); + return; + } + + const rowDisabledState = new RowDisabledState(currentRowDisabledState); + rowDisabledState.disableAll(); + this.actions.rowDisabledState = rowDisabledState; + }; + + areAllRowsEnabled = () => { + const rowDisabledState = this.getState().rowDisabledState; + return rowDisabledState ? rowDisabledState.areAllEnabled() : true; + }; + + areAllRowsDisabled = () => { + const rowDisabledState = this.getState().rowDisabledState; + return rowDisabledState ? rowDisabledState.areAllDisabled() : false; + }; } export function getCacheAffectedParts(state: DataSourceState): { diff --git a/source/src/components/DataSource/index.tsx b/source/src/components/DataSource/index.tsx index 6b86d66f..fc12cf9d 100644 --- a/source/src/components/DataSource/index.tsx +++ b/source/src/components/DataSource/index.tsx @@ -28,6 +28,7 @@ import { InfiniteTableRowInfo } from '../InfiniteTable'; // import { DataSourceCmp } from './DataSourceCmp'; import { useDataSourceInternal } from './privateHooks/useDataSource'; import { DataSourceProps } from './types'; +import { RowDisabledState } from './RowDisabledState'; const { // ManagedComponentContextProvider: ManagedDataSourceContextProvider, @@ -102,6 +103,7 @@ export { GroupRowsState, RowSelectionState, CellSelectionState, + RowDisabledState, multisort, defaultFilterTypes as filterTypes, useRowInfoReducers, diff --git a/source/src/components/DataSource/state/getInitialState.ts b/source/src/components/DataSource/state/getInitialState.ts index eb0cabe1..7bed3612 100644 --- a/source/src/components/DataSource/state/getInitialState.ts +++ b/source/src/components/DataSource/state/getInitialState.ts @@ -6,6 +6,7 @@ import { DataSourcePropOnCellSelectionChange_MultiCell, DataSourcePropOnCellSelectionChange_SingleCell, DataSourceRowInfoReducer, + RowDisabledStateObject, RowSelectionState, } from '..'; import { dbg } from '../../../utils/debug'; @@ -47,6 +48,7 @@ import { } from '../types'; import { normalizeSortInfo } from './normalizeSortInfo'; +import { RowDisabledState } from '../RowDisabledState'; const DataSourceLogger = dbg('DataSource') as DebugLogger; @@ -101,6 +103,7 @@ export function initSetupState(): DataSourceSetupState { propsCache: new Map, WeakMap>([ ['sortInfo', new WeakMap()], + ['rowDisabledState', new WeakMap()], ]), rowInfoReducerResults: undefined, @@ -202,6 +205,35 @@ export const forwardProps = ( }, batchOperationDelay: 1, isRowSelected: 1, + isRowDisabled: 1, + rowDisabledState: ( + rowDisabledState: + | RowDisabledState + | RowDisabledStateObject + | undefined, + ) => { + if (!rowDisabledState) { + return null; + } + if (rowDisabledState instanceof RowDisabledState) { + return rowDisabledState; + } + + const wMap = setupState.propsCache.get('rowDisabledState') ?? weakMap; + + let cachedRowDisabledState = wMap.get( + rowDisabledState, + ) as RowDisabledState; + + if (!cachedRowDisabledState) { + cachedRowDisabledState = new RowDisabledState(rowDisabledState); + wMap.set(rowDisabledState, cachedRowDisabledState); + } + + rowDisabledState = cachedRowDisabledState; + + return rowDisabledState ?? null; + }, onDataArrayChange: 1, onDataMutations: 1, aggregationReducers: 1, @@ -441,6 +473,22 @@ export function deriveStateFromProps(params: { const pivotMode = shouldReloadData.pivotBy ? 'remote' : 'local'; + const rowDisabledState = state.rowDisabledState; + + let isRowDisabled = props.isRowDisabled; + + if (!isRowDisabled && rowDisabledState) { + const cachedIsRowDisabled = weakMap.get(rowDisabledState); + if (cachedIsRowDisabled) { + isRowDisabled = cachedIsRowDisabled; + } else { + isRowDisabled = (rowInfo) => { + return rowDisabledState.isRowDisabled(rowInfo.id); + }; + weakMap.set(rowDisabledState, isRowDisabled); + } + } + const result: DataSourceDerivedState = { selectionMode, groupRowsState, @@ -459,6 +507,7 @@ export function deriveStateFromProps(params: { groupBy: props.shouldReloadData?.groupBy ?? groupMode === 'remote', pivotBy: props.shouldReloadData?.pivotBy ?? pivotMode === 'remote', }, + isRowDisabled, rowSelection: rowSelectionState, cellSelection: cellSelectionState, groupMode, diff --git a/source/src/components/DataSource/state/reducer.ts b/source/src/components/DataSource/state/reducer.ts index 75545c1b..0b9621c1 100644 --- a/source/src/components/DataSource/state/reducer.ts +++ b/source/src/components/DataSource/state/reducer.ts @@ -80,6 +80,7 @@ function toRowInfo( id: any, index: number, isRowSelected?: (rowInfo: InfiniteTableRowInfo) => boolean | null, + isRowDisabled?: (rowInfo: InfiniteTableRowInfo) => boolean, cellSelectionState?: CellSelectionState, ): InfiniteTable_NoGrouping_RowInfoNormal { const rowInfo: InfiniteTable_NoGrouping_RowInfoNormal = { @@ -90,12 +91,16 @@ function toRowInfo( isGroupRow: false, selfLoaded: true, rowSelected: false, + rowDisabled: false, isCellSelected: returnFalse, hasSelectedCells: returnFalse, }; if (isRowSelected) { rowInfo.rowSelected = isRowSelected(rowInfo); } + if (isRowDisabled) { + rowInfo.rowDisabled = isRowDisabled(rowInfo); + } if (cellSelectionState) { rowInfo.isCellSelected = (colId: string) => { @@ -312,6 +317,12 @@ export function concludeReducer(params: { const pivotBy = state.pivotBy; const shouldGroup = groupBy.length > 0 || !!pivotBy; + + const rowDisabledStateDepsChanged = haveDepsChanged(previousState, state, [ + 'rowDisabledState', + 'isRowDisabled', + ]); + const selectionDepsChanged = haveDepsChanged(previousState, state, [ 'rowSelection', 'cellSelection', @@ -344,6 +355,7 @@ export function concludeReducer(params: { !state.lastGroupDataArray || cacheAffectedParts.groupBy)) || selectionDepsChanged || + rowDisabledStateDepsChanged || rowInfoReducersChanged; const now = Date.now(); @@ -425,6 +437,7 @@ export function concludeReducer(params: { } : undefined; + const isRowDisabled = state.isRowDisabled || returnFalse; if (state.isRowSelected && state.selectionMode === 'multi-row') { isRowSelected = (rowInfo) => state.isRowSelected!( @@ -565,6 +578,7 @@ export function concludeReducer(params: { arrayDifferentAfterSortStep || groupsDepsChanged || selectionDepsChanged || + rowDisabledStateDepsChanged || rowInfoReducersChanged ) { const rowInfoReducerKeys = Object.keys( @@ -588,6 +602,7 @@ export function concludeReducer(params: { data ? toPrimaryKey(data) : index, index, isRowSelected, + isRowDisabled, cellSelectionState, ); diff --git a/source/src/components/DataSource/types.ts b/source/src/components/DataSource/types.ts index e83be8f3..54b09751 100644 --- a/source/src/components/DataSource/types.ts +++ b/source/src/components/DataSource/types.ts @@ -39,6 +39,7 @@ import { import { DataSourceCache, DataSourceMutation } from './DataSourceCache'; import { GroupRowsState } from './GroupRowsState'; import { Indexer } from './Indexer'; +import { RowDisabledState } from './RowDisabledState'; import { RowSelectionState, RowSelectionStateObject, @@ -123,6 +124,16 @@ export type RowDetailStateObject = { collapsedRows: true | KeyType[]; }; +export type RowDisabledStateObject = + | { + enabledRows: true; + disabledRows: KeyType[]; + } + | { + disabledRows: true; + enabledRows: KeyType[]; + }; + export type DataSourcePropGroupBy = DataSourceGroupBy[]; export type DataSourcePropPivotBy = DataSourcePivotBy[]; @@ -131,6 +142,7 @@ export interface DataSourceMappedState { livePagination: DataSourceProps['livePagination']; refetchKey: NonUndefined['refetchKey']>; isRowSelected: DataSourceProps['isRowSelected']; + isRowDisabled: DataSourceProps['isRowDisabled']; debugId: DataSourceProps['debugId']; batchOperationDelay: DataSourceProps['batchOperationDelay']; @@ -163,6 +175,8 @@ export interface DataSourceMappedState { DataSourceProps['collapseGroupRowsOnDataFunctionChange'] >; sortInfo: DataSourceSingleSortInfo[] | null; + + rowDisabledState: RowDisabledState | null; } export type DataSourceRawReducer = { @@ -439,6 +453,18 @@ export interface DataSourceApi { insertDataArray(data: T[], options: DataSourceInsertParam): Promise; setSortInfo(sortInfo: null | DataSourceSingleSortInfo[]): void; + + isRowDisabledAt: (rowIndex: number) => boolean; + isRowDisabled: (primaryKey: any) => boolean; + + setRowEnabledAt: (rowIndex: number, enabled: boolean) => void; + setRowEnabled: (primaryKey: any, enabled: boolean) => void; + + enableAllRows: () => void; + disableAllRows: () => void; + + areAllRowsEnabled: () => boolean; + areAllRowsDisabled: () => boolean; } export type DataSourcePropRowInfoReducers = Record< @@ -511,6 +537,12 @@ export type DataSourceProps = { | DataSourcePropCellSelection_SingleCell; onCellSelectionChange?: DataSourcePropOnCellSelectionChange; + rowDisabledState?: RowDisabledState | RowDisabledStateObject; + defaultRowDisabledState?: RowDisabledState | RowDisabledStateObject; + onRowDisabledStateChange?: (rowDisabledState: RowDisabledState) => void; + + isRowDisabled?: (rowInfo: InfiniteTableRowInfo) => boolean; + isRowSelected?: DataSourcePropIsRowSelected; // TODO maybe implement isCellSelected?: DataSourcePropIsCellSelected; @@ -757,6 +789,8 @@ export type DataSourceDerivedState = { livePaginationCursor?: DataSourceLivePaginationCursorValue; lazyLoadBatchSize?: number; rowSelection: RowSelectionState | null | number | string; + isRowDisabled: DataSourceProps['isRowDisabled']; + cellSelection: CellSelectionState | null; selectionMode: NonUndefined['selectionMode']>; }; diff --git a/source/src/components/HeadlessTable/index.tsx b/source/src/components/HeadlessTable/index.tsx index 3d3da8de..6a2cb5ec 100644 --- a/source/src/components/HeadlessTable/index.tsx +++ b/source/src/components/HeadlessTable/index.tsx @@ -167,6 +167,8 @@ export function HeadlessTable( ...domProps } = props; + const { autoFocus } = domProps; + const domRef = useRef(null); const [scrollSize, setTotalScrollSize] = useState({ @@ -185,6 +187,9 @@ export function HeadlessTable( if (!node) { return; } + if (autoFocus && document.activeElement !== node) { + node.focus(); + } const onResize = () => { // it's not enough to read the size from onResize // since that doesn't account for scrollbar presence and size diff --git a/source/src/components/InfiniteTable/components/ActiveCellIndicator.tsx b/source/src/components/InfiniteTable/components/ActiveCellIndicator.tsx index a119525e..f043ddff 100644 --- a/source/src/components/InfiniteTable/components/ActiveCellIndicator.tsx +++ b/source/src/components/InfiniteTable/components/ActiveCellIndicator.tsx @@ -105,7 +105,7 @@ const ActiveCellIndicatorFn = (props: ActiveCellIndicatorProps) => { ? ActiveCellIndicatorCls.visible : ActiveCellIndicatorCls.hidden }`} - > + /> ); }; diff --git a/source/src/components/InfiniteTable/components/ActiveRowIndicator.css.ts b/source/src/components/InfiniteTable/components/ActiveRowIndicator.css.ts index f496ceda..dbcddfbf 100644 --- a/source/src/components/InfiniteTable/components/ActiveRowIndicator.css.ts +++ b/source/src/components/InfiniteTable/components/ActiveRowIndicator.css.ts @@ -1,16 +1,16 @@ import { fallbackVar, style, styleVariants } from '@vanilla-extract/css'; import { ThemeVars } from '../vars.css'; -import { left, top, pointerEvents, position, width } from '../utilities.css'; +import { left, top, pointerEvents, position } from '../utilities.css'; export const ActiveRowIndicatorBaseCls = style( [ pointerEvents.none, - position.sticky, - width['100%'], + position.absolute, top['0'], left['0'], { + right: ThemeVars.runtime.browserScrollbarWidth, border: fallbackVar( ThemeVars.components.Row.activeBorder, `${fallbackVar( diff --git a/source/src/components/InfiniteTable/components/ActiveRowIndicator.tsx b/source/src/components/InfiniteTable/components/ActiveRowIndicator.tsx index 177fd21d..9ca53fb9 100644 --- a/source/src/components/InfiniteTable/components/ActiveRowIndicator.tsx +++ b/source/src/components/InfiniteTable/components/ActiveRowIndicator.tsx @@ -7,6 +7,7 @@ import { MatrixBrain } from '../../VirtualBrain/MatrixBrain'; import { internalProps } from '../internalProps'; import { InternalVars } from '../internalVars.css'; import { setInfiniteVarsOnNode } from '../utils/infiniteDOMUtils'; +import { ThemeVars } from '../vars.css'; import { ActiveIndicatorWrapperCls } from './ActiveCellIndicator.css'; import { ActiveRowIndicatorCls } from './ActiveRowIndicator.css'; @@ -25,6 +26,7 @@ const ActiveStyle: CSSProperties = { InternalVars.activeCellOffsetY } + var(${stripVar(InternalVars.scrollTopForActiveRow)}, 0px)), 0px)`, height: InternalVars.activeCellRowHeight, + maxWidth: ThemeVars.runtime.totalVisibleColumnsWidth, }; const ActiveRowIndicatorFn = (props: ActiveRowIndicatorProps) => { @@ -84,7 +86,7 @@ const ActiveRowIndicatorFn = (props: ActiveRowIndicatorProps) => { active ? ActiveRowIndicatorCls.visible : ActiveRowIndicatorCls.hidden }`} style={active ? ActiveStyle : undefined} - > + /> ); }; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx index 2723ec41..0e8ee8de 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx @@ -614,6 +614,7 @@ export function InfiniteTableHeaderCell( verticalAlign, rowSelected: false, cellSelected: false, + rowDisabled: false, zebra: false, rowActive: false, firstRow: true, diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/header.css.ts b/source/src/components/InfiniteTable/components/InfiniteTableHeader/header.css.ts index f41f87a7..efa63515 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/header.css.ts +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/header.css.ts @@ -176,6 +176,7 @@ export const HeaderCellRecipe = recipe({ cellSelected: { false: {}, true: {} }, rowSelected: { false: {}, true: {}, null: {} }, + rowDisabled: { false: {}, true: {} }, firstRow: { false: {}, true: {}, diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx index ab67d52f..d6804638 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx @@ -200,6 +200,8 @@ function InfiniteTableColumnCellFn(props: InfiniteTableColumnCellProps) { dataSourceApi, }; + const rowDisabled = rowInfo.rowDisabled; + const visibleColumnsIds = computed.computedVisibleColumns.map((x) => x.id); const colRenderingParams = getColumnRenderingParams({ horizontalLayoutPageIndex, @@ -252,14 +254,16 @@ function InfiniteTableColumnCellFn(props: InfiniteTableColumnCellProps) { }); if (keyboardNavigation === 'row') { - componentActions.activeRowIndex = rowIndex; + if (!rowDisabled) { + componentActions.activeRowIndex = rowIndex; + } return; } if (keyboardNavigation === 'cell') { componentActions.activeCellIndex = [rowIndex, colIndex]; } }, - [rowIndex, column.computedVisibleIndex, keyboardNavigation], + [rowIndex, rowDisabled, column.computedVisibleIndex, keyboardNavigation], ); const { selectionMode, cellSelection } = dataSourceState; @@ -629,6 +633,7 @@ function InfiniteTableColumnCellFn(props: InfiniteTableColumnCellProps) { zebra, align, verticalAlign, + rowDisabled, rowActive, cellSelected, rowSelected, diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableRow.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableRow.tsx deleted file mode 100644 index 8cbb64a0..00000000 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableRow.tsx +++ /dev/null @@ -1,107 +0,0 @@ -//@ts-nocheck - this file is not being used -import * as React from 'react'; -import { useRef } from 'react'; - -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; -import { InfiniteTableColumnCell } from './InfiniteTableColumnCell'; - -import { useRowDOMProps } from './useRowDOMProps'; - -import { InfiniteTableRowClassName } from './InfiniteTableRowClassName'; - -import type { InfiniteTableRowProps } from './InfiniteTableRowTypes'; -import { RawList } from '../../../RawList'; -import { RenderItem } from '../../../RawList/types'; - -function InfiniteTableRowFn( - props: InfiniteTableRowProps & React.HTMLAttributes, -) { - const { - rowWidth, - rowHeight, - getData, - rowInfo, - toggleGroupRow, - rowIndex, - //TODO continue here?? receive columnWidth from props - brain, - columns, - } = props; - const tableContextValue = useInfiniteTable(); - - const { state } = tableContextValue; - const { domRef: tableDOMRef } = state; - - const { groupRenderStrategy } = state; - - const { domProps } = useRowDOMProps( - props, - state.rowProps, - state.rowStyle, - state.rowClassName, - groupRenderStrategy, - tableDOMRef, - ); - - const style = { - width: rowWidth, - height: rowHeight, - ...domProps.style, - }; - - const renderCellRef = useRef(null); - - const renderCell: RenderItem = React.useCallback( - ({ domRef, itemIndex }) => { - const column = columns[itemIndex]; - - if (!column) { - // return null; - } - // const parentIndex = verticalBrain.getItemSpanParent(rowIndex); - const hidden = false; //parentIndex < rowIndex; - return ( - - getData={getData} - rowInfo={rowInfo} - groupRenderStrategy={groupRenderStrategy} - virtualized - hidden={hidden} - rowHeight={rowHeight} - toggleGroupRow={toggleGroupRow} - rowIndex={rowIndex} - domRef={domRef} - column={column} - /> - ); - }, - [columns, rowIndex, rowInfo, rowHeight, groupRenderStrategy, getData], // don't add repaintId here since it would make this out-of-sync with the available columns when columnOrder controlled changes - ); - - if (renderCellRef.current !== renderCell) { - renderCellRef.current = renderCell; - } - // (renderCell as any)._colscount = columns.length; - // (renderCell as any)._repaintId = repaintId; - // (globalThis as any).renderCell = renderCell; - - if (__DEV__) { - (domProps as any)['data-cmp-name'] = 'ITableRow'; - } - - return ( -
- -
- ); -} - -export const InfiniteTableRow = React.memo( - InfiniteTableRowFn, -) as typeof InfiniteTableRowFn; - -export { InfiniteTableRowClassName }; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableRowUnvirtualized.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableRowUnvirtualized.tsx deleted file mode 100644 index 6827664d..00000000 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableRowUnvirtualized.tsx +++ /dev/null @@ -1,75 +0,0 @@ -//@ts-nocheck - this file is not being used -import * as React from 'react'; - -import type { InfiniteTableRowProps } from './InfiniteTableRowTypes'; - -import { InfiniteTableColumnCell } from './InfiniteTableColumnCell'; -import { VirtualBrain } from '../../../VirtualBrain'; -import { useRowDOMProps } from './useRowDOMProps'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; - -function TableRowUnvirtualizedFn( - props: InfiniteTableRowProps & { - brain: VirtualBrain | null | undefined; - verticalBrain: VirtualBrain; - }, -) { - const { - rowHeight, - rowWidth, - getData, - rowInfo, - rowIndex, - columns, - toggleGroupRow, - } = props; - - const tableContextValue = useInfiniteTable(); - - const { state: componentState } = tableContextValue; - const { domRef: tableDOMRef } = componentState; - const { domProps } = useRowDOMProps( - props, - componentState.rowProps, - componentState.rowStyle, - componentState.rowClassName, - componentState.groupRenderStrategy, - tableDOMRef, - ); - - const style = { - width: rowWidth, - height: rowHeight, - ...domProps.style, - }; - - const children = columns.map((col) => { - // const parentIndex = verticalBrain.getItemSpanParent(rowIndex); - const hidden = false; //parentIndex < rowIndex; - return ( - - key={col.id} - virtualized={false} - rowHeight={rowHeight} - getData={getData} - hidden={hidden} - groupRenderStrategy={componentState.groupRenderStrategy} - toggleGroupRow={toggleGroupRow} - rowInfo={rowInfo} - rowIndex={rowIndex} - column={col} - /> - ); - }); - - return ( -
- {children} -
- ); -} - -export const TableRowUnvirtualized = React.memo( - TableRowUnvirtualizedFn, -) as typeof TableRowUnvirtualizedFn; -// export const TableRow = TableRowUnvirtualizedFn; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/index.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/index.tsx deleted file mode 100644 index 8bc2ee7b..00000000 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './InfiniteTableRow'; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/row.css.ts b/source/src/components/InfiniteTable/components/InfiniteTableRow/row.css.ts index f36d98ce..6e06d4f0 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/row.css.ts +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/row.css.ts @@ -3,20 +3,6 @@ import { recipe } from '@vanilla-extract/recipes'; import { ThemeVars } from '../../vars.css'; -const RowCls = style({ - willChange: 'transform', - position: 'absolute', - top: 0, - left: 0, - pointerEvents: ThemeVars.components.Row.pointerEventsWhileScrolling, - [ThemeVars.components.ColumnCell.background.slice(4, -1)]: - ThemeVars.components.Row.background, -}); - -const groupRowStyle = { - zIndex: 100, -}; - export const RowHoverCls = style({ vars: { [ThemeVars.components.Row.background]: @@ -28,35 +14,6 @@ export const RowHoverCls = style({ }, }); -export const RowClsRecipe = recipe({ - base: RowCls, - variants: { - zebra: { - false: {}, - even: { - background: ThemeVars.components.Row.background, - }, - odd: { - background: ThemeVars.components.Row.oddBackground, - }, - }, - groupRow: { - true: { - ...groupRowStyle, - }, - false: {}, - }, - inlineGroupRow: { - true: { ...groupRowStyle }, - false: {}, - }, - showHoverRows: { - true: {}, - false: {}, - }, - }, -}); - export const GroupRowExpanderCls = recipe({ variants: { align: { diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/useRowDOMProps.ts b/source/src/components/InfiniteTable/components/InfiniteTableRow/useRowDOMProps.ts deleted file mode 100644 index c012d445..00000000 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/useRowDOMProps.ts +++ /dev/null @@ -1,199 +0,0 @@ -//@ts-nocheck -import { - CSSProperties, - MutableRefObject, - RefCallback, - useCallback, - useRef, -} from 'react'; - -import { join } from '../../../../utils/join'; -import { stripVar } from '../../../../utils/stripVar'; -import { ThemeVars } from '../../vars.css'; -import { InfiniteTableRowStylingFnParams } from '../../types/InfiniteTableProps'; -import { InfiniteTableState } from '../../types/InfiniteTableState'; - -import { - InfiniteTableRowClassName, - InfiniteTableRowClassName__hover, -} from './InfiniteTableRowClassName'; -import type { InfiniteTableRowProps } from './InfiniteTableRowTypes'; -import { RowClsRecipe, RowHoverCls } from './row.css'; - -export type TableRowHTMLAttributes = React.HTMLAttributes & { - 'data-virtualize-columns': 'on' | 'off'; - 'data-hover-index': number; - 'data-row-index': number; - 'data-row-id': string; - ref: RefCallback; -} & any; - -export function useRowDOMProps( - props: InfiniteTableRowProps, - rowProps: InfiniteTableState['rowProps'], - rowStyle: InfiniteTableState['rowStyle'], - rowClassName: InfiniteTableState['rowClassName'], - groupRenderStrategy: InfiniteTableState['groupRenderStrategy'], - tableDOMRef: MutableRefObject, -): { - domProps: TableRowHTMLAttributes; - domRef: MutableRefObject; -} { - const domProps = props.domProps; - const { - showZebraRows = false, - showHoverRows = false, - rowIndex, - domRef: domRefFromProps, - rowInfo, - rowSpan, - } = props; - - const domRef = useRef(null); - const rowDOMRef = useCallback((node) => { - domRefFromProps(node); - domRef.current = node; - }, []); - - // TS hack to discriminate between for grouped vs non-grouped rows - const rest = rowInfo.isGroupRow - ? { data: rowInfo.data, rowInfo } - : { data: rowInfo.data, rowInfo }; - - const rowPropsAndStyleArgs: InfiniteTableRowStylingFnParams = { - ...rest, - rowIndex, - }; - - if (typeof rowProps === 'function') { - rowProps = rowProps(rowPropsAndStyleArgs); - } - - let style: CSSProperties | undefined = rowProps ? rowProps.style : undefined; - - const inlineGroupRoot = - groupRenderStrategy === 'inline' && rowInfo.indexInGroup === 0; - - if (rowStyle) { - style = - typeof rowStyle === 'function' - ? { ...style, ...rowStyle(rowPropsAndStyleArgs) } - : { ...style, ...rowStyle }; - } - - if (inlineGroupRoot || rowSpan) { - style = style || {}; - //TODO remove this harcoded value - should be datasource size - ... - style.zIndex = 2_000_000 - rowInfo.indexInAll; - } - - const odd = - (rowInfo.indexInAll != null ? rowInfo.indexInAll : rowIndex) % 2 === 1; - - const rowComputedClassName = - typeof rowClassName === 'function' - ? rowClassName(rowPropsAndStyleArgs) - : rowClassName; - - const className = join( - InfiniteTableRowClassName, - - RowClsRecipe({ - groupRow: rowInfo.isGroupRow, - inlineGroupRow: inlineGroupRoot, - zebra: showHoverRows ? (odd ? 'odd' : 'even') : false, - showHoverRows, - }), - `${InfiniteTableRowClassName}--${ - rowInfo.isGroupRow ? 'group' : 'normal' - }-row`, - - inlineGroupRoot ? `${InfiniteTableRowClassName}--inline-group-row` : '', - showZebraRows - ? `${InfiniteTableRowClassName}--${odd ? 'odd' : 'even'}` - : null, - showHoverRows ? `${InfiniteTableRowClassName}--show-hover` : null, - domProps?.className, - rowProps?.className, - rowComputedClassName, - ); - - const initialMouseEnter = rowProps?.onMouseEnter; - const initialMouseLeave = rowProps?.onMouseLeave; - - // const parentIndex = brain.getItemSpanParent(rowIndex); - // const covered = parentIndex !== rowIndex; - - const onMouseEnter = useCallback( - (event) => { - initialMouseEnter?.(event); - - const rowIndex = event.currentTarget?.dataset.rowIndex * 1; - - const parentNode = tableDOMRef.current; - - if (!parentNode || !showHoverRows) { - return; - } - - const hoverSelector = [ - `.${InfiniteTableRowClassName}[data-hover-index="${rowIndex}"]`, - ]; - - const rows = parentNode.querySelectorAll(hoverSelector.join(',')); - - rows.forEach((row) => - row.classList.add(InfiniteTableRowClassName__hover, RowHoverCls), - ); - }, - [initialMouseEnter, showHoverRows], - ); - - const onMouseLeave = useCallback( - (event) => { - initialMouseLeave?.(event); - - const rowIndex = event.currentTarget?.dataset.rowIndex; - - const parentNode = tableDOMRef.current; - - if (!parentNode || !showHoverRows) { - return; - } - - const hoverSelector = [ - `.${InfiniteTableRowClassName}[data-hover-index="${rowIndex}"]`, - ]; - const rows = parentNode.querySelectorAll(hoverSelector.join(',')); - rows.forEach((row) => - row.classList.remove(InfiniteTableRowClassName__hover, RowHoverCls), - ); - }, - [initialMouseLeave, showHoverRows], - ); - - if (rowInfo.dataSourceHasGrouping) { - style = style || {}; - //@ts-ignore - style[stripVar(ThemeVars.components.Row.groupNesting)] = - rowInfo.groupNesting! - 1; - } - return { - domRef, - domProps: { - ...rowProps, - ...domProps, - style, - 'data-virtualize-columns': props.virtualizeColumns ? 'on' : 'off', - 'data-row-index': rowIndex, - - // 'data-hover-index': covered ? null : rowIndex, - 'data-hover-index': rowIndex, - 'data-row-id': `${rowInfo.id}`, - className, - onMouseEnter, - onMouseLeave, - ref: rowDOMRef, - }, - }; -} diff --git a/source/src/components/InfiniteTable/components/cell.css.ts b/source/src/components/InfiniteTable/components/cell.css.ts index 713de009..742f68fe 100644 --- a/source/src/components/InfiniteTable/components/cell.css.ts +++ b/source/src/components/InfiniteTable/components/cell.css.ts @@ -209,6 +209,24 @@ export const ColumnCellRecipe = recipe({ false: {}, true: {}, }, + rowDisabled: { + false: {}, + true: { + opacity: ThemeVars.components.Row.disabledOpacity, + vars: { + [ThemeVars.components.Row.background]: + ThemeVars.components.Row.disabledBackground, + [ThemeVars.components.Row.oddBackground]: + ThemeVars.components.Row.oddDisabledBackground, + [ThemeVars.components.Row.hoverBackground]: + ThemeVars.components.Row.disabledBackground, + [ThemeVars.components.Row.activeBackground]: + ThemeVars.components.Row.background, + [ThemeVars.components.Row.selectedHoverBackground]: + ThemeVars.components.Row.selectedDisabledBackground, + }, + }, + }, zebra: { false: { background: ThemeVars.components.Row.background, @@ -318,6 +336,18 @@ export const ColumnCellRecipe = recipe({ justifyContent: 'center', }, }, + { + variants: { + rowDisabled: true, + zebra: 'odd', + }, + style: { + vars: { + [ThemeVars.components.Row.hoverBackground]: + ThemeVars.components.Row.oddDisabledBackground, + }, + }, + }, ], }); diff --git a/source/src/components/InfiniteTable/eventHandlers/eventHandlerTypes.ts b/source/src/components/InfiniteTable/eventHandlers/eventHandlerTypes.ts index 75200da4..78852176 100644 --- a/source/src/components/InfiniteTable/eventHandlers/eventHandlerTypes.ts +++ b/source/src/components/InfiniteTable/eventHandlers/eventHandlerTypes.ts @@ -33,7 +33,12 @@ export type InfiniteTableEventHandlerAbstractContext = { cellSelection: DataSourceState['cellSelection']; groupBy: DataSourceState['groupBy']; selectionMode: DataSourceState['selectionMode']; - dataArray: { id: string; isGroupRow: boolean; groupKeys?: any[] }[]; + dataArray: { + id: string; + isGroupRow: boolean; + groupKeys?: any[]; + rowDisabled: boolean; + }[]; }; dataSourceActions: DataSourceComponentActions; dataSourceApi: DataSourceApi; diff --git a/source/src/components/InfiniteTable/eventHandlers/keyboardNavigation.ts b/source/src/components/InfiniteTable/eventHandlers/keyboardNavigation.ts index b2e42f34..a41dc31f 100644 --- a/source/src/components/InfiniteTable/eventHandlers/keyboardNavigation.ts +++ b/source/src/components/InfiniteTable/eventHandlers/keyboardNavigation.ts @@ -1,6 +1,31 @@ import { clamp } from '../../utils/clamp'; import { InfiniteTableKeyboardEventHandlerContext } from './eventHandlerTypes'; +function getNextEnabledRowIndex( + getDataSourceState: InfiniteTableKeyboardEventHandlerContext['getDataSourceState'], + activeRowIndex: number, + direction: 1 | -1, +): number { + const dataArray = getDataSourceState().dataArray; + + const min = 0; + const max = dataArray.length - 1; + + let i = activeRowIndex; + + do { + i += direction; + const rowInfo = dataArray[i]; + if (!rowInfo) { + return i < min ? min : max; + } + if (!rowInfo.rowDisabled) { + return i; + } + } while (i >= min && i <= max); + + return clamp(i, min, max); +} export function handleRowNavigation( options: InfiniteTableKeyboardEventHandlerContext, event: { @@ -33,10 +58,18 @@ export function handleRowNavigation( const max = arrLength - 1; const KeyToFunction = { ArrowDown: () => { - activeRowIndex = clamp(activeRowIndex! + 1, min, max); + activeRowIndex = getNextEnabledRowIndex( + getDataSourceState, + activeRowIndex!, + 1, + ); }, ArrowUp: () => { - activeRowIndex = clamp(activeRowIndex! - 1, min, max); + activeRowIndex = getNextEnabledRowIndex( + getDataSourceState, + activeRowIndex!, + -1, + ); }, ArrowLeft: () => { const rowInfo = dataArray[activeRowIndex!]; diff --git a/source/src/components/InfiniteTable/eventHandlers/onCellClick.ts b/source/src/components/InfiniteTable/eventHandlers/onCellClick.ts index 31cf4ddb..bc47fae9 100644 --- a/source/src/components/InfiniteTable/eventHandlers/onCellClick.ts +++ b/source/src/components/InfiniteTable/eventHandlers/onCellClick.ts @@ -126,6 +126,12 @@ export function updateRowSelectionOnCellClick( return false; } + const rowInfo = dataArray[rowIndex]; + + if (rowInfo?.rowDisabled) { + return false; + } + if (selectionMode === 'multi-row') { if (renderSelectionCheckBox && !event.key) { // we should not click-select when we have a checkbox column diff --git a/source/src/components/InfiniteTable/hooks/useCellClassName.ts b/source/src/components/InfiniteTable/hooks/useCellClassName.ts index 15a86313..4f44c7ea 100644 --- a/source/src/components/InfiniteTable/hooks/useCellClassName.ts +++ b/source/src/components/InfiniteTable/hooks/useCellClassName.ts @@ -16,6 +16,7 @@ export function useCellClassName( groupCell: boolean; rowExpanded: boolean; rowActive: boolean; + rowDisabled: boolean; align: InfiniteTableColumnAlignValues; verticalAlign: InfiniteTableColumnVerticalAlignValues; rowSelected: boolean | null; @@ -38,6 +39,7 @@ export function useCellClassName( rowActive: extraFlags.rowActive, dragging: extraFlags.dragging, firstRow: extraFlags.firstRow ?? false, + rowDisabled: extraFlags.rowDisabled, groupRow: extraFlags.groupRow, groupCell: extraFlags.groupCell, verticalAlign: extraFlags.verticalAlign, @@ -75,6 +77,10 @@ export function useCellClassName( result.push(...baseClasses.map((c) => `${c}--first-row`)); } + if (extraFlags.rowDisabled) { + result.push(...baseClasses.map((c) => `${c}--disabled`)); + } + if (extraFlags.groupRow) { result.push(...baseClasses.map((c) => `${c}--group-row`)); diff --git a/source/src/components/InfiniteTable/hooks/useComputed.ts b/source/src/components/InfiniteTable/hooks/useComputed.ts index a5899733..9da38a49 100644 --- a/source/src/components/InfiniteTable/hooks/useComputed.ts +++ b/source/src/components/InfiniteTable/hooks/useComputed.ts @@ -152,6 +152,10 @@ export function useComputed(): InfiniteTableComputedValues { const [multiRowSelector] = useState(() => { const multiRowSelector = new MultiRowSelector({ + isRowDisabledAt: (index: number) => { + const dataItem = getDataSourceState().dataArray[index]; + return dataItem?.rowDisabled ?? false; + }, getIdForIndex: (index: number) => { const dataItem = getDataSourceState().dataArray[index]; diff --git a/source/src/components/InfiniteTable/hooks/useDOMProps.ts b/source/src/components/InfiniteTable/hooks/useDOMProps.ts index f9b14a6d..638a889d 100644 --- a/source/src/components/InfiniteTable/hooks/useDOMProps.ts +++ b/source/src/components/InfiniteTable/hooks/useDOMProps.ts @@ -39,6 +39,10 @@ const publicRuntimeVars: Record< name: stripVar(ThemeVars.runtime.visibleColumnsCount), value: '', }, + browserScrollbarWidth: { + name: stripVar(ThemeVars.runtime.browserScrollbarWidth), + value: '', + }, }; const scrollbarWidthHorizontal = stripVar( @@ -168,6 +172,15 @@ export function useDOMProps( {}, ); + // we need this if here - if it's not here, for whatever reason, + // the scrollbarWidth is not updated correctly on re-renders (as it comes 0 initially when server-side rendered in nextjs for example) + if (bodySize.width) { + //@ts-ignore + cssVars[ + publicRuntimeVars.browserScrollbarWidth.name + ] = `${getScrollbarWidth()}px`; + } + //@ts-ignore cssVars[ publicRuntimeVars.bodyWidth.name diff --git a/source/src/components/InfiniteTable/hooks/useUnpinnedRendering.tsx b/source/src/components/InfiniteTable/hooks/useUnpinnedRendering.tsx deleted file mode 100644 index e34873c3..00000000 --- a/source/src/components/InfiniteTable/hooks/useUnpinnedRendering.tsx +++ /dev/null @@ -1,165 +0,0 @@ -//@ts-nocheck - this file is not being used -import * as React from 'react'; -import { useCallback } from 'react'; - -import { getScrollbarWidth } from '../../utils/getScrollbarWidth'; -import { RenderRow } from '../../VirtualList/types'; -import type { VirtualBrain, VirtualBrainOptions } from '../../VirtualBrain'; - -import { VirtualRowList } from '../../VirtualList/VirtualRowList'; -import type { Size } from '../../types/Size'; -import type { - InfiniteTableComputedColumn, - InfiniteTableRowInfo, -} from '../types'; -import type { InfiniteTableRowProps } from '../components/InfiniteTableRow/InfiniteTableRowTypes'; - -import { InfiniteTableRow } from '../components/InfiniteTableRow'; -import { TableRowUnvirtualized } from '../components/InfiniteTableRow/InfiniteTableRowUnvirtualized'; -import { InfiniteTableState } from '../types/InfiniteTableState'; -import { InfiniteTableToggleGroupRowFn } from '../types/InfiniteTableColumn'; - -type UnpinnedRenderingParams = { - columnShifts: number[] | null; - bodySize: Size; - getData: () => InfiniteTableRowInfo[]; - rowHeight: number; - toggleGroupRow: InfiniteTableToggleGroupRowFn; - - repaintId: string | number; - applyScrollHorizontal: ({ scrollLeft }: { scrollLeft: number }) => void; - verticalVirtualBrain: VirtualBrain; - horizontalVirtualBrain: VirtualBrain; - - computedPinnedEndColumns: InfiniteTableComputedColumn[]; - computedUnpinnedColumns: InfiniteTableComputedColumn[]; - computedUnpinnedColumnsWidth: number; - computedPinnedStartWidth: number; - computedPinnedEndWidth: number; - rowSpan?: VirtualBrainOptions['itemSpan']; - - getState: () => InfiniteTableState; -}; -export function useUnpinnedRendering(params: UnpinnedRenderingParams) { - const { - columnShifts, - bodySize, - getData, - rowHeight, - toggleGroupRow, - - repaintId, - - applyScrollHorizontal, - verticalVirtualBrain, - horizontalVirtualBrain, - computedUnpinnedColumnsWidth, - computedPinnedStartWidth, - computedPinnedEndWidth, - - computedUnpinnedColumns, - - rowSpan, - getState, - } = params; - - const { virtualizeColumns } = getState(); - - const shouldVirtualizeColumns = - typeof virtualizeColumns === 'function' - ? virtualizeColumns(computedUnpinnedColumns) - : virtualizeColumns ?? true; - - const renderRowUnpinned: RenderRow = useCallback( - (rowParams) => { - const dataArray = getData(); - const rowInfo = dataArray[rowParams.rowIndex]; - - const { showZebraRows, showHoverRows } = getState(); - - const rowProps: InfiniteTableRowProps = { - rowInfo, - showZebraRows, - showHoverRows, - getData, - rowSpan, - toggleGroupRow, - virtualizeColumns: shouldVirtualizeColumns, - brain: null!, - verticalBrain: verticalVirtualBrain, - columns: computedUnpinnedColumns, - rowWidth: computedUnpinnedColumnsWidth, - ...rowParams, - }; - - if (shouldVirtualizeColumns) { - rowProps.brain = horizontalVirtualBrain; - rowProps.repaintId = repaintId; - } - - const TableRowComponent = shouldVirtualizeColumns - ? InfiniteTableRow - : TableRowUnvirtualized; - - return {...rowProps} />; - }, - [ - repaintId, - rowHeight, - rowSpan, - - computedUnpinnedColumns, - computedUnpinnedColumnsWidth, - - shouldVirtualizeColumns, - horizontalVirtualBrain, - verticalVirtualBrain, - ], - ); - - const fakeScrollbar = getScrollbarWidth() ? ( -
-
-
- ) : null; - - //TODO in the future, we can use something more lightweight instead of the VirtualRowList - // as the root of the InfiniteTable already renders a VirtualScrollContainer - // and the VirtualRowList contains another VirtualScrollContainer - // so I think we could optimize this 🤔 or remove the root VirtualScrollContainer there 🤷‍♂️ - return rowHeight != 0 ? ( - - ) : null; -} diff --git a/source/src/components/InfiniteTable/index.tsx b/source/src/components/InfiniteTable/index.tsx index 7757252d..dbd5c9fb 100644 --- a/source/src/components/InfiniteTable/index.tsx +++ b/source/src/components/InfiniteTable/index.tsx @@ -175,6 +175,7 @@ function InfiniteTableBody() { rowDetailRenderer, showHoverRows, wrapRowsHorizontally, + domProps, } = componentState; const LoadMaskCmp = components?.LoadMask ?? LoadMask; @@ -245,11 +246,14 @@ function InfiniteTableBody() { computed, }); + const { autoFocus, tabIndex } = domProps ?? {}; + return ( (componentState.domProps); + const { + // remove autoFocus and tabIndex from the domProps + // that will be spread on the root DOM element + // as they are meant for the scroller element + autoFocus: _, + tabIndex: __, + ...initialDOMProps + } = componentState.domProps ?? {}; + + const domProps = useDOMProps(initialDOMProps); React.useEffect(() => { brain.setScrollStopDelay(scrollStopDelay); diff --git a/source/src/components/InfiniteTable/utils/MultiRowSelector.ts b/source/src/components/InfiniteTable/utils/MultiRowSelector.ts index 4c9b7024..9b275565 100644 --- a/source/src/components/InfiniteTable/utils/MultiRowSelector.ts +++ b/source/src/components/InfiniteTable/utils/MultiRowSelector.ts @@ -2,6 +2,7 @@ import { RowSelectionState } from '../../DataSource/RowSelectionState'; export type MultiRowSelectorOptions = { getIdForIndex: (index: number) => string | number; + isRowDisabledAt: (index: number) => boolean; }; function ensureMinMax(start: number, end: number) { @@ -10,6 +11,7 @@ function ensureMinMax(start: number, end: number) { export class MultiRowSelector { getIdForIndex: MultiRowSelectorOptions['getIdForIndex']; + isRowDisabledAt: MultiRowSelectorOptions['isRowDisabledAt']; multiSelectStartIndex: number = 0; multiSelectEndIndex?: number; @@ -18,6 +20,7 @@ export class MultiRowSelector { constructor(options: MultiRowSelectorOptions) { this.getIdForIndex = options.getIdForIndex; + this.isRowDisabledAt = options.isRowDisabledAt; } set rowSelectionState(rowSelectionState: RowSelectionState) { @@ -35,7 +38,9 @@ export class MultiRowSelector { for (let i = start; i <= end; i++) { const id = this.getIdForIndex(i); - + if (this.isRowDisabledAt(i)) { + continue; + } rowSelectionState.selectRow(id); } } @@ -47,6 +52,10 @@ export class MultiRowSelector { for (let i = start; i <= end; i++) { const id = this.getIdForIndex(i); + if (this.isRowDisabledAt(i)) { + continue; + } + rowSelectionState.deselectRow(id); } } diff --git a/source/src/components/InfiniteTable/vars-default-dark.css.ts b/source/src/components/InfiniteTable/vars-default-dark.css.ts index a3f178e7..5f125c9f 100644 --- a/source/src/components/InfiniteTable/vars-default-dark.css.ts +++ b/source/src/components/InfiniteTable/vars-default-dark.css.ts @@ -16,6 +16,10 @@ export const DarkVars = { [ThemeVars.components.Row.selectedHoverBackground]: '#0b243a', [ThemeVars.components.Row.background]: ThemeVars.background, [ThemeVars.components.Row.oddBackground]: '#242a31', + + [ThemeVars.components.Row.disabledBackground]: '#292a2c', + [ThemeVars.components.Row.oddDisabledBackground]: '#2d2e30', + [ThemeVars.components.Cell.color]: '#c3c3c3', [ThemeVars.components.Menu.shadowColor]: `rgba(0,0,0,0.25)`, [ThemeVars.components.Menu.shadowColor]: `rgba(255,255,255,0.25)`, diff --git a/source/src/components/InfiniteTable/vars-default-light.css.ts b/source/src/components/InfiniteTable/vars-default-light.css.ts index 148a1c11..758d0e4d 100644 --- a/source/src/components/InfiniteTable/vars-default-light.css.ts +++ b/source/src/components/InfiniteTable/vars-default-light.css.ts @@ -125,6 +125,11 @@ const RowVars = { [ThemeVars.components.Row.background]: ThemeVars.background, [ThemeVars.components.Row.oddBackground]: '#f6f6f6', + [ThemeVars.components.Row.disabledOpacity]: '0.5', + [ThemeVars.components.Row.disabledBackground]: '#eeeeee', + [ThemeVars.components.Row.oddDisabledBackground]: '#f9f9f9', + [ThemeVars.components.Row.selectedDisabledBackground]: + ThemeVars.components.Row.selectedBackground, [ThemeVars.components.Row.selectedBackground]: '#d1e9ff', [ThemeVars.components.Row.selectedHoverBackground]: '#add8ff', [ThemeVars.components.Row.groupRowBackground]: '#cbc5c5', diff --git a/source/src/components/InfiniteTable/vars.css.ts b/source/src/components/InfiniteTable/vars.css.ts index 5af91aac..c54361c4 100644 --- a/source/src/components/InfiniteTable/vars.css.ts +++ b/source/src/components/InfiniteTable/vars.css.ts @@ -56,6 +56,7 @@ export const ThemeVars = createGlobalThemeContract( totalVisibleColumnsWidthValue: 'runtime-total-visible-columns-width', totalVisibleColumnsWidthVar: 'runtime-total-visible-columns-width-var', visibleColumnsCount: 'runtime-visible-columns-count', + browserScrollbarWidth: 'runtime-browser-scrollbar-width', }, components: { @@ -307,8 +308,23 @@ export const ThemeVars = createGlobalThemeContract( */ oddBackground: 'row-odd-background', + /* + * Background color for disabled rows. For setting the background of disabled odd rows, use [`--infinite-row-odd-disabled-background`](#row-odd-disabled-background). + * + */ + disabledBackground: 'row-disabled-background', + /** + * Background color for disabled rows. For setting the background of disabled even rows, use [`--infinite-row-disabled-background`](#row-disabled-background). + */ + oddDisabledBackground: 'row-odd-disabled-background', + selectedBackground: 'row-selected-background', + /** + * Opacity for disabled rows. Defaults to 0.5 + */ + disabledOpacity: 'row-disabled-opacity', + /** * The background color of the active row. Defaults to the value of `var(--infinite-active-cell-background)`. * @@ -365,6 +381,7 @@ export const ThemeVars = createGlobalThemeContract( */ hoverBackground: 'row-hover-background', selectedHoverBackground: 'row-selected-hover-background', + selectedDisabledBackground: 'row-selected-disabled-background', groupRowBackground: 'group-row-background', groupRowColumnNesting: 'group-row-column-nesting', groupNesting: 'dont-override-group-row-nesting-length', diff --git a/source/src/components/VirtualScrollContainer/index.tsx b/source/src/components/VirtualScrollContainer/index.tsx index 28aafa62..5b864545 100644 --- a/source/src/components/VirtualScrollContainer/index.tsx +++ b/source/src/components/VirtualScrollContainer/index.tsx @@ -22,6 +22,7 @@ export interface VirtualScrollContainerProps { scrollable?: Scrollable; tabIndex?: number; + autoFocus?: boolean; onContainerScroll?: (scrollPos: { scrollTop: number; @@ -42,6 +43,7 @@ export const VirtualScrollContainer = React.forwardRef( className, tabIndex, style, + autoFocus, } = props; const domRef = ref ?? useRef(null); @@ -54,6 +56,7 @@ export const VirtualScrollContainer = React.forwardRef(
= { value?: any; indexInAll: number; rowSelected: boolean | null; + rowDisabled: boolean; isCellSelected: (columnId: string) => boolean; hasSelectedCells: (columnIds: string[]) => boolean; }; @@ -1050,6 +1051,7 @@ function getEnhancedGroupData( id: `${groupKeys}`, //TODO improve this collapsed: false, dataSourceHasGrouping: true, + rowDisabled: false, isCellSelected: returnFalse, hasSelectedCells: returnFalse, selfLoaded, @@ -1112,6 +1114,7 @@ export type EnhancedFlattenParam = { toPrimaryKey: (data: DataType, index: number) => any; groupRowsState?: GroupRowsState; isRowSelected?: (rowInfo: InfiniteTableRowInfo) => boolean | null; + isRowDisabled?: (rowInfo: InfiniteTableRowInfo) => boolean; withRowInfo?: (rowInfo: InfiniteTableRowInfo) => void; @@ -1129,6 +1132,7 @@ export function enhancedFlatten( withRowInfo, toPrimaryKey, groupRowsState, + isRowDisabled, isRowSelected, rowSelectionState, generateGroupRows, @@ -1195,6 +1199,9 @@ export function enhancedFlatten( selectionCount.deselectedCount; } } + if (isRowDisabled) { + enhancedGroupData.rowDisabled = isRowDisabled(enhancedGroupData); + } const parent = parents[parents.length - 1]; @@ -1273,6 +1280,7 @@ export function enhancedFlatten( isGroupRow: false, selfLoaded: !!item, rowSelected: false, + rowDisabled: false, rootGroupBy: groupBy, collapsed, groupKeys, @@ -1287,6 +1295,9 @@ export function enhancedFlatten( if (isRowSelected) { rowInfo.rowSelected = isRowSelected(rowInfo); } + if (isRowDisabled) { + rowInfo.rowDisabled = isRowDisabled(rowInfo); + } if (withRowInfo) { withRowInfo(rowInfo); diff --git a/www/content/docs/learn/rows/custom-rendering-for-disabled-rows-example.page.tsx b/www/content/docs/learn/rows/custom-rendering-for-disabled-rows-example.page.tsx new file mode 100644 index 00000000..b0610d7a --- /dev/null +++ b/www/content/docs/learn/rows/custom-rendering-for-disabled-rows-example.page.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; + +import { + DataSource, + DataSourceApi, + DataSourceData, + InfiniteTable, + InfiniteTablePropColumns, +} from '@infinite-table/infinite-react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + + salary: number; +}; + +const data: DataSourceData = () => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + type: 'number', + defaultWidth: 100, + }, + salary: { + defaultFilterable: true, + field: 'salary', + type: 'number', + }, + + firstName: { + field: 'firstName', + renderValue: ({ rowInfo, value }) => { + return `${value} ${rowInfo.rowDisabled ? '🚫' : ''}`; + }, + }, + stack: { field: 'stack' }, + currency: { field: 'currency' }, +}; + +export default () => { + const [dataSourceApi, setDataSourceApi] = + React.useState>(); + return ( + <> + + + + onReady={setDataSourceApi} + data={data} + primaryKey="id" + defaultRowDisabledState={{ + enabledRows: [1, 2, 3, 5], + disabledRows: true, + }} + > + + columnDefaultWidth={120} + columnMinWidth={50} + columns={columns} + keyboardNavigation="row" + /> + + + ); +}; diff --git a/www/content/docs/learn/rows/disabled-rows.page.md b/www/content/docs/learn/rows/disabled-rows.page.md new file mode 100644 index 00000000..f4659415 --- /dev/null +++ b/www/content/docs/learn/rows/disabled-rows.page.md @@ -0,0 +1,78 @@ +--- +title: Disabled Rows +--- + +Disabling rows allows you to have some rows that are not selectable, not clickable, not reacheable via keyboard navigation and other interactions. + +The `DataSource` manages the disabled state of rows, via the (uncontrolled) prop and (controlled) prop. + +```tsx + + idProperty="id" + data={[]} + defaultRowDisabledState={{ + enabledRows: true, + disabledRows: ['id1', 'id4', 'id5'] + }} +/> + + {/* ... */} + /> + +``` + + + +In addition to using the / props, you can also specify the function prop, which overrides those other props and ultimately determines whether a row is disabled or not. + + + + + +```tsx file="initialRowDisabledState-example.page.tsx" +``` + + + +## Using disabled rows while rendering + +When rendering a cell, you have access to the row disabled state - the type has a `rowDisabled` property which is true if the row is disabled. + + + + + This example uses custom rendering for the `firstName` column to render an emoji for disabled rows. + + +```tsx file="custom-rendering-for-disabled-rows-example.page.tsx" +``` + + + +## Using the API to enable/disable rows + +You can use the `DataSourceApi` to enable or disable rows programmatically. + + + +```tsx +dataSourceApi.setRowEnabled(rowId, enabled); + +``` + + + +```tsx +dataSourceApi.setRowEnabledAt(rowIndex, enabled); +``` + + + + +Use the context menu on each row to toggle the disabled state of the respective row. + + +```tsx file="using-api-to-disable-rows-example.page.tsx" +``` + + \ No newline at end of file diff --git a/www/content/docs/learn/rows/initialRowDisabledState-example.page.tsx b/www/content/docs/learn/rows/initialRowDisabledState-example.page.tsx new file mode 100644 index 00000000..b8114383 --- /dev/null +++ b/www/content/docs/learn/rows/initialRowDisabledState-example.page.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; + +import { + DataSource, + DataSourceData, + InfiniteTable, + InfiniteTablePropColumns, +} from '@infinite-table/infinite-react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + + salary: number; +}; + +const data: DataSourceData = () => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + type: 'number', + defaultWidth: 100, + }, + salary: { + defaultFilterable: true, + field: 'salary', + type: 'number', + }, + + firstName: { + field: 'firstName', + }, + stack: { field: 'stack' }, + currency: { field: 'currency' }, +}; + +export default () => { + return ( + <> + + data={data} + primaryKey="id" + defaultRowDisabledState={{ + enabledRows: true, + disabledRows: [1, 3, 4, 5], + }} + > + + columnDefaultWidth={120} + columnMinWidth={50} + columns={columns} + keyboardNavigation="row" + /> + + + ); +}; diff --git a/www/content/docs/learn/rows/using-api-to-disable-rows-example.page.tsx b/www/content/docs/learn/rows/using-api-to-disable-rows-example.page.tsx new file mode 100644 index 00000000..505c0325 --- /dev/null +++ b/www/content/docs/learn/rows/using-api-to-disable-rows-example.page.tsx @@ -0,0 +1,125 @@ +import * as React from 'react'; + +import { + DataSource, + DataSourceApi, + DataSourceData, + InfiniteTable, + InfiniteTablePropColumns, +} from '@infinite-table/infinite-react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + + salary: number; +}; + +const data: DataSourceData = () => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + type: 'number', + defaultWidth: 100, + }, + salary: { + defaultFilterable: true, + field: 'salary', + type: 'number', + }, + + firstName: { + field: 'firstName', + renderValue: ({ rowInfo, value }) => { + return `${value} ${rowInfo.rowDisabled ? '🚫' : ''}`; + }, + }, + stack: { field: 'stack' }, + currency: { field: 'currency' }, +}; + +export default () => { + const [dataSourceApi, setDataSourceApi] = + React.useState>(); + return ( + <> + + + + onReady={setDataSourceApi} + data={data} + primaryKey="id" + defaultRowDisabledState={{ + enabledRows: [1, 2, 3, 5], + disabledRows: true, + }} + > + + getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { + return { + columns: [{ name: 'label' }], + items: [ + { + label: 'Disable row', + key: 'disable-row', + disabled: rowInfo.rowDisabled, + onAction: ({ hideMenu }) => { + dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); + hideMenu(); + }, + }, + { + label: 'Enable row', + key: 'enable-row', + disabled: !rowInfo.rowDisabled, + onAction: ({ hideMenu }) => { + dataSourceApi.setRowEnabled(rowInfo.id, true); + hideMenu(); + }, + }, + { + label: 'Toggle row disable/enable', + key: 'toggle-row-disable-enable', + onAction: ({ hideMenu }) => { + dataSourceApi.setRowEnabled( + rowInfo.id, + rowInfo.rowDisabled, + ); + hideMenu(); + }, + }, + ], + }; + }} + columnDefaultWidth={120} + columnMinWidth={50} + columns={columns} + keyboardNavigation="row" + /> + + + ); +}; diff --git a/www/content/docs/reference/datasource-api/index.page.md b/www/content/docs/reference/datasource-api/index.page.md index 033a52cc..38280ed2 100644 --- a/www/content/docs/reference/datasource-api/index.page.md +++ b/www/content/docs/reference/datasource-api/index.page.md @@ -34,6 +34,82 @@ For API on row/group selection, see the [Selection API page](/docs/reference/sel + + +> Returns `true` if the row at the specified index is disabled, `false` otherwise. + +See the prop for more information. + +For checking if a row is disabled by its primary key, see the method. + +For changing the enable/disable state for the row, see the . + + + +```ts file="../datasource-props/rowDisabledState-example.page.tsx" +``` + + + + + + + +> Returns `true` if the row with the specified primary key is disabled, `false` otherwise. + +See the prop for more information. + +For checking if a row is disabled by its index, see the method. + +For changing the enable/disable state for the row, see the . + + + +```ts file="../datasource-props/rowDisabledState-example.page.tsx" + +``` + + + + + + + +> Sets the enable/disable state for the row with the specified primary key. + +See the prop for more information. + +For setting the enable/disable state for a row by its index, see the method. + + + +```ts file="../datasource-props/rowDisabledState-example.page.tsx" + +``` + + + + + + + +> Sets the enable/disable state for the row at the specified index. + +See the prop for more information. + +For setting the enable/disable state for a row by its primary key, see the method. + + + +```ts file="../datasource-props/rowDisabledState-example.page.tsx" + +``` + + + + + + > Adds the specified data at the end of the data source. diff --git a/www/content/docs/reference/datasource-props/defaultRowDisabledState-example.page.tsx b/www/content/docs/reference/datasource-props/defaultRowDisabledState-example.page.tsx new file mode 100644 index 00000000..a56704a4 --- /dev/null +++ b/www/content/docs/reference/datasource-props/defaultRowDisabledState-example.page.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; + +import { + DataSource, + DataSourceData, + InfiniteTable, + InfiniteTablePropColumns, +} from '@infinite-table/infinite-react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + + salary: number; +}; + +const data: DataSourceData = () => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + type: 'number', + defaultWidth: 100, + }, + salary: { + defaultFilterable: true, + field: 'salary', + type: 'number', + }, + + firstName: { + field: 'firstName', + }, + stack: { field: 'stack' }, + currency: { field: 'currency' }, +}; + +export default () => { + return ( + <> + + data={data} + primaryKey="id" + defaultRowDisabledState={{ + enabledRows: true, + disabledRows: [1, 3, 4, 5], + }} + > + + keyboardNavigation="row" + columnDefaultWidth={120} + columnMinWidth={50} + columns={columns} + /> + + + ); +}; diff --git a/www/content/docs/reference/datasource-props/index.page.md b/www/content/docs/reference/datasource-props/index.page.md index b4ea88b3..11652c4b 100644 --- a/www/content/docs/reference/datasource-props/index.page.md +++ b/www/content/docs/reference/datasource-props/index.page.md @@ -42,6 +42,160 @@ Using functions (for more dynamic primary keys) is supported, but hasn't been te + + +> This function ultimately decides the disabled state of a row. It overrides both / props. + +It's called with a single argument - the row info object for the row in question. + +It should return `true` if the row is disabled, and `false` otherwise. + + + +When this prop is used, will not be called. + + + + + + + + +> The uncontrolled prop for managing row enabled/disabled state. For the controlled version see . For listening to row disabled state changes, see . + +The value for this prop is an object with two properties: + +- `enabledRows` - either `true` or an array of row ids that are enabled. When `true` is passed, `disabledRows` should be an array of row ids that are disabled. +- `disabledRows` - either `true` or an array of row ids that are disabled. When `true` is passed, `enabledRows` should be an array of row ids that are enabled. + + + +The values in the `enabledRows`/`disabledRows` arrays are row ids, and not indexes. + + + + + + +This prop can be overriden by using the prop. + + + +Here's an example of how to use the `defaultRowDisabledState` prop: + + + + + +Rows with ids `1`, `3`, `4` and `5` are disabled. + + + +```ts file="defaultRowDisabledState-example.page.tsx" + +``` + + + + + + + +> Manages row enabled/disabled state. For the uncontrolled version see . For listening to row disabled state changes, see . + +The value for this prop is an object with two properties: + +- `enabledRows` - either `true` or an array of row ids that are enabled. When `true` is passed, `disabledRows` should be an array of row ids that are disabled. +- `disabledRows` - either `true` or an array of row ids that are disabled. When `true` is passed, `enabledRows` should be an array of row ids that are enabled. + + +When using this controlled prop, you will need to update the `rowDisabledState` prop by using the callback. + + + + +This prop can be overriden by using the prop. + + + + + + + +Rows with ids `1`, `3`, `4` and `5` are disabled initially. + +Right click rows and use the context menu to enable/disable rows. + + + +```ts file="rowDisabledState-example.page.tsx" + +``` + + + + + + + + + + +> Called when the row disabled state changes. + +It's called with just 1 argument (`rowDisabledState`), which is an instance of the `RowDisabledState` class. To get a literal object that represents the row disabled state, call the `rowDisabledState.getState()` method. + +```tsx {3,19} +import { + DataSource, + RowDisabledStateObject, +} from '@infinite-table/infinite-react'; +function App() { + const [rowDisabledState, setRowDisabledState] = React.useState< + RowDisabledStateObject + >({ + enabledRows: true, + disabledRows: [1, 3, 4, 5], + }); + return ( + <> + + data={data} + primaryKey="id" + rowDisabledState={rowDisabledState} + onRowDisabledStateChange={(rowState) => { + setRowDisabledState(rowState.getState()); + }} + /> + + ); +} +``` + + +When using the controlled prop, you will need to update the `rowDisabledState` by using this callback. + + + + + + +Rows with ids `1`, `3`, `4` and `5` are disabled initially. + +Right click rows and use the context menu to enable/disable rows. + + + +```ts file="rowDisabledState-example.page.tsx" + +``` + + + + + + + > Specifies the functions to use for aggregating data. The object is a map where the keys are ids for aggregations and values are object of the shape described below. diff --git a/www/content/docs/reference/datasource-props/rowDisabledState-example.page.tsx b/www/content/docs/reference/datasource-props/rowDisabledState-example.page.tsx new file mode 100644 index 00000000..bcf69912 --- /dev/null +++ b/www/content/docs/reference/datasource-props/rowDisabledState-example.page.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; + +import { + DataSource, + DataSourceData, + InfiniteTable, + InfiniteTablePropColumns, + RowDisabledStateObject, +} from '@infinite-table/infinite-react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + + salary: number; +}; + +const data: DataSourceData = () => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + type: 'number', + defaultWidth: 100, + }, + salary: { + defaultFilterable: true, + field: 'salary', + type: 'number', + }, + + firstName: { + field: 'firstName', + }, + stack: { field: 'stack' }, + currency: { field: 'currency' }, +}; + +export default () => { + const [rowDisabledState, setRowDisabledState] = React.useState< + RowDisabledStateObject + >({ + enabledRows: true, + disabledRows: [1, 3, 4, 5], + }); + return ( + <> + + data={data} + primaryKey="id" + rowDisabledState={rowDisabledState} + onRowDisabledStateChange={(rowState) => { + setRowDisabledState(rowState.getState()); + }} + > + + getCellContextMenuItems={({ rowInfo }, { dataSourceApi }) => { + const rowDisabled = dataSourceApi.isRowDisabledAt( + rowInfo.indexInAll, + ); + return { + columns: [{ name: 'label' }], + items: [ + { + label: 'Disable row', + disabled: rowDisabled, + key: 'disable-row', + onAction: ({ hideMenu }) => { + dataSourceApi.setRowEnabledAt(rowInfo.indexInAll, false); + hideMenu(); + }, + }, + { + label: 'Enable row', + disabled: !rowDisabled, + key: 'enable-row', + onAction: ({ hideMenu }) => { + dataSourceApi.setRowEnabled(rowInfo.id, true); + hideMenu(); + }, + }, + { + label: 'Toggle row enable/disable', + key: 'toggle-row', + onAction: ({ hideMenu }) => { + dataSourceApi.setRowEnabled( + rowInfo.id, + dataSourceApi.isRowDisabled(rowInfo.id), + ); + hideMenu(); + }, + }, + ], + }; + }} + keyboardNavigation="row" + columnDefaultWidth={120} + columnMinWidth={50} + columns={columns} + /> + + + ); +}; diff --git a/www/content/docs/reference/infinite-table-props.page.md b/www/content/docs/reference/infinite-table-props.page.md index 304b687c..faf8f4b0 100644 --- a/www/content/docs/reference/infinite-table-props.page.md +++ b/www/content/docs/reference/infinite-table-props.page.md @@ -2449,7 +2449,7 @@ In addition, if you need to configure the context menu to have other columns rat ```tsx const getCellContextMenuItems = () => { return { - columns: [{ name: 'Label' }, { name: 'Icon' }], + columns: [{ name: 'label' }, { name: 'icon' }], items: [ { label: 'Welcome', diff --git a/www/content/docs/reference/type-definitions/index.page.md b/www/content/docs/reference/type-definitions/index.page.md index aef4f10b..03eaf31e 100644 --- a/www/content/docs/reference/type-definitions/index.page.md +++ b/www/content/docs/reference/type-definitions/index.page.md @@ -384,6 +384,7 @@ The common properties of the type (in all discriminated cases) are: - `id` - the primary key of the row, as retrieved using the prop. - `indexInAll` - the index in all currently visible rows. - `rowSelected` - whether the row is selected or not - `boolean | null`. +- `rowDisabled` - whether the row is disabled or not - `boolean`. ### InfiniteTable_NoGrouping_RowInfoNormal diff --git a/www/content/docs/releases/index.page.md b/www/content/docs/releases/index.page.md index ee58333a..20c0eae1 100644 --- a/www/content/docs/releases/index.page.md +++ b/www/content/docs/releases/index.page.md @@ -3,6 +3,11 @@ title: Releases description: All releases | Infinite Table DataGrid for React --- +## 4.4.1 +## 4.4.0 + +@milestone id="125" + ## 4.3.7 @milestone id="124" diff --git a/www/src/components/MDX/Prop.tsx b/www/src/components/MDX/Prop.tsx index 3e21c940..13595ffd 100644 --- a/www/src/components/MDX/Prop.tsx +++ b/www/src/components/MDX/Prop.tsx @@ -537,9 +537,8 @@ export function PropTable({ }, []); React.useLayoutEffect(() => { - const initialText = globalThis.location - ? globalThis.location.hash.slice(1).toLowerCase() - : ''; + const hash = globalThis.location ? globalThis.location.hash.slice(1) : ''; + const initialText = hash ? hash.toLowerCase() : ''; if (initialText) { const [search, value] = initialText.split('='); @@ -547,6 +546,7 @@ export function PropTable({ if (search === 'search' && value) { resetSearch(value); } + setHash(hash); } const onHashChange = debounce(function (_event: null | HashChangeEvent) { @@ -633,6 +633,13 @@ export function PropTable({ if (!hidden) { visibleCount++; } + if (highlight) { + console.log({ + highlight, + lowerName, + lowerHash, + }); + } return React.cloneElement(child, { //@ts-ignore hidden, diff --git a/www/src/sidebarLearn.json b/www/src/sidebarLearn.json index 0b631bd6..7ffdae7a 100644 --- a/www/src/sidebarLearn.json +++ b/www/src/sidebarLearn.json @@ -81,6 +81,7 @@ "title": "Working with Rows", "path": "/docs/learn/rows/styling-rows", "transient": true, + "badge": "new", "routes": [ { "path": "/docs/learn/rows/styling-rows", @@ -90,6 +91,11 @@ "path": "/docs/learn/rows/using-row-info", "title": "Using Rows at Runtime" }, + { + "path": "/docs/learn/rows/disabled-rows", + "title": "Disabled Rows", + "badge": "new" + }, { "path": "/docs/learn/selection/row-selection#", "title": "Selecting Rows" @@ -116,7 +122,7 @@ "title": "Keyboard Navigation", "path": "/docs/learn/keyboard-navigation/navigating-cells", "transient": true, - "badge": "new", + "routes": [ { "path": "/docs/learn/keyboard-navigation/navigating-cells", @@ -128,8 +134,7 @@ }, { "path": "/docs/learn/keyboard-navigation/keyboard-shortcuts", - "title": "Keyboard Shortcuts", - "badge": "new" + "title": "Keyboard Shortcuts" } ] }, @@ -246,7 +251,7 @@ "title": "Master-Detail", "path": "/docs/learn/master-detail/overview", "transient": true, - "badge": "new", + "routes": [ { "title": "Overview",