Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Body/MeasureCell.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import ResizeObserver from 'rc-resize-observer';
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
import { sanitizeCloneElement } from '../utils/reactNodeUtil';

export interface MeasureCellProps {
columnKey: React.Key;
Expand All @@ -26,7 +27,9 @@ export default function MeasureCell({
return (
<ResizeObserver data={columnKey}>
<th ref={cellRef} className={`${prefixCls}-measure-cell`}>
<div className={`${prefixCls}-measure-cell-content`}>{title || '\xa0'}</div>
<div className={`${prefixCls}-measure-cell-content`}>
{sanitizeCloneElement(title) || '\xa0'}
</div>
</th>
</ResizeObserver>
);
Expand Down
6 changes: 1 addition & 5 deletions src/Body/MeasureRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,13 @@ export default function MeasureRow({
>
{columnsKey.map(columnKey => {
const column = columns.find(col => col.key === columnKey);
const rawTitle = column?.title;
const titleForMeasure = React.isValidElement<React.RefAttributes<any>>(rawTitle)
? React.cloneElement(rawTitle, { ref: null })
: rawTitle;
return (
<MeasureCell
prefixCls={prefixCls}
key={columnKey}
columnKey={columnKey}
onColumnResize={onColumnResize}
title={titleForMeasure}
title={column?.title}
/>
);
})}
Expand Down
106 changes: 106 additions & 0 deletions src/utils/__tests__/reactNodeUtil.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from 'react';
import { sanitizeCloneElement } from '../reactNodeUtil';

describe('reactNodeUtil', () => {
describe('sanitizeCloneElement', () => {
it('should remove filtered props', () => {
const node = React.createElement(
'div',
{
id: 'test-id',
'data-test': 'test-data',
onFocus: () => {},
onBlur: () => {},
tabIndex: 0,
className: 'test-class',
},
'test content',
);

const result = sanitizeCloneElement(node) as React.ReactElement;

expect(result.props.id).toBeUndefined();
expect(result.props['data-test']).toBeUndefined();
expect(result.props.onFocus).toBeUndefined();
expect(result.props.onBlur).toBeUndefined();
expect(result.props.tabIndex).toBeUndefined();
expect(result.props.className).toBe('test-class');
expect(result.props.children).toBe('test content');
});

it('should handle nested elements', () => {
const nested = React.createElement('span', { id: 'nested-id' }, 'nested');
const parent = React.createElement(
'div',
{
id: 'parent-id',
className: 'parent',
},
nested,
);

const result = sanitizeCloneElement(parent) as React.ReactElement;

expect(result.props.id).toBeUndefined();
expect(result.props.className).toBe('parent');

const childElement = result.props.children as React.ReactElement;
expect(childElement.props.id).toBeUndefined();
expect(childElement.props.children).toBe('nested');
});

it('should limit recursion depth', () => {
// 创建深层嵌套结构
let deepNode: React.ReactElement = React.createElement('span', { id: 'deep' }, 'content');

for (let i = 0; i < 15; i++) {
deepNode = React.createElement('div', { id: `level-${i}` }, deepNode);
}

const result = sanitizeCloneElement(deepNode, 0, 5);
expect(result).toBeDefined();
// 应该在递归深度限制内正常工作
});

it('should return non-React elements unchanged', () => {
const textNode = 'plain text';
const numberNode = 42;
const nullNode = null;

expect(sanitizeCloneElement(textNode)).toBe(textNode);
expect(sanitizeCloneElement(numberNode)).toBe(numberNode);
expect(sanitizeCloneElement(nullNode)).toBe(nullNode);
});

it('should use cache for performance', () => {
const props = { className: 'test', id: 'will-be-removed' };
const node1 = React.createElement('div', props, 'content');
const node2 = React.createElement('span', props, 'other content');

const result1 = sanitizeCloneElement(node1) as React.ReactElement;
const result2 = sanitizeCloneElement(node2) as React.ReactElement;

// 虽然是不同的元素,但相同的 props 应该被缓存
expect(result1.props.className).toBe('test');
expect(result1.props.id).toBeUndefined();
expect(result2.props.className).toBe('test');
expect(result2.props.id).toBeUndefined();
});

it('should return original node if no props need filtering', () => {
const node = React.createElement(
'div',
{
className: 'test',
title: 'clean',
},
'content',
);

const result = sanitizeCloneElement(node);

// 如果没有需要过滤的属性,应该返回原节点以提高性能
expect(result).toBe(node);
});
});
});
76 changes: 76 additions & 0 deletions src/utils/reactNodeUtil.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { cloneElement, isValidElement } from 'react';

// Props type definition
type Props = Record<string, unknown>;

// Cache processed props to avoid redundant calculations
const propsCache = new WeakMap<Props, Props>();

// Set of properties to filter, using Set for better lookup performance
const FILTERED_PROPS = new Set(['id', 'ref', 'onFocus', 'onBlur', 'tabIndex']);

const stripProps = (props: Props) => {
// Check cache first
if (propsCache.has(props)) {
const cachedProps = propsCache.get(props);
return cachedProps || props;
}

const result: Props = {};
let hasFilteredProps = false;

for (const key in props) {
// Use Set for fast lookup, avoiding multiple || conditions
if (FILTERED_PROPS.has(key) || key.startsWith('data-')) {
hasFilteredProps = true;
continue;
}
result[key] = props[key];
}

// If no props need filtering, return original props directly
if (!hasFilteredProps) {
propsCache.set(props, props);
return props;
}

// Cache the result
propsCache.set(props, result);
return result;
};

/**
* Recursively clone ReactNode and remove data-*, id, ref, onFocus, onBlur props
* to avoid potential issues with nested elements in table cells.
* @param node - React node to sanitize
* @param depth - Current recursion depth (for performance control)
* @param maxDepth - Maximum recursion depth to prevent stack overflow
*/
const sanitizeCloneElement = (
node: React.ReactNode,
depth: number = 0,
maxDepth: number = 10,
): React.ReactNode => {
// Limit recursion depth to prevent performance issues and stack overflow
if (depth >= maxDepth || !isValidElement(node)) {
return node;
}

const cleanedProps = stripProps(node.props);

// If props haven't changed and no children, return original node directly
if (cleanedProps === node.props && !cleanedProps.children) {
return node;
}

// Process children
if (cleanedProps.children) {
cleanedProps.children = React.Children.map(cleanedProps.children, (child: React.ReactNode) =>

Check failure on line 68 in src/utils/reactNodeUtil.tsx

View workflow job for this annotation

GitHub Actions / test / react component workflow

tests/FixedHeader.spec.jsx > Table.FixedHeader > should support measureRowRender to wrap MeasureRow with custom provider

TypeError: Cannot assign to read only property 'children' of object '#<Object>' ❯ Module.sanitizeCloneElement src/utils/reactNodeUtil.tsx:68:18 ❯ MeasureCell src/Body/MeasureCell.tsx:31:12 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:14803:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:17482:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:18596:16 ❯ HTMLUnknownElement.callCallback node_modules/react-dom/cjs/react-dom.development.js:188:14 ❯ HTMLUnknownElement.callTheUserObjectsOperation node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30 ❯ innerInvokeEventListeners node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25 ❯ invokeEventListeners node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3 ❯ HTMLUnknownElementImpl._dispatch node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9

Check failure on line 68 in src/utils/reactNodeUtil.tsx

View workflow job for this annotation

GitHub Actions / test / react component workflow

tests/FixedHeader.spec.jsx > Table.FixedHeader > should support measureRowRender to wrap MeasureRow with custom provider

TypeError: Cannot assign to read only property 'children' of object '#<Object>' ❯ Module.sanitizeCloneElement src/utils/reactNodeUtil.tsx:68:18 ❯ MeasureCell src/Body/MeasureCell.tsx:31:12 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:14803:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:17482:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:18596:16 ❯ HTMLUnknownElement.callCallback node_modules/react-dom/cjs/react-dom.development.js:188:14 ❯ HTMLUnknownElement.callTheUserObjectsOperation node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30 ❯ innerInvokeEventListeners node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25 ❯ invokeEventListeners node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3 ❯ HTMLUnknownElementImpl._dispatch node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9
sanitizeCloneElement(child, depth + 1, maxDepth),
);
}

return cloneElement(node, cleanedProps);
};

export { sanitizeCloneElement };
Loading