Skip to content

Commit

Permalink
Merge pull request #2573 from XiaoMi/feature/resize-box(#2508)
Browse files Browse the repository at this point in the history
feat: add resize box(#2508)
  • Loading branch information
solarjoker authored Aug 25, 2023
2 parents 669e38d + 5aef6a5 commit 0bd0b2f
Show file tree
Hide file tree
Showing 25 changed files with 583 additions and 0 deletions.
1 change: 1 addition & 0 deletions .changeset-pending/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
暂时不发版的变更记录文件放在此目录
6 changes: 6 additions & 0 deletions .changeset-pending/sixty-elephants-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hi-ui/hiui": minor
"@hi-ui/resize-box": minor
---

feat: 增加 ResizeBox 组件
5 changes: 5 additions & 0 deletions .changeset/eleven-actors-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hi-ui/icons": patch
---

增加 DragDot 图标
1 change: 1 addition & 0 deletions packages/icons/icon-resources/common/outlined/drag-dot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions packages/icons/src/components/common/drag-dot-outlined.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

import React, { forwardRef } from 'react'
import { cx, getPrefixCls } from '@hi-ui/classname'
import { __DEV__ } from '@hi-ui/env'
import { IconProps } from '../../@types/props'

const _prefix = getPrefixCls('icon-drag-dot-outlined')

export const DragDotOutlined = forwardRef<SVGSVGElement | null, IconProps>(
({ prefixCls = _prefix, className, children, size, style: styleProp, ...rest }, ref) => {
const cls = cx(prefixCls, className)
const style = { fontSize: size, ...styleProp }

return (
<svg className={cls} ref={ref} role="icon" style={style} {...rest} viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9486" ><path d="M661.333333 85.333333a64 64 0 0 1 64 64v42.666667a64 64 0 0 1-64 64h-42.666666a64 64 0 0 1-64-64V149.333333a64 64 0 0 1 64-64h42.666666zM661.333333 405.333333a64 64 0 0 1 64 64v42.666667a64 64 0 0 1-64 64h-42.666666a64 64 0 0 1-64-64v-42.666667a64 64 0 0 1 64-64h42.666666zM725.333333 789.333333a64 64 0 0 0-64-64h-42.666666a64 64 0 0 0-64 64v42.666667a64 64 0 0 0 64 64h42.666666a64 64 0 0 0 64-64v-42.666667zM405.333333 85.333333a64 64 0 0 1 64 64v42.666667a64 64 0 0 1-64 64h-42.666666a64 64 0 0 1-64-64V149.333333a64 64 0 0 1 64-64h42.666666zM405.333333 405.333333a64 64 0 0 1 64 64v42.666667a64 64 0 0 1-64 64h-42.666666a64 64 0 0 1-64-64v-42.666667a64 64 0 0 1 64-64h42.666666zM469.333333 789.333333a64 64 0 0 0-64-64h-42.666666a64 64 0 0 0-64 64v42.666667a64 64 0 0 0 64 64h42.666666a64 64 0 0 0 64-64v-42.666667z" p-id="9487"></path></svg>
)
}
)

if (__DEV__) {
DragDotOutlined.displayName = 'DragDotOutlined'
}

1 change: 1 addition & 0 deletions packages/icons/src/icon-summation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export { DislikeOutlined } from './components/common/dislike-outlined'
export { DocumentOutlined } from './components/common/document-outlined'
export { DocumentExclamationOutlined } from './components/common/document-exclamation-outlined'
export { DownloadOutlined } from './components/common/download-outlined'
export { DragDotOutlined } from './components/common/drag-dot-outlined'
export { EndDateOutlined } from './components/common/end-date-outlined'
export { ExportOutlined } from './components/common/export-outlined'
export { ExpressionOutlined } from './components/common/expression-outlined'
Expand Down
2 changes: 2 additions & 0 deletions packages/icons/stories/basic.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ export const Basic = () => {

{ component: Icons.DownloadOutlined, tagName: 'DownloadOutlined' },

{ component: Icons.DragDotOutlined, tagName: 'DragDotOutlined' },

{ component: Icons.EndDateOutlined, tagName: 'EndDateOutlined' },

{ component: Icons.ExportOutlined, tagName: 'ExportOutlined' },
Expand Down
1 change: 1 addition & 0 deletions packages/ui/hiui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@hi-ui/provider": "^4.0.5",
"@hi-ui/radio": "^4.0.4",
"@hi-ui/rating": "^4.0.5",
"@hi-ui/resize-box": "^4.0.0",
"@hi-ui/result": "^4.0.4",
"@hi-ui/scrollbar": "^4.0.1",
"@hi-ui/search": "^4.0.8",
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/hiui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ export { default as Radio } from '@hi-ui/radio'
export * from '@hi-ui/rating'
export { default as Rating } from '@hi-ui/rating'

export * from '@hi-ui/resize-box'
export { default as ResizeBox } from '@hi-ui/resize-box'

export * from '@hi-ui/result'
export { default as Result } from '@hi-ui/result'

Expand Down
11 changes: 11 additions & 0 deletions packages/ui/resize-box/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `@hi-ui/resize-box`

> TODO: description
## Usage

```
const ResizeBox = require('@hi-ui/resize-box');
// TODO: DEMONSTRATE API
```
5 changes: 5 additions & 0 deletions packages/ui/resize-box/__tests__/resize-box.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const ResizeBox = require('../src')

describe('@hi-ui/resize-box', () => {
it('needs tests', () => {})
})
11 changes: 11 additions & 0 deletions packages/ui/resize-box/hi-docs.config.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ResizeBox 伸缩框

用于可调整大小的布局

## 使用示例

<!-- Inject Stories -->

## Props

<!-- Inject Props -->
1 change: 1 addition & 0 deletions packages/ui/resize-box/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../../jest.config')
63 changes: 63 additions & 0 deletions packages/ui/resize-box/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@hi-ui/resize-box",
"version": "4.0.0-alpha.0",
"description": "A sub-package for @hi-ui/hiui.",
"keywords": [],
"author": "HiUI <[email protected]>",
"homepage": "https://github.com/XiaoMi/hiui/tree/master/packages/ui/resize-box#readme",
"license": "MIT",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"main": "lib/cjs/index.js",
"module": "lib/esm/index.js",
"types": "lib/types/index.d.ts",
"typings": "lib/types/index.d.ts",
"exports": {
".": {
"require": "./lib/cjs/index.js",
"default": "./lib/esm/index.js"
}
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/XiaoMi/hiui.git"
},
"scripts": {
"test": "jest",
"clean": "rimraf lib",
"prebuild": "yarn clean",
"build:esm": "hi-build ./src/index.ts --format esm -d ./lib/esm",
"build:cjs": "hi-build ./src/index.ts --format cjs -d ./lib/cjs",
"build:types": "tsc --emitDeclarationOnly --declaration --declarationDir lib/types",
"build": "concurrently yarn:build:*"
},
"bugs": {
"url": "https://github.com/XiaoMi/hiui/issues"
},
"dependencies": {
"@hi-ui/classname": "^4.0.0",
"@hi-ui/env": "^4.0.0",
"@hi-ui/use-merge-refs": "^4.0.1",
"@hi-ui/use-uncontrolled-state": "^4.0.1",
"react-resizable": "^3.0.5"
},
"peerDependencies": {
"@hi-ui/core": ">=4.0.0",
"react": ">=16.8.6",
"react-dom": ">=16.8.6"
},
"devDependencies": {
"@hi-ui/core": "^4.0.0",
"@hi-ui/core-css": "^4.0.0",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}
223 changes: 223 additions & 0 deletions packages/ui/resize-box/src/ResizeBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import React, { forwardRef } from 'react'
import { Resizable } from 'react-resizable'
import { cx, getPrefixCls } from '@hi-ui/classname'
import { __DEV__ } from '@hi-ui/env'
import { HiBaseHTMLProps } from '@hi-ui/core'
import { useMergeRefs } from '@hi-ui/use-merge-refs'
import { useUncontrolledState } from '@hi-ui/use-uncontrolled-state'
import { ResizeBoxPane, ResizeBoxPaneProps } from './ResizeBoxPane'
import { Separator, SeparatorProps } from './Separator'

const RESIZE_BOX_PREFIX = getPrefixCls('resize-box')

export const ResizeBox = forwardRef<HTMLDivElement | null, ResizeBoxProps>(
(
{
prefixCls = RESIZE_BOX_PREFIX,
role = 'resize-box',
className,
children,
separatorProps,
...rest
},
ref
) => {
const cls = cx(prefixCls, className)

const containerRef = React.useRef<Element | null>(null)
const mergedRef = useMergeRefs(ref, containerRef)

const [colWidths, tryChangeColWidths] = useUncontrolledState<number[]>([])
const minColWidthsRef = React.useRef<number[]>([])

const draggableRef = React.useRef<boolean>(true)

/**
* 计算内容面板宽度
* 如果有设置默认宽度,则使用默认宽度,否则使用平均宽度
* 如果有设置最小宽度,则使用最小宽度,否则使用默认宽度的一半
*/
const calcPaneWidth = React.useCallback(() => {
const container = containerRef.current
const containerWidth = container?.getBoundingClientRect().width ?? 0
const minColWidths: number[] = []
let defaultColWidths: number[] = []
let calcWidth = 0
let avgWidth = 0

React.Children.forEach(children, (child) => {
const {
props: { defaultWidth = 0, minWidth = 0 },
} = child as React.ReactElement<ResizeBoxPaneProps>

defaultColWidths.push(defaultWidth)
minColWidths?.push(minWidth)

calcWidth += defaultWidth
})

if (calcWidth > containerWidth) {
console.error('default width is greater than container width')
return
}

if (calcWidth < containerWidth) {
const noDefaultWidthLength = defaultColWidths.filter((item) => !item).length

avgWidth = Math.floor((containerWidth - calcWidth) / noDefaultWidthLength)

defaultColWidths = defaultColWidths.map((item) => {
if (!item) {
return avgWidth
} else {
return item
}
})
}

tryChangeColWidths(defaultColWidths)

minColWidthsRef.current = minColWidths.map((item, index) => {
// 如果没有设置最小宽度,则最小宽度是默认宽度的一半
return item || defaultColWidths[index] * 0.5
})
}, [children, tryChangeColWidths])

const panesContent = React.useMemo(() => {
if (!children) {
console.error('children is required')
return null
}

if (!Array.isArray(children)) {
console.error('children must be array')
return children
}

return React.Children.map(
children as React.ReactElement<ResizeBoxPaneProps>[],
(child, index) => {
if (!React.isValidElement(child)) {
console.error('child is not valid element')
return
}

const { type, props } = child
const { style, onResizeStart, onResizeEnd, onResize, ...rest } = props

if (type !== ResizeBoxPane) {
console.error('ResizeBox children must be ResizeBoxPane')
return
}

if (index !== children?.length - 1) {
return (
<Resizable
className={`${prefixCls}__resizable`}
draggableOpts={{ enableUserSelectHack: false }}
handle={<Separator {...separatorProps} />}
height={0}
width={colWidths[index] ?? 0}
onResizeStart={() => {
draggableRef.current = true
onResizeStart?.()
}}
onResizeStop={onResizeEnd}
onResize={(evt, data) => {
const mouseEvent = evt as React.MouseEvent

mouseEvent.stopPropagation()
mouseEvent.preventDefault()

const { width: resizedWidth } = data.size

// 向左或向右拖动到最小宽度时禁止拖拽
if (
resizedWidth < minColWidthsRef.current[index] ||
colWidths[index] + colWidths[index + 1] - resizedWidth <
minColWidthsRef.current[index + 1]
) {
draggableRef.current = false
}

if (!draggableRef.current) return

tryChangeColWidths((prev) => {
const nextColWidths = [...prev]
const currentWidth = nextColWidths[index]
const siblingWidth = nextColWidths[index + 1]
const minColWidth = minColWidthsRef.current[index]
const siblingMinColWidth = minColWidthsRef.current[index + 1]
const width =
resizedWidth <= minColWidth
? // 显示最小宽度
minColWidth
: currentWidth + siblingWidth - resizedWidth < siblingMinColWidth
? // 能够显示的最大宽度
currentWidth + siblingWidth - siblingMinColWidth
: // 显示拖拽后的宽度
resizedWidth
const resizeWidth = width - currentWidth

nextColWidths[index] = width
nextColWidths[index + 1] = siblingWidth - resizeWidth

onResize?.(width)

return nextColWidths
})
}}
>
{React.cloneElement(child, {
...rest,
style: {
...style,
width: colWidths[index],
},
})}
</Resizable>
)
} else {
return React.cloneElement(child, {
...rest,
style: {
...style,
width: colWidths[index],
},
})
}
}
)
}, [children, colWidths, prefixCls, separatorProps, tryChangeColWidths])

React.useLayoutEffect(() => {
if (containerRef.current) {
calcPaneWidth()

const resizeObserver = new ResizeObserver(() => {
calcPaneWidth()
})

resizeObserver.observe(containerRef.current)

return () => {
resizeObserver.disconnect()
}
}
}, [calcPaneWidth])

return (
<div ref={mergedRef} role={role} className={cls} {...rest}>
{panesContent}
</div>
)
}
)

export interface ResizeBoxProps extends HiBaseHTMLProps<'div'> {
separatorProps?: SeparatorProps
}

if (__DEV__) {
ResizeBox.displayName = 'ResizeBox'
}
Loading

0 comments on commit 0bd0b2f

Please sign in to comment.