diff --git a/src/Body/index.tsx b/src/Body/index.tsx index ade0f5bf8..d1af4d75e 100644 --- a/src/Body/index.tsx +++ b/src/Body/index.tsx @@ -31,6 +31,7 @@ const Body = (props: BodyProps) => { getRowKey, expandedKeys, childrenColumnName, + rowExpandable, emptyNode, classNames, styles, @@ -44,6 +45,7 @@ const Body = (props: BodyProps) => { 'getRowKey', 'expandedKeys', 'childrenColumnName', + 'rowExpandable', 'emptyNode', 'classNames', 'styles', @@ -59,6 +61,7 @@ const Body = (props: BodyProps) => { childrenColumnName, expandedKeys, getRowKey, + rowExpandable, ); const rowKeys = React.useMemo(() => flattenData.map(item => item.rowKey), [flattenData]); diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index 11bf129ab..712b16398 100644 --- a/src/VirtualTable/BodyGrid.tsx +++ b/src/VirtualTable/BodyGrid.tsx @@ -39,6 +39,7 @@ const Grid = React.forwardRef((props, ref) => { expandedKeys, prefixCls, childrenColumnName, + rowExpandable, scrollX, direction, } = useContext(TableContext, [ @@ -48,6 +49,7 @@ const Grid = React.forwardRef((props, ref) => { 'prefixCls', 'expandedKeys', 'childrenColumnName', + 'rowExpandable', 'scrollX', 'direction', ]); @@ -64,7 +66,13 @@ const Grid = React.forwardRef((props, ref) => { const listRef = React.useRef(null); // =========================== Data =========================== - const flattenData = useFlattenRecords(data, childrenColumnName, expandedKeys, getRowKey); + const flattenData = useFlattenRecords( + data, + childrenColumnName, + expandedKeys, + getRowKey, + rowExpandable, + ); // ========================== Column ========================== const columnsWidth = React.useMemo<[key: React.Key, width: number, total: number][]>(() => { diff --git a/src/hooks/useExpand.ts b/src/hooks/useExpand.ts index a27418895..ad13c1a52 100644 --- a/src/hooks/useExpand.ts +++ b/src/hooks/useExpand.ts @@ -90,7 +90,12 @@ export default function useExpand( return defaultExpandedRowKeys; } if (defaultExpandAllRows) { - return findAllChildrenKeys(mergedData, getRowKey, mergedChildrenColumnName); + return findAllChildrenKeys( + mergedData, + getRowKey, + mergedChildrenColumnName, + rowExpandable, + ); } return []; }); diff --git a/src/hooks/useFlattenRecords.ts b/src/hooks/useFlattenRecords.ts index c5011a27f..e82758e91 100644 --- a/src/hooks/useFlattenRecords.ts +++ b/src/hooks/useFlattenRecords.ts @@ -10,6 +10,7 @@ function fillRecords( expandedKeys: Set, getRowKey: GetRowKey, index: number, + rowExpandable?: (record: T) => boolean, ) { const key = getRowKey(record, index); @@ -22,7 +23,12 @@ function fillRecords( const expanded = expandedKeys?.has(key); - if (record && Array.isArray(record[childrenColumnName]) && expanded) { + if ( + record && + (!rowExpandable || rowExpandable(record)) && + Array.isArray(record[childrenColumnName]) && + expanded + ) { // expanded state, flat record for (let i = 0; i < record[childrenColumnName].length; i += 1) { fillRecords( @@ -33,6 +39,7 @@ function fillRecords( expandedKeys, getRowKey, i, + rowExpandable, ); } } @@ -61,6 +68,7 @@ export default function useFlattenRecords( childrenColumnName: string, expandedKeys: Set, getRowKey: GetRowKey, + rowExpandable?: (record: T) => boolean, ): FlattenData[] { const arr = React.useMemo[]>(() => { if (expandedKeys?.size) { @@ -71,7 +79,7 @@ export default function useFlattenRecords( const record = data[i]; // using array.push or spread operator may cause "Maximum call stack size exceeded" exception if array size is big enough. - fillRecords(list, record, 0, childrenColumnName, expandedKeys, getRowKey, i); + fillRecords(list, record, 0, childrenColumnName, expandedKeys, getRowKey, i, rowExpandable); } return list; @@ -85,7 +93,7 @@ export default function useFlattenRecords( rowKey: getRowKey(item, index), }; }); - }, [data, childrenColumnName, expandedKeys, getRowKey]); + }, [data, childrenColumnName, expandedKeys, getRowKey, rowExpandable]); return arr; } diff --git a/src/hooks/useRowInfo.tsx b/src/hooks/useRowInfo.tsx index cdba4059e..ae651df6f 100644 --- a/src/hooks/useRowInfo.tsx +++ b/src/hooks/useRowInfo.tsx @@ -75,11 +75,14 @@ export default function useRowInfo( const nestExpandable = expandableType === 'nest'; const rowSupportExpand = expandableType === 'row' && (!rowExpandable || rowExpandable(record)); - const mergedExpandable = rowSupportExpand || nestExpandable; + const rowSupportNestExpand = nestExpandable && (!rowExpandable || rowExpandable(record)); const expanded = expandedKeys && expandedKeys.has(rowKey); - const hasNestChildren = childrenColumnName && record && record[childrenColumnName]; + const nestChildren = childrenColumnName && record && record[childrenColumnName]; + const hasNestChildren = + rowSupportNestExpand && Array.isArray(nestChildren) && nestChildren.length > 0; + const mergedExpandable = rowSupportExpand || hasNestChildren; const onInternalTriggerExpand = useEvent(onTriggerExpand); diff --git a/src/utils/expandUtil.tsx b/src/utils/expandUtil.tsx index 43fbe26a1..58bfcfeea 100644 --- a/src/utils/expandUtil.tsx +++ b/src/utils/expandUtil.tsx @@ -92,11 +92,16 @@ export function findAllChildrenKeys( data: readonly RecordType[], getRowKey: GetRowKey, childrenColumnName: string, + rowExpandable?: (record: RecordType) => boolean, ): Key[] { const keys: Key[] = []; function dig(list: readonly RecordType[]) { (list || []).forEach((item, index) => { + if (rowExpandable && !rowExpandable(item)) { + return; + } + keys.push(getRowKey(item, index)); dig((item as any)[childrenColumnName]); diff --git a/tests/ExpandRow.spec.jsx b/tests/ExpandRow.spec.jsx index 50430c325..c59235d1e 100644 --- a/tests/ExpandRow.spec.jsx +++ b/tests/ExpandRow.spec.jsx @@ -162,6 +162,81 @@ describe('Table.Expand', () => { expect(container.firstChild).toMatchSnapshot(); }); + it('honors rowExpandable for tree data', () => { + const onExpand = vi.fn(); + const data = [ + { + key: 'allowed', + name: 'Allowed parent', + children: [{ key: 'allowed-child', name: 'Allowed child' }], + }, + { + key: 'blocked', + name: 'Blocked parent', + children: [{ key: 'blocked-child', name: 'Blocked child' }], + }, + { key: 'empty', name: 'Empty parent', children: [] }, + ]; + const { container } = render( + createTable({ + data, + expandable: { + expandedRowKeys: ['allowed', 'blocked', 'empty'], + expandRowByClick: true, + onExpand, + rowExpandable: record => record.key === 'allowed', + }, + }), + ); + + expect(container.querySelector('[data-row-key="allowed-child"]')).toBeTruthy(); + expect(container.querySelector('[data-row-key="blocked-child"]')).toBeFalsy(); + + const allowedIcon = container.querySelector( + '[data-row-key="allowed"] .rc-table-row-expand-icon', + ); + const blockedIcon = container.querySelector( + '[data-row-key="blocked"] .rc-table-row-expand-icon', + ); + const emptyIcon = container.querySelector('[data-row-key="empty"] .rc-table-row-expand-icon'); + expect(allowedIcon).toHaveClass('rc-table-row-expanded'); + expect(blockedIcon).toHaveClass('rc-table-row-spaced'); + expect(emptyIcon).toHaveClass('rc-table-row-spaced'); + + fireEvent.click(container.querySelector('[data-row-key="blocked"]')); + expect(onExpand).not.toHaveBeenCalled(); + }); + + it('honors rowExpandable when expanding all tree rows by default', () => { + const data = [ + { + key: 'allowed', + name: 'Allowed parent', + children: [{ key: 'allowed-child', name: 'Allowed child' }], + }, + { + key: 'blocked', + name: 'Blocked parent', + children: [{ key: 'blocked-child', name: 'Blocked child' }], + }, + ]; + const { container } = render( + createTable({ + data, + expandable: { + defaultExpandAllRows: true, + rowExpandable: record => record.key === 'allowed', + }, + }), + ); + + expect(container.querySelector('[data-row-key="allowed-child"]')).toBeTruthy(); + expect(container.querySelector('[data-row-key="blocked-child"]')).toBeFalsy(); + expect( + container.querySelector('[data-row-key="blocked"] .rc-table-row-expand-icon'), + ).toHaveClass('rc-table-row-spaced'); + }); + it('not use nest when children is invalidate', () => { const data = [ { key: 2, name: 'Jack', age: 28, children: null }, diff --git a/tests/Table.spec.jsx b/tests/Table.spec.jsx index a4d1a74c1..0d72c3e33 100644 --- a/tests/Table.spec.jsx +++ b/tests/Table.spec.jsx @@ -1013,7 +1013,7 @@ describe('Table.Basic', () => { { key: 'parent', children: [ - { key: 'light', children: [] }, + { key: 'light', children: [{ key: 'spark' }] }, { key: 'bamboo', children: [{ key: 'little' }] }, ], }, diff --git a/tests/Virtual.spec.tsx b/tests/Virtual.spec.tsx index be7911615..4f333c70f 100644 --- a/tests/Virtual.spec.tsx +++ b/tests/Virtual.spec.tsx @@ -163,6 +163,31 @@ describe('Table.Virtual', () => { }); describe('expandable', () => { + it('honors rowExpandable for tree data', () => { + const { container } = getTable({ + data: [ + { + name: 'allowed', + children: [{ name: 'allowed-child' }], + }, + { + name: 'blocked', + children: [{ name: 'blocked-child' }], + }, + ], + expandable: { + expandedRowKeys: ['allowed', 'blocked'], + rowExpandable: record => record.name === 'allowed', + }, + }); + + expect(container.querySelector('[data-row-key="allowed-child"]')).toBeTruthy(); + expect(container.querySelector('[data-row-key="blocked-child"]')).toBeFalsy(); + expect( + container.querySelector('[data-row-key="blocked"] .rc-table-row-expand-icon'), + ).toHaveClass('rc-table-row-spaced'); + }); + it('basic', () => { (['bamboo', () => 'bamboo'] as const).forEach(cls => { const { container } = getTable({ diff --git a/tests/__snapshots__/ExpandRow.spec.jsx.snap b/tests/__snapshots__/ExpandRow.spec.jsx.snap index e589629f2..7d4baa297 100644 --- a/tests/__snapshots__/ExpandRow.spec.jsx.snap +++ b/tests/__snapshots__/ExpandRow.spec.jsx.snap @@ -907,7 +907,7 @@ exports[`Table.Expand > renders tree row correctly with different children 1`] = class="rc-table-row-indent indent-level-0" /> Jack