@rainbow-oh/yee-c 0.8.15 → 0.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Search/search.js +1 -1
- package/dist/cjs/Table/demo/fixed.d.ts +3 -0
- package/dist/cjs/Table/demo/fixed.js +123 -0
- package/dist/cjs/Table/hooks/useColumns.d.ts +2 -1
- package/dist/cjs/Table/hooks/useColumns.js +4 -3
- package/dist/cjs/Table/style/index.less +23 -3
- package/dist/cjs/Table/table.js +67 -21
- package/dist/cjs/Table/util/index.d.ts +1 -1
- package/dist/cjs/Table/util/index.js +16 -3
- package/dist/cjs/Tree/interface.d.ts +16 -0
- package/dist/cjs/Tree/tree-node.js +7 -2
- package/dist/cjs/Tree/tree.d.ts +2 -1
- package/dist/cjs/Tree/tree.js +6 -2
- package/dist/cjs/Tree/utils/tree-tools.d.ts +1 -0
- package/dist/cjs/Tree/utils/tree-tools.js +3 -1
- package/dist/cjs/style/themes.css +1 -1
- package/dist/cjs/style/variables.css +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Search/search.js +1 -1
- package/dist/esm/Table/demo/fixed.d.ts +3 -0
- package/dist/esm/Table/demo/fixed.js +84 -0
- package/dist/esm/Table/hooks/useColumns.d.ts +2 -1
- package/dist/esm/Table/hooks/useColumns.js +4 -3
- package/dist/esm/Table/style/index.less +23 -3
- package/dist/esm/Table/table.js +85 -26
- package/dist/esm/Table/util/index.d.ts +1 -1
- package/dist/esm/Table/util/index.js +16 -3
- package/dist/esm/Tree/interface.d.ts +16 -0
- package/dist/esm/Tree/tree-node.js +12 -2
- package/dist/esm/Tree/tree.d.ts +2 -1
- package/dist/esm/Tree/tree.js +15 -13
- package/dist/esm/Tree/utils/tree-tools.d.ts +1 -0
- package/dist/esm/Tree/utils/tree-tools.js +4 -1
- package/dist/esm/style/themes.css +1 -1
- package/dist/esm/style/variables.css +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +1 -1
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Search/search.tsx +1 -1
- package/src/Table/demo/fixed.tsx +92 -0
- package/src/Table/hooks/useColumns.ts +4 -2
- package/src/Table/index.en-US.md +1 -0
- package/src/Table/index.zh-CN.md +1 -0
- package/src/Table/style/index.less +23 -3
- package/src/Table/table.tsx +88 -23
- package/src/Table/util/index.ts +32 -7
- package/src/Tree/index.en-US.md +20 -2
- package/src/Tree/index.zh-CN.md +20 -2
- package/src/Tree/interface.ts +16 -1
- package/src/Tree/tree-node.tsx +15 -2
- package/src/Tree/tree.tsx +6 -2
- package/src/Tree/utils/tree-tools.ts +4 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/Search/search.tsx
CHANGED
|
@@ -233,7 +233,7 @@ const Search = React.forwardRef<HTMLInputElement, SearchProps>(
|
|
|
233
233
|
aria-autocomplete={hasPopup ? 'list' : undefined}
|
|
234
234
|
aria-controls={hasPopup && open ? listboxId : undefined}
|
|
235
235
|
aria-activedescendant={
|
|
236
|
-
hasPopup && open && focusedKey
|
|
236
|
+
hasPopup && open && focusedKey !== null
|
|
237
237
|
? String(focusedKey)
|
|
238
238
|
: undefined
|
|
239
239
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ColumnProps } from '@rainbow-oh/yee-c';
|
|
2
|
+
import { Table } from '@rainbow-oh/yee-c';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
const columns: ColumnProps[] = [
|
|
6
|
+
{
|
|
7
|
+
title: 'Name',
|
|
8
|
+
dataIndex: 'name',
|
|
9
|
+
key: 'name',
|
|
10
|
+
fixed: 'left',
|
|
11
|
+
width: 140,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
title: 'Age',
|
|
15
|
+
dataIndex: 'age',
|
|
16
|
+
key: 'age',
|
|
17
|
+
fixed: 'left',
|
|
18
|
+
width: 100,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
title: 'Address',
|
|
22
|
+
dataIndex: 'address',
|
|
23
|
+
key: 'address',
|
|
24
|
+
width: 240,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: 'Company',
|
|
28
|
+
dataIndex: 'company',
|
|
29
|
+
key: 'company',
|
|
30
|
+
width: 180,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: 'Department',
|
|
34
|
+
dataIndex: 'department',
|
|
35
|
+
key: 'department',
|
|
36
|
+
width: 180,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: 'Email',
|
|
40
|
+
dataIndex: 'email',
|
|
41
|
+
key: 'email',
|
|
42
|
+
width: 240,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'Action',
|
|
46
|
+
key: 'action',
|
|
47
|
+
fixed: 'right',
|
|
48
|
+
width: 100,
|
|
49
|
+
render: (record) => <a onClick={() => console.log(record)}>View</a>,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const dataSource = [
|
|
54
|
+
{
|
|
55
|
+
key: '1',
|
|
56
|
+
name: 'John Brown',
|
|
57
|
+
age: 32,
|
|
58
|
+
address: 'New York No. 1 Lake Park',
|
|
59
|
+
company: 'Acme Inc.',
|
|
60
|
+
department: 'Engineering',
|
|
61
|
+
email: 'john.brown@example.com',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: '2',
|
|
65
|
+
name: 'Jim Green',
|
|
66
|
+
age: 42,
|
|
67
|
+
address: 'London No. 1 Lake Park',
|
|
68
|
+
company: 'Globex',
|
|
69
|
+
department: 'Marketing',
|
|
70
|
+
email: 'jim.green@example.com',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: '3',
|
|
74
|
+
name: 'Joe Black',
|
|
75
|
+
age: 28,
|
|
76
|
+
address: 'Sidney No. 1 Lake Park',
|
|
77
|
+
company: 'Initech',
|
|
78
|
+
department: 'Product',
|
|
79
|
+
email: 'joe.black@example.com',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export default () => (
|
|
84
|
+
<Table
|
|
85
|
+
columns={columns}
|
|
86
|
+
dataSource={dataSource}
|
|
87
|
+
rowKey="key"
|
|
88
|
+
pagination={false}
|
|
89
|
+
scroll={{ x: 1180 }}
|
|
90
|
+
tableLayout="fixed"
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
@@ -31,6 +31,7 @@ export default function useColumns({
|
|
|
31
31
|
columns,
|
|
32
32
|
rowSelection,
|
|
33
33
|
expandable,
|
|
34
|
+
measuredColumnWidths,
|
|
34
35
|
}: {
|
|
35
36
|
children?:
|
|
36
37
|
| React.ReactElement<ColumnProps>
|
|
@@ -38,6 +39,7 @@ export default function useColumns({
|
|
|
38
39
|
columns: ColumnProps[];
|
|
39
40
|
rowSelection?: RowSelectionType;
|
|
40
41
|
expandable?: ExpandableType;
|
|
42
|
+
measuredColumnWidths?: number[];
|
|
41
43
|
}) {
|
|
42
44
|
const childColumns: Array<ColumnProps> | null = useMemo(() => {
|
|
43
45
|
return children
|
|
@@ -53,10 +55,10 @@ export default function useColumns({
|
|
|
53
55
|
[]) as WrapedColumnProps[];
|
|
54
56
|
const tree = injectSelectionExpand(base, rowSelection, expandable);
|
|
55
57
|
const leaves = flattenLeafColumns(tree);
|
|
56
|
-
const wraped = handleColumns(leaves);
|
|
58
|
+
const wraped = handleColumns(leaves, measuredColumnWidths);
|
|
57
59
|
const rows = parseHeaderRows(tree, wraped);
|
|
58
60
|
return { wrapedColumns: wraped, headerRows: rows };
|
|
59
|
-
}, [childColumns, columns, rowSelection, expandable]);
|
|
61
|
+
}, [childColumns, columns, rowSelection, expandable, measuredColumnWidths]);
|
|
60
62
|
|
|
61
63
|
return {
|
|
62
64
|
wrapedColumns,
|
package/src/Table/index.en-US.md
CHANGED
|
@@ -23,6 +23,7 @@ A table displays rows of data.
|
|
|
23
23
|
<code src="./demo/pagination.tsx" title="Pagination" description="Table with pagination"></code>
|
|
24
24
|
<code src="./demo/column-filter.tsx" title="column filter" description="Table column filter"></code>
|
|
25
25
|
<code src="./demo/grouping.tsx" title="Grouping" description="Multi-level header"></code>
|
|
26
|
+
<code src="./demo/fixed.tsx" title="Fixed Columns" description="Fix columns on both sides and scroll horizontally to see the effect"></code>
|
|
26
27
|
<code src="./demo/components-override.tsx" title="components override" description="tbody / row rendering that can be taken over by external libs (e.g. dnd-kit)"></code>
|
|
27
28
|
## API
|
|
28
29
|
|
package/src/Table/index.zh-CN.md
CHANGED
|
@@ -24,6 +24,7 @@ toc: 'content'
|
|
|
24
24
|
<code src="./demo/pagination.tsx" title="分页" description="带分页的表格"></code>
|
|
25
25
|
<code src="./demo/merge-cell.tsx" title="合并单元格" description="合并单元格的表格"></code>
|
|
26
26
|
<code src="./demo/grouping.tsx" title="表头分组" description="多级表头"></code>
|
|
27
|
+
<code src="./demo/fixed.tsx" title="固定列" description="固定表格左右列,横向滚动可查看效果"></code>
|
|
27
28
|
<code src="./demo/summary.tsx" title="统计" description="表格的统计行"></code>
|
|
28
29
|
<code src="./demo/filter.tsx" title="筛选" description="表格的筛选功能"></code>
|
|
29
30
|
<code src="./demo/column-filter.tsx" title="列筛选" description="表格的列筛选功能"></code>
|
|
@@ -85,15 +85,34 @@
|
|
|
85
85
|
overflow: auto;
|
|
86
86
|
|
|
87
87
|
&-wrapper {
|
|
88
|
-
overflow: auto;
|
|
89
88
|
width: 100%;
|
|
90
89
|
|
|
91
90
|
&-scroll-left @{pls}-cell-fixed-left-last {
|
|
92
|
-
|
|
91
|
+
&::after {
|
|
92
|
+
content: '';
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
position: absolute;
|
|
95
|
+
display: block;
|
|
96
|
+
height: 100%;
|
|
97
|
+
width: 15px;
|
|
98
|
+
right: -15px;
|
|
99
|
+
top: 0;
|
|
100
|
+
box-shadow: inset 4px 0 8px -4px var(--yee-table-fixed-left-after-bs-color);
|
|
101
|
+
}
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
&-scroll-right @{pls}-cell-fixed-right-first {
|
|
96
|
-
|
|
105
|
+
&::after {
|
|
106
|
+
content: '';
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
position: absolute;
|
|
109
|
+
display: block;
|
|
110
|
+
height: 100%;
|
|
111
|
+
width: 15px;
|
|
112
|
+
left: -15px;
|
|
113
|
+
top: 0;
|
|
114
|
+
box-shadow: inset -4px 0 8px -4px var(--yee-table-fixed-right-after-bs-color);
|
|
115
|
+
}
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
|
|
@@ -156,6 +175,7 @@
|
|
|
156
175
|
&-fixed-left,
|
|
157
176
|
&-fixed-right {
|
|
158
177
|
position: sticky;
|
|
178
|
+
z-index: 1;
|
|
159
179
|
background-color: var(--yee-table-fixed-column-bg-color);
|
|
160
180
|
}
|
|
161
181
|
}
|
package/src/Table/table.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import TFooter from './tfooter';
|
|
|
12
12
|
import THeader from './theader';
|
|
13
13
|
|
|
14
14
|
import { GlobalContext } from '../Config-Provider';
|
|
15
|
+
import useLatest from '../hooks/useLatest';
|
|
15
16
|
import mergeContextToProps from '../utils/mergeContextToProps';
|
|
16
17
|
import omit from '../utils/omit';
|
|
17
18
|
import { pickDataAttrs } from '../utils/types';
|
|
@@ -61,6 +62,8 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
61
62
|
} = props;
|
|
62
63
|
|
|
63
64
|
const dataAttrs = pickDataAttrs(rest as Record<string, unknown>);
|
|
65
|
+
const [measuredColumnWidths, setMeasuredColumnWidths] =
|
|
66
|
+
React.useState<number[]>();
|
|
64
67
|
|
|
65
68
|
const getRowKey = React.useCallback(
|
|
66
69
|
(record: Record<string, any>, key = rowKey) => {
|
|
@@ -80,6 +83,7 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
80
83
|
columns: propColumns,
|
|
81
84
|
expandable,
|
|
82
85
|
rowSelection,
|
|
86
|
+
measuredColumnWidths,
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
// Filter
|
|
@@ -153,27 +157,22 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
153
157
|
|
|
154
158
|
// Track filter changes to emit onChange after filtered data is recalculated
|
|
155
159
|
const prevFilterRecordsRef = useRef(filterRecords);
|
|
156
|
-
const contentWrapperRef = useRef<HTMLDivElement>(null);
|
|
157
160
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
161
|
+
const tableRef = useRef<HTMLTableElement>(null);
|
|
158
162
|
const [horizontalScroll, setHorizontalScroll] = React.useState({
|
|
159
163
|
left: false,
|
|
160
164
|
right: false,
|
|
161
165
|
});
|
|
162
166
|
|
|
163
167
|
const updateHorizontalScroll = React.useCallback(() => {
|
|
164
|
-
const wrapper = contentWrapperRef.current;
|
|
165
168
|
const content = contentRef.current;
|
|
166
|
-
if (!
|
|
169
|
+
if (!content) return;
|
|
167
170
|
|
|
168
|
-
const
|
|
169
|
-
content.scrollWidth - content.clientWidth > 1 ? content : wrapper;
|
|
170
|
-
const maxScrollLeft =
|
|
171
|
-
scrollContainer.scrollWidth - scrollContainer.clientWidth;
|
|
171
|
+
const maxScrollLeft = content.scrollWidth - content.clientWidth;
|
|
172
172
|
const hasHorizontalScroll = maxScrollLeft > 1;
|
|
173
173
|
const next = {
|
|
174
|
-
left: hasHorizontalScroll &&
|
|
175
|
-
right:
|
|
176
|
-
hasHorizontalScroll && scrollContainer.scrollLeft < maxScrollLeft - 1,
|
|
174
|
+
left: hasHorizontalScroll && content.scrollLeft > 1,
|
|
175
|
+
right: hasHorizontalScroll && content.scrollLeft < maxScrollLeft - 1,
|
|
177
176
|
};
|
|
178
177
|
|
|
179
178
|
setHorizontalScroll((current) =>
|
|
@@ -183,6 +182,54 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
183
182
|
);
|
|
184
183
|
}, []);
|
|
185
184
|
|
|
185
|
+
// A primitive signature of everything in the columns that can affect the
|
|
186
|
+
// measured layout, used as an effect dependency instead of the array itself.
|
|
187
|
+
const columnLayoutKey = useMemo(
|
|
188
|
+
() =>
|
|
189
|
+
wrapedColumns
|
|
190
|
+
.map(
|
|
191
|
+
(column, index) =>
|
|
192
|
+
`${column.key ?? column.dataIndex ?? index}:${
|
|
193
|
+
column.fixed ?? ''
|
|
194
|
+
}:${column.width ?? ''}`,
|
|
195
|
+
)
|
|
196
|
+
.join('|'),
|
|
197
|
+
[wrapedColumns],
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Keep the latest columns in a ref so that the measure callback stays stable:
|
|
201
|
+
// `wrapedColumns` is derived from `measuredColumnWidths`, so depending on it
|
|
202
|
+
// directly would recreate the callback (and the ResizeObserver) on every
|
|
203
|
+
// measurement.
|
|
204
|
+
const wrapedColumnsRef = useLatest(wrapedColumns);
|
|
205
|
+
|
|
206
|
+
const updateMeasuredColumnWidths = React.useCallback(() => {
|
|
207
|
+
const columns = wrapedColumnsRef.current;
|
|
208
|
+
if (!columns.some((column) => column.fixed)) return;
|
|
209
|
+
|
|
210
|
+
const colElements = tableRef.current?.querySelectorAll(
|
|
211
|
+
':scope > colgroup > col',
|
|
212
|
+
);
|
|
213
|
+
if (!colElements || colElements.length !== columns.length) return;
|
|
214
|
+
|
|
215
|
+
// Round to whole pixels: sticky offsets do not need sub-pixel precision,
|
|
216
|
+
// and raw fractional widths would otherwise flip-flop (scrollbar showing /
|
|
217
|
+
// hiding, browser zoom) and keep re-triggering renders.
|
|
218
|
+
const nextWidths = Array.from(colElements, (column) =>
|
|
219
|
+
Math.round(column.getBoundingClientRect().width),
|
|
220
|
+
);
|
|
221
|
+
if (nextWidths.some((width) => !Number.isFinite(width) || width <= 0)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setMeasuredColumnWidths((current) => {
|
|
226
|
+
const unchanged =
|
|
227
|
+
current?.length === nextWidths.length &&
|
|
228
|
+
current.every((width, index) => width === nextWidths[index]);
|
|
229
|
+
return unchanged ? current : nextWidths;
|
|
230
|
+
});
|
|
231
|
+
}, [wrapedColumnsRef]);
|
|
232
|
+
|
|
186
233
|
useEffect(() => {
|
|
187
234
|
if (prevFilterRecordsRef.current === filterRecords) return;
|
|
188
235
|
|
|
@@ -207,24 +254,43 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
207
254
|
onChange,
|
|
208
255
|
]);
|
|
209
256
|
|
|
257
|
+
// Observers are attached once: both callbacks are referentially stable.
|
|
210
258
|
useEffect(() => {
|
|
211
|
-
updateHorizontalScroll();
|
|
212
|
-
|
|
213
|
-
const wrapper = contentWrapperRef.current;
|
|
214
259
|
const content = contentRef.current;
|
|
215
|
-
|
|
260
|
+
const tableElement = tableRef.current;
|
|
261
|
+
if (!content || !tableElement) return undefined;
|
|
262
|
+
|
|
263
|
+
const handleResize = () => {
|
|
264
|
+
updateHorizontalScroll();
|
|
265
|
+
updateMeasuredColumnWidths();
|
|
266
|
+
};
|
|
216
267
|
|
|
217
268
|
if (typeof ResizeObserver === 'undefined') {
|
|
218
|
-
window.addEventListener('resize',
|
|
219
|
-
return () => window.removeEventListener('resize',
|
|
269
|
+
window.addEventListener('resize', handleResize);
|
|
270
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
220
271
|
}
|
|
221
272
|
|
|
222
|
-
const resizeObserver = new ResizeObserver(
|
|
223
|
-
resizeObserver.observe(wrapper);
|
|
273
|
+
const resizeObserver = new ResizeObserver(handleResize);
|
|
224
274
|
resizeObserver.observe(content);
|
|
275
|
+
resizeObserver.observe(tableElement);
|
|
225
276
|
|
|
226
277
|
return () => resizeObserver.disconnect();
|
|
227
|
-
}, [updateHorizontalScroll,
|
|
278
|
+
}, [updateHorizontalScroll, updateMeasuredColumnWidths]);
|
|
279
|
+
|
|
280
|
+
// Re-sync when something that can change the layout changes. `columnLayoutKey`
|
|
281
|
+
// is a primitive, so a measurement round-trip (which produces a new
|
|
282
|
+
// `wrapedColumns` array with identical layout config) does not retrigger it.
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
updateHorizontalScroll();
|
|
285
|
+
updateMeasuredColumnWidths();
|
|
286
|
+
}, [
|
|
287
|
+
columnLayoutKey,
|
|
288
|
+
pageData,
|
|
289
|
+
scroll?.x,
|
|
290
|
+
scroll?.y,
|
|
291
|
+
updateHorizontalScroll,
|
|
292
|
+
updateMeasuredColumnWidths,
|
|
293
|
+
]);
|
|
228
294
|
|
|
229
295
|
const renderHeader = () => {
|
|
230
296
|
return (
|
|
@@ -283,6 +349,7 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
283
349
|
const tableProps = {
|
|
284
350
|
style: {
|
|
285
351
|
...styles?.table,
|
|
352
|
+
minWidth: scroll?.x,
|
|
286
353
|
tableLayout: pageData?.length ? tableLayout : undefined,
|
|
287
354
|
},
|
|
288
355
|
className: clsx(
|
|
@@ -302,7 +369,7 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
302
369
|
rowKey,
|
|
303
370
|
}}
|
|
304
371
|
>
|
|
305
|
-
<table {...tableProps}>
|
|
372
|
+
<table ref={tableRef} {...tableProps}>
|
|
306
373
|
<ColGroup columns={wrapedColumns} />
|
|
307
374
|
{renderTHeader()}
|
|
308
375
|
{renderTBody()}
|
|
@@ -313,12 +380,10 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
313
380
|
|
|
314
381
|
return (
|
|
315
382
|
<div
|
|
316
|
-
ref={contentWrapperRef}
|
|
317
383
|
className={clsx(`${prefixCls}-content-wrapper`, {
|
|
318
384
|
[`${prefixCls}-content-wrapper-scroll-left`]: horizontalScroll.left,
|
|
319
385
|
[`${prefixCls}-content-wrapper-scroll-right`]: horizontalScroll.right,
|
|
320
386
|
})}
|
|
321
|
-
onScroll={updateHorizontalScroll}
|
|
322
387
|
>
|
|
323
388
|
<div
|
|
324
389
|
ref={contentRef}
|
|
@@ -326,7 +391,7 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
|
|
|
326
391
|
[`${prefixCls}-${size}-size`]: size,
|
|
327
392
|
[`${prefixCls}-fixed-header`]: scroll?.y,
|
|
328
393
|
})}
|
|
329
|
-
style={{ height: scroll?.y
|
|
394
|
+
style={{ height: scroll?.y }}
|
|
330
395
|
onScroll={updateHorizontalScroll}
|
|
331
396
|
>
|
|
332
397
|
{table}
|
package/src/Table/util/index.ts
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import { WrapedColumnProps } from '../interface';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const getColumnWidth = (
|
|
4
|
+
column: WrapedColumnProps,
|
|
5
|
+
index: number,
|
|
6
|
+
measuredColumnWidths?: number[],
|
|
7
|
+
) => {
|
|
8
|
+
const measuredWidth = measuredColumnWidths?.[index];
|
|
9
|
+
if (Number.isFinite(measuredWidth) && measuredWidth && measuredWidth > 0) {
|
|
10
|
+
return measuredWidth;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof column.width === 'number') {
|
|
14
|
+
return column.width;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
typeof column.width === 'string' &&
|
|
19
|
+
/^\s*\d+(?:\.\d+)?px\s*$/.test(column.width)
|
|
20
|
+
) {
|
|
21
|
+
return Number.parseFloat(column.width);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return 0;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function handleColumns(
|
|
28
|
+
columns: Array<WrapedColumnProps>,
|
|
29
|
+
measuredColumnWidths?: number[],
|
|
30
|
+
) {
|
|
4
31
|
let leftWidthCount = 0;
|
|
5
32
|
let rightWidthCount = 0;
|
|
6
33
|
|
|
7
34
|
for (let i = 0; i < columns.length; i++) {
|
|
8
|
-
const col = columns[i] || ({} as
|
|
35
|
+
const col = columns[i] || ({} as WrapedColumnProps);
|
|
9
36
|
if (col.fixed === true || col.fixed === 'left') {
|
|
10
37
|
col.fixed = 'left';
|
|
11
38
|
col.style = { ...col.style, left: leftWidthCount };
|
|
@@ -14,20 +41,18 @@ export function handleColumns(columns: Array<WrapedColumnProps>) {
|
|
|
14
41
|
if (!nextIsFixedLeft) {
|
|
15
42
|
col.isFixedLeftLast = true;
|
|
16
43
|
}
|
|
17
|
-
leftWidthCount
|
|
18
|
-
leftWidthCount + (col.width ? parseFloat(col.width as string) : 0);
|
|
44
|
+
leftWidthCount += getColumnWidth(col, i, measuredColumnWidths);
|
|
19
45
|
}
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
for (let j = columns.length - 1; j >= 0; j--) {
|
|
23
|
-
const col = columns[j] || ({} as
|
|
49
|
+
const col = columns[j] || ({} as WrapedColumnProps);
|
|
24
50
|
if (col.fixed === 'right') {
|
|
25
51
|
col.style = { ...col.style, right: rightWidthCount };
|
|
26
52
|
if (columns[j - 1]?.fixed !== 'right') {
|
|
27
53
|
col.isFixedRightFirst = true;
|
|
28
54
|
}
|
|
29
|
-
rightWidthCount
|
|
30
|
-
rightWidthCount + (col.width ? parseFloat(col.width as string) : 0);
|
|
55
|
+
rightWidthCount += getColumnWidth(col, j, measuredColumnWidths);
|
|
31
56
|
}
|
|
32
57
|
}
|
|
33
58
|
return columns;
|
package/src/Tree/index.en-US.md
CHANGED
|
@@ -44,7 +44,8 @@ Tree is a hierarchical data display component.
|
|
|
44
44
|
| icon | `React.ReactNode \| ((props: TreeProps<T>) => React.ReactNode)` | Icon before title (requires showIcon) | - |
|
|
45
45
|
| switcherIcon | `[React.ReactNode, React.ReactNode]` | Expand/collapse switcher icons | - |
|
|
46
46
|
| multiple | `boolean` | Whether tree node supports multiple selection | `false` |
|
|
47
|
-
| fieldNames | `{ key?: keyof T; label?: keyof T; children?: keyof T }` | Custom key, label, children fields | - |
|
|
47
|
+
| fieldNames | `{ key?: keyof T; label?: keyof T; children?: keyof T; extra?: keyof T }` | Custom key, label, children, extra fields | - |
|
|
48
|
+
| labelRender | `(node: TreeNode<T>) => React.ReactNode` | Custom label render function. When provided, takes full control of the label area; the node's `extra` will NOT be appended automatically (access it via `node.extra` in your render function) | - |
|
|
48
49
|
| loadData | `() => void` | Async load data | - |
|
|
49
50
|
| dataSource | `Array<T> \| T` | Data source | - |
|
|
50
51
|
| checkedKeys | `Array<string \| number>` | Checked tree nodes (controlled) | - |
|
|
@@ -53,4 +54,21 @@ Tree is a hierarchical data display component.
|
|
|
53
54
|
| defaultSelectedKeys | `Array<string \| number>` | Default selected tree nodes | - |
|
|
54
55
|
| onCheck | `(checkedKeys: Array<string \| number>, params: { checked: boolean; checkedNodes: T[]; node: T }) => void` | Callback when checkbox is checked | - |
|
|
55
56
|
| onSelect | `(selectedKeys: Array<string \| number>, params: { selected: boolean; selectedNodes: T[]; node: T }) => void` | Callback when tree node is clicked | - |
|
|
56
|
-
| onExpand | `(expandedKeys: Array<string \| number>, params: { expanded: boolean; expandedNodes: T[]; node: T[] }) => void` | Callback when expand/collapse | - |
|
|
57
|
+
| onExpand | `(expandedKeys: Array<string \| number>, params: { expanded: boolean; expandedNodes: T[]; node: T[] }) => void` | Callback when expand/collapse | - |
|
|
58
|
+
|
|
59
|
+
### Data Node Fields
|
|
60
|
+
|
|
61
|
+
| Property | Type | Description |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| key | `string \| number` | Unique node identifier |
|
|
64
|
+
| label | `string` | Display text |
|
|
65
|
+
| children | `Array<T>` | Child nodes |
|
|
66
|
+
| extra | `React.ReactNode \| ((node: T) => React.ReactNode)` | Extra content rendered after the label (e.g. tags, badges, action buttons). When `labelRender` is provided, extra is NOT rendered automatically — access it via `node.extra` in your render function |
|
|
67
|
+
| disabled | `boolean` | Whether disabled |
|
|
68
|
+
| icon | `React.ReactNode \| (() => React.ReactNode)` | Custom icon |
|
|
69
|
+
|
|
70
|
+
### Relationship between extra and labelRender
|
|
71
|
+
|
|
72
|
+
- **`extra` only**: Automatically appended after the default label text. Ideal for simple tags, badges, etc.
|
|
73
|
+
- **`labelRender` only**: Full custom rendering of the node label area.
|
|
74
|
+
- **Both present**: `labelRender` takes priority; extra is NOT automatically appended. To use extra in your custom render, access it via the callback parameter `node.extra`.
|
package/src/Tree/index.zh-CN.md
CHANGED
|
@@ -44,7 +44,8 @@ toc: 'content'
|
|
|
44
44
|
| icon | `React.ReactNode \| ((props: TreeProps<T>) => React.ReactNode)` | 标题前的图标,需设置showIcon为true | - |
|
|
45
45
|
| switcherIcon | `[React.ReactNode, React.ReactNode]` | 展开收起按钮图标 | - |
|
|
46
46
|
| multiple | `boolean` | 树节点数据是否支持多选 | `false` |
|
|
47
|
-
| fieldNames | `{ key?: keyof T; label?: keyof T; children?: keyof T }` | 自定义节点key, label, children字段 | - |
|
|
47
|
+
| fieldNames | `{ key?: keyof T; label?: keyof T; children?: keyof T; extra?: keyof T }` | 自定义节点key, label, children, extra字段 | - |
|
|
48
|
+
| labelRender | `(node: TreeNode<T>) => React.ReactNode` | 自定义节点标签渲染函数。提供后将完全接管 label 区域的渲染,此时节点的 `extra` 不会自动追加(可通过 `node.extra` 自行渲染) | - |
|
|
48
49
|
| loadData | `() => void` | 异步加载数据 | - |
|
|
49
50
|
| dataSource | `Array<T> \| T` | 数据源 | - |
|
|
50
51
|
| checkedKeys | `Array<string \| number>` | 选中复选框的树节点,受控 | - |
|
|
@@ -53,4 +54,21 @@ toc: 'content'
|
|
|
53
54
|
| defaultSelectedKeys | `Array<string \| number>` | 设置默认选中的树节点,多选需设置 multiple 为 true | - |
|
|
54
55
|
| onCheck | `(checkedKeys: Array<string \| number>, params: { checked: boolean; checkedNodes: T[]; node: T }) => void` | 复选框选中的回调 | - |
|
|
55
56
|
| onSelect | `(selectedKeys: Array<string \| number>, params: { selected: boolean; selectedNodes: T[]; node: T }) => void` | 点击树节点的回调 | - |
|
|
56
|
-
| onExpand | `(expandedKeys: Array<string \| number>, params: { expanded: boolean; expandedNodes: T[]; node: T[] }) => void` | 展开收起的回调 | - |
|
|
57
|
+
| onExpand | `(expandedKeys: Array<string \| number>, params: { expanded: boolean; expandedNodes: T[]; node: T[] }) => void` | 展开收起的回调 | - |
|
|
58
|
+
|
|
59
|
+
### 数据节点字段
|
|
60
|
+
|
|
61
|
+
| 属性名 | 类型 | 描述 |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| key | `string \| number` | 节点唯一标识 |
|
|
64
|
+
| label | `string` | 节点显示文本 |
|
|
65
|
+
| children | `Array<T>` | 子节点 |
|
|
66
|
+
| extra | `React.ReactNode \| ((node: T) => React.ReactNode)` | 节点标签后的附加内容(如标签、徽标、操作按钮)。当 `labelRender` 存在时不会自动渲染,需在 `labelRender` 中通过 `node.extra` 自行处理 |
|
|
67
|
+
| disabled | `boolean` | 是否禁用 |
|
|
68
|
+
| icon | `React.ReactNode \| (() => React.ReactNode)` | 自定义图标 |
|
|
69
|
+
|
|
70
|
+
### extra 与 labelRender 的关系
|
|
71
|
+
|
|
72
|
+
- **仅使用 `extra`**:在默认 label 文本后自动追加 extra 内容,适合简单的标签、徽标等场景
|
|
73
|
+
- **仅使用 `labelRender`**:完全自定义节点标签区域的渲染
|
|
74
|
+
- **同时存在时**:`labelRender` 优先级更高,extra 不会自动追加。如需在自定义渲染中使用 extra,可通过回调参数 `node.extra` 取得原始值自行渲染
|
package/src/Tree/interface.ts
CHANGED
|
@@ -64,6 +64,13 @@ export type TreeNode<T> = {
|
|
|
64
64
|
* All descendant uids (collected bottom-up). Used for check/uncheck propagation.
|
|
65
65
|
*/
|
|
66
66
|
childUids?: Array<string>;
|
|
67
|
+
/**
|
|
68
|
+
* Extra content rendered after the label (e.g. tags, badges, action buttons).
|
|
69
|
+
* Only takes effect when `labelRender` is NOT provided at the Tree level.
|
|
70
|
+
* When `labelRender` is present, access this value via `node.extra` in your
|
|
71
|
+
* custom render function.
|
|
72
|
+
*/
|
|
73
|
+
extra?: React.ReactNode | ((node: T) => React.ReactNode);
|
|
67
74
|
/**
|
|
68
75
|
* Original data
|
|
69
76
|
*/
|
|
@@ -162,10 +169,18 @@ export interface TreeProps<T> extends DataAttributeProps {
|
|
|
162
169
|
* @default false
|
|
163
170
|
*/
|
|
164
171
|
multiple?: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* Custom render function for node label.
|
|
174
|
+
* When provided, takes full control of the label area rendering.
|
|
175
|
+
* The node's `extra` will NOT be appended automatically in this case;
|
|
176
|
+
* access it via `node.extra` if you need it in your custom render.
|
|
177
|
+
* The original `label` string is still used for `title` attribute fallback.
|
|
178
|
+
*/
|
|
179
|
+
labelRender?: (node: TreeNode<T>) => React.ReactNode;
|
|
165
180
|
/**
|
|
166
181
|
* Customize node key, title, children field names
|
|
167
182
|
*/
|
|
168
|
-
fieldNames?: { key?: keyof T; label?: keyof T; children?: keyof T };
|
|
183
|
+
fieldNames?: { key?: keyof T; label?: keyof T; children?: keyof T; extra?: keyof T };
|
|
169
184
|
/**
|
|
170
185
|
* Asynchronous data loading
|
|
171
186
|
*/
|
package/src/Tree/tree-node.tsx
CHANGED
|
@@ -21,6 +21,7 @@ const Node = <T extends Record<string, unknown> = any>(
|
|
|
21
21
|
disabled,
|
|
22
22
|
depth,
|
|
23
23
|
icon,
|
|
24
|
+
extra,
|
|
24
25
|
original,
|
|
25
26
|
lines,
|
|
26
27
|
} = node;
|
|
@@ -35,6 +36,7 @@ const Node = <T extends Record<string, unknown> = any>(
|
|
|
35
36
|
indeterminateUids,
|
|
36
37
|
expandedUids,
|
|
37
38
|
switcherIcon,
|
|
39
|
+
labelRender,
|
|
38
40
|
draggable,
|
|
39
41
|
draggingUid,
|
|
40
42
|
dropState,
|
|
@@ -105,6 +107,14 @@ const Node = <T extends Record<string, unknown> = any>(
|
|
|
105
107
|
const _icon =
|
|
106
108
|
typeof mergedIcon === 'function' ? mergedIcon(original) : mergedIcon;
|
|
107
109
|
|
|
110
|
+
// labelRender takes full control of the label content when provided
|
|
111
|
+
const content = labelRender ? labelRender(node) : label;
|
|
112
|
+
|
|
113
|
+
// extra: appended after the label content (only when labelRender is NOT used)
|
|
114
|
+
const _extra = !labelRender && extra
|
|
115
|
+
? (typeof extra === 'function' ? extra(original) : extra)
|
|
116
|
+
: null;
|
|
117
|
+
|
|
108
118
|
return (
|
|
109
119
|
<Button
|
|
110
120
|
className={clsx(`${prefixCls}-node-label`, {
|
|
@@ -116,7 +126,10 @@ const Node = <T extends Record<string, unknown> = any>(
|
|
|
116
126
|
icon={showIcon ? _icon : undefined}
|
|
117
127
|
onClick={disabled ? undefined : () => onSelect(uid)}
|
|
118
128
|
>
|
|
119
|
-
{
|
|
129
|
+
{content}
|
|
130
|
+
{_extra && (
|
|
131
|
+
<span className={`${prefixCls}-node-extra`}>{_extra}</span>
|
|
132
|
+
)}
|
|
120
133
|
</Button>
|
|
121
134
|
);
|
|
122
135
|
};
|
|
@@ -139,7 +152,7 @@ const Node = <T extends Record<string, unknown> = any>(
|
|
|
139
152
|
<span
|
|
140
153
|
aria-hidden="true"
|
|
141
154
|
className={`${prefixCls}-node-indent`}
|
|
142
|
-
key={index}
|
|
155
|
+
key={`node-empty-${index}`}
|
|
143
156
|
></span>
|
|
144
157
|
));
|
|
145
158
|
};
|
package/src/Tree/tree.tsx
CHANGED
|
@@ -35,6 +35,7 @@ export const TreeContext = createContext<{
|
|
|
35
35
|
icon?: React.ReactNode | ((props: any) => React.ReactNode);
|
|
36
36
|
showLine?: boolean;
|
|
37
37
|
switcherIcon?: [React.ReactNode, React.ReactNode];
|
|
38
|
+
labelRender?: (node: TreeNode<any>) => React.ReactNode;
|
|
38
39
|
selectedUids: Set<string>;
|
|
39
40
|
checkedUids: Set<string>;
|
|
40
41
|
indeterminateUids: Set<string>;
|
|
@@ -203,6 +204,7 @@ const Tree = <T extends Record<string, unknown> = any>(
|
|
|
203
204
|
onSelect,
|
|
204
205
|
onCheck,
|
|
205
206
|
onExpand,
|
|
207
|
+
labelRender,
|
|
206
208
|
virtual = false,
|
|
207
209
|
height,
|
|
208
210
|
itemHeight = 28,
|
|
@@ -553,6 +555,7 @@ const Tree = <T extends Record<string, unknown> = any>(
|
|
|
553
555
|
icon,
|
|
554
556
|
showLine,
|
|
555
557
|
switcherIcon,
|
|
558
|
+
labelRender,
|
|
556
559
|
selectedUids: selectedUidSet,
|
|
557
560
|
checkedUids: checkedUidSet,
|
|
558
561
|
indeterminateUids: indeterminateUidSet,
|
|
@@ -575,6 +578,7 @@ const Tree = <T extends Record<string, unknown> = any>(
|
|
|
575
578
|
icon,
|
|
576
579
|
showLine,
|
|
577
580
|
switcherIcon,
|
|
581
|
+
labelRender,
|
|
578
582
|
selectedUidSet,
|
|
579
583
|
checkedUidSet,
|
|
580
584
|
indeterminateUidSet,
|
|
@@ -687,7 +691,7 @@ const Tree = <T extends Record<string, unknown> = any>(
|
|
|
687
691
|
>
|
|
688
692
|
<TreeContext.Provider value={contextValue}>
|
|
689
693
|
<AnimatePresence initial={false}>
|
|
690
|
-
{segments.map((segment) => {
|
|
694
|
+
{segments.map((segment, index) => {
|
|
691
695
|
if (segment.type === 'node') {
|
|
692
696
|
return (
|
|
693
697
|
<TreeNodeComp node={segment.node} key={segment.node.uid} />
|
|
@@ -698,7 +702,7 @@ const Tree = <T extends Record<string, unknown> = any>(
|
|
|
698
702
|
// source and cancels the drag before dragOver/drop can fire.
|
|
699
703
|
return (
|
|
700
704
|
<motion.div
|
|
701
|
-
key={segment.pUid + '\x01children'}
|
|
705
|
+
key={segment.pUid + index + '\x01children'}
|
|
702
706
|
initial={{ height: 0, opacity: 0 }}
|
|
703
707
|
animate={{ height: 'auto', opacity: 1 }}
|
|
704
708
|
exit={{ height: 0, opacity: 0 }}
|