diff --git a/.changeset-pending/README.md b/.changeset-pending/README.md new file mode 100644 index 000000000..a0665e70f --- /dev/null +++ b/.changeset-pending/README.md @@ -0,0 +1 @@ +暂时不发版的变更记录文件放在此目录 diff --git a/.changeset-pending/sixty-elephants-join.md b/.changeset-pending/sixty-elephants-join.md new file mode 100644 index 000000000..cb29c087f --- /dev/null +++ b/.changeset-pending/sixty-elephants-join.md @@ -0,0 +1,6 @@ +--- +"@hi-ui/hiui": minor +"@hi-ui/resize-box": minor +--- + +feat: 增加 ResizeBox 组件 diff --git a/.changeset/eleven-actors-beg.md b/.changeset/eleven-actors-beg.md new file mode 100644 index 000000000..0f2ea8ab9 --- /dev/null +++ b/.changeset/eleven-actors-beg.md @@ -0,0 +1,5 @@ +--- +"@hi-ui/icons": patch +--- + +增加 DragDot 图标 diff --git a/packages/icons/icon-resources/common/outlined/drag-dot.svg b/packages/icons/icon-resources/common/outlined/drag-dot.svg new file mode 100644 index 000000000..fa2bfdc6a --- /dev/null +++ b/packages/icons/icon-resources/common/outlined/drag-dot.svg @@ -0,0 +1 @@ + diff --git a/packages/icons/src/components/common/drag-dot-outlined.tsx b/packages/icons/src/components/common/drag-dot-outlined.tsx new file mode 100644 index 000000000..ccb44a946 --- /dev/null +++ b/packages/icons/src/components/common/drag-dot-outlined.tsx @@ -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( + ({ prefixCls = _prefix, className, children, size, style: styleProp, ...rest }, ref) => { + const cls = cx(prefixCls, className) + const style = { fontSize: size, ...styleProp } + + return ( + + ) + } +) + +if (__DEV__) { + DragDotOutlined.displayName = 'DragDotOutlined' +} + \ No newline at end of file diff --git a/packages/icons/src/icon-summation.ts b/packages/icons/src/icon-summation.ts index 9c690a274..e8dc77b67 100644 --- a/packages/icons/src/icon-summation.ts +++ b/packages/icons/src/icon-summation.ts @@ -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' diff --git a/packages/icons/stories/basic.stories.tsx b/packages/icons/stories/basic.stories.tsx index c8de37c98..b36d1a097 100644 --- a/packages/icons/stories/basic.stories.tsx +++ b/packages/icons/stories/basic.stories.tsx @@ -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' }, diff --git a/packages/ui/hiui/package.json b/packages/ui/hiui/package.json index 67d208977..b6f4be8ee 100644 --- a/packages/ui/hiui/package.json +++ b/packages/ui/hiui/package.json @@ -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", diff --git a/packages/ui/hiui/src/index.ts b/packages/ui/hiui/src/index.ts index 9bd2077c6..9b5168556 100644 --- a/packages/ui/hiui/src/index.ts +++ b/packages/ui/hiui/src/index.ts @@ -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' diff --git a/packages/ui/resize-box/README.md b/packages/ui/resize-box/README.md new file mode 100644 index 000000000..0c5cb050d --- /dev/null +++ b/packages/ui/resize-box/README.md @@ -0,0 +1,11 @@ +# `@hi-ui/resize-box` + +> TODO: description + +## Usage + +``` +const ResizeBox = require('@hi-ui/resize-box'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/ui/resize-box/__tests__/resize-box.test.js b/packages/ui/resize-box/__tests__/resize-box.test.js new file mode 100644 index 000000000..ec3ed8c08 --- /dev/null +++ b/packages/ui/resize-box/__tests__/resize-box.test.js @@ -0,0 +1,5 @@ +const ResizeBox = require('../src') + +describe('@hi-ui/resize-box', () => { + it('needs tests', () => {}) +}) diff --git a/packages/ui/resize-box/hi-docs.config.mdx b/packages/ui/resize-box/hi-docs.config.mdx new file mode 100644 index 000000000..1f1c95531 --- /dev/null +++ b/packages/ui/resize-box/hi-docs.config.mdx @@ -0,0 +1,11 @@ +# ResizeBox 伸缩框 + +用于可调整大小的布局 + +## 使用示例 + + + +## Props + + diff --git a/packages/ui/resize-box/jest.config.js b/packages/ui/resize-box/jest.config.js new file mode 100644 index 000000000..e33c14b5d --- /dev/null +++ b/packages/ui/resize-box/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../../jest.config') diff --git a/packages/ui/resize-box/package.json b/packages/ui/resize-box/package.json new file mode 100644 index 000000000..8f53191b1 --- /dev/null +++ b/packages/ui/resize-box/package.json @@ -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 ", + "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" + } +} diff --git a/packages/ui/resize-box/src/ResizeBox.tsx b/packages/ui/resize-box/src/ResizeBox.tsx new file mode 100644 index 000000000..ab61296a0 --- /dev/null +++ b/packages/ui/resize-box/src/ResizeBox.tsx @@ -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( + ( + { + prefixCls = RESIZE_BOX_PREFIX, + role = 'resize-box', + className, + children, + separatorProps, + ...rest + }, + ref + ) => { + const cls = cx(prefixCls, className) + + const containerRef = React.useRef(null) + const mergedRef = useMergeRefs(ref, containerRef) + + const [colWidths, tryChangeColWidths] = useUncontrolledState([]) + const minColWidthsRef = React.useRef([]) + + const draggableRef = React.useRef(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 + + 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[], + (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 ( + } + 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], + }, + })} + + ) + } 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 ( +
+ {panesContent} +
+ ) + } +) + +export interface ResizeBoxProps extends HiBaseHTMLProps<'div'> { + separatorProps?: SeparatorProps +} + +if (__DEV__) { + ResizeBox.displayName = 'ResizeBox' +} diff --git a/packages/ui/resize-box/src/ResizeBoxPane.tsx b/packages/ui/resize-box/src/ResizeBoxPane.tsx new file mode 100644 index 000000000..1660c1d97 --- /dev/null +++ b/packages/ui/resize-box/src/ResizeBoxPane.tsx @@ -0,0 +1,45 @@ +import React, { forwardRef } from 'react' +import { cx, getPrefixCls } from '@hi-ui/classname' +import { __DEV__ } from '@hi-ui/env' +import { HiBaseHTMLProps } from '@hi-ui/core' + +const RESIZE_BOX_PANE_PREFIX = getPrefixCls('resize-box-pane') + +export const ResizeBoxPane = forwardRef( + ( + { + prefixCls = RESIZE_BOX_PANE_PREFIX, + role = 'resize-box-pane', + className, + children, + defaultWidth, + minWidth, + onResizeStart, + onResizeEnd, + onResize, + ...rest + }, + ref + ) => { + const cls = cx(prefixCls, className) + + return ( +
+ {children} +
+ ) + } +) + +export interface ResizeBoxPaneProps extends HiBaseHTMLProps<'div'> { + defaultWidth?: number + width?: number + minWidth?: number + onResizeStart?: () => void + onResizeEnd?: () => void + onResize?: (width: number) => void +} + +if (__DEV__) { + ResizeBoxPane.displayName = 'ResizeBoxPane' +} diff --git a/packages/ui/resize-box/src/Separator.tsx b/packages/ui/resize-box/src/Separator.tsx new file mode 100644 index 000000000..117857d4f --- /dev/null +++ b/packages/ui/resize-box/src/Separator.tsx @@ -0,0 +1,27 @@ +import React, { forwardRef } from 'react' +import { cx, getPrefixCls } from '@hi-ui/classname' +import { __DEV__ } from '@hi-ui/env' +import { HiBaseHTMLProps } from '@hi-ui/core' + +const SEPARATOR_PREFIX = getPrefixCls('resize-box-separator') + +export const Separator = forwardRef( + ( + { prefixCls = SEPARATOR_PREFIX, role = 'resize-box-separator', className, children, ...rest }, + ref + ) => { + const cls = cx(prefixCls, className) + + return ( +
+ +
+ ) + } +) + +export interface SeparatorProps extends HiBaseHTMLProps<'div'> {} + +if (__DEV__) { + Separator.displayName = 'SEPARATOR' +} diff --git a/packages/ui/resize-box/src/index.ts b/packages/ui/resize-box/src/index.ts new file mode 100644 index 000000000..9c6ec5311 --- /dev/null +++ b/packages/ui/resize-box/src/index.ts @@ -0,0 +1,6 @@ +import './styles/index.scss' + +export * from './ResizeBox' +export { ResizeBox as default } from './ResizeBox' + +export * from './ResizeBoxPane' diff --git a/packages/ui/resize-box/src/styles/index.scss b/packages/ui/resize-box/src/styles/index.scss new file mode 100644 index 000000000..846141425 --- /dev/null +++ b/packages/ui/resize-box/src/styles/index.scss @@ -0,0 +1 @@ +@import './resize-box.scss'; diff --git a/packages/ui/resize-box/src/styles/resize-box.scss b/packages/ui/resize-box/src/styles/resize-box.scss new file mode 100644 index 000000000..9200b87f3 --- /dev/null +++ b/packages/ui/resize-box/src/styles/resize-box.scss @@ -0,0 +1,38 @@ +@import '~@hi-ui/core-css/lib/index.scss'; + +$prefix: '#{$component-prefix}-resize-box' !default; + +.#{$prefix} { + display: flex; + box-sizing: border-box; + user-select: auto; + + &-pane { + display: flex; + justify-content: space-between; + overflow: hidden; + } + + &-separator { + position: relative; + height: 100%; + padding: 0 2px; + cursor: col-resize; + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + + &-content { + width: 2px; + height: 100%; + background-color: use-color('gray', 300); + } + + &:hover { + .#{$prefix}-separator-content { + background-color: use-color('brandblue', 300); + } + } + } +} diff --git a/packages/ui/resize-box/stories/basic.stories.tsx b/packages/ui/resize-box/stories/basic.stories.tsx new file mode 100644 index 000000000..633ab13ba --- /dev/null +++ b/packages/ui/resize-box/stories/basic.stories.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import ResizeBox, { ResizeBoxPane } from '../src' + +/** + * @title 基础用法 + */ +export const Basic = () => { + return ( + <> +

Basic

+
+ + +
+ left content +
+
+ +
+ right content +
+
+
+
+ + ) +} diff --git a/packages/ui/resize-box/stories/index.stories.tsx b/packages/ui/resize-box/stories/index.stories.tsx new file mode 100644 index 000000000..d7a14925c --- /dev/null +++ b/packages/ui/resize-box/stories/index.stories.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import ResizeBox from '../src' + +export * from './basic.stories' +export * from './min-width.stories' + +export default { + title: 'Others/ResizeBox', + component: ResizeBox, + decorators: [(story: Function) =>
{story()}
], +} diff --git a/packages/ui/resize-box/stories/min-width.stories.tsx b/packages/ui/resize-box/stories/min-width.stories.tsx new file mode 100644 index 000000000..db99a32f1 --- /dev/null +++ b/packages/ui/resize-box/stories/min-width.stories.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import ResizeBox, { ResizeBoxPane } from '../src' + +/** + * @title 设置 pane 最小宽度 + */ +export const MinWidth = () => { + return ( + <> +

MinWidth

+
+ + +
+ left content +
+
+ +
+ right content +
+
+
+
+ + ) +} diff --git a/packages/ui/resize-box/tsconfig.json b/packages/ui/resize-box/tsconfig.json new file mode 100644 index 000000000..f7bbdb2fe --- /dev/null +++ b/packages/ui/resize-box/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./src"] +} diff --git a/yarn.lock b/yarn.lock index d194e712d..55f06d8b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15601,6 +15601,14 @@ react-resizable@^3.0.4: prop-types "15.x" react-draggable "^4.0.3" +react-resizable@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1" + integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w== + dependencies: + prop-types "15.x" + react-draggable "^4.0.3" + react-resize-detector@^6.7.6: version "6.7.6" resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.7.6.tgz#4416994e5ead7eba76606e3a248a1dfca49b67a3"