@rainbow-oh/yee-c 0.14.2 → 0.14.4

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 (37) hide show
  1. package/dist/cjs/Form/index.d.ts +1 -1
  2. package/dist/cjs/Splitter/demo/animation.d.ts +3 -0
  3. package/dist/cjs/Splitter/demo/animation.js +43 -0
  4. package/dist/cjs/Splitter/hooks/useMove.d.ts +1 -1
  5. package/dist/cjs/Splitter/hooks/useMove.js +3 -1
  6. package/dist/cjs/Splitter/interface.d.ts +6 -0
  7. package/dist/cjs/Splitter/splitter-handler.d.ts +3 -1
  8. package/dist/cjs/Splitter/splitter-handler.js +3 -2
  9. package/dist/cjs/Splitter/splitter.js +79 -11
  10. package/dist/cjs/style/themes.css +1 -1
  11. package/dist/cjs/style/variables.css +1 -1
  12. package/dist/cjs/version.d.ts +1 -1
  13. package/dist/cjs/version.js +1 -1
  14. package/dist/esm/Form/index.d.ts +1 -1
  15. package/dist/esm/Splitter/demo/animation.d.ts +3 -0
  16. package/dist/esm/Splitter/demo/animation.js +51 -0
  17. package/dist/esm/Splitter/hooks/useMove.d.ts +1 -1
  18. package/dist/esm/Splitter/hooks/useMove.js +3 -1
  19. package/dist/esm/Splitter/interface.d.ts +6 -0
  20. package/dist/esm/Splitter/splitter-handler.d.ts +3 -1
  21. package/dist/esm/Splitter/splitter-handler.js +3 -2
  22. package/dist/esm/Splitter/splitter.js +107 -14
  23. package/dist/esm/style/themes.css +1 -1
  24. package/dist/esm/style/variables.css +1 -1
  25. package/dist/esm/version.d.ts +1 -1
  26. package/dist/esm/version.js +1 -1
  27. package/dist/umd/yee-c.min.js +1 -1
  28. package/package.json +1 -1
  29. package/src/Form/index.ts +1 -0
  30. package/src/Splitter/demo/animation.tsx +29 -0
  31. package/src/Splitter/hooks/useMove.ts +3 -0
  32. package/src/Splitter/index.en-US.md +2 -0
  33. package/src/Splitter/index.zh-CN.md +2 -0
  34. package/src/Splitter/interface.ts +6 -0
  35. package/src/Splitter/splitter-handler.tsx +4 -1
  36. package/src/Splitter/splitter.tsx +93 -6
  37. 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.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "yee basic components",
5
5
  "keywords": [
6
6
  "react",
package/src/Form/index.ts CHANGED
@@ -34,6 +34,7 @@ export type {
34
34
  FormInstance,
35
35
  FormListProps,
36
36
  FormProps,
37
+ Rule as FormRule,
37
38
  ValidateMessage,
38
39
  } from './interface';
39
40
 
@@ -0,0 +1,29 @@
1
+ import { Button, Splitter } from '@rainbow-oh/yee-c';
2
+ import React from 'react';
3
+
4
+ export default () => {
5
+ const [open, setOpen] = React.useState(false);
6
+
7
+ return (
8
+ <div>
9
+ <div style={{ marginBottom: 8 }}>
10
+ <Button type="primary" onClick={() => setOpen((v) => !v)}>
11
+ {open ? 'Close Panel' : 'Open Panel'}
12
+ </Button>
13
+ </div>
14
+ <div style={{ height: 300 }}>
15
+ <Splitter animation>
16
+ <Splitter.Item min="30%">
17
+ <div style={{ padding: 16, backgroundColor: '#f0f0f0' }}>Content Panel</div>
18
+ </Splitter.Item>
19
+ {open ? (
20
+ // 条件渲染的面板:挂载时从 0 展开到目标尺寸,卸载时过渡收合到 0 后移除
21
+ <Splitter.Item size="40%" min="200px" max="60%">
22
+ <div style={{ padding: 16, backgroundColor: '#e0e0e0' }}>Assistant Panel</div>
23
+ </Splitter.Item>
24
+ ) : null}
25
+ </Splitter>
26
+ </div>
27
+ </div>
28
+ );
29
+ };
@@ -5,6 +5,7 @@ export default function useMove(
5
5
  handler: React.RefObject<HTMLDivElement | null>,
6
6
  direction: 'horizontal' | 'vertical',
7
7
  onMove: (dis: number) => void,
8
+ onMovingChange?: (moving: boolean) => void,
8
9
  ) {
9
10
  const [moving, setMoving] = React.useState(false);
10
11
 
@@ -20,6 +21,7 @@ export default function useMove(
20
21
  let x = e.pageX;
21
22
  let y = e.pageY;
22
23
  setMoving(true);
24
+ onMovingChange?.(true);
23
25
 
24
26
  const onMouseMove = debounce((e: MouseEvent) => {
25
27
  document.body.style.userSelect = 'none';
@@ -38,6 +40,7 @@ export default function useMove(
38
40
 
39
41
  const onMouseUp = () => {
40
42
  setMoving(false);
43
+ onMovingChange?.(false);
41
44
  window.removeEventListener('mousemove', onMouseMove);
42
45
  window.removeEventListener('mouseup', onMouseUp);
43
46
  ele.removeEventListener('mouseup', onMouseUp);
@@ -21,6 +21,7 @@ Split panels for layout.
21
21
  <code src="./demo/size.tsx" title="Size Control" description="Control panel size"></code>
22
22
  <code src="./demo/nested.tsx" title="Nested" description="Nested splitters"></code>
23
23
  <code src="./demo/bordered.tsx" title="Bordered" description="Splitter with borders"></code>
24
+ <code src="./demo/animation.tsx" title="Animation" description="With animation enabled, conditionally rendered panels mount/unmount and programmatic size changes transition smoothly; dragging stays real-time"></code>
24
25
 
25
26
  ## API
26
27
 
@@ -36,6 +37,7 @@ Split panels for layout.
36
37
  | bordered | `boolean` | Display border or not | - |
37
38
  | itemPadding | `number \| string` | Item padding | - |
38
39
  | onResize | `(sizes: number[]) => void` | Panel size change event | - |
40
+ | animation | `boolean` | Animate programmatic size changes, collapsible expand/collapse and panel mount/unmount with a flex-basis transition; dragging always tracks the pointer in real time | `false` |
39
41
  | data-* | `string` | - | Supports all data-* attributes, forwarded to root DOM element |
40
42
 
41
43
  ### SplitterItemProps
@@ -21,6 +21,7 @@ toc: 'content'
21
21
  <code src="./demo/size.tsx" title="尺寸控制" description="控制面板尺寸"></code>
22
22
  <code src="./demo/nested.tsx" title="嵌套" description="嵌套分割面板"></code>
23
23
  <code src="./demo/bordered.tsx" title="边框" description="带边框的分割面板"></code>
24
+ <code src="./demo/animation.tsx" title="过渡动画" description="开启 animation 后,条件渲染面板的挂载/卸载与受控尺寸变更有平滑过渡动画;拖拽缩放始终实时跟手"></code>
24
25
 
25
26
  ## API
26
27
 
@@ -36,6 +37,7 @@ toc: 'content'
36
37
  | bordered | `boolean` | 是否显示边框 | - |
37
38
  | itemPadding | `number \| string` | 项目内边距 | - |
38
39
  | onResize | `(sizes: number[]) => void` | 面板尺寸变化事件 | - |
40
+ | animation | `boolean` | 受控尺寸变更、collapsible 收展及面板挂载/卸载时的 flex-basis 过渡动画;拖拽缩放始终实时跟手,不受影响 | `false` |
39
41
  | data-* | `string` | - | 支持所有 data-* 属性,透传到组件根 DOM 元素 |
40
42
 
41
43
  ### SplitterItemProps
@@ -82,4 +82,10 @@ export interface SplitterProps extends DataAttributeProps {
82
82
  * panel size change event
83
83
  * */
84
84
  onResize?: (sizes: PanelRect[]) => void;
85
+ /**
86
+ * animate programmatic size changes and panel mount/unmount with a
87
+ * flex-basis transition; dragging always tracks the pointer in real time
88
+ * @default false
89
+ */
90
+ animation?: boolean;
85
91
  }
@@ -18,6 +18,8 @@ interface SplitterHandlerProps {
18
18
  size: { startSize: number; endSize: number },
19
19
  eles: { start: HTMLDivElement; end: HTMLDivElement },
20
20
  ) => void;
21
+ /** 拖拽开始/结束回调(animation 模式下拖拽期间需关闭 flex-basis 过渡) */
22
+ onDraggingChange?: (dragging: boolean) => void;
21
23
  }
22
24
 
23
25
  const SplitterHandler = ({
@@ -28,6 +30,7 @@ const SplitterHandler = ({
28
30
  records,
29
31
  setRecords,
30
32
  onClick,
33
+ onDraggingChange,
31
34
  }: SplitterHandlerProps) => {
32
35
  // eslint-disable-line @typescript-eslint/no-unused-vars
33
36
  const { start, end } = collapsible
@@ -91,7 +94,7 @@ const SplitterHandler = ({
91
94
  });
92
95
  };
93
96
 
94
- useMove(handler, layout, onMove);
97
+ useMove(handler, layout, onMove, onDraggingChange);
95
98
 
96
99
  const handleClick = (action: 'collapse' | 'expand') => {
97
100
  const eles = refs.current;
@@ -35,12 +35,15 @@ const getPanelSize = (
35
35
  },
36
36
  ) => {
37
37
  const panelsRect = [] as PanelRect[];
38
- const count = React.Children.count(children);
38
+ // toArray 剔除 null/undefined/boolean(如 `{open ? <Item/> : null}` 条件面板)。
39
+ // Children.count 会把 null 也计为一面板,导致 auto 面板被平分出错误尺寸。
40
+ const items = React.Children.toArray(children);
41
+ const count = items.length;
39
42
 
40
43
  let remainingSize = totalSize;
41
44
  let remainingItem = count;
42
45
 
43
- React.Children.forEach(children, (child) => {
46
+ items.forEach((child) => {
44
47
  if (React.isValidElement(child)) {
45
48
  const { style, size, defaultSize, min, max } = child.props || ({} as any);
46
49
 
@@ -111,6 +114,8 @@ const getPanelSize = (
111
114
 
112
115
  panelsRect.forEach((item) => {
113
116
  if (item.type === 'default') {
117
+ // currentSize 缺失会导致 Wrapper 注入的 flexBasis 为空,defaultSize 失效(宽度由内容撑开)
118
+ item.currentSize = item.size;
114
119
  remainingSize -= item.size || 0;
115
120
  remainingItem--;
116
121
  } else if (item.type === 'percent') {
@@ -124,7 +129,8 @@ const getPanelSize = (
124
129
  if (remainingItem > 0) {
125
130
  panelsRect.forEach((item) => {
126
131
  if (item.type === 'auto') {
127
- item.size = remainingSize / remainingItem;
132
+ // 显式尺寸总和超出 totalSize 时 remainingSize 为负,负 flexBasis 会被浏览器丢弃导致面板回退内容尺寸
133
+ item.size = Math.max(0, remainingSize / remainingItem);
128
134
  item.currentSize = item.size;
129
135
  }
130
136
  });
@@ -133,7 +139,15 @@ const getPanelSize = (
133
139
  return panelsRect;
134
140
  };
135
141
 
136
- const Wrapper = ({ children, itemsSize, onClick, ...props }: any) => {
142
+ const Wrapper = ({
143
+ children,
144
+ itemsSize,
145
+ itemTransition,
146
+ animation,
147
+ onDraggingChange,
148
+ onClick,
149
+ ...props
150
+ }: any) => {
137
151
  const wrappered = [] as React.ReactElement[];
138
152
  const refs = useRef<
139
153
  Record<
@@ -145,9 +159,10 @@ const Wrapper = ({ children, itemsSize, onClick, ...props }: any) => {
145
159
  }
146
160
  >
147
161
  >({});
162
+ // 与 getPanelSize 同口径:toArray 剔除 null,index/handler 判断不受条件渲染干扰
148
163
  const arr = React.Children.toArray(children);
149
164
 
150
- React.Children.forEach(children, (child, index) => {
165
+ arr.forEach((child, index) => {
151
166
  if (React.isValidElement(child)) {
152
167
  const n = index + 1;
153
168
  const { style } = child.props || ({} as any);
@@ -164,6 +179,8 @@ const Wrapper = ({ children, itemsSize, onClick, ...props }: any) => {
164
179
  style: {
165
180
  ...style, // If there is a border or other size-related design, it may cause calculation issues
166
181
  flexBasis: currentSize,
182
+ // animation 模式下受控尺寸变更走 flex-basis 过渡;拖拽期间为 undefined 保证跟手
183
+ transition: itemTransition,
167
184
  },
168
185
  min,
169
186
  max,
@@ -178,6 +195,7 @@ const Wrapper = ({ children, itemsSize, onClick, ...props }: any) => {
178
195
  },
179
196
  }),
180
197
  );
198
+
181
199
  const next = arr[index + 1];
182
200
 
183
201
  if (!next || !React.isValidElement(next)) {
@@ -204,15 +222,55 @@ const Wrapper = ({ children, itemsSize, onClick, ...props }: any) => {
204
222
  flexable={flexable}
205
223
  expandable={_expandable}
206
224
  onClick={onClick}
225
+ onDraggingChange={onDraggingChange}
207
226
  key={`handler-${n}`}
208
227
  />,
209
228
  );
210
229
  }
211
230
  });
212
231
 
232
+ // ── 出场动画:children 数量减少(条件渲染的面板卸载)时,保留上一次渲染的
233
+ // 尾部面板克隆,flex-basis 过渡到 0 后再真正移除,避免右栏瞬间消失导致跳变。
234
+ const prevCountRef = React.useRef(arr.length);
235
+ const prevClonesRef = React.useRef<React.ReactElement[]>([]);
236
+ const [exiting, setExiting] = React.useState<React.ReactElement<any>[]>([]);
237
+
238
+ React.useEffect(() => {
239
+ const prevCount = prevCountRef.current;
240
+ prevCountRef.current = arr.length;
241
+ if (!animation || arr.length >= prevCount) {
242
+ return;
243
+ }
244
+ // 上一帧克隆里已带原 flex-basis(本帧 records 已不含被移除面板)
245
+ const removed = prevClonesRef.current.slice(arr.length);
246
+ if (!removed.length) {
247
+ return;
248
+ }
249
+ setExiting(removed);
250
+ // 第二阶段:下一帧 flex-basis 归 0,触发过渡
251
+ const raf = requestAnimationFrame(() => {
252
+ setExiting((list) =>
253
+ list.map((el) => React.cloneElement(el, { style: { ...el.props.style, flexBasis: 0 } })),
254
+ );
255
+ });
256
+ const timer = window.setTimeout(() => setExiting([]), 320);
257
+ return () => {
258
+ // 过渡期间面板重新打开:立即丢弃残留克隆
259
+ cancelAnimationFrame(raf);
260
+ window.clearTimeout(timer);
261
+ setExiting([]);
262
+ };
263
+ }, [arr.length, animation]);
264
+
265
+ // 记录本帧克隆,供下一次渲染的移除检测读取(声明在其后:上方 effect 读到的是旧值)
266
+ React.useEffect(() => {
267
+ prevClonesRef.current = wrappered;
268
+ });
269
+
213
270
  return (
214
271
  <SplitterWrapperCtx.Provider value={{ refs }}>
215
272
  {wrappered}
273
+ {exiting}
216
274
  </SplitterWrapperCtx.Provider>
217
275
  );
218
276
  };
@@ -228,6 +286,7 @@ const Splitter = React.forwardRef(
228
286
  children,
229
287
  bordered,
230
288
  onResize,
289
+ animation,
231
290
  ...rest
232
291
  } = props;
233
292
 
@@ -237,6 +296,15 @@ const Splitter = React.forwardRef(
237
296
 
238
297
  const [records, setRecords] = useState<PanelRect[]>([]);
239
298
 
299
+ // animation 模式:拖拽期间关掉面板的 flex-basis 过渡(onMove 直接写 DOM,过渡会导致跟不
300
+ // 上鼠标);其余程序性尺寸变更(受控 size、collapsible 收展、面板增减)都走过渡。
301
+ const [dragging, setDragging] = React.useState(false);
302
+ const itemTransition = animation
303
+ ? dragging
304
+ ? undefined
305
+ : 'flex-basis 0.28s cubic-bezier(0.22, 0.61, 0.36, 1)'
306
+ : undefined;
307
+
240
308
  const handleClick = (index: number, action: 'expand' | 'collapse') => {
241
309
  const c = index - 1;
242
310
 
@@ -295,15 +363,31 @@ const Splitter = React.forwardRef(
295
363
  }
296
364
  }, [layout]);
297
365
 
366
+ const prevPanelCountRef = React.useRef(-1);
367
+
298
368
  React.useEffect(() => {
299
369
  if (totalSize) {
300
370
  const panelsSize = getPanelSize(children, {
301
371
  totalSize: totalSize,
302
372
  layout,
303
373
  });
374
+ // animation 模式下面板新增(条件渲染的面板挂载)时先以 0 尺寸入库,下一帧再落到
375
+ // 目标尺寸,配合 flex-basis 过渡播放入场动画;否则新面板瞬间撑开、旧面板瞬间收窄。
376
+ const prevCount = prevPanelCountRef.current;
377
+ prevPanelCountRef.current = panelsSize.length;
378
+ if (animation && prevCount >= 0 && panelsSize.length > prevCount) {
379
+ panelsSize.slice(prevCount).forEach((item) => {
380
+ item.currentSize = 0;
381
+ });
382
+ setRecords(panelsSize);
383
+ const raf = requestAnimationFrame(() => {
384
+ setRecords(getPanelSize(children, { totalSize: totalSize, layout }));
385
+ });
386
+ return () => cancelAnimationFrame(raf);
387
+ }
304
388
  setRecords(panelsSize);
305
389
  }
306
- }, [children, totalSize, layout]);
390
+ }, [children, totalSize, layout, animation]);
307
391
 
308
392
  return (
309
393
  <div className={cls} {...rest} ref={componentRef}>
@@ -315,6 +399,9 @@ const Splitter = React.forwardRef(
315
399
  totalSize={totalSize}
316
400
  itemsSize={records}
317
401
  onClick={handleClick}
402
+ animation={animation}
403
+ itemTransition={itemTransition}
404
+ onDraggingChange={setDragging}
318
405
  >
319
406
  {children}
320
407
  </Wrapper>
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@ declare global {
4
4
  }
5
5
  }
6
6
 
7
- export const version = "0.14.2";
7
+ export const version = "0.14.4";
8
8
 
9
9
  if (typeof window !== 'undefined') {
10
10
  window.__yee_c_version__ = version;