@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-oh/yee-c",
3
- "version": "0.6.4",
3
+ "version": "0.8.0",
4
4
  "description": "yee basic components",
5
5
  "keywords": [
6
6
  "react",
@@ -171,7 +171,7 @@
171
171
  --m-btn-hover-color: var(--yee-primary-color-hover);
172
172
  --m-btn-active-color: var(--yee-primary-color-active);
173
173
  --m-btn-bg-color: var(--yee-btn-primary-bg-color);
174
- --m-btn-hover-bg-color: var(--yee-primary-color-hover);
174
+ --m-btn-hover-bg-color: var(--yee-primary-color-border);
175
175
  --m-btn-active-bg-color: var(--yee-primary-color-active);
176
176
  --m-btn-border-color: var(--yee-primary-color);
177
177
  --m-btn-border-hover-color: var(--yee-primary-color-hover);
@@ -186,7 +186,7 @@
186
186
  --m-btn-border-color: var(--yee-success-color);
187
187
  --m-btn-border-hover-color: var(--yee-success-color-hover);
188
188
  --m-btn-border-active-color: var(--yee-success-color-active);
189
- --m-btn-hover-bg-color: var(--yee-success-color-hover);
189
+ --m-btn-hover-bg-color: var(--yee-success-color-border);
190
190
  --m-btn-active-bg-color: var(--yee-success-color-active);
191
191
  --m-btn-active-shadow-color: var(--yee-success-color-active);
192
192
  }
@@ -198,7 +198,8 @@
198
198
  --m-btn-border-color: var(--yee-error-color);
199
199
  --m-btn-border-hover-color: var(--yee-error-color-hover);
200
200
  --m-btn-border-active-color: var(--yee-error-color-active);
201
- --m-btn-hover-bg-color: var(--yee-error-color-hover);
201
+ // --m-btn-hover-bg-color: var(--yee-error-color-hover);
202
+ --m-btn-hover-bg-color: var(--yee-error-color-border);
202
203
  --m-btn-active-bg-color: var(--yee-error-color-active);
203
204
  --m-btn-active-shadow-color: var(--yee-error-color-active);
204
205
  }
@@ -210,7 +211,7 @@
210
211
  --m-btn-border-color: var(--yee-warning-color);
211
212
  --m-btn-border-hover-color: var(--yee-warning-color-hover);
212
213
  --m-btn-border-active-color: var(--yee-warning-color-active);
213
- --m-btn-hover-bg-color: var(--yee-warning-color-hover);
214
+ --m-btn-hover-bg-color: var(--yee-warning-color-border);
214
215
  --m-btn-active-bg-color: var(--yee-warning-color-active);
215
216
  --m-btn-active-shadow-color: var(--yee-warning-color-active);
216
217
  }
@@ -198,7 +198,7 @@ const Drawer = (baseprops: DrawerProps) => {
198
198
  className={clsx(`${prefixCls}-header`, classNames?.header)}
199
199
  style={styles?.header}
200
200
  >
201
- <div className={`${prefixCls}-title`}>{title}</div>
201
+ <div className={clsx(`${prefixCls}-title`, classNames?.title)} style={styles?.title}>{title}</div>
202
202
  </div>
203
203
  ) : null}
204
204
  <div
@@ -5,6 +5,7 @@ export type SemanticDOM =
5
5
  | 'wrapper'
6
6
  | 'close'
7
7
  | 'header'
8
+ | 'title'
8
9
  | 'content'
9
10
  | 'body'
10
11
  | 'footer';
@@ -38,6 +38,7 @@
38
38
  }
39
39
 
40
40
  &-title {
41
+ font-size: var(--yee-drawer-title-font-size);
41
42
  font-weight: var(--yee-drawer-title-font-weight);
42
43
  }
43
44
 
@@ -32,6 +32,11 @@ function move<T>(array: T[], from: number, to: number): T[] {
32
32
 
33
33
  const GLOBAL_WATCH_KEY = '__ALL__';
34
34
 
35
+ const isRelatedNamePath = (first: string, second: string): boolean =>
36
+ first === second ||
37
+ first.startsWith(`${second}.`) ||
38
+ second.startsWith(`${first}.`);
39
+
35
40
  export class FormStore {
36
41
  private store: Store = {};
37
42
  private callbacks: Callbacks = {};
@@ -145,6 +150,9 @@ export class FormStore {
145
150
  });
146
151
  }
147
152
 
153
+ const changedKeys = Object.keys(newStore);
154
+ const relatedFieldNames = this.getRelatedFieldNames(changedKeys);
155
+
148
156
  // Validate on onChange and onBlur (async — fire-and-forget so setFieldsValue
149
157
  // stays synchronous for its callers; results surface via onStoreChange).
150
158
  if (
@@ -152,10 +160,10 @@ export class FormStore {
152
160
  trigger === 'onBlur' ||
153
161
  trigger === 'onSubmit'
154
162
  ) {
155
- void this.validateFields(Object.keys(newStore), trigger);
163
+ void this.validateFields(relatedFieldNames, trigger);
156
164
  }
157
165
 
158
- this.refreshField(newStore);
166
+ this.refreshField(newStore, relatedFieldNames);
159
167
  };
160
168
 
161
169
  resetFields = (name?: Name[]) => {
@@ -279,16 +287,22 @@ export class FormStore {
279
287
  return err;
280
288
  };
281
289
 
282
- refreshField = (newStore?: Store) => {
290
+ private getRelatedFieldNames = (changedKeys: string[]): Name[] =>
291
+ Array.from(this.fieldMap.keys()).filter((fieldName) =>
292
+ changedKeys.some((changedKey) =>
293
+ isRelatedNamePath(String(fieldName), changedKey),
294
+ ),
295
+ );
296
+
297
+ refreshField = (newStore?: Store, relatedFieldNames?: Name[]) => {
283
298
  if (newStore) {
284
- const keys = Object.keys(newStore);
285
- keys.forEach((key) => {
286
- const entity = this.fieldMap.get(key);
287
- if (entity) {
288
- entity?.onStoreChange();
289
- }
290
- this.notifyWatchers(key);
299
+ const changedKeys = Object.keys(newStore);
300
+ const fieldNames =
301
+ relatedFieldNames ?? this.getRelatedFieldNames(changedKeys);
302
+ fieldNames.forEach((fieldName) => {
303
+ this.fieldMap.get(fieldName)?.onStoreChange();
291
304
  });
305
+ this.notifyWatchers(changedKeys);
292
306
  } else {
293
307
  this.fieldEntities.forEach((entity) => {
294
308
  entity?.onStoreChange();
@@ -308,14 +322,27 @@ export class FormStore {
308
322
  }
309
323
  this.watchers.get(key)!.add(callback);
310
324
  return () => {
311
- this.watchers.get(key)?.delete(callback);
325
+ const watchers = this.watchers.get(key);
326
+ watchers?.delete(callback);
327
+ if (watchers?.size === 0) {
328
+ this.watchers.delete(key);
329
+ }
312
330
  };
313
331
  };
314
332
 
315
- private notifyWatchers = (changedKey?: string) => {
316
- if (changedKey) {
317
- this.watchers.get(changedKey)?.forEach((cb) => cb());
318
- this.watchers.get(GLOBAL_WATCH_KEY)?.forEach((cb) => cb());
333
+ private notifyWatchers = (changedKeys?: string | string[]) => {
334
+ if (changedKeys) {
335
+ const keys = Array.isArray(changedKeys) ? changedKeys : [changedKeys];
336
+ const callbacks = new Set<() => void>();
337
+ this.watchers.forEach((watchers, watchedKey) => {
338
+ if (
339
+ watchedKey === GLOBAL_WATCH_KEY ||
340
+ keys.some((changedKey) => isRelatedNamePath(watchedKey, changedKey))
341
+ ) {
342
+ watchers.forEach((callback) => callbacks.add(callback));
343
+ }
344
+ });
345
+ callbacks.forEach((callback) => callback());
319
346
  } else {
320
347
  this.watchers.forEach((set) => set.forEach((cb) => cb()));
321
348
  }
@@ -0,0 +1,20 @@
1
+ import { List } from '@rainbow-oh/yee-c';
2
+ import React from 'react';
3
+
4
+ const items = Array.from({ length: 10000 }, (_, i) => ({
5
+ key: String(i),
6
+ label: `List item ${i + 1}`,
7
+ value: String(i),
8
+ }));
9
+
10
+ export default () => {
11
+ return (
12
+ <List
13
+ virtual
14
+ height={300}
15
+ itemHeight={32}
16
+ items={items}
17
+ onClick={(item) => console.log('Clicked:', item.label)}
18
+ />
19
+ );
20
+ };
@@ -0,0 +1,235 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import type { ListItemProps } from '../interface';
3
+
4
+ interface UseListKeyboardParams {
5
+ items: Array<ListItemProps>;
6
+ /**
7
+ * Number of columns in the grid layout.
8
+ * Affects ArrowUp/Down (jump by columns) and ArrowLeft/Right behavior.
9
+ * @default 1
10
+ */
11
+ columns?: number;
12
+ /**
13
+ * Controlled focused key from the parent.
14
+ * When provided, the hook uses this value instead of internal state.
15
+ */
16
+ focusedKey?: string | number;
17
+ /**
18
+ * Callback when focused key changes.
19
+ */
20
+ onFocusChange?: (key: string | number) => void;
21
+ /**
22
+ * Callback when Enter is pressed on a focused item.
23
+ */
24
+ onClick?: (item: ListItemProps) => void;
25
+ }
26
+
27
+ export default function useListKeyboard({
28
+ items,
29
+ columns = 1,
30
+ focusedKey: controlledFocusedKey,
31
+ onFocusChange,
32
+ onClick,
33
+ }: UseListKeyboardParams) {
34
+ const [internalFocusedKey, setInternalFocusedKey] = useState<
35
+ string | number
36
+ >('');
37
+
38
+ const isControlled = controlledFocusedKey !== undefined;
39
+ const focusedKey = isControlled ? controlledFocusedKey : internalFocusedKey;
40
+
41
+ const setFocusedKey = useCallback(
42
+ (key: string | number) => {
43
+ if (!isControlled) {
44
+ setInternalFocusedKey(key);
45
+ }
46
+ onFocusChange?.(key);
47
+ },
48
+ [isControlled, onFocusChange],
49
+ );
50
+
51
+ // Reset internal focused key if it no longer exists in items
52
+ useEffect(() => {
53
+ if (
54
+ !isControlled &&
55
+ internalFocusedKey !== '' &&
56
+ !items.some((item) => item.key === internalFocusedKey)
57
+ ) {
58
+ setInternalFocusedKey('');
59
+ }
60
+ }, [items, isControlled, internalFocusedKey]);
61
+
62
+ const getFocusedIndex = (): number => {
63
+ if (focusedKey === '' || focusedKey === undefined) return -1;
64
+ return items.findIndex((item) => item.key === focusedKey);
65
+ };
66
+
67
+ const findNextEnabledIndex = (
68
+ startIndex: number,
69
+ direction: 1 | -1,
70
+ ): number => {
71
+ const count = items.length;
72
+ if (count === 0) return -1;
73
+ let next =
74
+ direction === 1
75
+ ? startIndex + 1 >= count
76
+ ? 0
77
+ : startIndex + 1
78
+ : startIndex - 1 < 0
79
+ ? count - 1
80
+ : startIndex - 1;
81
+
82
+ // Guard against infinite loop if all items are disabled
83
+ let attempts = 0;
84
+ while (items[next].disabled && attempts < count) {
85
+ next =
86
+ direction === 1
87
+ ? next + 1 >= count
88
+ ? 0
89
+ : next + 1
90
+ : next - 1 < 0
91
+ ? count - 1
92
+ : next - 1;
93
+ attempts++;
94
+ }
95
+ return items[next].disabled ? -1 : next;
96
+ };
97
+
98
+ const handleArrowUp = () => {
99
+ const focusedIndex = getFocusedIndex();
100
+ let nextIndex: number;
101
+
102
+ if (columns > 1) {
103
+ if (focusedIndex === -1) {
104
+ nextIndex = findNextEnabledIndex(items.length - 1, -1);
105
+ } else {
106
+ const target = focusedIndex - columns;
107
+ if (target >= 0) {
108
+ nextIndex = items[target].disabled
109
+ ? findNextEnabledIndex(target, -1)
110
+ : target;
111
+ } else {
112
+ // Wrap to last row, same column
113
+ const lastRowStart =
114
+ Math.floor((items.length - 1) / columns) * columns;
115
+ const col = focusedIndex % columns;
116
+ const wrapTarget = Math.min(lastRowStart + col, items.length - 1);
117
+ nextIndex = items[wrapTarget]?.disabled
118
+ ? findNextEnabledIndex(wrapTarget, -1)
119
+ : wrapTarget;
120
+ }
121
+ }
122
+ } else {
123
+ if (focusedIndex === -1) {
124
+ nextIndex = findNextEnabledIndex(items.length - 1, -1);
125
+ } else {
126
+ nextIndex = findNextEnabledIndex(focusedIndex, -1);
127
+ }
128
+ }
129
+
130
+ if (nextIndex !== -1) {
131
+ setFocusedKey(items[nextIndex].key);
132
+ }
133
+ };
134
+
135
+ const handleArrowDown = () => {
136
+ const focusedIndex = getFocusedIndex();
137
+ let nextIndex: number;
138
+
139
+ if (columns > 1) {
140
+ if (focusedIndex === -1) {
141
+ nextIndex = findNextEnabledIndex(-1, 1);
142
+ } else {
143
+ const target = focusedIndex + columns;
144
+ if (target < items.length) {
145
+ nextIndex = items[target].disabled
146
+ ? findNextEnabledIndex(target, 1)
147
+ : target;
148
+ } else {
149
+ // Wrap to first row, same column
150
+ const col = focusedIndex % columns;
151
+ const wrapTarget = col;
152
+ nextIndex = items[wrapTarget]?.disabled
153
+ ? findNextEnabledIndex(wrapTarget, 1)
154
+ : wrapTarget;
155
+ }
156
+ }
157
+ } else {
158
+ if (focusedIndex === -1) {
159
+ nextIndex = findNextEnabledIndex(-1, 1);
160
+ } else {
161
+ nextIndex = findNextEnabledIndex(focusedIndex, 1);
162
+ }
163
+ }
164
+
165
+ if (nextIndex !== -1) {
166
+ setFocusedKey(items[nextIndex].key);
167
+ }
168
+ };
169
+
170
+ const handleArrowLeft = () => {
171
+ const focusedIndex = getFocusedIndex();
172
+ if (focusedIndex <= 0) return;
173
+ const nextIndex = findNextEnabledIndex(focusedIndex, -1);
174
+ if (nextIndex !== -1) {
175
+ setFocusedKey(items[nextIndex].key);
176
+ }
177
+ };
178
+
179
+ const handleArrowRight = () => {
180
+ const focusedIndex = getFocusedIndex();
181
+ if (focusedIndex >= items.length - 1 && focusedIndex !== -1) return;
182
+ const nextIndex = findNextEnabledIndex(
183
+ focusedIndex === -1 ? -1 : focusedIndex,
184
+ 1,
185
+ );
186
+ if (nextIndex !== -1) {
187
+ setFocusedKey(items[nextIndex].key);
188
+ }
189
+ };
190
+
191
+ const handleEnter = () => {
192
+ if (focusedKey === '' || focusedKey === undefined) return;
193
+ const item = items.find((i) => i.key === focusedKey);
194
+ if (item && !item.disabled) {
195
+ onClick?.(item);
196
+ }
197
+ };
198
+
199
+ const onKeyDown = (event: React.KeyboardEvent) => {
200
+ if (!items || items.length === 0) return;
201
+
202
+ switch (event.key) {
203
+ case 'ArrowUp':
204
+ event.preventDefault();
205
+ handleArrowUp();
206
+ break;
207
+ case 'ArrowDown':
208
+ event.preventDefault();
209
+ handleArrowDown();
210
+ break;
211
+ case 'ArrowLeft':
212
+ if (columns > 1) {
213
+ event.preventDefault();
214
+ handleArrowLeft();
215
+ }
216
+ break;
217
+ case 'ArrowRight':
218
+ if (columns > 1) {
219
+ event.preventDefault();
220
+ handleArrowRight();
221
+ }
222
+ break;
223
+ case 'Enter':
224
+ event.preventDefault();
225
+ handleEnter();
226
+ break;
227
+ default:
228
+ }
229
+ };
230
+
231
+ return {
232
+ focusedKey,
233
+ onKeyDown,
234
+ };
235
+ }
@@ -18,6 +18,7 @@ A list component for displaying a collection of items.
18
18
  <code src="./demo/bordered.tsx" title="Bordered" description="List with border"></code>
19
19
  <code src="./demo/renderItem.tsx" title="Custom Render" description="List with custom item rendering"></code>
20
20
  <code src="./demo/disabled.tsx" title="Disabled Items" description="List with disabled items"></code>
21
+ <code src="./demo/virtual.tsx" title="Virtual Scroll" description="Virtual scrolling for large datasets (10,000 items), only rendering items in the visible area"></code>
21
22
 
22
23
  ## API
23
24
 
@@ -33,6 +34,11 @@ A list component for displaying a collection of items.
33
34
  | focusedKey | `string \| number` | Key of the focused item | - |
34
35
  | itemRender | `(item: ListItemProps) => React.ReactNode` | Function to render list items | - |
35
36
  | onClick | `(item: ListItemProps) => void` | Click event for list items | - |
37
+ | onFocusChange | `(key: string \| number) => void` | Callback when focused item changes via keyboard navigation | - |
38
+ | virtual | `boolean` | Enable virtual scrolling. Requires `height` to be set | `false` |
39
+ | height | `number` | Scroll container height in pixels. Required when `virtual` is true | - |
40
+ | itemHeight | `number` | Fixed height of each item in pixels | `32` |
41
+ | columns | `number` | Number of columns. Enables grid layout when greater than 1 | `1` |
36
42
 
37
43
  ### ListItemProps
38
44
 
@@ -18,6 +18,7 @@ toc: 'content'
18
18
  <code src="./demo/bordered.tsx" title="带边框" description="带边框的列表"></code>
19
19
  <code src="./demo/renderItem.tsx" title="自定义渲染" description="自定义列表项渲染"></code>
20
20
  <code src="./demo/disabled.tsx" title="禁用项" description="包含禁用项的列表"></code>
21
+ <code src="./demo/virtual.tsx" title="虚拟滚动" description="大数据量(万条)下的虚拟滚动,只渲染可视区域内的列表项"></code>
21
22
 
22
23
  ## API
23
24
 
@@ -33,6 +34,11 @@ toc: 'content'
33
34
  | focusedKey | `string \| number` | 聚焦项的 key | - |
34
35
  | itemRender | `(item: ListItemProps) => React.ReactNode` | 函数形式返回列表项 | - |
35
36
  | onClick | `(item: ListItemProps) => void` | 列表项的点击事件 | - |
37
+ | onFocusChange | `(key: string \| number) => void` | 键盘导航焦点变化时的回调 | - |
38
+ | virtual | `boolean` | 是否开启虚拟滚动,需同时设置 `height` | `false` |
39
+ | height | `number` | 滚动容器高度(px),开启 `virtual` 时必填 | - |
40
+ | itemHeight | `number` | 每项固定高度(px) | `32` |
41
+ | columns | `number` | 列数,大于 1 时启用网格布局 | `1` |
36
42
 
37
43
  ### ListItemProps
38
44
 
@@ -52,8 +52,8 @@ export interface ListProps extends Omit<
52
52
  */
53
53
  bordered?: boolean;
54
54
  /**
55
- * Set the focused item
56
- * */
55
+ * Set the focused item (controlled)
56
+ */
57
57
  focusedKey?: string | number;
58
58
  /**
59
59
  * Render list item as a function
@@ -63,6 +63,33 @@ export interface ListProps extends Omit<
63
63
  * Click event for list items
64
64
  */
65
65
  onClick?: (item: ListItemProps) => void;
66
+ /**
67
+ * Callback when focused item changes via keyboard navigation
68
+ */
69
+ onFocusChange?: (key: string | number) => void;
70
+ /**
71
+ * Enable virtual scrolling for large lists.
72
+ * Requires `height` to be set.
73
+ * @default false
74
+ */
75
+ virtual?: boolean;
76
+ /**
77
+ * Height of the scrollable container in pixels.
78
+ * Required when `virtual` is true.
79
+ */
80
+ height?: number;
81
+ /**
82
+ * Fixed height of each list item row in pixels.
83
+ * Used for virtual scroll calculation.
84
+ * @default 32
85
+ */
86
+ itemHeight?: number;
87
+ /**
88
+ * Number of columns in the list layout.
89
+ * When greater than 1, items are displayed in a grid.
90
+ * @default 1
91
+ */
92
+ columns?: number;
66
93
  }
67
94
 
68
95
  export type ListCtxType = {