@rainbow-oh/yee-c 0.6.4 → 0.8.0

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 (145) hide show
  1. package/dist/cjs/Button/style/index.less +5 -4
  2. package/dist/cjs/Drawer/drawer.js +1 -1
  3. package/dist/cjs/Drawer/interface.d.ts +1 -1
  4. package/dist/cjs/Drawer/style/index.less +1 -0
  5. package/dist/cjs/Form/form-store.d.ts +2 -1
  6. package/dist/cjs/Form/form-store.js +32 -17
  7. package/dist/cjs/List/demo/virtual.d.ts +3 -0
  8. package/dist/cjs/List/demo/virtual.js +53 -0
  9. package/dist/cjs/List/hooks/useListKeyboard.d.ts +28 -0
  10. package/dist/cjs/List/hooks/useListKeyboard.js +188 -0
  11. package/dist/cjs/List/interface.d.ts +29 -2
  12. package/dist/cjs/List/list.js +141 -21
  13. package/dist/cjs/List/style/index.less +24 -0
  14. package/dist/cjs/Select/demo/columns.d.ts +3 -0
  15. package/dist/cjs/Select/demo/columns.js +62 -0
  16. package/dist/cjs/Select/hooks/useSelectKeyboard.d.ts +13 -1
  17. package/dist/cjs/Select/hooks/useSelectKeyboard.js +81 -10
  18. package/dist/cjs/Select/interface.d.ts +30 -3
  19. package/dist/cjs/Select/option.js +4 -3
  20. package/dist/cjs/Select/options.js +90 -8
  21. package/dist/cjs/Select/select.js +32 -8
  22. package/dist/cjs/Select/style/index.less +14 -0
  23. package/dist/cjs/Selector/interface.d.ts +4 -0
  24. package/dist/cjs/Selector/selector.js +5 -3
  25. package/dist/cjs/Table/hooks/useColumns.js +1 -1
  26. package/dist/cjs/Table/interface.d.ts +1 -1
  27. package/dist/cjs/Table/style/index.less +35 -11
  28. package/dist/cjs/Table/table.js +57 -7
  29. package/dist/cjs/Table/tbody/cell.js +1 -1
  30. package/dist/cjs/Table/util/header.d.ts +3 -1
  31. package/dist/cjs/Table/util/header.js +30 -3
  32. package/dist/cjs/Tree/demo/draggable.js +2 -0
  33. package/dist/cjs/Tree/interface.d.ts +46 -23
  34. package/dist/cjs/Tree/tree-node.d.ts +1 -1
  35. package/dist/cjs/Tree/tree-node.js +30 -23
  36. package/dist/cjs/Tree/tree.d.ts +13 -12
  37. package/dist/cjs/Tree/tree.js +452 -227
  38. package/dist/cjs/Tree/utils/tree-tools.d.ts +43 -10
  39. package/dist/cjs/Tree/utils/tree-tools.js +129 -91
  40. package/dist/cjs/locale/context.d.ts +5 -4
  41. package/dist/cjs/locale/context.js +15 -8
  42. package/dist/cjs/locale/interface.d.ts +20 -0
  43. package/dist/cjs/locale/util.d.ts +3 -3
  44. package/dist/cjs/style/color.css +3 -0
  45. package/dist/cjs/style/size.css +2 -1
  46. package/dist/cjs/style/themes.css +1 -1
  47. package/dist/cjs/style/variables.css +6 -2
  48. package/dist/cjs/utils/isEqual.js +5 -7
  49. package/dist/cjs/version.d.ts +1 -1
  50. package/dist/cjs/version.js +1 -1
  51. package/dist/esm/Button/style/index.less +5 -4
  52. package/dist/esm/Drawer/drawer.js +2 -1
  53. package/dist/esm/Drawer/interface.d.ts +1 -1
  54. package/dist/esm/Drawer/style/index.less +1 -0
  55. package/dist/esm/Form/form-store.d.ts +2 -1
  56. package/dist/esm/Form/form-store.js +40 -19
  57. package/dist/esm/List/demo/virtual.d.ts +3 -0
  58. package/dist/esm/List/demo/virtual.js +22 -0
  59. package/dist/esm/List/hooks/useListKeyboard.d.ts +28 -0
  60. package/dist/esm/List/hooks/useListKeyboard.js +173 -0
  61. package/dist/esm/List/interface.d.ts +29 -2
  62. package/dist/esm/List/list.js +171 -14
  63. package/dist/esm/List/style/index.less +24 -0
  64. package/dist/esm/Select/demo/columns.d.ts +3 -0
  65. package/dist/esm/Select/demo/columns.js +36 -0
  66. package/dist/esm/Select/hooks/useSelectKeyboard.d.ts +13 -1
  67. package/dist/esm/Select/hooks/useSelectKeyboard.js +91 -14
  68. package/dist/esm/Select/interface.d.ts +30 -3
  69. package/dist/esm/Select/option.js +6 -2
  70. package/dist/esm/Select/options.js +114 -32
  71. package/dist/esm/Select/select.js +40 -11
  72. package/dist/esm/Select/style/index.less +14 -0
  73. package/dist/esm/Selector/interface.d.ts +4 -0
  74. package/dist/esm/Selector/selector.js +10 -4
  75. package/dist/esm/Table/hooks/useColumns.js +1 -1
  76. package/dist/esm/Table/interface.d.ts +1 -1
  77. package/dist/esm/Table/style/index.less +35 -11
  78. package/dist/esm/Table/table.js +54 -2
  79. package/dist/esm/Table/tbody/cell.js +1 -1
  80. package/dist/esm/Table/util/header.d.ts +3 -1
  81. package/dist/esm/Table/util/header.js +30 -2
  82. package/dist/esm/Tree/demo/draggable.js +2 -0
  83. package/dist/esm/Tree/interface.d.ts +46 -23
  84. package/dist/esm/Tree/tree-node.d.ts +1 -1
  85. package/dist/esm/Tree/tree-node.js +26 -31
  86. package/dist/esm/Tree/tree.d.ts +13 -12
  87. package/dist/esm/Tree/tree.js +572 -163
  88. package/dist/esm/Tree/utils/tree-tools.d.ts +43 -10
  89. package/dist/esm/Tree/utils/tree-tools.js +219 -104
  90. package/dist/esm/locale/context.d.ts +5 -4
  91. package/dist/esm/locale/context.js +25 -9
  92. package/dist/esm/locale/interface.d.ts +20 -0
  93. package/dist/esm/locale/util.d.ts +3 -3
  94. package/dist/esm/style/color.css +3 -0
  95. package/dist/esm/style/size.css +2 -1
  96. package/dist/esm/style/themes.css +1 -1
  97. package/dist/esm/style/variables.css +6 -2
  98. package/dist/esm/utils/isEqual.js +5 -7
  99. package/dist/esm/version.d.ts +1 -1
  100. package/dist/esm/version.js +1 -1
  101. package/dist/umd/yee-c.min.css +4 -4
  102. package/dist/umd/yee-c.min.js +1 -1
  103. package/package.json +1 -1
  104. package/src/Button/style/index.less +5 -4
  105. package/src/Drawer/drawer.tsx +1 -1
  106. package/src/Drawer/interface.ts +1 -0
  107. package/src/Drawer/style/index.less +1 -0
  108. package/src/Form/form-store.ts +42 -15
  109. package/src/List/demo/virtual.tsx +20 -0
  110. package/src/List/hooks/useListKeyboard.ts +235 -0
  111. package/src/List/index.en-US.md +6 -0
  112. package/src/List/index.zh-CN.md +6 -0
  113. package/src/List/interface.ts +29 -2
  114. package/src/List/list.tsx +157 -19
  115. package/src/List/style/index.less +24 -0
  116. package/src/Select/demo/columns.tsx +35 -0
  117. package/src/Select/hooks/useSelectKeyboard.ts +112 -15
  118. package/src/Select/index.en-US.md +4 -0
  119. package/src/Select/index.zh-CN.md +4 -0
  120. package/src/Select/interface.ts +31 -4
  121. package/src/Select/option.tsx +9 -3
  122. package/src/Select/options.tsx +119 -26
  123. package/src/Select/select.tsx +41 -8
  124. package/src/Select/style/index.less +14 -0
  125. package/src/Selector/interface.ts +4 -0
  126. package/src/Selector/selector.tsx +8 -3
  127. package/src/Table/hooks/useColumns.ts +1 -1
  128. package/src/Table/interface.ts +1 -1
  129. package/src/Table/style/index.less +35 -11
  130. package/src/Table/table.tsx +60 -1
  131. package/src/Table/tbody/cell.tsx +1 -1
  132. package/src/Table/util/header.ts +48 -2
  133. package/src/Tree/demo/draggable.tsx +2 -0
  134. package/src/Tree/interface.ts +46 -23
  135. package/src/Tree/tree-node.tsx +26 -29
  136. package/src/Tree/tree.tsx +573 -274
  137. package/src/Tree/utils/tree-tools.ts +231 -125
  138. package/src/locale/_README.md +22 -13
  139. package/src/locale/context.tsx +33 -16
  140. package/src/locale/interface.ts +24 -0
  141. package/src/locale/util.ts +6 -6
  142. package/src/style/color.css +3 -0
  143. package/src/style/size.css +2 -1
  144. package/src/utils/isEqual.ts +5 -7
  145. package/src/version.ts +1 -1
@@ -33,18 +33,23 @@ const Options = forwardRef<HTMLDivElement, OptionsProps>(
33
33
  itemHeight = 32,
34
34
  listHeight = 200,
35
35
  virtualApiRef,
36
+ columns = 1,
37
+ looseMatch = false,
36
38
  onSelect,
37
39
  ...rest
38
40
  } = props;
39
41
 
42
+ const multiColumn = columns > 1;
43
+
40
44
  const rootRef = useRef<HTMLDivElement | null>(null);
41
- // Forward the popup root element to the parent (Select uses it as the
42
- // keyboard-nav containerRef) while keeping a stable RefObject for the
43
- // effects below.
44
45
  useImperativeHandle(ref, () => rootRef.current as HTMLDivElement, []);
45
46
 
46
- // Hook is always called (rules of hooks). When `virtual` is off the
47
- // returned viewportRef is never attached, so the observer stays idle.
47
+ // In multi-column virtual mode, each "row" contains `columns` options.
48
+ // The virtual list tracks rows, not individual options.
49
+ const rowCount = multiColumn
50
+ ? Math.ceil(options.length / columns)
51
+ : options.length;
52
+
48
53
  const {
49
54
  start,
50
55
  end,
@@ -54,37 +59,38 @@ const Options = forwardRef<HTMLDivElement, OptionsProps>(
54
59
  onScroll,
55
60
  scrollToIndex,
56
61
  } = useVirtualList({
57
- itemCount: options.length,
62
+ itemCount: virtual ? rowCount : 0,
58
63
  itemHeight,
59
64
  });
60
65
 
61
- // Expose the virtual scroll API to the parent so keyboard navigation
62
- // can drive the virtual viewport (replaces scrollIntoView).
66
+ // Expose the virtual scroll API to the parent.
67
+ // In multi-column mode, the parent passes a flat option index, so we
68
+ // convert it to a row index before scrolling.
63
69
  useEffect(() => {
64
70
  if (!virtual || !virtualApiRef) return;
65
- virtualApiRef.current = { scrollToIndex };
71
+ virtualApiRef.current = {
72
+ scrollToIndex: (flatIndex: number) => {
73
+ const rowIndex = multiColumn
74
+ ? Math.floor(flatIndex / columns)
75
+ : flatIndex;
76
+ scrollToIndex(rowIndex);
77
+ },
78
+ };
66
79
  return () => {
67
80
  if (virtualApiRef.current) {
68
81
  virtualApiRef.current = null;
69
82
  }
70
83
  };
71
- }, [virtual, virtualApiRef, scrollToIndex]);
84
+ }, [virtual, virtualApiRef, scrollToIndex, multiColumn, columns]);
72
85
 
73
- // Reset the scroll position whenever the option set changes (e.g. after
74
- // filtering via search), otherwise the viewport may sit at an offset
75
- // where none of the new results are visible.
86
+ // Reset scroll position when options change.
76
87
  useEffect(() => {
77
88
  if (virtual) {
78
89
  scrollToIndex(0);
79
90
  }
80
91
  }, [options, virtual, scrollToIndex]);
81
92
 
82
- // Bring the focused option into view on open and whenever it changes.
83
- // - virtual mode: drive the virtual viewport via scrollToIndex.
84
- // - default mode: scrollIntoView on the option's DOM node.
85
- // Options owns this (rather than the keyboard hook) so it also covers the
86
- // initial open, where the keyboard effect can fire before the popup is
87
- // mounted / the virtual API ref is wired up.
93
+ // Bring the focused option into view.
88
94
  useEffect(() => {
89
95
  if (options.length === 0) return;
90
96
  if (focusedKey === '') return;
@@ -92,7 +98,10 @@ const Options = forwardRef<HTMLDivElement, OptionsProps>(
92
98
  if (focusedIndex < 0) return;
93
99
 
94
100
  if (virtual) {
95
- scrollToIndex(focusedIndex);
101
+ const rowIndex = multiColumn
102
+ ? Math.floor(focusedIndex / columns)
103
+ : focusedIndex;
104
+ scrollToIndex(rowIndex);
96
105
  return;
97
106
  }
98
107
 
@@ -103,9 +112,18 @@ const Options = forwardRef<HTMLDivElement, OptionsProps>(
103
112
  | HTMLElement
104
113
  | undefined;
105
114
  if (targetElement) {
106
- targetElement.scrollIntoView({ block: 'nearest' });
115
+ // Use manual scroll calculation instead of scrollIntoView to avoid
116
+ // scrolling the entire page (the popup is rendered in a portal).
117
+ const containerRect = container.getBoundingClientRect();
118
+ const targetRect = targetElement.getBoundingClientRect();
119
+ if (targetRect.bottom > containerRect.bottom) {
120
+ container.scrollTop +=
121
+ targetRect.bottom - containerRect.bottom;
122
+ } else if (targetRect.top < containerRect.top) {
123
+ container.scrollTop -= containerRect.top - targetRect.top;
124
+ }
107
125
  }
108
- }, [focusedKey, virtual, options, scrollToIndex, rootRef]);
126
+ }, [focusedKey, virtual, options, scrollToIndex, rootRef, multiColumn, columns]);
109
127
 
110
128
  const isEmpty = options.length === 0;
111
129
  const ctxValue = {
@@ -113,10 +131,76 @@ const Options = forwardRef<HTMLDivElement, OptionsProps>(
113
131
  selectedKeys,
114
132
  focusedKey,
115
133
  multiple,
134
+ looseMatch,
116
135
  onSelect,
117
136
  };
118
137
 
119
- // ----- Virtual mode: only render the visible window -----
138
+ // ----- Virtual + multi-column: render rows of N options -----
139
+ if (virtual && multiColumn && !isEmpty) {
140
+ const visibleRows = [];
141
+ for (let rowIdx = start; rowIdx < end; rowIdx++) {
142
+ const rowOptions = [];
143
+ for (let col = 0; col < columns; col++) {
144
+ const flatIdx = rowIdx * columns + col;
145
+ if (flatIdx < options.length) {
146
+ rowOptions.push({ option: options[flatIdx], flatIdx });
147
+ }
148
+ }
149
+ visibleRows.push({ rowIdx, rowOptions });
150
+ }
151
+
152
+ return (
153
+ <div
154
+ {...rest}
155
+ className={clsx(
156
+ `${prefixCls}-popup`,
157
+ `${prefixCls}-popup-virtual`,
158
+ popupClassName,
159
+ )}
160
+ style={popupStyle}
161
+ ref={rootRef}
162
+ tabIndex={-1}
163
+ >
164
+ <OptionsCtx.Provider value={ctxValue}>
165
+ <div
166
+ className={`${prefixCls}-popup-viewport`}
167
+ ref={viewportRef}
168
+ onScroll={onScroll}
169
+ style={{ maxHeight: listHeight, overflowY: 'auto' }}
170
+ >
171
+ <div style={{ height: totalHeight, position: 'relative' }}>
172
+ {visibleRows.map(({ rowIdx, rowOptions }, ri) => (
173
+ <div
174
+ key={rowIdx}
175
+ className={`${prefixCls}-popup-grid-row`}
176
+ style={{
177
+ position: 'absolute',
178
+ top: 0,
179
+ left: 0,
180
+ right: 0,
181
+ height: itemHeight,
182
+ transform: `translateY(${offsetY + ri * itemHeight}px)`,
183
+ display: 'grid',
184
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
185
+ }}
186
+ >
187
+ {rowOptions.map(({ option, flatIdx }) => (
188
+ <Option
189
+ {...option}
190
+ key={option.value}
191
+ dataTestId={`${dataTestId}-${flatIdx}`}
192
+ />
193
+ ))}
194
+ </div>
195
+ ))}
196
+ </div>
197
+ </div>
198
+ </OptionsCtx.Provider>
199
+ </div>
200
+ );
201
+ }
202
+
203
+ // ----- Virtual single-column: render visible window -----
120
204
  if (virtual && !isEmpty) {
121
205
  const visibleOptions = options.slice(start, end);
122
206
  return (
@@ -161,12 +245,21 @@ const Options = forwardRef<HTMLDivElement, OptionsProps>(
161
245
  );
162
246
  }
163
247
 
164
- // ----- Default mode: render every option (unchanged behavior) -----
248
+ // ----- Default mode (non-virtual) -----
165
249
  return (
166
250
  <div
167
251
  {...rest}
168
- className={clsx(`${prefixCls}-popup`, popupClassName)}
169
- style={popupStyle}
252
+ className={clsx(
253
+ `${prefixCls}-popup`,
254
+ { [`${prefixCls}-popup-grid`]: multiColumn },
255
+ popupClassName,
256
+ )}
257
+ style={{
258
+ ...popupStyle,
259
+ ...(multiColumn
260
+ ? { '--yee-select-columns': columns } as React.CSSProperties
261
+ : undefined),
262
+ }}
170
263
  ref={rootRef}
171
264
  tabIndex={-1}
172
265
  >
@@ -15,6 +15,30 @@ import { Option, SelectProps } from './interface';
15
15
  import Options from './options';
16
16
  import './style/index.less';
17
17
 
18
+ // ---------------------------------------------------------------------------
19
+ // Value comparison utilities (module-level for stable references)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** Compare two values: loose mode uses String() conversion, strict uses ===. */
23
+ function matchValue(
24
+ a: string | number,
25
+ b: string | number,
26
+ loose: boolean,
27
+ ): boolean {
28
+ if (loose) return String(a) === String(b);
29
+ return a === b;
30
+ }
31
+
32
+ /** Check if a value exists in a keys array with optional loose matching. */
33
+ function keysIncludes(
34
+ keys: Array<string | number>,
35
+ val: string | number,
36
+ loose: boolean,
37
+ ): boolean {
38
+ if (loose) return keys.some((k) => String(k) === String(val));
39
+ return keys.includes(val);
40
+ }
41
+
18
42
  const Select = (baseprops: SelectProps) => {
19
43
  const { select } = useContext(GlobalContext);
20
44
  const props = mergeContextToProps(baseprops, select);
@@ -32,6 +56,9 @@ const Select = (baseprops: SelectProps) => {
32
56
  virtual,
33
57
  itemHeight,
34
58
  listHeight,
59
+ columns = 1,
60
+ popupWidth,
61
+ looseMatch = false,
35
62
  orphanClassName,
36
63
  orphanStyle,
37
64
  ...rest
@@ -80,16 +107,14 @@ const Select = (baseprops: SelectProps) => {
80
107
  if (keys.length === 0) {
81
108
  return [];
82
109
  }
83
- // 按 keys 顺序回填,找不到的 key 构造 label=value 的兜底项(孤儿值),
84
- // 与 Selector 的显示保持一致,避免 onChange 第二参数丢值。
85
110
  return keys.map((key) => {
86
- const opt = options.find((o) => o.value === key);
111
+ const opt = options.find((o) => matchValue(o.value, key, looseMatch));
87
112
  return opt ?? { value: key, label: String(key) };
88
113
  });
89
114
  }
90
115
 
91
116
  return (
92
- options.find((opt) => opt.value === keys) ?? {
117
+ options.find((opt) => matchValue(opt.value, keys, looseMatch)) ?? {
93
118
  value: keys,
94
119
  label: String(keys),
95
120
  }
@@ -107,8 +132,8 @@ const Select = (baseprops: SelectProps) => {
107
132
  setSearchValue('');
108
133
  setOpen(false);
109
134
  } else {
110
- if (mergedValue.includes(key)) {
111
- keys = mergedValue.filter((k: string | number) => k !== key);
135
+ if (keysIncludes(mergedValue, key, looseMatch)) {
136
+ keys = mergedValue.filter((k: string | number) => !matchValue(k, key, looseMatch));
112
137
  } else {
113
138
  keys = [...mergedValue, key];
114
139
  }
@@ -128,7 +153,9 @@ const Select = (baseprops: SelectProps) => {
128
153
  };
129
154
 
130
155
  const handleRemove = (option: Option) => {
131
- const keys = mergedValue.filter((k: string | number) => k !== option.value);
156
+ const keys = mergedValue.filter(
157
+ (k: string | number) => !matchValue(k, option.value, looseMatch),
158
+ );
132
159
  const filteredOpts = getOptions(keys);
133
160
  setMergedValue(keys);
134
161
  const key = single ? keys[0] : keys;
@@ -162,6 +189,8 @@ const Select = (baseprops: SelectProps) => {
162
189
  options: filteredOpts,
163
190
  open,
164
191
  selectedKeys: mergedValue,
192
+ columns,
193
+ looseMatch,
165
194
  onSelect: handleSelect,
166
195
  onClose: () => setOpen(false),
167
196
  onOpenChange: handleOpenChange,
@@ -183,6 +212,8 @@ const Select = (baseprops: SelectProps) => {
183
212
  itemHeight={itemHeight}
184
213
  listHeight={listHeight}
185
214
  virtualApiRef={virtualApiRef}
215
+ columns={columns}
216
+ looseMatch={looseMatch}
186
217
  />
187
218
  );
188
219
 
@@ -191,7 +222,8 @@ const Select = (baseprops: SelectProps) => {
191
222
  {...rest}
192
223
  placement={placement}
193
224
  popup={popup}
194
- stretch="width"
225
+ stretch={popupWidth ? undefined : 'width'}
226
+ popupStyle={popupWidth ? { width: popupWidth } : undefined}
195
227
  open={disabled ? false : open}
196
228
  onOpenChange={handleOpenChange}
197
229
  >
@@ -209,6 +241,7 @@ const Select = (baseprops: SelectProps) => {
209
241
  onKeyDown={onKeyDown}
210
242
  orphanClassName={orphanClassName}
211
243
  orphanStyle={orphanStyle}
244
+ looseMatch={looseMatch}
212
245
  />
213
246
  </Trigger>
214
247
  );
@@ -54,6 +54,20 @@
54
54
  align-items: center;
55
55
  }
56
56
 
57
+ // Multi-column grid layout (non-virtual mode).
58
+ // The column count is driven by the CSS custom property --yee-select-columns
59
+ // set inline by the Options component.
60
+ &-popup-grid {
61
+ display: grid;
62
+ grid-template-columns: repeat(var(--yee-select-columns, 1), 1fr);
63
+ }
64
+
65
+ // Virtual multi-column: each row container is already a grid via inline style;
66
+ // options inside it should fill cells evenly and center vertically.
67
+ &-popup-grid-row {
68
+ align-items: center;
69
+ }
70
+
57
71
  &-empty {
58
72
  padding: var(--yee-select-option-padding);
59
73
  }
@@ -63,6 +63,10 @@ export interface PureSelectorProps {
63
63
  * Selected keys
64
64
  */
65
65
  selectedKeys: Array<string | number>;
66
+ /**
67
+ * Enable loose value matching (String comparison instead of strict equality)
68
+ */
69
+ looseMatch?: boolean;
66
70
  /**
67
71
  * Custom suffix
68
72
  */
@@ -37,6 +37,7 @@ const Selector = React.forwardRef(
37
37
  disabled,
38
38
  options = [],
39
39
  selectedKeys,
40
+ looseMatch = false,
40
41
  orphanClassName,
41
42
  orphanStyle,
42
43
  allowClear = true,
@@ -67,7 +68,9 @@ const Selector = React.forwardRef(
67
68
  }
68
69
  // 按 selectedKeys 顺序渲染;找不到的 key 作为孤儿值回退显示 value 本身
69
70
  return selectedKeys.map((key) => {
70
- const opt = options.find((o) => o.value === key);
71
+ const opt = looseMatch
72
+ ? options.find((o) => String(o.value) === String(key))
73
+ : options.find((o) => o.value === key);
71
74
  if (opt) {
72
75
  return {
73
76
  ...opt,
@@ -85,7 +88,7 @@ const Selector = React.forwardRef(
85
88
  _orphan: true,
86
89
  };
87
90
  });
88
- }, [options, selectedKeys, optionLabelProp]);
91
+ }, [options, selectedKeys, optionLabelProp, looseMatch]);
89
92
 
90
93
  const handleClick = () => {
91
94
  if (inputRef.current && !disabled) {
@@ -183,7 +186,9 @@ const Selector = React.forwardRef(
183
186
 
184
187
  const key = selectedKeys.length ? selectedKeys[0] : '';
185
188
  const opt = Array.isArray(options)
186
- ? options.find((opt) => opt.value === key)
189
+ ? looseMatch
190
+ ? options.find((opt) => String(opt.value) === String(key))
191
+ : options.find((opt) => opt.value === key)
187
192
  : null;
188
193
  // 孤儿值:key 非空但在 options 中找不到时,回退显示 value 本身
189
194
  const isOrphan = !opt && key !== '';
@@ -58,7 +58,7 @@ export default function useColumns({
58
58
  const tree = injectSelectionExpand(base, rowSelection, expandable);
59
59
  const leaves = flattenLeafColumns(tree);
60
60
  const wraped = handleColumns(leaves);
61
- const rows = parseHeaderRows(tree);
61
+ const rows = parseHeaderRows(tree, wraped);
62
62
  return { wrapedColumns: wraped, headerRows: rows };
63
63
  }, [childColumns, columns, shouldRenderSelection, shouldRenderExpand]);
64
64
 
@@ -577,7 +577,7 @@ export interface TableProps
577
577
  /**
578
578
  * Row class name, unlike classNames, this prop can be set dynamically via a function
579
579
  */
580
- rowClassName?: (record: object) => string | undefined;
580
+ rowClassName?: (record: Record<string, any>) => string | undefined;
581
581
  /**
582
582
  * Row style, unlike styles, this prop can be set dynamically via a function
583
583
  */
@@ -49,6 +49,12 @@
49
49
  background-color: var(--yee-table-head-cell-split-line-color, #eee);
50
50
  }
51
51
  }
52
+
53
+ &-fixed-left,
54
+ &-fixed-right {
55
+ position: sticky;
56
+ z-index: 1;
57
+ }
52
58
  }
53
59
 
54
60
  &-cell {
@@ -84,6 +90,16 @@
84
90
  &-wrapper {
85
91
  overflow: auto;
86
92
  width: 100%;
93
+
94
+ &-scroll-left @{pls}-cell-fixed-left-last {
95
+ box-shadow: 6px 0 6px -4px
96
+ var(--yee-table-fixed-left-after-bs-color);
97
+ }
98
+
99
+ &-scroll-right @{pls}-cell-fixed-right-first {
100
+ box-shadow: -6px 0 6px -4px
101
+ var(--yee-table-fixed-right-after-bs-color);
102
+ }
87
103
  }
88
104
 
89
105
  > table {
@@ -101,10 +117,18 @@
101
117
  }
102
118
  }
103
119
 
120
+ &-row {
121
+ background-color: var(--yee-table-row-bg-color);
122
+ }
123
+
104
124
  &-even-row {
105
125
  background-color: var(--yee-table-even-row-bg-color);
106
126
  }
107
127
 
128
+ &-odd-row {
129
+ background-color: var(--yee-table-odd-row-bg-color);
130
+ }
131
+
108
132
  &-column-sorters {
109
133
  display: inline-flex;
110
134
  align-items: center;
@@ -137,7 +161,7 @@
137
161
  &-fixed-left,
138
162
  &-fixed-right {
139
163
  position: sticky;
140
- background-color: inherit;
164
+ background-color: var(--yee-table-fixed-column-bg-color);
141
165
  }
142
166
  }
143
167
 
@@ -163,17 +187,17 @@
163
187
  }
164
188
  }
165
189
 
166
- &-odd-row {
167
- > @{pls}-cell {
168
- background-color: var(--yee-table-odd-row-bg-color);
169
- }
170
- }
190
+ // &-odd-row {
191
+ // > @{pls}-cell {
192
+ // background-color: var(--yee-table-odd-row-bg-color);
193
+ // }
194
+ // }
171
195
 
172
- &-even-row {
173
- > @{pls}-cell {
174
- background-color: var(--yee-table-even-row-bg-color);
175
- }
176
- }
196
+ // &-even-row {
197
+ // > @{pls}-cell {
198
+ // background-color: var(--yee-table-even-row-bg-color);
199
+ // }
200
+ // }
177
201
 
178
202
  &-footer {
179
203
  display: flex;
@@ -126,6 +126,36 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
126
126
 
127
127
  // Track previous values to determine what action triggered onChange
128
128
  const prevSortersRef = useRef<typeof sorters>({});
129
+ const contentWrapperRef = useRef<HTMLDivElement>(null);
130
+ const contentRef = useRef<HTMLDivElement>(null);
131
+ const [horizontalScroll, setHorizontalScroll] = React.useState({
132
+ left: false,
133
+ right: false,
134
+ });
135
+
136
+ const updateHorizontalScroll = React.useCallback(() => {
137
+ const wrapper = contentWrapperRef.current;
138
+ const content = contentRef.current;
139
+ if (!wrapper || !content) return;
140
+
141
+ const scrollContainer =
142
+ content.scrollWidth - content.clientWidth > 1 ? content : wrapper;
143
+ const maxScrollLeft =
144
+ scrollContainer.scrollWidth - scrollContainer.clientWidth;
145
+ const hasHorizontalScroll = maxScrollLeft > 1;
146
+ const next = {
147
+ left: hasHorizontalScroll && scrollContainer.scrollLeft > 1,
148
+ right:
149
+ hasHorizontalScroll &&
150
+ scrollContainer.scrollLeft < maxScrollLeft - 1,
151
+ };
152
+
153
+ setHorizontalScroll((current) =>
154
+ current.left === next.left && current.right === next.right
155
+ ? current
156
+ : next,
157
+ );
158
+ }, []);
129
159
 
130
160
  // Wrap onFilter to trigger onChange
131
161
  const onFilter = React.useCallback(
@@ -173,6 +203,25 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
173
203
  }
174
204
  }, [sorters, current, pageSize, pageData, sortedData, onChange]);
175
205
 
206
+ useEffect(() => {
207
+ updateHorizontalScroll();
208
+
209
+ const wrapper = contentWrapperRef.current;
210
+ const content = contentRef.current;
211
+ if (!wrapper || !content) return undefined;
212
+
213
+ if (typeof ResizeObserver === 'undefined') {
214
+ window.addEventListener('resize', updateHorizontalScroll);
215
+ return () => window.removeEventListener('resize', updateHorizontalScroll);
216
+ }
217
+
218
+ const resizeObserver = new ResizeObserver(updateHorizontalScroll);
219
+ resizeObserver.observe(wrapper);
220
+ resizeObserver.observe(content);
221
+
222
+ return () => resizeObserver.disconnect();
223
+ }, [updateHorizontalScroll, wrapedColumns, pageData, scroll?.x]);
224
+
176
225
  const renderHeader = () => {
177
226
  return (
178
227
  <Header
@@ -259,13 +308,23 @@ const Table = React.forwardRef<HTMLDivElement, TableProps>((baseprops, ref) => {
259
308
  );
260
309
 
261
310
  return (
262
- <div className={`${prefixCls}-content-wrapper`}>
311
+ <div
312
+ ref={contentWrapperRef}
313
+ className={clsx(`${prefixCls}-content-wrapper`, {
314
+ [`${prefixCls}-content-wrapper-scroll-left`]: horizontalScroll.left,
315
+ [`${prefixCls}-content-wrapper-scroll-right`]:
316
+ horizontalScroll.right,
317
+ })}
318
+ onScroll={updateHorizontalScroll}
319
+ >
263
320
  <div
321
+ ref={contentRef}
264
322
  className={clsx(`${prefixCls}-content`, {
265
323
  [`${prefixCls}-${size}-size`]: size,
266
324
  [`${prefixCls}-fixed-header`]: scroll?.y,
267
325
  })}
268
326
  style={{ height: scroll?.y, width: scroll?.x }}
327
+ onScroll={updateHorizontalScroll}
269
328
  >
270
329
  {table}
271
330
  </div>
@@ -61,7 +61,7 @@ const TableCell: React.FC<BodyCellProps> = (props) => {
61
61
  {
62
62
  [`${prefixCls}-cell-fixed-${fixed}`]: fixed,
63
63
  [`${prefixCls}-cell-fixed-${fixed}-first`]: isFixedRightFirst,
64
- [`${prefixCls}-fixed-${fixed}-last`]: isFixedLeftLast,
64
+ [`${prefixCls}-cell-fixed-${fixed}-last`]: isFixedLeftLast,
65
65
  },
66
66
  className,
67
67
  );
@@ -32,13 +32,17 @@ export function flattenLeafColumns(
32
32
  *
33
33
  * - Pass 1 (DFS): place each column into the row matching its depth; a group's
34
34
  * colSpan equals the sum of its children's colSpans.
35
+ * - Fixed metadata from the resolved leaf columns is reused by leaf cells and
36
+ * aggregated for group cells whose descendants share the same fixed side.
35
37
  * - Pass 2: leaf cells (no sub-columns) get rowSpan = rowCount - rowIndex so
36
38
  * they stretch down to the last header row; group cells keep rowSpan = 1.
37
39
  */
38
40
  export function parseHeaderRows(
39
41
  rootColumns: WrapedColumnProps[],
42
+ resolvedLeafColumns: WrapedColumnProps[] = flattenLeafColumns(rootColumns),
40
43
  ): HeaderCell[][] {
41
44
  const rows: HeaderCell[][] = [];
45
+ let leafIndex = 0;
42
46
 
43
47
  const fillRowCells = (
44
48
  columns: WrapedColumnProps[],
@@ -46,17 +50,59 @@ export function parseHeaderRows(
46
50
  ): number => {
47
51
  rows[rowIndex] = rows[rowIndex] || [];
48
52
  let colSpanTotal = 0;
49
- columns.filter(Boolean).forEach((column) => {
53
+ columns.filter(Boolean).forEach((sourceColumn) => {
54
+ let column = sourceColumn;
55
+ const subColumns = sourceColumn.children as
56
+ | WrapedColumnProps[]
57
+ | undefined;
58
+ const leafStartIndex = leafIndex;
50
59
  const cell: HeaderCell = {
51
60
  column,
52
61
  colSpan: 1,
53
62
  rowSpan: 1,
54
63
  };
55
- const subColumns = column.children as WrapedColumnProps[] | undefined;
64
+
56
65
  if (subColumns && subColumns.length > 0) {
57
66
  cell.colSpan = fillRowCells(subColumns, rowIndex + 1);
58
67
  cell.hasSubColumns = true;
68
+
69
+ const groupLeaves = resolvedLeafColumns.slice(
70
+ leafStartIndex,
71
+ leafIndex,
72
+ );
73
+ const fixed = groupLeaves[0]?.fixed;
74
+ const isFixedGroup =
75
+ (fixed === 'left' || fixed === 'right') &&
76
+ groupLeaves.every((leaf) => leaf.fixed === fixed);
77
+
78
+ if (isFixedGroup) {
79
+ const offsetLeaf =
80
+ fixed === 'left'
81
+ ? groupLeaves[0]
82
+ : groupLeaves[groupLeaves.length - 1];
83
+ const boundaryLeaf =
84
+ fixed === 'left'
85
+ ? groupLeaves[groupLeaves.length - 1]
86
+ : groupLeaves[0];
87
+ column = {
88
+ ...sourceColumn,
89
+ fixed,
90
+ style: {
91
+ ...sourceColumn.style,
92
+ [fixed]: offsetLeaf.style?.[fixed],
93
+ },
94
+ isFixedLeftLast: fixed === 'left' && boundaryLeaf.isFixedLeftLast,
95
+ isFixedRightFirst:
96
+ fixed === 'right' && boundaryLeaf.isFixedRightFirst,
97
+ };
98
+ cell.column = column;
99
+ }
100
+ } else {
101
+ column = resolvedLeafColumns[leafIndex] || sourceColumn;
102
+ cell.column = column;
103
+ leafIndex += 1;
59
104
  }
105
+
60
106
  rows[rowIndex].push(cell);
61
107
  colSpanTotal += cell.colSpan;
62
108
  });