@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.
- 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/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 +44 -0
- 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/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/themes.css +1 -1
- package/dist/cjs/style/variables.css +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +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/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 +44 -0
- 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/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/themes.css +1 -1
- package/dist/esm/style/variables.css +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +1 -1
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -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/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 +48 -2
- 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/Table/style/index.less +19 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useVirtualList.ts +169 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import useEvent from './useEvent';
|
|
3
|
+
import useResizeObserver from './useResizeObserver';
|
|
4
|
+
|
|
5
|
+
export interface UseVirtualListOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Total number of items in the list.
|
|
8
|
+
*/
|
|
9
|
+
itemCount: number;
|
|
10
|
+
/**
|
|
11
|
+
* Fixed height of every item in pixels.
|
|
12
|
+
*/
|
|
13
|
+
itemHeight: number;
|
|
14
|
+
/**
|
|
15
|
+
* Extra items rendered above and below the visible window
|
|
16
|
+
* to avoid white flashes while scrolling.
|
|
17
|
+
* @default 4
|
|
18
|
+
*/
|
|
19
|
+
overscan?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseVirtualListResult {
|
|
23
|
+
/**
|
|
24
|
+
* Inclusive start index of the render window (already includes overscan).
|
|
25
|
+
*/
|
|
26
|
+
start: number;
|
|
27
|
+
/**
|
|
28
|
+
* Exclusive end index of the render window (already includes overscan).
|
|
29
|
+
*/
|
|
30
|
+
end: number;
|
|
31
|
+
/**
|
|
32
|
+
* Vertical offset to apply to the rendered block, equal to `start * itemHeight`.
|
|
33
|
+
* Use it as `transform: translateY(offsetY)` or `top: offsetY`.
|
|
34
|
+
*/
|
|
35
|
+
offsetY: number;
|
|
36
|
+
/**
|
|
37
|
+
* Total scrollable height, equal to `itemCount * itemHeight`.
|
|
38
|
+
* Apply it to the inner spacer element so the scrollbar matches the full list.
|
|
39
|
+
*/
|
|
40
|
+
totalHeight: number;
|
|
41
|
+
/**
|
|
42
|
+
* Ref to bind to the scroll container element.
|
|
43
|
+
*/
|
|
44
|
+
viewportRef: React.RefObject<HTMLDivElement | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Stable scroll handler (rAF-throttled). Bind to the viewport's `onScroll`.
|
|
47
|
+
*/
|
|
48
|
+
onScroll: () => void;
|
|
49
|
+
/**
|
|
50
|
+
* Scroll the viewport so that the item at `index` is visible.
|
|
51
|
+
* Used by keyboard navigation to replace `scrollIntoView`.
|
|
52
|
+
*/
|
|
53
|
+
scrollToIndex: (index: number) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Fixed-height virtual scrolling hook.
|
|
58
|
+
*
|
|
59
|
+
* Renders only the items inside the visible window (plus an `overscan` buffer)
|
|
60
|
+
* and keeps the DOM node count constant regardless of `itemCount`.
|
|
61
|
+
*
|
|
62
|
+
* Usage:
|
|
63
|
+
* const { start, end, offsetY, totalHeight, viewportRef, onScroll, scrollToIndex } =
|
|
64
|
+
* useVirtualList({ itemCount, itemHeight });
|
|
65
|
+
*
|
|
66
|
+
* <div ref={viewportRef} onScroll={onScroll} style={{ maxHeight: 200, overflowY: 'auto' }}>
|
|
67
|
+
* <div style={{ height: totalHeight, position: 'relative' }}>
|
|
68
|
+
* {items.slice(start, end).map((it, i) => (
|
|
69
|
+
* <div key={it.key} style={{ position: 'absolute', height: itemHeight, transform: `translateY(${offsetY + i * itemHeight}px)` }}>
|
|
70
|
+
* {it.content}
|
|
71
|
+
* </div>
|
|
72
|
+
* ))}
|
|
73
|
+
* </div>
|
|
74
|
+
* </div>
|
|
75
|
+
*/
|
|
76
|
+
export default function useVirtualList({
|
|
77
|
+
itemCount,
|
|
78
|
+
itemHeight,
|
|
79
|
+
overscan = 4,
|
|
80
|
+
}: UseVirtualListOptions): UseVirtualListResult {
|
|
81
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
82
|
+
const [visibleHeight, setVisibleHeight] = useState(0);
|
|
83
|
+
const rafRef = useRef<number | null>(null);
|
|
84
|
+
|
|
85
|
+
// useResizeObserver owns its ref; we reuse it as the viewport ref so the
|
|
86
|
+
// scroll container and the measured element are the same node.
|
|
87
|
+
const { ref: viewportRef } = useResizeObserver<HTMLDivElement>((entries) => {
|
|
88
|
+
const entry = entries[0];
|
|
89
|
+
if (!entry) return;
|
|
90
|
+
const h = entry.contentRect.height;
|
|
91
|
+
setVisibleHeight((prev) => (prev === h ? prev : h));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const totalHeight = itemCount * itemHeight;
|
|
95
|
+
|
|
96
|
+
// Read the live scrollTop / clientHeight from the viewport node.
|
|
97
|
+
const readViewport = useEvent(() => {
|
|
98
|
+
const viewport = viewportRef.current;
|
|
99
|
+
if (!viewport) return;
|
|
100
|
+
const top = viewport.scrollTop;
|
|
101
|
+
const h = viewport.clientHeight;
|
|
102
|
+
setScrollTop((prev) => (prev === top ? prev : top));
|
|
103
|
+
setVisibleHeight((prev) => (prev === h ? prev : h));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const onScroll = useEvent(() => {
|
|
107
|
+
if (rafRef.current !== null) return;
|
|
108
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
109
|
+
rafRef.current = null;
|
|
110
|
+
readViewport();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
useEffect(
|
|
115
|
+
() => () => {
|
|
116
|
+
if (rafRef.current !== null) {
|
|
117
|
+
cancelAnimationFrame(rafRef.current);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Measure once on mount (ResizeObserver fires shortly after, this covers the first frame).
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
readViewport();
|
|
126
|
+
}, [readViewport]);
|
|
127
|
+
|
|
128
|
+
// Keep scrollTop within bounds when the data shrinks (e.g. after filtering),
|
|
129
|
+
// otherwise the viewport would sit at an offset with empty space below.
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const viewport = viewportRef.current;
|
|
132
|
+
if (!viewport) return;
|
|
133
|
+
const maxScroll = Math.max(0, totalHeight - viewport.clientHeight);
|
|
134
|
+
if (viewport.scrollTop > maxScroll) {
|
|
135
|
+
viewport.scrollTop = maxScroll;
|
|
136
|
+
setScrollTop(maxScroll);
|
|
137
|
+
}
|
|
138
|
+
}, [totalHeight, viewportRef]);
|
|
139
|
+
|
|
140
|
+
const scrollToIndex = useEvent((index: number) => {
|
|
141
|
+
const viewport = viewportRef.current;
|
|
142
|
+
if (!viewport) return;
|
|
143
|
+
const maxScroll = Math.max(0, totalHeight - viewport.clientHeight);
|
|
144
|
+
const target = Math.min(Math.max(0, index * itemHeight), maxScroll);
|
|
145
|
+
if (viewport.scrollTop !== target) {
|
|
146
|
+
viewport.scrollTop = target;
|
|
147
|
+
}
|
|
148
|
+
readViewport();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Fallback for the first paint before ResizeObserver reports a height,
|
|
152
|
+
// so we still render at least one screen + buffer.
|
|
153
|
+
const effectiveVisibleHeight = visibleHeight || itemHeight;
|
|
154
|
+
const visibleCount = Math.ceil(effectiveVisibleHeight / itemHeight);
|
|
155
|
+
|
|
156
|
+
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
|
|
157
|
+
const end = Math.min(itemCount, start + visibleCount + overscan * 2);
|
|
158
|
+
const offsetY = start * itemHeight;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
start,
|
|
162
|
+
end,
|
|
163
|
+
offsetY,
|
|
164
|
+
totalHeight,
|
|
165
|
+
viewportRef,
|
|
166
|
+
onScroll,
|
|
167
|
+
scrollToIndex,
|
|
168
|
+
};
|
|
169
|
+
}
|