فهرست منبع

Fix:resize group bugs (#2551)

* chore: move ts styleBlock to scss

* feat: use 'calc(percent - px)' to manage Item size and eliminate offset caused by float

* fix: bug caused by strictMode

* fix: ensure min/max prop valid when browser resizing

* feat: add dynamic direction handle in resize group

* feat: implement dynamic direction for resizeGroup

* chore: add some annotations

* docs: add dynamic direction example

* style: move scss constant to variable.scss in resizable

* feat: add debounce for ensure constraints

* fix: Optimize Resizable scss token

* docs: fix resizable docs after move category form show to basic

---------

Co-authored-by: yanzhuoran <[email protected]>
Co-authored-by: zhangyumei.0319 <[email protected]>
Nathon2Y 11 ماه پیش
والد
کامیت
2ac586be39

+ 80 - 22
content/basic/resizable/index-en-US.md

@@ -225,6 +225,7 @@ function Demo() {
 ```
 
 ### Control Width/Height
+
 You can control the size of the element through the size prop.
 
 ```jsx live=true
@@ -232,22 +233,20 @@ import React, { useState } from 'react';
 import { Resizable } from '@douyinfe/semi-ui';
 
 function Demo() {
-  const [size, setSize] = useState({ width: 200, height: 300 });
-
-  const onChange = (() => {
+  const [size, setSize] = useState({ width: 200, height: 100 });
+  const onButtonClick = (() => {
     let realSize = { width: size.width + 10, height: size.height + 10 };
     setSize(realSize);
   })
+  const onChange = (s) => { setSize(s); }
+
   return (
     <div style={{ width: '500px', height: '60%' }}>
-      <Button onClick={onChange}>set += 10</Button>
+      <Button onClick={onButtonClick}>set += 10</Button>
       <Resizable
         style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', marginTop: '10px' }}
-        defaultSize={{
-          width: 100,
-          height: 100,
-        }}
         size={size}
+        onChange={onChange}
       >
         <div style={{ marginLeft: '20%' }}>
           Control Width/Height
@@ -256,7 +255,6 @@ function Demo() {
     </div>
   );
 }
-
 ```
 
 
@@ -675,16 +673,76 @@ function Demo() {
 }
 ```
 
+### Dynamic Direction
+```jsx live=true 
+import React, { useState } from 'react';
+import { ResizeItem, ResizeHandler, ResizeGroup } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('drag to resize')
+  const [direction, setDirection] = useState('horizontal')
+
+  const changeDirection = () => {
+    if (direction === 'horizontal') {
+      setDirection('vertical')
+    } else {
+      setDirection('horizontal')
+    }
+  }
+  return (
+    <div style={{ width: '400px', height: '300px' }}>
+      <Button onClick={changeDirection}>{direction}</Button>
+      <ResizeGroup direction={direction} >
+        <ResizeItem
+          onChange={() => { setText('resizing') }}
+          onResizeEnd={() => { setText('drag to resize') }}
+          defaultSize={5}
+        >
+            <ResizeGroup direction='horizontal'>
+              <ResizeItem
+                style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+                onChange={() => { setText('resizing') }}
+                onResizeEnd={() => { setText('drag to resize') }}
+              >
+                <div style={{ marginLeft: '20%',  padding:'5px' }}>
+                  {text}
+                </div>
+              </ResizeItem>
+              <ResizeHandler></ResizeHandler>
+              <ResizeItem
+                style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+                onChange={() => { setText('resizing') }}
+              >
+                <div style={{ marginLeft: '20%',  padding:'5px' }}>
+                  {text}
+                </div>
+              </ResizeItem>
+            </ResizeGroup>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)',  }}
+          defaultSize={1.3}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%',  padding:'5px' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+```
+
 
 ## API
 
 ### Resizable
 
-单个伸缩框组件。
-
 | 参数      | 说明                                                                          | 类型                    | 默认值     | 版本   |
 | --------- | ----------------------------------------------------------------------------- | ----------------------- | ---------- | ------ |
-| className | 类名                                                                          | string                  |            |        |
+| className | ClassName                                                                          | string                  |            |        |
 | size   | Controls the size of the resizable box, supports both numeric and string (px/vw/vh/%) formats | [Size](#basic-usage-and-callbacks)                  |           |        |
 | defaultSize   | Sets the initial width and height, supports both numeric and string (px/vw/vh/%) formats | [Size](#basic-usage-and-callbacks)                  |           |        |
 | minWidth | Specifies the minimum width of the resizable box      |  string \| number                  |   |        |
@@ -698,40 +756,40 @@ function Demo() {
 | handleNode     | Custom nodes for the drag handles in each direction             | [HandleNode](#customizing-corner-handler-styles)          |            |        |
 | handleStyle    | Styles for the drag handles in each direction             | [HandleNode](#customizing-corner-handler-styles)            |            |        |
 | handleClass   | Class names for the drag handles in each direction              | [HandleNode](#customizing-corner-handler-styles)            |            |        |
-| style |  | CSSProperties |      |
+| style | Style | CSSProperties |      |
 | snapGap      | Specifies the minimum gap required to snap to the next target                        | number                  | 0       |  |
 | snap      | Specifies the pixel values to snap to during resizing. Both x and y are optional, allowing the definition of specific axes only                        | [Snap](#allowing-incremental-width-and-height-adjustment)                  | null       |  |
 | grid      | Specifies the increment to align to when resizing                          | \[number, number\]                  | \[1,1\]       |  |
-| onChange  | Callback during the dragging process                                                    | (e: Event; direction: String;size: Size) => void | -          |  |
+| onChange  | Callback during the dragging process                                                    | (size: Size; e: Event; direction: String) => void | -          |  |
 | onResizeStart  | Callback when resizing starts                                                  | (e: Event; direction: String) => void | -          |  |
-| onResizeEnd  | Callback when resizing ends                                                   | (e: Event; direction: String) => void | -          |  |
+| onResizeEnd  | Callback when resizing ends                                                   | (size: Size; e: Event; direction: String) => void | -          |  |
 
 ### ResizeGroup
 
 | 参数        | 说明                                                                                                                        | 类型                               | 默认值 | 版本 |
 | ----------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |
-| className   |                                                                                                                         | string                             |        |      |
+| className   | ClassName | string                             |        |      |
 | direction | Specifies the resize direction within the group  | 'horizontal' \| 'vertical' | 'horizontal' |      |
 
 ### ResizeHandler
 
 | 参数        | 说明                                                                                                                        | 类型                               | 默认值 | 版本 |
 | ----------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |
-| className   |                                                                                                                         | string                             |        |      |
-| style |  | CSSProperties |      |
+| className   | ClassName | string  |        |      |
+| style | Style | CSSProperties |      |
 
 ### ResizeItem
 
 | 参数      | 说明                                                                          | 类型                    | 默认值     | 版本   |
 | --------- | ----------------------------------------------------------------------------- | ----------------------- | ---------- | ------ |
-| className |                                                                           | string                  |            |        |
+| className | ClassName                                                                     | string                  |            |        |
 | defaultSize   | Used to set the initial width and height. **The string supports % and px units, and when the string is a pure number or a number is set directly, it represents the proportional allocation of the remaining space based on the value.**  | string \| number                  |           |        |
 | min | Specifies the minimum size of the resizable box (as percentage or pixel)     |  string                  |   |        |
 | max | Specifies the maximum size of the resizable box (as percentage or pixel)     |  string                  |   |        |   
-| style |  | CSSProperties |      |
-| onChange  | Callback during the dragging process                                                    | (e: Event; direction: String;size: Size) => void | -          |  |
+| style | Style | CSSProperties |      |
+| onChange  | Callback during the dragging process                                                    | (size: Size; e: Event; direction: String) => void | -          |  |
 | onResizeStart  | Callback when resizing starts                                                  | (e: Event; direction: String) => void | -          |  |
-| onResizeEnd  | Callback when resizing ends                                                   | (e: Event; direction: String) => void | -          |  |
+| onResizeEnd  | Callback when resizing ends                                                   | (size: Size; e: Event; direction: String) => void | -          |  |
 
 
 ## Design Tokens

+ 124 - 64
content/basic/resizable/index.md

@@ -219,27 +219,27 @@ import React, { useState } from 'react';
 import { Resizable } from '@douyinfe/semi-ui';
 
 function Demo() {
-    const [size, setSize] = useState({ width: 200, height: 300 });
-
-    const onChange = () => {
-        let realSize = { width: size.width + 10, height: size.height + 10 };
-        setSize(realSize);
-    };
-    return (
-        <div style={{ width: '500px', height: '60%' }}>
-            <Button onClick={onChange}>set += 10</Button>
-            <Resizable
-                style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', marginTop: '10px' }}
-                defaultSize={{
-                    width: 100,
-                    height: 100,
-                }}
-                size={size}
-            >
-                <div style={{ marginLeft: '20%' }}>受控</div>
-            </Resizable>
+  const [size, setSize] = useState({ width: 200, height: 100 });
+  const onButtonClick = () => {
+    let realSize = { width: size.width + 10, height: size.height + 10 };
+    setSize(realSize);
+  };
+  const onChange = (s) => { setSize(s); }
+
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Button onClick={onButtonClick}>set += 10</Button>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', marginTop: '10px' }}
+        size={size}
+        onChange={onChange}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          受控
         </div>
-    );
+      </Resizable>
+    </div>
+  );
 }
 ```
 
@@ -698,62 +698,122 @@ function Demo() {
 }
 ```
 
+### 动态方向
+
+```jsx live=true
+import React, { useState } from 'react';
+import { ResizeItem, ResizeHandler, ResizeGroup } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('drag to resize')
+  const [direction, setDirection] = useState('horizontal')
+
+  const changeDirection = () => {
+    if (direction === 'horizontal') {
+      setDirection('vertical')
+    } else {
+      setDirection('horizontal')
+    }
+  }
+  return (
+    <div style={{ width: '400px', height: '300px' }}>
+      <Button onClick={changeDirection}>{direction}</Button>
+      <ResizeGroup direction={direction} >
+        <ResizeItem
+          onChange={() => { setText('resizing') }}
+          onResizeEnd={() => { setText('drag to resize') }}
+          defaultSize={5}
+        >
+            <ResizeGroup direction='horizontal'>
+              <ResizeItem
+                style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+                onChange={() => { setText('resizing') }}
+                onResizeEnd={() => { setText('drag to resize') }}
+              >
+                <div style={{ marginLeft: '20%',  padding:'5px' }}>
+                  {text}
+                </div>
+              </ResizeItem>
+              <ResizeHandler></ResizeHandler>
+              <ResizeItem
+                style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+                onChange={() => { setText('resizing') }}
+              >
+                <div style={{ marginLeft: '20%',  padding:'5px' }}>
+                  {text}
+                </div>
+              </ResizeItem>
+            </ResizeGroup>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)',  }}
+          defaultSize={1.3}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%',  padding:'5px' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+```
+
 ## API 参考
 
 ### Resizable
 
 单个伸缩框组件。
 
-| 参数 | 说明 | 类型 | 默认值 | 版本 |
-| --- | --- | --- | --- | --- |
-| className | 类名 | string |  |  |
-| size | 控制伸缩框的大小,支持数字和字符串(px/vw/vh/%)两种格式 | [Size](#基本使用与回调) |  |  |
-| defaultSize | 用于设置初始宽高,支持数字和字符串(px/vw/vh/%)两种格式 | [Size](#基本使用与回调) |  |  |
-| minWidth | 指定伸缩框最小宽度 | string \| number |  |  |
-| maxWidth | 指定伸缩框最大宽度 | string \| number |  |  |
-| minHeight | 指定伸缩框最小高度 | string \| number |  |  |
-| maxHeight | 指定伸缩框最大高度 | string \| number |  |
-| lockAspectRatio | 设置伸缩框横纵比,当为`true`时按照初始宽高锁定 | boolean \| number |  |  |
-| enable | 指定伸缩框可以伸缩的方向,没有设置为 false,则默认允许该方向的拖动 | [Enable](#控制伸缩方向) |
-| scale | 可伸缩元素被缩放的比例 | number | 1 |  |
-| boundElement | 用于限制可伸缩元素宽高的元素,传入 `parent` 设置父节点为限制节点 | string |  |  |
-| handleNode | 用于设置拖拽处理元素各个方向的自定义节点 | [HandleNode](#自定义边角handler样式) |  |  |
-| handleStyle | 用于设置拖拽处理元素各个方向的样式 | [HandleStyles](#自定义边角handler样式) |  |  |
-| handleClass | 用于设置拖拽处理元素各个方向的类名称 | [HandleClasses](#自定义边角handler样式) |  |  |
-| style | 样式 | CSSProperties |  |
-| snapGap | 用于指定移动到下一个目标所需的最小间隙。 | number | 0 |  |
-| snap | 指定调整大小时应对齐的绝对像素值。 x 和 y 都是可选的,允许仅包含要定义的轴 | [Snap](#允许阶段性调整宽高) | null |  |
-| grid | 指定调整大小应对齐的增量 | \[number, number\] | \[1,1\] |  |
-| onChange | 拖拽过程中的回调 | (e: Event; direction: String;size: Size) => void | - |  |
-| onResizeStart | 开始伸缩的回调 | (e: Event; direction: String) => void | - |  |
-| onResizeEnd | 结束伸缩的回调 | (e: Event; direction: String) => void | - |  |
+| 参数      | 说明                                                                          | 类型                    | 默认值     | 版本   |
+| --------- | ----------------------------------------------------------------------------- | ----------------------- | ---------- | ------ |
+| className | 类名                                                                          | string                  |            |        |
+| size   | 控制伸缩框的大小,支持数字和字符串(px/vw/vh/%)两种格式 | [Size](#基本使用与回调)                  |           |        |
+| defaultSize   | 用于设置初始宽高,支持数字和字符串(px/vw/vh/%)两种格式 | [Size](#基本使用与回调)                  |           |        |
+| minWidth | 指定伸缩框最小宽度      |  string \| number                  |   |        |
+| maxWidth | 指定伸缩框最大宽度      |  string \| number                  |   |        |
+| minHeight | 指定伸缩框最小高度      |  string \| number                  |   |        |
+| maxHeight | 指定伸缩框最大高度      |  string \| number                  |   |     
+| lockAspectRatio | 设置伸缩框横纵比,当为`true`时按照初始宽高锁定    |  boolean \| number                  |   |        |
+| enable | 指定伸缩框可以伸缩的方向,没有设置为 false,则默认允许该方向的拖动      |    [Enable](#控制伸缩方向) 
+| scale | 可伸缩元素被缩放的比例      |   number                  |  1 |        |   
+| boundElement | 用于限制可伸缩元素宽高的元素,传入 `parent` 设置父节点为限制节点    | string                  |            |        |
+| handleNode     | 用于设置拖拽处理元素各个方向的自定义节点             | [HandleNode](#自定义边角handler样式)          |            |        |
+| handleStyle    | 用于设置拖拽处理元素各个方向的样式              | [HandleStyles](#自定义边角handler样式)            |            |        |
+| handleClass  | 用于设置拖拽处理元素各个方向的类名称              | [HandleClasses](#自定义边角handler样式)            |            |        |
+| style | 样式 | CSSProperties |      |
+| snapGap      | 用于指定移动到下一个目标所需的最小间隙。                        | number                  | 0       |  |
+| snap      | 指定调整大小时应对齐的绝对像素值。 x 和 y 都是可选的,允许仅包含要定义的轴                        | [Snap](#允许阶段性调整宽高)                  | null       |  |
+| grid      | 指定调整大小应对齐的增量                           | \[number, number\]                  | \[1,1\]       |  |
+| onChange  | 拖拽过程中的回调                                                    | (size: Size; e: Event; direction: String) => void | -          |  |
+| onResizeStart  | 开始伸缩的回调                                                   | (e: Event; direction: String) => void | -          |  |
+| onResizeEnd  | 结束伸缩的回调                                                    | (size: Size; e: Event; direction: String) => void | -          |  |
 
 ### ResizeGroup
 
-| 参数      | 说明                    | 类型                       | 默认值       | 版本 |
-| --------- | ----------------------- | -------------------------- | ------------ | ---- |
-| className | 类名                    | string                     |              |      |
-| direction | 指定 Group 内的伸缩方向 | 'horizontal' \| 'vertical' | 'horizontal' |      |
-
+| 参数        | 说明                                                                                                                        | 类型                               | 默认值 | 版本 |
+| ----------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |
+| className   | 类名                                                                                                                        | string                             |        |      |
+| direction | 指定Group内的伸缩方向  | 'horizontal' \| 'vertical' | 'horizontal' |      |
 ### ResizeHandler
-
-| 参数      | 说明 | 类型          | 默认值 | 版本 |
-| --------- | ---- | ------------- | ------ | ---- |
-| className | 类名 | string        |        |      |
-| style     | 样式 | CSSProperties |        |
-
+| 参数        | 说明                                                                                                                        | 类型                               | 默认值 | 版本 |
+| ----------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |
+| className   | 类名                                                                                                                        | string                             |        |      |
+| style | 样式 | CSSProperties |      |
 ### ResizeItem
+| 参数      | 说明                                                                          | 类型                    | 默认值     | 版本   |
+| --------- | ----------------------------------------------------------------------------- | ----------------------- | ---------- | ------ |
+| className | 类名                                                                          | string                  |            |        |
+| defaultSize   | 用于设置初始宽高,**字符串支持%和px单位,当字符串为纯数字或直接设置数字时表示按照值的比例分配剩余空间** | string \| number                 |           |        |
+| min | 指定伸缩框最小尺寸(百分比或像素值)      |  string                   |   |        |
+| max | 指定伸缩框最大尺寸(百分比或像素值)     |  string                   |   |        |
+| style | 样式 | CSSProperties |      |
+| onChange  | 拖拽过程中的回调                                                    | (size: Size; e: Event; direction: String) => void | -          |  |
+| onResizeStart  | 开始伸缩的回调                                                   | (e: Event; direction: String) => void | -          |  |
+| onResizeEnd  | 结束伸缩的回调                                                    | (size: Size; e: Event; direction: String) => void | -          |  |
 
-| 参数 | 说明 | 类型 | 默认值 | 版本 |
-| --- | --- | --- | --- | --- |
-| className | 类名 | string |  |  |
-| defaultSize | 用于设置初始宽高,**字符串支持%和 px 单位,当字符串为纯数字或直接设置数字时表示按照值的比例分配剩余空间** | string \| number |  |  |
-| min | 指定伸缩框最小尺寸(百分比或像素值) | string |  |  |
-| max | 指定伸缩框最大尺寸(百分比或像素值) | string |  |  |
-| style | 样式 | CSSProperties |  |
-| onChange | 拖拽过程中的回调 | (e: Event; direction: String;size: Size) => void | - |  |
-| onResizeStart | 开始伸缩的回调 | (e: Event; direction: String) => void | - |  |
-| onResizeEnd | 结束伸缩的回调 | (e: Event; direction: String) => void | - |  |
 
 ## 设计变量
 

+ 5 - 16
packages/semi-foundation/resizable/foundation.ts

@@ -1,26 +1,15 @@
-import { 
+export {
+    ResizableHandlerAdapter, 
     ResizableHandlerFoundation, 
     ResizableFoundation, 
+    ResizableAdapter 
 } from './single';
 
-import { 
-    ResizeGroupFoundation,
-    ResizeItemFoundation,
-    ResizeHandlerFoundation
-} from './group';
-
-import type { ResizableHandlerAdapter, ResizableAdapter } from './single';
-import type { ResizeGroupAdapter, ResizeItemAdapter, ResizeHandlerAdapter } from './group';
-
-export { 
-    ResizableHandlerAdapter, 
-    ResizableHandlerFoundation, 
-    ResizableFoundation, 
-    ResizableAdapter,
+export {
     ResizeGroupAdapter,
     ResizeItemAdapter,
     ResizeHandlerAdapter,
     ResizeGroupFoundation,
     ResizeItemFoundation,
     ResizeHandlerFoundation
-}; 
+} from './group';

+ 92 - 29
packages/semi-foundation/resizable/group/index.ts

@@ -1,7 +1,8 @@
 import { getItemDirection, getPixelSize } from "../utils";
 import BaseFoundation, { DefaultAdapter } from '../../base/foundation';
-import { ResizeStartCallback, ResizeCallback } from "../singleConstants";
+import { ResizeStartCallback, ResizeCallback } from "../types";
 import { adjustNewSize, judgeConstraint, getOffset } from "../utils";
+import { debounce } from "lodash";
 export interface ResizeHandlerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
     registerEvents: () => void;
     unregisterEvents: () => void
@@ -61,16 +62,23 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
         return this._adapter.getGroupRef();
     }
 
+    get groupSize(): number {
+        const { direction } = this.getProps();
+        let groupSize = direction === 'horizontal' ? this.groupRef.offsetWidth : this.groupRef.offsetHeight;
+        return groupSize;
+    }
+
     direction: 'horizontal' | 'vertical'
-    itemMinusMap: Map<number, number>;
+    itemMinusMap: Map<number, number>; // 这个是为了给handler留出空间,方便维护每一个item的size为cal(percent% - minus)
     totalMinus: number;
-    avaliableSize: number;
+    itemPercentMap: Map<number, number>; // 内部维护一个百分比数组,消除浮点计算误差
 
 
     init(): void {
         this.direction = this.getProp('direction');
         this.itemMinusMap = new Map();
-        this.calculateSpace();
+        this.itemPercentMap = new Map();
+        this.initSpace();
     }
     get window(): Window | null {
         return this.groupRef.ownerDocument.defaultView as Window ?? null;
@@ -93,16 +101,18 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
         const lastStyle = this.window.getComputedStyle(lastItem);
         const nextStyle = this.window.getComputedStyle(nextItem);
 
-        lastOffset = getOffset(lastStyle, this.direction);
-        nextOffset = getOffset(nextStyle, this.direction);
+        lastOffset = getOffset(lastStyle, this.direction) + this.itemMinusMap.get(handlerIndex);
+        nextOffset = getOffset(nextStyle, this.direction) + this.itemMinusMap.get(handlerIndex + 1);
+        let lastItemSize = (this.direction === 'horizontal' ? lastItem.offsetWidth : lastItem.offsetHeight) + this.itemMinusMap.get(handlerIndex),
+            nextItemSize = (this.direction === 'horizontal' ? nextItem.offsetWidth : nextItem.offsetHeight) + this.itemMinusMap.get(handlerIndex + 1);
         const states = this.getStates();
         this.setState({
             isResizing: true,
             originalPosition: {
                 x: clientX,
                 y: clientY,
-                lastItemSize: (this.direction === 'horizontal' ? lastItem.offsetWidth : lastItem.offsetHeight),
-                nextItemSize: (this.direction === 'horizontal' ? nextItem.offsetWidth : nextItem.offsetHeight),
+                lastItemSize,
+                nextItemSize,
                 lastOffset,
                 nextOffset,
             },
@@ -138,33 +148,38 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
         const props = this.getProps();
         const { direction } = props;
         let lastItem = this._adapter.getItem(curHandler), nextItem = this._adapter.getItem(curHandler + 1);
-        let parentSize = this.direction === 'horizontal' ? this.groupRef.offsetWidth : this.groupRef.offsetHeight;
-        let availableSize = parentSize - this.totalMinus;
-
+        let parentSize = this.groupSize;
         let delta = direction === 'horizontal' ? (clientX - initX) : (clientY - initY);
         let lastNewSize = lastItemSize + delta;
         let nextNewSize = nextItemSize - delta;
 
         // 判断是否超出限制
-        let lastFlag = judgeConstraint(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), availableSize, lastOffset),
-            nextFlag = judgeConstraint(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), availableSize, nextOffset);
+        let lastFlag = judgeConstraint(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), parentSize, lastOffset),
+            nextFlag = judgeConstraint(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), parentSize, nextOffset);
 
         if (lastFlag) {
-            lastNewSize = adjustNewSize(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), availableSize, lastOffset);
+            lastNewSize = adjustNewSize(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), parentSize, lastOffset);
             nextNewSize = lastItemSize + nextItemSize - lastNewSize;
         }
 
         if (nextFlag) {
-            nextNewSize = adjustNewSize(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), availableSize, nextOffset);
+            nextNewSize = adjustNewSize(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), parentSize, nextOffset);
             lastNewSize = lastItemSize + nextItemSize - nextNewSize;
         }
 
+        let lastItemPercent = this.itemPercentMap.get(curHandler),
+            nextItemPercent = this.itemPercentMap.get(curHandler + 1);
+
+        let lastNewPercent = (lastNewSize) / parentSize * 100;
+        let nextNewPercent = lastItemPercent + nextItemPercent - lastNewPercent; // 消除浮点误差
+        this.itemPercentMap.set(curHandler, lastNewPercent);
+        this.itemPercentMap.set(curHandler + 1, nextNewPercent);
         if (direction === 'horizontal') {     
-            lastItem.style.width = (lastNewSize) / parentSize * 100 + '%';
-            nextItem.style.width = (nextNewSize) / parentSize * 100 + '%';
+            lastItem.style.width = `calc(${lastNewPercent}% - ${this.itemMinusMap.get(curHandler)}px)`;
+            nextItem.style.width = `calc(${nextNewPercent}% - ${this.itemMinusMap.get(curHandler + 1)}px)`;
         } else if (direction === 'vertical') {
-            lastItem.style.height = (lastNewSize) / parentSize * 100 + '%';
-            nextItem.style.height = (nextNewSize) / parentSize * 100 + '%';
+            lastItem.style.height = `calc(${lastNewPercent}% - ${this.itemMinusMap.get(curHandler)}px)`;
+            nextItem.style.height = `calc(${nextNewPercent}% - ${this.itemMinusMap.get(curHandler + 1)}px)`;
         }
 
         let lastFunc = this._adapter.getItemChange(curHandler),
@@ -197,13 +212,13 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
         this.unregisterEvents();
     }
 
-    calculateSpace = () => {
+    initSpace = () => {
         const props = this.getProps();
         const { direction } = props;
 
         // calculate accurate space for group item
         let handlerSizes = new Array(this._adapter.getHandlerCount()).fill(0);
-        let groupSize = direction === 'horizontal' ? this.groupRef.offsetWidth : this.groupRef.offsetHeight;
+        let parentSize = this.groupSize;
         this.totalMinus = 0;
         for (let i = 0; i < this._adapter.getHandlerCount(); i++) {
             let handlerSize = direction === 'horizontal' ? this._adapter.getHandler(i).offsetWidth : this._adapter.getHandler(i).offsetHeight;
@@ -225,8 +240,8 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
             }
             const child = this._adapter.getItem(i);
             let minSize = this._adapter.getItemMin(i), maxSize = this._adapter.getItemMax(i);
-            let minSizePercent = minSize ? getPixelSize(minSize, groupSize) / groupSize * 100 : 0,
-                maxSizePercent = maxSize ? getPixelSize(maxSize, groupSize) / groupSize * 100 : 100;
+            let minSizePercent = minSize ? getPixelSize(minSize, parentSize) / parentSize * 100 : 0,
+                maxSizePercent = maxSize ? getPixelSize(maxSize, parentSize) / parentSize * 100 : 100;
             if (minSizePercent > maxSizePercent) {
                 console.warn('[Semi ResizableItem]: min size bigger than max size');
             }    
@@ -237,21 +252,21 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
                 if (typeof defaultSize === 'string') {
                     if (defaultSize.endsWith('%')) {
                         itemSizePercent = parseFloat(defaultSize.slice(0, -1));
+                        this.itemPercentMap.set(i, itemSizePercent);
                     } else if (defaultSize.endsWith('px')) {
-                        itemSizePercent = parseFloat(defaultSize.slice(0, -2)) / groupSize * 100;
+                        itemSizePercent = parseFloat(defaultSize.slice(0, -2)) / parentSize * 100;
+                        this.itemPercentMap.set(i, itemSizePercent);
                     } else if (/^-?\d+(\.\d+)?$/.test(defaultSize)) {
                         // 仅由数字组成,表示按比例分配剩下空间
                         undefineLoc.set(i, parseFloat(defaultSize));
                         undefinedTotal += parseFloat(defaultSize);
                         continue;
                     }
-                } else {
+                } else if (typeof defaultSize === 'number') {
                     undefineLoc.set(i, defaultSize);
                     undefinedTotal += defaultSize;
                     continue;
                 }
-                
-
                 totalSizePercent += itemSizePercent;
                 
                 if (direction === 'horizontal') {
@@ -279,14 +294,62 @@ export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, a
     
         undefineLoc.forEach((value, key) => {
             const child = this._adapter.getItem(key);
+            const percent = value / undefinedTotal * undefineSizePercent;
+            this.itemPercentMap.set(key, percent);
             if (direction === 'horizontal') {
-                child.style.width = `calc(${undefineSizePercent / undefinedTotal * value}% - ${this.itemMinusMap.get(key)}px)`;
+                child.style.width = `calc(${percent}% - ${this.itemMinusMap.get(key)}px)`;
             } else {
-                child.style.height = `calc(${undefineSizePercent / undefinedTotal * value}% - ${this.itemMinusMap.get(key)}px)`;
+                child.style.height = `calc(${percent}% - ${this.itemMinusMap.get(key)}px)`;
             }
         });
     }
 
+    ensureConstraint = debounce(() => {
+        // 浏览器拖拽时保证px值最大最小仍生效
+        const { direction } = this.getProps();
+        const itemCount = this._adapter.getItemCount();
+        let continueFlag = true;
+        for (let i = 0; i < itemCount; i++) {
+            const child = this._adapter.getItem(i);
+            const childSize = direction === 'horizontal' ? child.offsetWidth : child.offsetHeight;
+            // 判断由非鼠标拖拽导致item的size变化过程中是否有超出限制的情况
+            const childFlag = judgeConstraint(childSize, this._adapter.getItemMin(i), this._adapter.getItemMax(i), this.groupSize, this.itemMinusMap.get(i));
+            if (childFlag) {
+                const childNewSize = adjustNewSize(childSize, this._adapter.getItemMin(i), this._adapter.getItemMax(i), this.groupSize, this.itemMinusMap.get(i));
+                for (let j = i + 1; j < itemCount; j++) {
+                    // 找到下一个没有超出限制的item
+                    const item = this._adapter.getItem(j);
+                    const itemSize = direction === 'horizontal' ? item.offsetWidth : item.offsetHeight;
+                    const itemFlag = judgeConstraint(itemSize, this._adapter.getItemMin(j), this._adapter.getItemMax(j), this.groupSize, this.itemMinusMap.get(j));
+                    if (!itemFlag) {
+                        let childPercent = this.itemPercentMap.get(i),
+                            itemPercent = this.itemPercentMap.get(j);
+                        let childNewPercent = childNewSize / this.groupSize * 100;
+                        let itemNewPercent = childPercent + itemPercent - childNewPercent;
+                        this.itemPercentMap.set(i, childNewPercent);
+                        this.itemPercentMap.set(j, itemNewPercent);
+                        if (direction === 'horizontal') {
+                            child.style.width = `calc(${childNewPercent}% - ${this.itemMinusMap.get(i)}px)`;
+                            item.style.width = `calc(${itemNewPercent}% - ${this.itemMinusMap.get(j)}px)`;
+                        } else {
+                            child.style.height = `calc(${childNewPercent}% - ${this.itemMinusMap.get(i)}px)`;
+                            item.style.height = `calc(${itemNewPercent}% - ${this.itemMinusMap.get(j)}px)`;
+                        }
+                        break;
+                    } else {
+                        if (j === itemCount - 1) {
+                            continueFlag = false;
+                            console.warn('[Semi ResizableGroup]: no enough space to adjust min/max size');
+                        }
+                    }
+                }
+            }
+            if (!continueFlag) {
+                break;
+            }
+        }
+    }, 200) 
+
     destroy(): void {
         
     }

+ 0 - 25
packages/semi-foundation/resizable/groupConstants.ts

@@ -1,25 +0,0 @@
-// group
-const rowStyleBase = {
-    width: '100%',
-    height: '8px',
-    flexShrink: 0,
-    margin: '0',
-    cursor: 'row-resize',
-} as const;
-const colStyleBase = {
-    width: '8px',
-    flexShrink: 0,
-    height: '100%',
-    margin: '0',
-    cursor: 'col-resize',
-} as const;
-
-export const directionStyles = {
-    horizontal: {
-        ...colStyleBase,
-    },
-    vertical: {
-        ...rowStyleBase,
-    }
-} as const;
-

+ 100 - 3
packages/semi-foundation/resizable/resizable.scss

@@ -1,5 +1,4 @@
-@import './variables.scss';
-
+@import "./variables.scss";
 $module: #{$prefix}-resizable;
 
 .#{$module} {
@@ -13,8 +12,83 @@ $module: #{$prefix}-resizable;
         position: absolute;
         user-select: none;
         z-index: $z-resizable_handler;
+
+        // 基础样式
+        @mixin row-resize-base {
+            width: 100%;
+            height: $height-row-handler;
+            top: 0;
+            left: 0;
+            cursor: row-resize;
+        }
+
+        @mixin col-resize-base {
+            width: $width-col-handler;
+            height: 100%;
+            top: 0;
+            left: 0;
+            cursor: col-resize;
+        }
+
+        @mixin edge-resize-base {
+            width: $width-edge-handler;
+            height: $height-edge-handler;
+            position: absolute;
+        }
+
+        // 方向样式
+        &-top {
+            @include row-resize-base;
+            top: calc(-1 * $height-row-handler / 2);
+        }
+
+        &-right {
+            @include col-resize-base;
+            left: auto;
+            right: calc(-1 * $width-col-handler / 2);
+        }
+
+        &-bottom {
+            @include row-resize-base;
+            top: auto;
+            bottom: calc(-1 * $height-row-handler / 2);
+        }
+
+        &-left {
+            @include col-resize-base;
+            left: calc(-1 * $width-col-handler / 2);
+        }
+
+        // 边角样式
+        &-topRight {
+            @include edge-resize-base;
+            right: calc(-1 * $width-edge-handler / 2);
+            top: calc(-1 * $height-edge-handler / 2);
+            cursor: ne-resize;
+        }
+
+        &-bottomRight {
+            @include edge-resize-base;
+            right: calc(-1 * $width-edge-handler / 2);
+            bottom: calc(-1 * $height-edge-handler / 2);
+            cursor: se-resize;
+        }
+
+        &-bottomLeft {
+            @include edge-resize-base;
+            left: calc(-1 * $width-edge-handler / 2);
+            bottom: calc(-1 * $height-edge-handler / 2);
+            cursor: sw-resize;
+        }
+
+        &-topLeft {
+            @include edge-resize-base;
+            left: calc(-1 * $width-edge-handler / 2);
+            top: calc(-1 * $height-edge-handler / 2);
+            cursor: nw-resize;
+        }
     }
-    
+
     &-group {
         display: flex;
         position: relative;
@@ -37,5 +111,28 @@ $module: #{$prefix}-resizable;
         justify-content: center;
         background-color: var(--semi-color-fill-0);
         opacity: 1;
+
+        &-vertical {
+            width: 100%;
+            height: $height-vertical-handler;
+            flex-shrink: 0;
+            cursor: row-resize;
+        }
+
+        &-horizontal {
+            height: 100%;
+            width: $width-horizontal-handler;
+            flex-shrink: 0;
+            cursor: col-resize;
+        }
+    }
+
+    &-background {
+        height: 100%;
+        width: 100%;
+        inset: 0;
+        z-index: $z-resizable_background;
+        opacity: 0;
+        position: fixed;
     }
 }

+ 1 - 1
packages/semi-foundation/resizable/single/index.ts

@@ -1,5 +1,5 @@
 import BaseFoundation, { DefaultAdapter } from '../../base/foundation';
-import { DEFAULT_SIZE, Size, NumberSize, Direction, NewSize } from "../singleConstants";
+import { DEFAULT_SIZE, Size, NumberSize, Direction, NewSize } from "../types";
 import { getStringSize, getNumberSize, has, calculateNewMax, findNextSnap, snap, clamp } from "../utils";
 export interface ResizableHandlerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
     registerEvent: () => void;

+ 0 - 65
packages/semi-foundation/resizable/singleConstants.ts → packages/semi-foundation/resizable/types.ts

@@ -1,71 +1,6 @@
 // single
-const rowStyleBase = {
-    width: '100%',
-    height: '10px',
-    top: '0px',
-    left: '0px',
-    cursor: 'row-resize',
-} as const;
-const colStyleBase = {
-    width: '10px',
-    height: '100%',
-    top: '0px',
-    left: '0px',
-    cursor: 'col-resize',
-} as const;
-const edgeStyleBase = {
-    width: '20px',
-    height: '20px',
-    position: 'absolute',
-} as const;
-
 export const directions = ['top', 'right', 'bottom', 'left', 'topRight', 'bottomRight', 'bottomLeft', 'topLeft'] as const;
 
-export const directionStyles = {
-    top: {
-        ...rowStyleBase,
-        top: '-5px',
-    },
-    right: {
-        ...colStyleBase,
-        left: undefined,
-        right: '-5px',
-    },
-    bottom: {
-        ...rowStyleBase,
-        top: undefined,
-        bottom: '-5px',
-    },
-    left: {
-        ...colStyleBase,
-        left: '-5px',
-    },
-    topRight: {
-        ...edgeStyleBase,
-        right: '-10px',
-        top: '-10px',
-        cursor: 'ne-resize',
-    },
-    bottomRight: {
-        ...edgeStyleBase,
-        right: '-10px',
-        bottom: '-10px',
-        cursor: 'se-resize',
-    },
-    bottomLeft: {
-        ...edgeStyleBase,
-        left: '-10px',
-        bottom: '-10px',
-        cursor: 'sw-resize',
-    },
-    topLeft: {
-        ...edgeStyleBase,
-        left: '-10px',
-        top: '-10px',
-        cursor: 'nw-resize',
-    },
-} as const;
-
 export type Direction = 'top' | 'right' | 'bottom' | 'left' | 'topRight' | 'bottomRight' | 'bottomLeft' | 'topLeft';
 
 export interface HandleClassName {

+ 7 - 6
packages/semi-foundation/resizable/utils.ts

@@ -80,7 +80,8 @@ export const calculateNewMax = (
         minWidth,
         minHeight,
     };
-};export const getItemDirection = (dir: 'vertical' | 'horizontal') => {
+};
+export const getItemDirection = (dir: 'vertical' | 'horizontal') => {
     if (dir === 'vertical') {
         return ['bottom', 'top'];
     } else {
@@ -104,10 +105,10 @@ export const judgeConstraint = (newSize: number, min: string, max: string, paren
     max = max ?? "100%";
     const minSize = getPixelSize(min, parentSize);
     const maxSize = getPixelSize(max, parentSize);
-    if (newSize <= minSize + offset) {
+    if (newSize < minSize + offset) {
         return true;
     }
-    if (newSize >= maxSize - offset) {
+    if (newSize > maxSize) {
         return true;
     }
     return false;
@@ -118,11 +119,11 @@ export const adjustNewSize = (newSize: number, min: string, max: string, parentS
     max = max ?? "100%";
     const minSize = getPixelSize(min, parentSize);
     const maxSize = getPixelSize(max, parentSize);
-    if (newSize <= minSize + offset) {
+    if (newSize < minSize + offset) {
         return minSize + offset;
     }
-    if (newSize >= maxSize - offset) {
-        return maxSize - offset;
+    if (newSize > maxSize) {
+        return maxSize;
     }
     return newSize;
 };

+ 8 - 0
packages/semi-foundation/resizable/variables.scss

@@ -1 +1,9 @@
 $z-resizable_handler: 2000 !default; // 伸缩框组件中handler的z-index
+$z-resizable_background: 2010; // 伸缩框组件中背景的z-index
+
+$height-row-handler: 10px; // 单个伸缩框中上下handler的高度
+$width-col-handler: 10px;  // 单个伸缩框中左右handler的宽度
+$width-edge-handler: 20px; // 单个伸缩框中边角handler的宽度
+$height-edge-handler: 20px; // 单个伸缩框中边角handler的高度
+$width-horizontal-handler: 10px; // 组合伸缩框中水平方向handler的宽度
+$height-vertical-handler: 10px;  // 组合伸缩框中垂直方向handler的高度

+ 2 - 1
packages/semi-theme-default/scss/variables.scss

@@ -48,7 +48,8 @@ $z-image_preview_header: 1; // Image 组件预览层中 header 部分 z-index
 // 正在拖拽中的元素的 z-index,需要高于所有的弹出层组件 z-index
 $z-transfer_right_item_drag_item_move: 2000; // 穿梭框右侧面板中正在拖拽元素的z-index
 $z-tagInput_drag_item_move: 2000; // 标签输入框中正在拖拽元素的z-index
-$z-resizable_handler: 2000; // 伸缩框组件中handler的z-index   
+$z-resizable_handler: 2000; // 伸缩框组件中handler的z-index 
+// $z-resizable_background: 2010; // 伸缩框组件中背景的z-index
 
 // font
 $font-family-regular: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',

+ 163 - 5
packages/semi-ui/resizable/_story/resizable.stories.jsx

@@ -7,6 +7,108 @@ export default {
 
 import { ResizeItem, ResizeHandler, ResizeGroup } from '../../index'
 
+export const Group = () => {
+  const [text, setText] = useState('Drag to resize')
+  return (
+    <div style={{ width: '1118px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          defaultSize={"80%"}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"25%"}
+              min={'10%'}
+              max={'30%'}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text + ' min:10% max:30%'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ height: '100%' }}>
+                <ResizeGroup direction='vertical'>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                    defaultSize={'33%'}
+                    min={'10%'}
+                    onChange={(a, b, c) => { setText('resizing'); console.log(a, b, c)  }}
+                    onResizeEnd={() => { setText('Drag to resize') }}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text + " min:10%"}
+                    </div>
+                  </ResizeItem>
+                  <ResizeHandler></ResizeHandler>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                    defaultSize={'33%'}
+                    min={'10%'}
+                    max={'40%'}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text + " min:10% max:40%"}
+                    </div>
+                  </ResizeItem>
+                  <ResizeHandler></ResizeHandler>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text}
+                    </div>
+                  </ResizeItem>
+                </ResizeGroup>
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"1"}
+              max={'30%'}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text + ' max:30%'}
+              </div>
+            </ResizeItem>
+            
+          </ResizeGroup>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'tab'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'content'}
+              </div>
+            </ResizeItem>
+          </ResizeGroup>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
 export const Group_layout = () => {
   const [text, setText] = useState('test')
   const opts_1 = {
@@ -228,13 +330,13 @@ export const Group_horizontal = () => {
       <ResizeGroup direction='horizontal'>
         <ResizeItem
           style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
-          defaultSize={2}
-          min={'10%'}
+          defaultSize={'15%'}
+          min={'50px'}
           onChange={() => { setText('resizing') }}
           onResizeEnd={() => { setText('test') }}
         >
           <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
-            {text + " min:10%"}
+            {text + " min:50px"}
           </div>
         </ResizeItem>
         <ResizeHandler></ResizeHandler>
@@ -252,13 +354,69 @@ export const Group_horizontal = () => {
         <ResizeHandler></ResizeHandler>
         <ResizeItem
           style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)',  }}
-          defaultSize={'600px'}
+          defaultSize={'300px'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)',  }}
+          defaultSize={1.3}
           onChange={() => { setText('resizing') }}
         >
           <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
             {text}
           </div>
         </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+export const Group_dynamic_direction = () => {
+  const [text, setText] = useState('drag to resize')
+  const [direction, setDirection] = useState('horizontal')
+
+  const changeDirection = () => {
+    if (direction === 'horizontal') {
+      setDirection('vertical')
+    } else {
+      setDirection('horizontal')
+    }
+  }
+  return (
+    <div style={{ width: '1000px', height: '500px' }}>
+      <Button onClick={changeDirection}>{direction}</Button>
+      <ResizeGroup direction={direction}>
+        <ResizeItem
+          onChange={() => { setText('resizing') }}
+          onResizeEnd={() => { setText('drag to resize') }}
+          defaultSize={8}
+        >
+            <ResizeGroup direction='horizontal'>
+              <ResizeItem
+                style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+                onChange={() => { setText('resizing') }}
+                onResizeEnd={() => { setText('drag to resize') }}
+              >
+                <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+                  {text}
+                </div>
+              </ResizeItem>
+              <ResizeHandler></ResizeHandler>
+              <ResizeItem
+                style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+                onChange={() => { setText('resizing') }}
+              >
+                <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+                  {text}
+                </div>
+              </ResizeItem>
+            </ResizeGroup>
+        </ResizeItem>
         <ResizeHandler></ResizeHandler>
         <ResizeItem
           style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)',  }}
@@ -307,7 +465,7 @@ export const Single_defaultSize = () => {
   );
 }
 
-export const Single_Enabel = () => {
+export const Single_Enable = () => {
   const [b, setB] = useState(false)
   return (
     <div style={{ width: '500px', height: '60%' }}>

+ 1 - 1
packages/semi-ui/resizable/group/resizeContext.ts

@@ -1,5 +1,5 @@
 import React, { createContext, RefObject } from 'react';
-import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/types';
 
 export interface ResizeContextProps {
     direction: 'horizontal' | 'vertical';

+ 51 - 40
packages/semi-ui/resizable/group/resizeGroup.tsx

@@ -1,11 +1,11 @@
-import React, { createContext, createRef, ReactNode, Ref, RefObject } from 'react';
+import React, { createRef, ReactNode, RefObject } from 'react';
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import { ResizeGroupFoundation, ResizeGroupAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
 import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
 import BaseComponent from '../../_base/baseComponent';
 import { ResizeContext, ResizeContextProps } from './resizeContext';
-import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/types';
 import "@douyinfe/semi-foundation/resizable/resizable.scss";
 
 const prefixCls = cssClasses.PREFIX;
@@ -27,7 +27,8 @@ export interface ResizeGroupState {
         nextOffset: number
     };
     backgroundStyle: React.CSSProperties;
-    curHandler: number
+    curHandler: number;
+    contextValue: ResizeContextProps
 }
 
 class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
@@ -41,6 +42,8 @@ class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
 
     constructor(props: ResizeGroupProps) {
         super(props);
+        this.groupRef = createRef();
+        this.foundation = new ResizeGroupFoundation(this.adapter);
         this.state = {
             isResizing: false,
             originalPosition: {
@@ -52,68 +55,69 @@ class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
                 nextOffset: 0,
             },
             backgroundStyle: {
-                height: '100%',
-                width: '100%',
-                backgroundColor: 'rgba(0,0,0,0)',
                 cursor: 'auto',
-                opacity: 0,
-                position: 'fixed',
-                zIndex: 9999,
-                top: '0',
-                left: '0',
-                bottom: '0',
-                right: '0',
             },
             curHandler: null,
-        };
-        
-        this.groupRef = createRef();
-        this.foundation = new ResizeGroupFoundation(this.adapter);
-        this.contextValue = {
-            direction: props.direction,
-            registerItem: this.registerItem,
-            registerHandler: this.registerHandler,
-            notifyResizeStart: this.foundation.onResizeStart,
-            getGroupSize: this.getGroupSize,
+            contextValue: {
+                direction: props.direction,
+                registerItem: this.registerItem,
+                registerHandler: this.registerHandler,
+                notifyResizeStart: this.foundation.onResizeStart,
+                getGroupSize: this.getGroupSize,
+            },
         };
     }
 
-    contextValue: ResizeContextProps;
     foundation: ResizeGroupFoundation;
     groupRef: React.RefObject<HTMLDivElement>;
     groupSize: number;
     availableSize: number;
     static contextType = ResizeContext;
     context: ResizeGroupProps;
-    itemRefs: RefObject<HTMLDivElement>[] = [];
+    // 在context中使用的属性需要考虑在strictMode下会执行两次,所以用Map来维护
+    itemRefs: Map<number, RefObject<HTMLDivElement>> = new Map();
     itemMinMap: Map<number, string> = new Map();
     itemMaxMap: Map<number, string> = new Map();
     itemMinusMap: Map<number, number> = new Map();
-    itemDefaultSizeList: (string|number)[] = []
+    itemDefaultSizeList: Map<number, (string|number)> = new Map();
     itemResizeStart: Map<number, ResizeStartCallback> = new Map();
     itemResizing: Map<number, ResizeCallback> = new Map();
     itemResizeEnd: Map<number, ResizeCallback> = new Map();
-    handlerRefs: RefObject<HTMLDivElement>[] = [];
+    handlerRefs: Map<number, RefObject<HTMLDivElement>> = new Map();
 
     componentDidMount() {
         this.foundation.init();
+        // 监听窗口大小变化,保证一些限制仍生效
+        window.addEventListener('resize', this.foundation.ensureConstraint);
     }
 
-    componentDidUpdate(_prevProps: ResizeGroupProps) {
+    componentDidUpdate(prevProps: ResizeGroupProps) {
+        // 支持动态调整伸缩direction
+        if (this.props.direction !== prevProps.direction) {
+            this.setState((prevState) => ({
+                ...prevState, // 保留其他状态
+                contextValue: {
+                    ...prevState.contextValue, // 保留其他上下文值
+                    direction: this.props.direction,
+                }
+            }));
+            this.foundation.direction = this.props.direction;
+        }
     }
 
     componentWillUnmount() {
         this.foundation.destroy();
+        window.removeEventListener('resize', this.foundation.ensureConstraint);
     }
 
     get adapter(): ResizeGroupAdapter<ResizeGroupProps, ResizeGroupState> {
         return {
             ...super.adapter,
             getGroupRef: () => this.groupRef.current,
-            getItem: (id: number) => this.itemRefs[id].current,
-            getItemCount: () => this.itemRefs.length,
-            getHandler: (id: number) => this.handlerRefs[id].current,
-            getHandlerCount: () => this.handlerRefs.length,
+            getItem: (id: number) => this.itemRefs.get(id).current,
+            getItemCount: () => this.itemRefs.size,
+            getHandler: (id: number) => this.handlerRefs.get(id).current,
+            getHandlerCount: () => this.handlerRefs.size,
             getItemMin: (index) => {
                 return this.itemMinMap.get(index);
             },
@@ -130,7 +134,7 @@ class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
                 return this.itemResizeStart.get(index);
             },
             getItemDefaultSize: (index) => {
-                return this.itemDefaultSizeList[index];
+                return this.itemDefaultSizeList.get(index);
             },
             registerEvents: this.registerEvent,
             unregisterEvents: this.unregisterEvent,
@@ -161,11 +165,14 @@ class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
         min: string, max: string, defaultSize: string|number,
         onResizeStart: ResizeStartCallback, onChange: ResizeCallback, onResizeEnd: ResizeCallback
     ) => {
-        this.itemRefs.push(ref);
-        let index = this.itemRefs.length - 1;
+        if (Array.from(this.itemRefs.values()).some(r => r === ref)) {
+            return -1;
+        }
+        let index = this.itemRefs.size;
+        this.itemRefs.set(index, ref);
         this.itemMinMap.set(index, min);
         this.itemMaxMap.set(index, max);
-        this.itemDefaultSizeList.push(defaultSize);
+        this.itemDefaultSizeList.set(index, defaultSize);
         this.itemResizeStart.set(index, onResizeStart);
         this.itemResizing.set(index, onChange);
         this.itemResizeEnd.set(index, onResizeEnd);
@@ -173,8 +180,12 @@ class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
     }
 
     registerHandler = (ref: RefObject<HTMLDivElement>) => {
-        this.handlerRefs.push(ref);
-        return this.handlerRefs.length - 1;
+        if (Array.from(this.handlerRefs.values()).some(r => r === ref)) {
+            return -1;
+        }
+        let index = this.handlerRefs.size;
+        this.handlerRefs.set(index, ref);
+        return index;
     }
 
     getGroupSize = () => {
@@ -184,7 +195,7 @@ class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
     render() {
         const { children, direction, className, ...rest } = this.props;
         return (
-            <ResizeContext.Provider value={this.contextValue}>
+            <ResizeContext.Provider value={this.state.contextValue}>
                 <div
                     style={{
                         flexDirection: direction === 'vertical' ? 'column' : 'row',
@@ -193,7 +204,7 @@ class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
                     className={classNames(className, prefixCls + '-group')}
                     {...rest}
                 >
-                    {this.state.isResizing && <div style={this.state.backgroundStyle} />}
+                    {this.state.isResizing && <div style={this.state.backgroundStyle} className={classNames(className, prefixCls + '-background')}/>}
                     {children}
                 </div>
             </ResizeContext.Provider>

+ 8 - 9
packages/semi-ui/resizable/group/resizeHandler.tsx

@@ -3,8 +3,7 @@ import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import { ResizeHandlerFoundation, ResizeHandlerAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
 import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
-import { Direction, HandlerCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
-import { directionStyles } from '@douyinfe/semi-foundation/resizable/groupConstants';
+import { Direction, HandlerCallback } from '@douyinfe/semi-foundation/resizable/types';
 import BaseComponent from '../../_base/baseComponent';
 import { ResizeContext, ResizeContextProps } from './resizeContext';
 import { IconHandle } from '@douyinfe/semi-icons';
@@ -43,11 +42,14 @@ class ResizeHandler extends BaseComponent<ResizeHandlerProps, ResizeHandlerState
         };
         this.handlerRef = createRef();
         this.foundation = new ResizeHandlerFoundation(this.adapter);
+        this.handlerIndex = -1;
     }
 
     componentDidMount() {
         this.foundation.init();
-        this.handlerIndex = this.context.registerHandler(this.handlerRef);
+        if (this.handlerIndex === -1) {
+            this.handlerIndex = this.context.registerHandler(this.handlerRef);
+        }
     }
 
     componentDidUpdate(_prevProps: ResizeHandlerProps) {
@@ -85,15 +87,12 @@ class ResizeHandler extends BaseComponent<ResizeHandlerProps, ResizeHandlerState
     handlerIndex: number;
 
     render() {
-        
         const { style, className, children } = this.props;
+        const { direction } = this.context;
         return (
             <div
-                className={classNames(className, prefixCls + '-handler')}
-                style={{
-                    ...directionStyles[this.context.direction],
-                    ...style
-                }}
+                className={classNames(className, prefixCls + '-handler', prefixCls + '-handler-' + direction)}
+                style={style}
                 ref={this.handlerRef}
             >
                 {children ?? <IconHandle size='inherit' style={{

+ 22 - 4
packages/semi-ui/resizable/group/resizeItem.tsx

@@ -1,10 +1,10 @@
-import React, { createRef, ReactNode, useContext } from 'react';
+import React, { createRef } from 'react';
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import { ResizeItemFoundation, ResizeItemAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
 import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
 import BaseComponent from '../../_base/baseComponent';
-import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/types';
 import { ResizeContext, ResizeContextProps } from './resizeContext';
 import { noop } from 'lodash';
 
@@ -51,16 +51,33 @@ class ResizeItem extends BaseComponent<ResizeItemProps, ResizeItemState> {
         this.state = {
             isResizing: false,
         };
-
+        this.itemIndex = -1;
     }
 
     componentDidMount() {
         this.foundation.init();
         const { min, max, onResizeStart, onChange, onResizeEnd, defaultSize } = this.props;
-        this.itemIndex = this.context.registerItem(this.itemRef, min, max, defaultSize, onResizeStart, onChange, onResizeEnd);
+        if (this.itemIndex === -1) {
+            // 开发过程在StrictMode下context方法会执行两次,需要判断一下是否已经注册过
+            this.itemIndex = this.context.registerItem(this.itemRef, min, max, defaultSize, onResizeStart, onChange, onResizeEnd);
+        }
+        this.direction = this.context.direction; // 留一个direction的引用,方便在componentDidUpdate中判断方向是否有变化
     }
 
     componentDidUpdate(_prevProps: ResizeItemProps) {
+        // 支持动态方向,修改item的style
+        if (this.context.direction !== this.direction) {
+            this.direction = this.context.direction;
+            if (this.direction === 'horizontal') {
+                const newWidth = this.itemRef.current?.style.height;
+                this.itemRef.current.style.width = newWidth;
+                this.itemRef.current.style.removeProperty('height');
+            } else {
+                const newHeight = this.itemRef.current?.style.width;
+                this.itemRef.current.style.height = newHeight;
+                this.itemRef.current.style.removeProperty('width');
+            }
+        }
     }
 
     componentWillUnmount() {
@@ -74,6 +91,7 @@ class ResizeItem extends BaseComponent<ResizeItemProps, ResizeItemState> {
     }
     static contextType = ResizeContext;
     context: ResizeContextProps;
+    direction: 'horizontal' | 'vertical';
     itemRef: React.RefObject<HTMLDivElement | null>;
     itemIndex: number;
 

+ 2 - 12
packages/semi-ui/resizable/single/resizable.tsx

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { ResizableFoundation, ResizableAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
 
 import { cssClasses, } from '@douyinfe/semi-foundation/resizable/constants';
-import { Direction, Size, Enable, ResizeStartCallback, ResizeCallback, HandleClassName, directions } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import { Direction, Size, Enable, ResizeStartCallback, ResizeCallback, HandleClassName, directions } from '@douyinfe/semi-foundation/resizable/types';
 import BaseComponent from '../../_base/baseComponent';
 import ResizableHandler from './resizableHandler';
 import '@douyinfe/semi-foundation/resizable/resizable.scss';
@@ -159,17 +159,7 @@ class Resizable extends BaseComponent<ResizableProps, ResizableState> {
                 height: 0,
             },
             backgroundStyle: {
-                height: '100%',
-                width: '100%',
-                backgroundColor: 'rgba(0,0,0,0)',
                 cursor: 'auto',
-                opacity: 0,
-                position: 'fixed',
-                zIndex: 9999,
-                top: '0',
-                left: '0',
-                bottom: '0',
-                right: '0',
             },
             flexBasis: undefined,
         };        
@@ -262,7 +252,7 @@ class Resizable extends BaseComponent<ResizableProps, ResizableState> {
                 ref={this.resizableRef}
                 {...this.getDataAttr(this.props)}
             >
-                {this.state.isResizing && <div style={this.state.backgroundStyle} />}
+                {this.state.isResizing && <div style={this.state.backgroundStyle} className={classNames(className, prefixCls + '-background')}/>}
                 {children}
                 {this.renderResizeHandler()}
             </div>

+ 2 - 3
packages/semi-ui/resizable/single/resizableHandler.tsx

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { ResizableHandlerFoundation, ResizableHandlerAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
 
 import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
-import { directionStyles, Direction, HandlerCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import { Direction, HandlerCallback } from '@douyinfe/semi-foundation/resizable/types';
 import BaseComponent from '../../_base/baseComponent';
 
 const prefixCls = cssClasses.PREFIX;
@@ -74,9 +74,8 @@ class ResizableHandler extends BaseComponent<ResizableHandlerProps, ResizableHan
         const { children, style, className } = this.props;
         return (
             <div 
-                className={classNames(className, prefixCls + '-resizableHandler')}
+                className={classNames(className, prefixCls + '-resizableHandler', prefixCls + '-resizableHandler-' + this.props.direction)}
                 style={{
-                    ...directionStyles[this.props.direction],
                     ...style
                 }} 
                 ref={this.resizeHandlerRef}