@rainbow-oh/yee-c 0.4.5 → 0.5.1
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.
- package/dist/cjs/Descriptions/descriptions.js +8 -1
- package/dist/cjs/Descriptions/interface.d.ts +1 -1
- package/dist/cjs/Dialog/demo/basic.js +1 -1
- package/dist/cjs/Dialog/dialog.js +1 -1
- package/dist/cjs/Field/demo/validation.js +23 -3
- package/dist/cjs/Field/field.js +1 -1
- package/dist/cjs/Form/Field.js +1 -4
- package/dist/cjs/Form/Form.js +1 -1
- package/dist/cjs/Form/Group.js +36 -14
- package/dist/cjs/Form/List.js +23 -3
- package/dist/cjs/Form/demo/async-validate.d.ts +3 -0
- package/dist/cjs/Form/demo/async-validate.js +108 -0
- package/dist/cjs/Form/demo/virtual.js +1 -1
- package/dist/cjs/Form/form-store.d.ts +4 -4
- package/dist/cjs/Form/form-store.js +55 -25
- package/dist/cjs/Form/interface.d.ts +10 -7
- package/dist/cjs/Form/utils/validate.d.ts +23 -2
- package/dist/cjs/Form/utils/validate.js +59 -10
- package/dist/cjs/Select/demo/basic.js +1 -1
- package/dist/cjs/Select/demo/size.js +2 -0
- package/dist/cjs/Select/demo/virtual.d.ts +3 -0
- package/dist/cjs/Select/demo/virtual.js +55 -0
- package/dist/cjs/Select/hooks/useSelectKeyboard.d.ts +7 -1
- package/dist/cjs/Select/hooks/useSelectKeyboard.js +16 -6
- package/dist/cjs/Select/interface.d.ts +46 -1
- package/dist/cjs/Select/option.js +3 -2
- package/dist/cjs/Select/options.js +115 -15
- package/dist/cjs/Select/select.js +20 -4
- package/dist/cjs/Select/style/index.less +17 -0
- package/dist/cjs/Selector/style/index.less +8 -0
- package/dist/cjs/Table/style/index.less +19 -0
- package/dist/cjs/hooks/index.d.ts +2 -1
- package/dist/cjs/hooks/index.js +3 -1
- package/dist/cjs/hooks/useVirtualList.d.ts +71 -0
- package/dist/cjs/hooks/useVirtualList.js +117 -0
- package/dist/cjs/style/size.css +4 -0
- package/dist/cjs/style/themes.css +1 -1
- package/dist/cjs/style/variables.css +5 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Descriptions/descriptions.js +2 -1
- package/dist/esm/Descriptions/interface.d.ts +1 -1
- package/dist/esm/Dialog/demo/basic.js +1 -1
- package/dist/esm/Dialog/dialog.js +1 -1
- package/dist/esm/Field/demo/validation.js +30 -9
- package/dist/esm/Field/field.js +2 -1
- package/dist/esm/Form/Field.js +3 -4
- package/dist/esm/Form/Form.js +1 -1
- package/dist/esm/Form/Group.js +142 -53
- package/dist/esm/Form/List.js +61 -42
- package/dist/esm/Form/demo/async-validate.d.ts +3 -0
- package/dist/esm/Form/demo/async-validate.js +95 -0
- package/dist/esm/Form/demo/virtual.js +1 -1
- package/dist/esm/Form/form-store.d.ts +4 -4
- package/dist/esm/Form/form-store.js +187 -93
- package/dist/esm/Form/interface.d.ts +10 -7
- package/dist/esm/Form/utils/validate.d.ts +23 -2
- package/dist/esm/Form/utils/validate.js +122 -17
- package/dist/esm/Select/demo/basic.js +1 -1
- package/dist/esm/Select/demo/size.js +2 -0
- package/dist/esm/Select/demo/virtual.d.ts +3 -0
- package/dist/esm/Select/demo/virtual.js +38 -0
- package/dist/esm/Select/hooks/useSelectKeyboard.d.ts +7 -1
- package/dist/esm/Select/hooks/useSelectKeyboard.js +25 -9
- package/dist/esm/Select/interface.d.ts +46 -1
- package/dist/esm/Select/option.js +4 -2
- package/dist/esm/Select/options.js +132 -13
- package/dist/esm/Select/select.js +19 -4
- package/dist/esm/Select/style/index.less +17 -0
- package/dist/esm/Selector/style/index.less +8 -0
- package/dist/esm/Table/style/index.less +19 -0
- package/dist/esm/hooks/index.d.ts +2 -1
- package/dist/esm/hooks/index.js +2 -1
- package/dist/esm/hooks/useVirtualList.d.ts +71 -0
- package/dist/esm/hooks/useVirtualList.js +129 -0
- package/dist/esm/style/size.css +4 -0
- package/dist/esm/style/themes.css +1 -1
- package/dist/esm/style/variables.css +5 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +2 -2
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Descriptions/descriptions.tsx +4 -1
- package/src/Descriptions/interface.ts +7 -1
- package/src/Dialog/demo/basic.tsx +1 -1
- package/src/Dialog/dialog.tsx +1 -1
- package/src/Field/demo/validation.tsx +2 -2
- package/src/Field/field.tsx +2 -1
- package/src/Field/index.en-US.md +1 -1
- package/src/Field/index.zh-CN.md +1 -1
- package/src/Form/Field.tsx +3 -4
- package/src/Form/Form.tsx +1 -1
- package/src/Form/Group.tsx +15 -12
- package/src/Form/List.tsx +3 -3
- package/src/Form/demo/async-validate.tsx +71 -0
- package/src/Form/demo/virtual.tsx +1 -1
- package/src/Form/form-store.ts +37 -21
- package/src/Form/index.en-US.md +6 -5
- package/src/Form/index.zh-CN.md +6 -5
- package/src/Form/interface.ts +13 -7
- package/src/Form/utils/validate.ts +65 -13
- package/src/Select/demo/basic.tsx +1 -1
- package/src/Select/demo/size.tsx +2 -0
- package/src/Select/demo/virtual.tsx +25 -0
- package/src/Select/hooks/useSelectKeyboard.ts +30 -6
- package/src/Select/index.en-US.md +4 -0
- package/src/Select/index.zh-CN.md +4 -0
- package/src/Select/interface.ts +51 -3
- package/src/Select/option.tsx +2 -1
- package/src/Select/options.tsx +139 -8
- package/src/Select/select.tsx +23 -1
- package/src/Select/style/index.less +17 -0
- package/src/Selector/style/index.less +8 -0
- package/src/Table/style/index.less +19 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useVirtualList.ts +169 -0
- package/src/style/size.css +4 -0
- package/src/version.ts +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { Rule } from '../interface';
|
|
1
|
+
import type { Rule } from '../interface';
|
|
3
2
|
|
|
4
3
|
function isEmpty(v: unknown) {
|
|
5
4
|
return v === undefined || v === null || v === '' ? true : false;
|
|
@@ -60,11 +59,15 @@ function verifyRuleOfRegexp(regexp: RegExp, value: unknown) {
|
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
export interface ValidateResult {
|
|
63
|
+
passed: boolean;
|
|
64
|
+
// Custom message from a thrown/rejected validator; falls back to rule.message upstream
|
|
65
|
+
message?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check the synchronous built-in rules (min/max/minLength/maxLength/regexp).
|
|
69
|
+
function verifySyncRules(rule: Rule, value: unknown): boolean {
|
|
70
|
+
const { min, max, minLength, maxLength, regexp } = rule;
|
|
68
71
|
|
|
69
72
|
if (typeof min !== 'undefined' && !verifyRuleOfMin(min, value)) {
|
|
70
73
|
return false;
|
|
@@ -87,18 +90,67 @@ function verifyMain(rule: Rule, value: any): boolean {
|
|
|
87
90
|
if (regexp instanceof RegExp && !verifyRuleOfRegexp(regexp, value)) {
|
|
88
91
|
return false;
|
|
89
92
|
}
|
|
90
|
-
if (typeof validator === 'function' && !validator(value)) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
93
|
return true;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Run a custom validator, dispatching between sync and async at runtime.
|
|
98
|
+
*
|
|
99
|
+
* Supports all of the following shapes (backwards compatible):
|
|
100
|
+
* - sync `return true` / `return undefined` → pass
|
|
101
|
+
* - sync `return false` → fail (use rule.message)
|
|
102
|
+
* - sync `throw new Error(msg)` → fail (use msg)
|
|
103
|
+
* - async `return Promise.resolve()` → pass
|
|
104
|
+
* - async `return Promise.resolve(false)` → fail (use rule.message)
|
|
105
|
+
* - async `throw` / `return Promise.reject(new Error(msg))` → fail (use msg)
|
|
106
|
+
*/
|
|
107
|
+
export async function runValidator(
|
|
108
|
+
validator: NonNullable<Rule['validator']>,
|
|
109
|
+
value: unknown,
|
|
110
|
+
): Promise<ValidateResult> {
|
|
111
|
+
try {
|
|
112
|
+
const res = validator(value);
|
|
113
|
+
if (res instanceof Promise) {
|
|
114
|
+
const resolved = await res;
|
|
115
|
+
if (resolved === false) {
|
|
116
|
+
return { passed: false };
|
|
117
|
+
}
|
|
118
|
+
return { passed: true };
|
|
119
|
+
}
|
|
120
|
+
if (res === false) {
|
|
121
|
+
return { passed: false };
|
|
122
|
+
}
|
|
123
|
+
return { passed: true };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return {
|
|
126
|
+
passed: false,
|
|
127
|
+
message: e instanceof Error ? e.message : String(e),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate a single rule against a value.
|
|
134
|
+
* Returns { passed, message? }. Sync rules short-circuit; the custom validator
|
|
135
|
+
* is awaited so async validators are supported.
|
|
136
|
+
*/
|
|
137
|
+
async function validate(
|
|
138
|
+
rule: Rule,
|
|
139
|
+
value: unknown,
|
|
140
|
+
): Promise<ValidateResult> {
|
|
97
141
|
if (rule.required && isEmpty(value)) {
|
|
98
|
-
return false;
|
|
142
|
+
return { passed: false };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!verifySyncRules(rule, value)) {
|
|
146
|
+
return { passed: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof rule.validator === 'function') {
|
|
150
|
+
return runValidator(rule.validator, value);
|
|
99
151
|
}
|
|
100
152
|
|
|
101
|
-
return
|
|
153
|
+
return { passed: true };
|
|
102
154
|
}
|
|
103
155
|
|
|
104
156
|
export default validate;
|
package/src/Select/demo/size.tsx
CHANGED
|
@@ -16,6 +16,7 @@ export default () => {
|
|
|
16
16
|
value={value}
|
|
17
17
|
onChange={(val) => setValue(val as string)}
|
|
18
18
|
options={options}
|
|
19
|
+
size="small"
|
|
19
20
|
style={{ width: 200 }}
|
|
20
21
|
placeholder="Small Size"
|
|
21
22
|
/>
|
|
@@ -32,6 +33,7 @@ export default () => {
|
|
|
32
33
|
value={value}
|
|
33
34
|
onChange={(val) => setValue(val as string)}
|
|
34
35
|
options={options}
|
|
36
|
+
size="large"
|
|
35
37
|
style={{ width: 200 }}
|
|
36
38
|
placeholder="Large Size"
|
|
37
39
|
/>
|
|
@@ -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
|
-
|
|
172
|
-
return;
|
|
186
|
+
focusedValue = firstSelected.value;
|
|
173
187
|
}
|
|
174
188
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
package/src/Select/interface.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SelectorProps } from '../Selector';
|
|
1
2
|
import type { DataAttributeProps } from '../utils/types';
|
|
2
3
|
|
|
3
4
|
export type Option = {
|
|
@@ -23,7 +24,8 @@ export type Option = {
|
|
|
23
24
|
[prop: string]: any;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
export interface SelectProps
|
|
27
|
+
export interface SelectProps
|
|
28
|
+
extends DataAttributeProps, Pick<SelectorProps, 'size'> {
|
|
27
29
|
/**
|
|
28
30
|
* Custom class name prefix
|
|
29
31
|
* */
|
|
@@ -104,6 +106,26 @@ export interface SelectProps extends DataAttributeProps {
|
|
|
104
106
|
* Callback when searching
|
|
105
107
|
*/
|
|
106
108
|
onFilter?: (value: string, options: Array<Option>) => Array<Option>;
|
|
109
|
+
/**
|
|
110
|
+
* Enable virtual scrolling for the dropdown.
|
|
111
|
+
*
|
|
112
|
+
* When enabled, only the options inside the visible window are rendered
|
|
113
|
+
* (the DOM node count stays constant regardless of how many options there
|
|
114
|
+
* are). Recommended for large option sets (hundreds or thousands).
|
|
115
|
+
* @default false
|
|
116
|
+
*/
|
|
117
|
+
virtual?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Height of each option in pixels. Required to be a fixed value when
|
|
120
|
+
* `virtual` is enabled (the list assumes every option has exactly this height).
|
|
121
|
+
* @default 32
|
|
122
|
+
*/
|
|
123
|
+
itemHeight?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Max height of the dropdown popup in pixels.
|
|
126
|
+
* @default 200
|
|
127
|
+
*/
|
|
128
|
+
listHeight?: number;
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
/**
|
|
@@ -130,6 +152,11 @@ export interface OptionProps {
|
|
|
130
152
|
* Test id forwarded to the option element
|
|
131
153
|
*/
|
|
132
154
|
dataTestId?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Inline style forwarded to the option element.
|
|
157
|
+
* Used by virtual scrolling to position each option absolutely.
|
|
158
|
+
*/
|
|
159
|
+
style?: React.CSSProperties;
|
|
133
160
|
}
|
|
134
161
|
|
|
135
162
|
/**
|
|
@@ -149,8 +176,10 @@ export interface OptionsContextValue {
|
|
|
149
176
|
/**
|
|
150
177
|
* Props for the internal Options (popup list) component.
|
|
151
178
|
*/
|
|
152
|
-
export interface OptionsProps
|
|
153
|
-
|
|
179
|
+
export interface OptionsProps extends Omit<
|
|
180
|
+
React.HTMLAttributes<HTMLDivElement>,
|
|
181
|
+
'onSelect'
|
|
182
|
+
> {
|
|
154
183
|
/**
|
|
155
184
|
* Custom class name prefix
|
|
156
185
|
*/
|
|
@@ -184,6 +213,25 @@ export interface OptionsProps
|
|
|
184
213
|
* Whether multiple selection mode is active
|
|
185
214
|
*/
|
|
186
215
|
multiple: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Whether virtual scrolling is enabled
|
|
218
|
+
*/
|
|
219
|
+
virtual?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Fixed option height in pixels (only meaningful when `virtual` is enabled)
|
|
222
|
+
*/
|
|
223
|
+
itemHeight?: number;
|
|
224
|
+
/**
|
|
225
|
+
* Max popup height in pixels (only meaningful when `virtual` is enabled)
|
|
226
|
+
*/
|
|
227
|
+
listHeight?: number;
|
|
228
|
+
/**
|
|
229
|
+
* Ref that receives the virtual list API (currently `scrollToIndex`),
|
|
230
|
+
* so the parent can wire keyboard navigation to the virtual viewport.
|
|
231
|
+
*/
|
|
232
|
+
virtualApiRef?: React.MutableRefObject<{
|
|
233
|
+
scrollToIndex: (index: number) => void;
|
|
234
|
+
} | null>;
|
|
187
235
|
/**
|
|
188
236
|
* Callback when an option is selected
|
|
189
237
|
*/
|
package/src/Select/option.tsx
CHANGED
|
@@ -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 && (
|
package/src/Select/options.tsx
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import 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
|
-
|
|
30
|
-
|
|
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={
|
|
170
|
+
ref={rootRef}
|
|
38
171
|
tabIndex={-1}
|
|
39
172
|
>
|
|
40
|
-
{
|
|
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}
|
package/src/Select/select.tsx
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import 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 {
|