Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add resize box(#2508) #2573

Merged
merged 6 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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