@rainbow-oh/yee-c 0.4.5 → 0.5.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 (104) hide show
  1. package/dist/cjs/Dialog/demo/basic.js +1 -1
  2. package/dist/cjs/Dialog/dialog.js +1 -1
  3. package/dist/cjs/Field/demo/validation.js +23 -3
  4. package/dist/cjs/Field/field.js +1 -1
  5. package/dist/cjs/Form/Field.js +1 -4
  6. package/dist/cjs/Form/Form.js +1 -1
  7. package/dist/cjs/Form/Group.js +36 -14
  8. package/dist/cjs/Form/List.js +23 -3
  9. package/dist/cjs/Form/demo/async-validate.d.ts +3 -0
  10. package/dist/cjs/Form/demo/async-validate.js +108 -0
  11. package/dist/cjs/Form/demo/virtual.js +1 -1
  12. package/dist/cjs/Form/form-store.d.ts +4 -4
  13. package/dist/cjs/Form/form-store.js +55 -25
  14. package/dist/cjs/Form/interface.d.ts +10 -7
  15. package/dist/cjs/Form/utils/validate.d.ts +23 -2
  16. package/dist/cjs/Form/utils/validate.js +59 -10
  17. package/dist/cjs/Select/demo/basic.js +1 -1
  18. package/dist/cjs/Select/demo/virtual.d.ts +3 -0
  19. package/dist/cjs/Select/demo/virtual.js +55 -0
  20. package/dist/cjs/Select/hooks/useSelectKeyboard.d.ts +7 -1
  21. package/dist/cjs/Select/hooks/useSelectKeyboard.js +16 -6
  22. package/dist/cjs/Select/interface.d.ts +44 -0
  23. package/dist/cjs/Select/option.js +3 -2
  24. package/dist/cjs/Select/options.js +115 -15
  25. package/dist/cjs/Select/select.js +20 -4
  26. package/dist/cjs/Select/style/index.less +17 -0
  27. package/dist/cjs/Table/style/index.less +19 -0
  28. package/dist/cjs/hooks/index.d.ts +2 -1
  29. package/dist/cjs/hooks/index.js +3 -1
  30. package/dist/cjs/hooks/useVirtualList.d.ts +71 -0
  31. package/dist/cjs/hooks/useVirtualList.js +117 -0
  32. package/dist/cjs/style/themes.css +1 -1
  33. package/dist/cjs/style/variables.css +1 -1
  34. package/dist/cjs/version.d.ts +1 -1
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/esm/Dialog/demo/basic.js +1 -1
  37. package/dist/esm/Dialog/dialog.js +1 -1
  38. package/dist/esm/Field/demo/validation.js +30 -9
  39. package/dist/esm/Field/field.js +2 -1
  40. package/dist/esm/Form/Field.js +3 -4
  41. package/dist/esm/Form/Form.js +1 -1
  42. package/dist/esm/Form/Group.js +142 -53
  43. package/dist/esm/Form/List.js +61 -42
  44. package/dist/esm/Form/demo/async-validate.d.ts +3 -0
  45. package/dist/esm/Form/demo/async-validate.js +95 -0
  46. package/dist/esm/Form/demo/virtual.js +1 -1
  47. package/dist/esm/Form/form-store.d.ts +4 -4
  48. package/dist/esm/Form/form-store.js +187 -93
  49. package/dist/esm/Form/interface.d.ts +10 -7
  50. package/dist/esm/Form/utils/validate.d.ts +23 -2
  51. package/dist/esm/Form/utils/validate.js +122 -17
  52. package/dist/esm/Select/demo/basic.js +1 -1
  53. package/dist/esm/Select/demo/virtual.d.ts +3 -0
  54. package/dist/esm/Select/demo/virtual.js +38 -0
  55. package/dist/esm/Select/hooks/useSelectKeyboard.d.ts +7 -1
  56. package/dist/esm/Select/hooks/useSelectKeyboard.js +25 -9
  57. package/dist/esm/Select/interface.d.ts +44 -0
  58. package/dist/esm/Select/option.js +4 -2
  59. package/dist/esm/Select/options.js +132 -13
  60. package/dist/esm/Select/select.js +19 -4
  61. package/dist/esm/Select/style/index.less +17 -0
  62. package/dist/esm/Table/style/index.less +19 -0
  63. package/dist/esm/hooks/index.d.ts +2 -1
  64. package/dist/esm/hooks/index.js +2 -1
  65. package/dist/esm/hooks/useVirtualList.d.ts +71 -0
  66. package/dist/esm/hooks/useVirtualList.js +129 -0
  67. package/dist/esm/style/themes.css +1 -1
  68. package/dist/esm/style/variables.css +1 -1
  69. package/dist/esm/version.d.ts +1 -1
  70. package/dist/esm/version.js +1 -1
  71. package/dist/umd/yee-c.min.css +1 -1
  72. package/dist/umd/yee-c.min.js +1 -1
  73. package/package.json +1 -1
  74. package/src/Dialog/demo/basic.tsx +1 -1
  75. package/src/Dialog/dialog.tsx +1 -1
  76. package/src/Field/demo/validation.tsx +2 -2
  77. package/src/Field/field.tsx +2 -1
  78. package/src/Field/index.en-US.md +1 -1
  79. package/src/Field/index.zh-CN.md +1 -1
  80. package/src/Form/Field.tsx +3 -4
  81. package/src/Form/Form.tsx +1 -1
  82. package/src/Form/Group.tsx +15 -12
  83. package/src/Form/List.tsx +3 -3
  84. package/src/Form/demo/async-validate.tsx +71 -0
  85. package/src/Form/demo/virtual.tsx +1 -1
  86. package/src/Form/form-store.ts +37 -21
  87. package/src/Form/index.en-US.md +6 -5
  88. package/src/Form/index.zh-CN.md +6 -5
  89. package/src/Form/interface.ts +13 -7
  90. package/src/Form/utils/validate.ts +65 -13
  91. package/src/Select/demo/basic.tsx +1 -1
  92. package/src/Select/demo/virtual.tsx +25 -0
  93. package/src/Select/hooks/useSelectKeyboard.ts +30 -6
  94. package/src/Select/index.en-US.md +4 -0
  95. package/src/Select/index.zh-CN.md +4 -0
  96. package/src/Select/interface.ts +48 -2
  97. package/src/Select/option.tsx +2 -1
  98. package/src/Select/options.tsx +139 -8
  99. package/src/Select/select.tsx +23 -1
  100. package/src/Select/style/index.less +17 -0
  101. package/src/Table/style/index.less +19 -0
  102. package/src/hooks/index.ts +2 -0
  103. package/src/hooks/useVirtualList.ts +169 -0
  104. package/src/version.ts +1 -1
@@ -0,0 +1,25 @@
1
+ import { Select } from '@rainbow-oh/yee-c';
2
+ import React, { useState } from 'react';
3
+
4
+ // Build a large option set (5000 items) to demonstrate virtual scrolling.
5
+ // Even with this many options, the DOM only holds the visible window.
6
+ const options = Array.from({ length: 5000 }, (_, i) => ({
7
+ label: `Option ${i + 1}`,
8
+ value: i,
9
+ }));
10
+
11
+ export default () => {
12
+ const [value, setValue] = useState<number>(100);
13
+
14
+ return (
15
+ <Select
16
+ virtual
17
+ searchable
18
+ value={value}
19
+ onChange={(val) => setValue(val as number)}
20
+ options={options}
21
+ style={{ width: 200 }}
22
+ placeholder="Please select"
23
+ />
24
+ );
25
+ };
@@ -13,6 +13,12 @@ interface UseSelectKeyboardParams {
13
13
  onClose?: () => void;
14
14
  onOpenChange?: (open: boolean) => void;
15
15
  containerRef?: React.RefObject<HTMLElement>;
16
+ /**
17
+ * Virtual-mode scroll hook. When provided, keyboard navigation drives the
18
+ * virtual viewport through this callback instead of calling scrollIntoView
19
+ * (the focused option may not be in the DOM while virtualized).
20
+ */
21
+ scrollToIndex?: (index: number) => void;
16
22
  }
17
23
 
18
24
  export default function useSelectKeyboard({
@@ -23,6 +29,7 @@ export default function useSelectKeyboard({
23
29
  onClose,
24
30
  onOpenChange,
25
31
  containerRef,
32
+ scrollToIndex,
26
33
  }: UseSelectKeyboardParams) {
27
34
  const [focusedKey, setFocusedKey] = useState<string | number>('');
28
35
  const latestFocusedKey = useLatest(focusedKey);
@@ -59,6 +66,13 @@ export default function useSelectKeyboard({
59
66
 
60
67
  // Scroll the focused option into view
61
68
  const scrollOptionIntoView = (index: number) => {
69
+ // Virtual mode: drive the virtual viewport directly, since the option
70
+ // element may not be mounted when it lies outside the render window.
71
+ if (scrollToIndex) {
72
+ scrollToIndex(index);
73
+ return;
74
+ }
75
+
62
76
  const container = containerRef?.current;
63
77
  if (!container) return;
64
78
 
@@ -163,20 +177,30 @@ export default function useSelectKeyboard({
163
177
  useEffect(() => {
164
178
  if (open) {
165
179
  // Focus the first selected option, or the first enabled option
180
+ let focusedValue: string | number = '';
166
181
  if (selectedKeys?.length > 0) {
167
182
  const firstSelected = options?.find(
168
183
  (opt) => opt.value === selectedKeys[0],
169
184
  );
170
185
  if (firstSelected && !firstSelected.disabled) {
171
- setFocusedKey(firstSelected.value);
172
- return;
186
+ focusedValue = firstSelected.value;
173
187
  }
174
188
  }
175
- // Find the first enabled option
176
- const firstEnabled = options?.find((opt) => !opt.disabled);
177
- if (firstEnabled) {
178
- setFocusedKey(firstEnabled.value);
189
+ if (!focusedValue) {
190
+ // Find the first enabled option
191
+ const firstEnabled = options?.find((opt) => !opt.disabled);
192
+ if (firstEnabled) {
193
+ focusedValue = firstEnabled.value;
194
+ }
195
+ }
196
+
197
+ if (focusedValue) {
198
+ setFocusedKey(focusedValue);
179
199
  }
200
+ // Scrolling the focused option into view in virtual mode is handled by
201
+ // <Options> itself (it owns the viewport + virtual list), which avoids
202
+ // the first-open timing issue where this effect runs before the virtual
203
+ // API ref is wired up.
180
204
  } else {
181
205
  // Clear focused key when closing
182
206
  setFocusedKey('');
@@ -21,6 +21,7 @@ Select component for choosing from a set of options.
21
21
  <code src="./demo/multiple.tsx" title="Multiple" description="Multiple selection mode"></code>
22
22
  <code src="./demo/search.tsx" title="Search" description="Searchable Select"></code>
23
23
  <code src="./demo/tags.tsx" title="Tags" description="Tags mode Select"></code>
24
+ <code src="./demo/virtual.tsx" title="Virtual Scroll" description="Virtual scrolling for large datasets (thousands of options)"></code>
24
25
 
25
26
  ## API
26
27
 
@@ -46,6 +47,9 @@ Select component for choosing from a set of options.
46
47
  | optionLabelProp | `string \| (() => string)` | Property to display in the input box | - |
47
48
  | onChange | `(value: string \| number \| Array<string \| number>, options?: Option \| undefined \| Option[]) => void` | Value change callback | - |
48
49
  | onFilter | `(value: string, options: Array<Option>) => Array<Option>` | Search callback | - |
50
+ | virtual | `boolean` | Enable virtual scrolling. Only the options in the visible window are rendered, so the DOM node count stays constant regardless of the total. Recommended for hundreds or thousands of options | `false` |
51
+ | itemHeight | `number` | Fixed height of each option in pixels. Must be a fixed value when `virtual` is enabled | `32` |
52
+ | listHeight | `number` | Max height of the dropdown popup in pixels | `200` |
49
53
  | data-* | `string` | - | Supports all data-* attributes, forwarded to root DOM element |
50
54
 
51
55
  ### Option
@@ -21,6 +21,7 @@ toc: 'content'
21
21
  <code src="./demo/multiple.tsx" title="多选" description="多选模式"></code>
22
22
  <code src="./demo/search.tsx" title="可搜索" description="可搜索的Select"></code>
23
23
  <code src="./demo/tags.tsx" title="标签模式" description="标签模式的Select"></code>
24
+ <code src="./demo/virtual.tsx" title="虚拟滚动" description="大数据量(几千条)下的虚拟滚动"></code>
24
25
 
25
26
  ## API
26
27
 
@@ -46,6 +47,9 @@ toc: 'content'
46
47
  | optionLabelProp | `string \| (() => string)` | 回填到选择框里的属性 | - |
47
48
  | onChange | `(value: string \| number \| Array<string \| number>, options?: Option \| undefined \| Option[]) => void` | 值改变时的回调函数 | - |
48
49
  | onFilter | `(value: string, options: Array<Option>) => Array<Option>` | 搜索时的回调函数 | - |
50
+ | virtual | `boolean` | 是否开启虚拟滚动。开启后仅渲染可视区内的选项,DOM 节点数与数据总量无关,适合成百上千条数据 | `false` |
51
+ | itemHeight | `number` | 每个选项的固定高度(px),开启 `virtual` 时必须为定值 | `32` |
52
+ | listHeight | `number` | 下拉框最大高度(px) | `200` |
49
53
  | data-* | `string` | - | 支持所有 data-* 属性,透传到组件根 DOM 元素 |
50
54
 
51
55
  ### Option
@@ -104,6 +104,26 @@ export interface SelectProps extends DataAttributeProps {
104
104
  * Callback when searching
105
105
  */
106
106
  onFilter?: (value: string, options: Array<Option>) => Array<Option>;
107
+ /**
108
+ * Enable virtual scrolling for the dropdown.
109
+ *
110
+ * When enabled, only the options inside the visible window are rendered
111
+ * (the DOM node count stays constant regardless of how many options there
112
+ * are). Recommended for large option sets (hundreds or thousands).
113
+ * @default false
114
+ */
115
+ virtual?: boolean;
116
+ /**
117
+ * Height of each option in pixels. Required to be a fixed value when
118
+ * `virtual` is enabled (the list assumes every option has exactly this height).
119
+ * @default 32
120
+ */
121
+ itemHeight?: number;
122
+ /**
123
+ * Max height of the dropdown popup in pixels.
124
+ * @default 200
125
+ */
126
+ listHeight?: number;
107
127
  }
108
128
 
109
129
  /**
@@ -130,6 +150,11 @@ export interface OptionProps {
130
150
  * Test id forwarded to the option element
131
151
  */
132
152
  dataTestId?: string;
153
+ /**
154
+ * Inline style forwarded to the option element.
155
+ * Used by virtual scrolling to position each option absolutely.
156
+ */
157
+ style?: React.CSSProperties;
133
158
  }
134
159
 
135
160
  /**
@@ -149,8 +174,10 @@ export interface OptionsContextValue {
149
174
  /**
150
175
  * Props for the internal Options (popup list) component.
151
176
  */
152
- export interface OptionsProps
153
- extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
177
+ export interface OptionsProps extends Omit<
178
+ React.HTMLAttributes<HTMLDivElement>,
179
+ 'onSelect'
180
+ > {
154
181
  /**
155
182
  * Custom class name prefix
156
183
  */
@@ -184,6 +211,25 @@ export interface OptionsProps
184
211
  * Whether multiple selection mode is active
185
212
  */
186
213
  multiple: boolean;
214
+ /**
215
+ * Whether virtual scrolling is enabled
216
+ */
217
+ virtual?: boolean;
218
+ /**
219
+ * Fixed option height in pixels (only meaningful when `virtual` is enabled)
220
+ */
221
+ itemHeight?: number;
222
+ /**
223
+ * Max popup height in pixels (only meaningful when `virtual` is enabled)
224
+ */
225
+ listHeight?: number;
226
+ /**
227
+ * Ref that receives the virtual list API (currently `scrollToIndex`),
228
+ * so the parent can wire keyboard navigation to the virtual viewport.
229
+ */
230
+ virtualApiRef?: React.MutableRefObject<{
231
+ scrollToIndex: (index: number) => void;
232
+ } | null>;
187
233
  /**
188
234
  * Callback when an option is selected
189
235
  */
@@ -6,7 +6,7 @@ import type { OptionProps } from './interface';
6
6
  import { OptionsCtx } from './options';
7
7
 
8
8
  const Option = (props: OptionProps) => {
9
- const { label, value, disabled, title, dataTestId } = props;
9
+ const { label, value, disabled, title, dataTestId, style } = props;
10
10
  const { prefixCls, selectedKeys, focusedKey, multiple, onSelect } =
11
11
  useContext(OptionsCtx);
12
12
 
@@ -42,6 +42,7 @@ const Option = (props: OptionProps) => {
42
42
  tabIndex={disabled ? undefined : 0}
43
43
  title={htmlTitle}
44
44
  data-testid={dataTestId}
45
+ style={style}
45
46
  >
46
47
  <div className={`${prefixCls}-option-content`}>{label}</div>
47
48
  {multiple && selected && (
@@ -1,5 +1,12 @@
1
1
  import clsx from 'clsx';
2
- import React, { createContext, forwardRef, useRef } from 'react';
2
+ import React, {
3
+ createContext,
4
+ forwardRef,
5
+ useEffect,
6
+ useImperativeHandle,
7
+ useRef,
8
+ } from 'react';
9
+ import useVirtualList from '../hooks/useVirtualList';
3
10
  import { useLocale } from '../locale';
4
11
  import type { OptionsContextValue, OptionsProps } from './interface';
5
12
  import Option from './option';
@@ -22,27 +29,151 @@ const Options = forwardRef<HTMLDivElement, OptionsProps>(
22
29
  focusedKey,
23
30
  dataTestId,
24
31
  multiple,
32
+ virtual,
33
+ itemHeight = 32,
34
+ listHeight = 200,
35
+ virtualApiRef,
25
36
  onSelect,
26
37
  ...rest
27
38
  } = props;
28
39
 
29
- // eslint-disable-next-line react-hooks/rules-of-hooks
30
- const componentRef = ref || useRef<HTMLDivElement>(null);
40
+ 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
+ useImperativeHandle(ref, () => rootRef.current as HTMLDivElement, []);
31
45
 
46
+ // Hook is always called (rules of hooks). When `virtual` is off the
47
+ // returned viewportRef is never attached, so the observer stays idle.
48
+ const {
49
+ start,
50
+ end,
51
+ offsetY,
52
+ totalHeight,
53
+ viewportRef,
54
+ onScroll,
55
+ scrollToIndex,
56
+ } = useVirtualList({
57
+ itemCount: options.length,
58
+ itemHeight,
59
+ });
60
+
61
+ // Expose the virtual scroll API to the parent so keyboard navigation
62
+ // can drive the virtual viewport (replaces scrollIntoView).
63
+ useEffect(() => {
64
+ if (!virtual || !virtualApiRef) return;
65
+ virtualApiRef.current = { scrollToIndex };
66
+ return () => {
67
+ if (virtualApiRef.current) {
68
+ virtualApiRef.current = null;
69
+ }
70
+ };
71
+ }, [virtual, virtualApiRef, scrollToIndex]);
72
+
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.
76
+ useEffect(() => {
77
+ if (virtual) {
78
+ scrollToIndex(0);
79
+ }
80
+ }, [options, virtual, scrollToIndex]);
81
+
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.
88
+ useEffect(() => {
89
+ if (options.length === 0) return;
90
+ if (focusedKey === '') return;
91
+ const focusedIndex = options.findIndex((o) => o.value === focusedKey);
92
+ if (focusedIndex < 0) return;
93
+
94
+ if (virtual) {
95
+ scrollToIndex(focusedIndex);
96
+ return;
97
+ }
98
+
99
+ const container = rootRef.current;
100
+ if (!container) return;
101
+ const optionElements = container.querySelectorAll('[role="option"]');
102
+ const targetElement = optionElements[focusedIndex] as
103
+ | HTMLElement
104
+ | undefined;
105
+ if (targetElement) {
106
+ targetElement.scrollIntoView({ block: 'nearest' });
107
+ }
108
+ }, [focusedKey, virtual, options, scrollToIndex, rootRef]);
109
+
110
+ const isEmpty = options.length === 0;
111
+ const ctxValue = {
112
+ prefixCls,
113
+ selectedKeys,
114
+ focusedKey,
115
+ multiple,
116
+ onSelect,
117
+ };
118
+
119
+ // ----- Virtual mode: only render the visible window -----
120
+ if (virtual && !isEmpty) {
121
+ const visibleOptions = options.slice(start, end);
122
+ return (
123
+ <div
124
+ {...rest}
125
+ className={clsx(
126
+ `${prefixCls}-popup`,
127
+ `${prefixCls}-popup-virtual`,
128
+ popupClassName,
129
+ )}
130
+ style={popupStyle}
131
+ ref={rootRef}
132
+ tabIndex={-1}
133
+ >
134
+ <OptionsCtx.Provider value={ctxValue}>
135
+ <div
136
+ className={`${prefixCls}-popup-viewport`}
137
+ ref={viewportRef}
138
+ onScroll={onScroll}
139
+ style={{ maxHeight: listHeight, overflowY: 'auto' }}
140
+ >
141
+ <div style={{ height: totalHeight, position: 'relative' }}>
142
+ {visibleOptions.map((option, i) => (
143
+ <Option
144
+ {...option}
145
+ key={option.value}
146
+ dataTestId={`${dataTestId}-${start + i}`}
147
+ style={{
148
+ position: 'absolute',
149
+ top: 0,
150
+ left: 0,
151
+ right: 0,
152
+ height: itemHeight,
153
+ transform: `translateY(${offsetY + i * itemHeight}px)`,
154
+ }}
155
+ />
156
+ ))}
157
+ </div>
158
+ </div>
159
+ </OptionsCtx.Provider>
160
+ </div>
161
+ );
162
+ }
163
+
164
+ // ----- Default mode: render every option (unchanged behavior) -----
32
165
  return (
33
166
  <div
34
167
  {...rest}
35
168
  className={clsx(`${prefixCls}-popup`, popupClassName)}
36
169
  style={popupStyle}
37
- ref={componentRef}
170
+ ref={rootRef}
38
171
  tabIndex={-1}
39
172
  >
40
- {options.length === 0 ? (
173
+ {isEmpty ? (
41
174
  <div className={`${prefixCls}-empty`}>{select.noData}</div>
42
175
  ) : (
43
- <OptionsCtx.Provider
44
- value={{ prefixCls, selectedKeys, focusedKey, multiple, onSelect }}
45
- >
176
+ <OptionsCtx.Provider value={ctxValue}>
46
177
  {options.map((option, index: number) => (
47
178
  <Option
48
179
  {...option}
@@ -1,4 +1,10 @@
1
- import React, { useContext, useMemo, useRef, useState } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useContext,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
2
8
  import { GlobalContext } from '../Config-Provider';
3
9
  import Selector from '../Selector';
4
10
  import Trigger from '../Trigger';
@@ -23,6 +29,9 @@ const Select = (baseprops: SelectProps) => {
23
29
  disabled,
24
30
  onChange,
25
31
  onFilter,
32
+ virtual,
33
+ itemHeight,
34
+ listHeight,
26
35
  ...rest
27
36
  } = props;
28
37
 
@@ -50,6 +59,14 @@ const Select = (baseprops: SelectProps) => {
50
59
 
51
60
  const [open, setOpen] = useState(false);
52
61
  const popupRef = useRef<HTMLDivElement>(null);
62
+ // Holds the virtual list API exposed by <Options>, so keyboard navigation
63
+ // can drive the virtual viewport (only populated when `virtual` is on).
64
+ const virtualApiRef = useRef<{
65
+ scrollToIndex: (index: number) => void;
66
+ } | null>(null);
67
+ const handleVirtualScrollToIndex = useCallback((index: number) => {
68
+ virtualApiRef.current?.scrollToIndex(index);
69
+ }, []);
53
70
 
54
71
  const getOptions = (keys: string | number | Array<string | number>) => {
55
72
  if (keys === '' || keys === null) {
@@ -135,6 +152,7 @@ const Select = (baseprops: SelectProps) => {
135
152
  onClose: () => setOpen(false),
136
153
  onOpenChange: handleOpenChange,
137
154
  containerRef: popupRef as React.RefObject<HTMLDivElement>,
155
+ scrollToIndex: virtual ? handleVirtualScrollToIndex : undefined,
138
156
  });
139
157
 
140
158
  const popup = (
@@ -147,6 +165,10 @@ const Select = (baseprops: SelectProps) => {
147
165
  dataTestId={rest['data-testid']}
148
166
  onSelect={handleSelect}
149
167
  ref={popupRef}
168
+ virtual={virtual}
169
+ itemHeight={itemHeight}
170
+ listHeight={listHeight}
171
+ virtualApiRef={virtualApiRef}
150
172
  />
151
173
  );
152
174
 
@@ -8,6 +8,17 @@
8
8
  overflow: auto;
9
9
  }
10
10
 
11
+ // Virtual mode: the popup no longer scrolls itself; the inner viewport does.
12
+ // Without this override there would be two scrollbars.
13
+ &-popup-virtual {
14
+ max-height: none;
15
+ overflow: visible;
16
+ }
17
+
18
+ &-popup-viewport {
19
+ overflow-y: auto;
20
+ }
21
+
11
22
  &-option {
12
23
  display: flex;
13
24
  padding: var(--yee-select-option-padding);
@@ -37,6 +48,12 @@
37
48
  }
38
49
  }
39
50
 
51
+ // In virtual mode each option has a fixed inline height; center its content
52
+ // vertically so it stays aligned regardless of the inherited line-height.
53
+ &-popup-virtual &-option {
54
+ align-items: center;
55
+ }
56
+
40
57
  &-empty {
41
58
  padding: var(--yee-select-option-padding);
42
59
  }
@@ -23,13 +23,32 @@
23
23
  var(--yee-table-tbody-td-borderbottom-color);
24
24
  }
25
25
  }
26
+
27
+ @{pls}-cell {
28
+ &::before {
29
+ display: none;
30
+ }
31
+ }
26
32
  }
27
33
 
28
34
  &-thead {
29
35
  color: var(--yee-table-thead-color);
30
36
 
31
37
  @{pls}-cell {
38
+ position: relative;
32
39
  background-color: var(--yee-table-thead-bg-color);
40
+
41
+ &:not(:last-child):not(@{pls}-expand-col) {
42
+ &::before {
43
+ position: absolute;
44
+ content: '';
45
+ width: 1.5px;
46
+ height: 30%;
47
+ top: 35%;
48
+ right: 0;
49
+ background-color: var(--yee-table-head-cell-split-line-color, #eee);
50
+ }
51
+ }
33
52
  }
34
53
 
35
54
  &-cell {
@@ -16,6 +16,7 @@ import useMount from './useMount';
16
16
  import usePrevious from './usePrevious';
17
17
  import useUpdateEffect from './useUpdateEffect';
18
18
  import useVirtualForm from './useVirtualForm';
19
+ import useVirtualList from './useVirtualList';
19
20
 
20
21
  export {
21
22
  useBreakpoint,
@@ -36,4 +37,5 @@ export {
36
37
  usePrevious,
37
38
  useUpdateEffect,
38
39
  useVirtualForm,
40
+ useVirtualList,
39
41
  };