@rainbow-oh/yee-c 0.6.3 → 0.7.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 (131) hide show
  1. package/dist/cjs/Button/style/index.less +5 -4
  2. package/dist/cjs/Checkbox/style/index.less +1 -1
  3. package/dist/cjs/Drawer/drawer.js +1 -1
  4. package/dist/cjs/Drawer/interface.d.ts +1 -1
  5. package/dist/cjs/Drawer/style/index.less +1 -0
  6. package/dist/cjs/List/demo/virtual.d.ts +3 -0
  7. package/dist/cjs/List/demo/virtual.js +53 -0
  8. package/dist/cjs/List/hooks/useListKeyboard.d.ts +28 -0
  9. package/dist/cjs/List/hooks/useListKeyboard.js +188 -0
  10. package/dist/cjs/List/interface.d.ts +29 -2
  11. package/dist/cjs/List/list.js +141 -21
  12. package/dist/cjs/List/style/index.less +24 -0
  13. package/dist/cjs/Select/demo/columns.d.ts +3 -0
  14. package/dist/cjs/Select/demo/columns.js +62 -0
  15. package/dist/cjs/Select/hooks/useSelectKeyboard.d.ts +13 -1
  16. package/dist/cjs/Select/hooks/useSelectKeyboard.js +81 -10
  17. package/dist/cjs/Select/interface.d.ts +30 -3
  18. package/dist/cjs/Select/option.js +4 -3
  19. package/dist/cjs/Select/options.js +90 -8
  20. package/dist/cjs/Select/select.js +32 -8
  21. package/dist/cjs/Select/style/index.less +14 -0
  22. package/dist/cjs/Selector/interface.d.ts +4 -0
  23. package/dist/cjs/Selector/selector.js +5 -3
  24. package/dist/cjs/Table/interface.d.ts +1 -1
  25. package/dist/cjs/Table/style/index.less +1 -1
  26. package/dist/cjs/Tree/demo/draggable.js +2 -0
  27. package/dist/cjs/Tree/interface.d.ts +46 -23
  28. package/dist/cjs/Tree/tree-node.d.ts +1 -1
  29. package/dist/cjs/Tree/tree-node.js +30 -23
  30. package/dist/cjs/Tree/tree.d.ts +13 -12
  31. package/dist/cjs/Tree/tree.js +452 -227
  32. package/dist/cjs/Tree/utils/tree-tools.d.ts +43 -10
  33. package/dist/cjs/Tree/utils/tree-tools.js +129 -91
  34. package/dist/cjs/locale/context.d.ts +5 -4
  35. package/dist/cjs/locale/context.js +15 -8
  36. package/dist/cjs/locale/interface.d.ts +20 -0
  37. package/dist/cjs/locale/util.d.ts +3 -3
  38. package/dist/cjs/style/color.css +1 -0
  39. package/dist/cjs/style/size.css +2 -1
  40. package/dist/cjs/style/themes.css +1 -1
  41. package/dist/cjs/style/variables.css +4 -2
  42. package/dist/cjs/utils/isEqual.d.ts +1 -1
  43. package/dist/cjs/utils/isEqual.js +24 -13
  44. package/dist/cjs/version.d.ts +1 -1
  45. package/dist/cjs/version.js +1 -1
  46. package/dist/esm/Button/style/index.less +5 -4
  47. package/dist/esm/Checkbox/style/index.less +1 -1
  48. package/dist/esm/Drawer/drawer.js +2 -1
  49. package/dist/esm/Drawer/interface.d.ts +1 -1
  50. package/dist/esm/Drawer/style/index.less +1 -0
  51. package/dist/esm/List/demo/virtual.d.ts +3 -0
  52. package/dist/esm/List/demo/virtual.js +22 -0
  53. package/dist/esm/List/hooks/useListKeyboard.d.ts +28 -0
  54. package/dist/esm/List/hooks/useListKeyboard.js +173 -0
  55. package/dist/esm/List/interface.d.ts +29 -2
  56. package/dist/esm/List/list.js +171 -14
  57. package/dist/esm/List/style/index.less +24 -0
  58. package/dist/esm/Select/demo/columns.d.ts +3 -0
  59. package/dist/esm/Select/demo/columns.js +36 -0
  60. package/dist/esm/Select/hooks/useSelectKeyboard.d.ts +13 -1
  61. package/dist/esm/Select/hooks/useSelectKeyboard.js +91 -14
  62. package/dist/esm/Select/interface.d.ts +30 -3
  63. package/dist/esm/Select/option.js +6 -2
  64. package/dist/esm/Select/options.js +114 -32
  65. package/dist/esm/Select/select.js +40 -11
  66. package/dist/esm/Select/style/index.less +14 -0
  67. package/dist/esm/Selector/interface.d.ts +4 -0
  68. package/dist/esm/Selector/selector.js +10 -4
  69. package/dist/esm/Table/interface.d.ts +1 -1
  70. package/dist/esm/Table/style/index.less +1 -1
  71. package/dist/esm/Tree/demo/draggable.js +2 -0
  72. package/dist/esm/Tree/interface.d.ts +46 -23
  73. package/dist/esm/Tree/tree-node.d.ts +1 -1
  74. package/dist/esm/Tree/tree-node.js +26 -31
  75. package/dist/esm/Tree/tree.d.ts +13 -12
  76. package/dist/esm/Tree/tree.js +572 -163
  77. package/dist/esm/Tree/utils/tree-tools.d.ts +43 -10
  78. package/dist/esm/Tree/utils/tree-tools.js +219 -104
  79. package/dist/esm/locale/context.d.ts +5 -4
  80. package/dist/esm/locale/context.js +25 -9
  81. package/dist/esm/locale/interface.d.ts +20 -0
  82. package/dist/esm/locale/util.d.ts +3 -3
  83. package/dist/esm/style/color.css +1 -0
  84. package/dist/esm/style/size.css +2 -1
  85. package/dist/esm/style/themes.css +1 -1
  86. package/dist/esm/style/variables.css +4 -2
  87. package/dist/esm/utils/isEqual.d.ts +1 -1
  88. package/dist/esm/utils/isEqual.js +31 -15
  89. package/dist/esm/version.d.ts +1 -1
  90. package/dist/esm/version.js +1 -1
  91. package/dist/umd/yee-c.min.css +2 -2
  92. package/dist/umd/yee-c.min.js +1 -1
  93. package/package.json +1 -1
  94. package/src/Button/style/index.less +5 -4
  95. package/src/Checkbox/style/index.less +1 -1
  96. package/src/Drawer/drawer.tsx +1 -1
  97. package/src/Drawer/interface.ts +1 -0
  98. package/src/Drawer/style/index.less +1 -0
  99. package/src/List/demo/virtual.tsx +20 -0
  100. package/src/List/hooks/useListKeyboard.ts +235 -0
  101. package/src/List/index.en-US.md +6 -0
  102. package/src/List/index.zh-CN.md +6 -0
  103. package/src/List/interface.ts +29 -2
  104. package/src/List/list.tsx +157 -19
  105. package/src/List/style/index.less +24 -0
  106. package/src/Select/demo/columns.tsx +35 -0
  107. package/src/Select/hooks/useSelectKeyboard.ts +112 -15
  108. package/src/Select/index.en-US.md +4 -0
  109. package/src/Select/index.zh-CN.md +4 -0
  110. package/src/Select/interface.ts +31 -4
  111. package/src/Select/option.tsx +9 -3
  112. package/src/Select/options.tsx +119 -26
  113. package/src/Select/select.tsx +41 -8
  114. package/src/Select/style/index.less +14 -0
  115. package/src/Selector/interface.ts +4 -0
  116. package/src/Selector/selector.tsx +8 -3
  117. package/src/Table/interface.ts +1 -1
  118. package/src/Table/style/index.less +1 -1
  119. package/src/Tree/demo/draggable.tsx +2 -0
  120. package/src/Tree/interface.ts +46 -23
  121. package/src/Tree/tree-node.tsx +26 -29
  122. package/src/Tree/tree.tsx +573 -274
  123. package/src/Tree/utils/tree-tools.ts +231 -125
  124. package/src/locale/_README.md +22 -13
  125. package/src/locale/context.tsx +33 -16
  126. package/src/locale/interface.ts +24 -0
  127. package/src/locale/util.ts +6 -6
  128. package/src/style/color.css +1 -0
  129. package/src/style/size.css +2 -1
  130. package/src/utils/isEqual.ts +35 -14
  131. 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.3",
3
+ "version": "0.7.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
  }
@@ -63,7 +63,7 @@
63
63
  border-color: var(--yee-checkbox-border-color);
64
64
  }
65
65
 
66
- &&-checked {
66
+ &@{pls}-checked {
67
67
  @{pls}-inner {
68
68
  --ye-checkbox-checked-border-color: var(
69
69
  --yee-checkbox-disabled-checked-border-color
@@ -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
 
@@ -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 = {
package/src/List/list.tsx CHANGED
@@ -1,7 +1,9 @@
1
1
  import clsx from 'clsx';
2
2
  import React, { createContext, forwardRef, useContext } from 'react';
3
3
  import { GlobalContext } from '../Config-Provider';
4
+ import useVirtualList from '../hooks/useVirtualList';
4
5
  import mergeContextToProps from '../utils/mergeContextToProps';
6
+ import useListKeyboard from './hooks/useListKeyboard';
5
7
  import type { ListCtxType, ListProps } from './interface';
6
8
  import ListItem from './list-item';
7
9
 
@@ -16,43 +18,179 @@ const List = forwardRef<HTMLUListElement, ListProps>((baseprops, ref) => {
16
18
  const {
17
19
  prefixCls = 'yee-list',
18
20
  className,
21
+ style,
19
22
  items,
20
23
  bordered,
21
- focusedKey,
24
+ focusedKey: controlledFocusedKey,
22
25
  itemRender,
23
26
  onClick,
27
+ onFocusChange,
28
+ virtual = false,
29
+ height,
30
+ itemHeight = 32,
31
+ columns = 1,
24
32
  ...rest
25
33
  } = props;
26
34
 
35
+ const multiColumn = columns > 1;
36
+ const useVirtual = virtual && height !== null && height !== undefined && Array.isArray(items);
37
+ const safeItems = Array.isArray(items) ? items : [];
38
+
39
+ // Keyboard navigation
40
+ const { focusedKey, onKeyDown } = useListKeyboard({
41
+ items: safeItems,
42
+ columns,
43
+ focusedKey: controlledFocusedKey,
44
+ onFocusChange,
45
+ onClick,
46
+ });
47
+
48
+ // Virtual scroll — in multi-column mode, track rows not individual items
49
+ const rowCount = multiColumn
50
+ ? Math.ceil(safeItems.length / columns)
51
+ : safeItems.length;
52
+
53
+ const { start, end, offsetY, totalHeight, viewportRef, onScroll } =
54
+ useVirtualList({
55
+ itemCount: useVirtual ? rowCount : 0,
56
+ itemHeight,
57
+ overscan: 4,
58
+ });
59
+
27
60
  const cls = clsx(
28
61
  prefixCls,
29
62
  {
30
63
  [`${prefixCls}-bordered`]: bordered,
64
+ [`${prefixCls}-grid`]: multiColumn && !useVirtual,
31
65
  },
32
66
  className,
33
67
  );
34
68
 
35
- const itemRenders = () => {
36
- return Array.isArray(items)
37
- ? items.map((item, index) => {
38
- if (typeof itemRender === 'function') {
39
- return itemRender(item);
40
- }
41
- return <ListItem {...item} $key={item.key} key={item.key || index} />;
42
- })
43
- : null;
69
+ const renderItem = (item: any, index: number) => {
70
+ if (typeof itemRender === 'function') {
71
+ return itemRender(item);
72
+ }
73
+ return <ListItem {...item} $key={item.key} key={item.key || index} />;
44
74
  };
45
75
 
46
- return (
47
- <ul ref={ref} className={cls} tabIndex={0} aria-label="list" {...rest}>
48
- <ListCtx.Provider
49
- value={{
50
- prefixCls,
51
- focusedKey,
52
- onClick: onClick,
53
- }}
76
+ // ----- Virtual + multi-column: render rows of N items -----
77
+ if (useVirtual && multiColumn) {
78
+ const visibleRows = [];
79
+ for (let rowIdx = start; rowIdx < end; rowIdx++) {
80
+ const rowItems = [];
81
+ for (let col = 0; col < columns; col++) {
82
+ const flatIdx = rowIdx * columns + col;
83
+ if (flatIdx < safeItems.length) {
84
+ rowItems.push({ item: safeItems[flatIdx], flatIdx });
85
+ }
86
+ }
87
+ visibleRows.push({ rowIdx, rowItems });
88
+ }
89
+
90
+ return (
91
+ <ul
92
+ ref={ref}
93
+ className={cls}
94
+ tabIndex={0}
95
+ aria-label="list"
96
+ onKeyDown={onKeyDown}
97
+ {...rest}
98
+ >
99
+ <ListCtx.Provider value={{ prefixCls, focusedKey, onClick }}>
100
+ <div
101
+ ref={viewportRef}
102
+ onScroll={onScroll}
103
+ style={{ height, overflowY: 'auto' }}
104
+ >
105
+ <div style={{ height: totalHeight, position: 'relative' }}>
106
+ {visibleRows.map(({ rowIdx, rowItems }, ri) => (
107
+ <div
108
+ key={rowIdx}
109
+ className={`${prefixCls}-grid-row`}
110
+ style={{
111
+ position: 'absolute',
112
+ top: 0,
113
+ left: 0,
114
+ right: 0,
115
+ height: itemHeight,
116
+ transform: `translateY(${offsetY + ri * itemHeight}px)`,
117
+ display: 'grid',
118
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
119
+ }}
120
+ >
121
+ {rowItems.map(({ item, flatIdx }) =>
122
+ renderItem(item, flatIdx),
123
+ )}
124
+ </div>
125
+ ))}
126
+ </div>
127
+ </div>
128
+ </ListCtx.Provider>
129
+ </ul>
130
+ );
131
+ }
132
+
133
+ // ----- Virtual single-column -----
134
+ if (useVirtual) {
135
+ const slicedItems = safeItems.slice(start, end);
136
+ return (
137
+ <ul
138
+ ref={ref}
139
+ className={cls}
140
+ tabIndex={0}
141
+ aria-label="list"
142
+ onKeyDown={onKeyDown}
143
+ {...rest}
54
144
  >
55
- {itemRenders()}
145
+ <ListCtx.Provider value={{ prefixCls, focusedKey, onClick }}>
146
+ <div
147
+ ref={viewportRef}
148
+ onScroll={onScroll}
149
+ style={{ height, overflowY: 'auto' }}
150
+ >
151
+ <div style={{ height: totalHeight, position: 'relative' }}>
152
+ {slicedItems.map((item, i) => {
153
+ const element = renderItem(item, start + i);
154
+ return (
155
+ <div
156
+ key={item.key || start + i}
157
+ style={{
158
+ position: 'absolute',
159
+ top: 0,
160
+ left: 0,
161
+ width: '100%',
162
+ height: itemHeight,
163
+ transform: `translateY(${offsetY + i * itemHeight}px)`,
164
+ }}
165
+ >
166
+ {element}
167
+ </div>
168
+ );
169
+ })}
170
+ </div>
171
+ </div>
172
+ </ListCtx.Provider>
173
+ </ul>
174
+ );
175
+ }
176
+
177
+ // ----- Default mode (non-virtual) -----
178
+ return (
179
+ <ul
180
+ ref={ref}
181
+ className={cls}
182
+ tabIndex={0}
183
+ aria-label="list"
184
+ onKeyDown={onKeyDown}
185
+ style={
186
+ multiColumn
187
+ ? ({ '--yee-list-columns': columns, ...style } as React.CSSProperties)
188
+ : style
189
+ }
190
+ {...rest}
191
+ >
192
+ <ListCtx.Provider value={{ prefixCls, focusedKey, onClick }}>
193
+ {safeItems.map((item, index) => renderItem(item, index))}
56
194
  </ListCtx.Provider>
57
195
  </ul>
58
196
  );
@@ -6,18 +6,42 @@
6
6
  list-style: none;
7
7
  background-color: var(--yee-list-bg-color);
8
8
 
9
+ &:focus {
10
+ outline: none;
11
+ }
12
+
9
13
  &-item {
10
14
  padding: var(--yee-list-item-padding);
11
15
 
12
16
  &:hover:not(&-disabled) {
13
17
  background-color: var(--yee-list-item-bg-hover-color);
18
+ }
19
+
20
+ &-disabled {
14
21
  cursor: not-allowed;
22
+ opacity: 0.5;
23
+ }
24
+
25
+ &-clickable:not(&-disabled) {
26
+ cursor: pointer;
15
27
  }
16
28
 
17
29
  &-focused {
18
30
  background-color: var(--yee-list-item-bg-hover-color);
19
31
  }
20
32
  }
33
+
34
+ // Multi-column grid layout (non-virtual mode).
35
+ // Column count driven by CSS custom property --yee-list-columns set inline.
36
+ &-grid {
37
+ display: grid;
38
+ grid-template-columns: repeat(var(--yee-list-columns, 1), 1fr);
39
+ }
40
+
41
+ // Virtual multi-column: each row is already a grid via inline style.
42
+ &-grid-row {
43
+ align-items: center;
44
+ }
21
45
  }
22
46
 
23
47
  @{pls}-bordered {