@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.
Files changed (56) hide show
  1. package/dist/cjs/Search/search.js +1 -1
  2. package/dist/cjs/Table/demo/fixed.d.ts +3 -0
  3. package/dist/cjs/Table/demo/fixed.js +123 -0
  4. package/dist/cjs/Table/hooks/useColumns.d.ts +2 -1
  5. package/dist/cjs/Table/hooks/useColumns.js +4 -3
  6. package/dist/cjs/Table/style/index.less +23 -3
  7. package/dist/cjs/Table/table.js +67 -21
  8. package/dist/cjs/Table/util/index.d.ts +1 -1
  9. package/dist/cjs/Table/util/index.js +16 -3
  10. package/dist/cjs/Tree/interface.d.ts +16 -0
  11. package/dist/cjs/Tree/tree-node.js +7 -2
  12. package/dist/cjs/Tree/tree.d.ts +2 -1
  13. package/dist/cjs/Tree/tree.js +6 -2
  14. package/dist/cjs/Tree/utils/tree-tools.d.ts +1 -0
  15. package/dist/cjs/Tree/utils/tree-tools.js +3 -1
  16. package/dist/cjs/style/themes.css +1 -1
  17. package/dist/cjs/style/variables.css +1 -1
  18. package/dist/cjs/version.d.ts +1 -1
  19. package/dist/cjs/version.js +1 -1
  20. package/dist/esm/Search/search.js +1 -1
  21. package/dist/esm/Table/demo/fixed.d.ts +3 -0
  22. package/dist/esm/Table/demo/fixed.js +84 -0
  23. package/dist/esm/Table/hooks/useColumns.d.ts +2 -1
  24. package/dist/esm/Table/hooks/useColumns.js +4 -3
  25. package/dist/esm/Table/style/index.less +23 -3
  26. package/dist/esm/Table/table.js +85 -26
  27. package/dist/esm/Table/util/index.d.ts +1 -1
  28. package/dist/esm/Table/util/index.js +16 -3
  29. package/dist/esm/Tree/interface.d.ts +16 -0
  30. package/dist/esm/Tree/tree-node.js +12 -2
  31. package/dist/esm/Tree/tree.d.ts +2 -1
  32. package/dist/esm/Tree/tree.js +15 -13
  33. package/dist/esm/Tree/utils/tree-tools.d.ts +1 -0
  34. package/dist/esm/Tree/utils/tree-tools.js +4 -1
  35. package/dist/esm/style/themes.css +1 -1
  36. package/dist/esm/style/variables.css +1 -1
  37. package/dist/esm/version.d.ts +1 -1
  38. package/dist/esm/version.js +1 -1
  39. package/dist/umd/yee-c.min.css +1 -1
  40. package/dist/umd/yee-c.min.js +1 -1
  41. package/package.json +1 -1
  42. package/src/Search/search.tsx +1 -1
  43. package/src/Table/demo/fixed.tsx +92 -0
  44. package/src/Table/hooks/useColumns.ts +4 -2
  45. package/src/Table/index.en-US.md +1 -0
  46. package/src/Table/index.zh-CN.md +1 -0
  47. package/src/Table/style/index.less +23 -3
  48. package/src/Table/table.tsx +88 -23
  49. package/src/Table/util/index.ts +32 -7
  50. package/src/Tree/index.en-US.md +20 -2
  51. package/src/Tree/index.zh-CN.md +20 -2
  52. package/src/Tree/interface.ts +16 -1
  53. package/src/Tree/tree-node.tsx +15 -2
  54. package/src/Tree/tree.tsx +6 -2
  55. package/src/Tree/utils/tree-tools.ts +4 -1
  56. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-oh/yee-c",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
4
4
  "description": "yee basic components",
5
5
  "keywords": [
6
6
  "react",
@@ -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 != null
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,
@@ -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
 
@@ -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
- box-shadow: 6px 0 6px -4px var(--yee-table-fixed-left-after-bs-color);
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
- box-shadow: -6px 0 6px -4px var(--yee-table-fixed-right-after-bs-color);
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
  }
@@ -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 (!wrapper || !content) return;
169
+ if (!content) return;
167
170
 
168
- const scrollContainer =
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 && scrollContainer.scrollLeft > 1,
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
- if (!wrapper || !content) return undefined;
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', updateHorizontalScroll);
219
- return () => window.removeEventListener('resize', updateHorizontalScroll);
269
+ window.addEventListener('resize', handleResize);
270
+ return () => window.removeEventListener('resize', handleResize);
220
271
  }
221
272
 
222
- const resizeObserver = new ResizeObserver(updateHorizontalScroll);
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, wrapedColumns, pageData, scroll?.x]);
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, width: scroll?.x }}
394
+ style={{ height: scroll?.y }}
330
395
  onScroll={updateHorizontalScroll}
331
396
  >
332
397
  {table}
@@ -1,11 +1,38 @@
1
1
  import { WrapedColumnProps } from '../interface';
2
2
 
3
- export function handleColumns(columns: Array<WrapedColumnProps>) {
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 any);
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 any);
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;
@@ -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`.
@@ -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` 取得原始值自行渲染
@@ -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
  */
@@ -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
- {label}
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 }}