diff --git a/package.json b/package.json index 9695a4377..9b3dbeedb 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "react-markdown": "~8.0.6", "react-syntax-highlighter": "~15.5.0", "remark-gfm": "~3.0.1", + "react-draggable": "~4.4.6", "shortid": "^2.2.16", "showdown": "^1.9.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea058f473..9f05053e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,7 @@ specifiers: rc-virtual-list: ^3.4.13 react: ^18.0.0 react-dom: ^18.0.0 + react-draggable: ~4.4.6 react-markdown: ~8.0.6 react-syntax-highlighter: ~15.5.0 react-test-renderer: ^18.2.0 @@ -69,6 +70,7 @@ dependencies: lodash-es: 4.17.21 rc-drawer: 5.1.0_react-dom@18.2.0+react@18.2.0 rc-virtual-list: 3.11.2_react-dom@18.2.0+react@18.2.0 + react-draggable: 4.4.6_react-dom@18.2.0+react@18.2.0 react-markdown: 8.0.7_d51bdd6a322172e118eec6adc1172a28 react-syntax-highlighter: 15.5.0_react@18.2.0 remark-gfm: 3.0.1 @@ -4898,6 +4900,11 @@ packages: engines: {node: '>=0.8'} dev: true + /clsx/1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + /co/4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -13615,6 +13622,18 @@ packages: scheduler: 0.23.0 dev: true + /react-draggable/4.4.6_react-dom@18.2.0+react@18.2.0: + resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /react-error-boundary/3.1.4_react@18.2.0: resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} engines: {node: '>=10', npm: '>=6'} diff --git a/src/float/__tests__/__snapshots__/index.test.tsx.snap b/src/float/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..b1d95c456 --- /dev/null +++ b/src/float/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Float Component should match snapshot 1`] = ` + +
+ Test +
+
+`; diff --git a/src/float/__tests__/index.test.tsx b/src/float/__tests__/index.test.tsx new file mode 100644 index 000000000..e3312f92b --- /dev/null +++ b/src/float/__tests__/index.test.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { cleanup, fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import Float, { IFloatProps } from '../index'; + +function dragFromTo( + ele: HTMLElement, + from: NonNullable, + to: NonNullable +) { + fireEvent.mouseDown(ele, { clientX: from.x, clientY: from.y }); + fireEvent.mouseMove(document, { clientX: to.x, clientY: to.y }); + return { + mouseUp: () => fireEvent.mouseUp(ele, { clientX: to.x, clientY: to.y }), + }; +} + +describe('Float Component', () => { + const defaultProps: IFloatProps = { + className: 'test-class', + style: { color: 'red' }, + draggable: true, + position: { x: 0, y: 0 }, + }; + + beforeEach(() => { + cleanup(); + }); + + it('should match snapshot', () => { + const { asFragment } = render(Test); + expect(asFragment()).toMatchSnapshot(); + }); + + it('should handle drag events', () => { + const fn = jest.fn(); + const { container } = render( + + Test + + ); + const floatContainer = container.firstChild as HTMLElement; + const { mouseUp } = dragFromTo(floatContainer, { x: 0, y: 0 }, { x: 100, y: 100 }); + expect(floatContainer).toHaveClass('dtc-float-container__dragging'); + + mouseUp(); + + expect(fn.mock.calls[0][1]).toEqual(expect.objectContaining({ x: 100, y: 100 })); + expect(floatContainer).not.toHaveClass('dtc-float-container__dragging'); + }); + + it('should disable dragging when draggable is set to false', () => { + const fn = jest.fn(); + const { container } = render( + + Test + + ); + const floatContainer = container.firstChild as HTMLElement; + dragFromTo(floatContainer, { x: 0, y: 0 }, { x: 100, y: 100 }).mouseUp(); + + expect(fn).not.toHaveBeenCalled(); + }); + + it('should support pass through draggable options', () => { + const fn = jest.fn(); + const { container } = render( + + Test + + ); + + const floatContainer = container.firstChild as HTMLElement; + dragFromTo(floatContainer, { x: 0, y: 0 }, { x: 100, y: 100 }).mouseUp(); + + expect(fn).toBeCalled(); + }); +}); diff --git a/src/float/demos/backTop.tsx b/src/float/demos/backTop.tsx new file mode 100644 index 000000000..9d2cd491b --- /dev/null +++ b/src/float/demos/backTop.tsx @@ -0,0 +1,62 @@ +import React, { HTMLAttributes, useState } from 'react'; +import { Float, Resize } from 'dt-react-component'; + +export default function () { + const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight }); + + return ( + setSize({ width: window.innerWidth, height: window.innerHeight })}> +
+ {Array.from({ length: 1000 }).map((_, idx) => ( +
{idx}. This is the segment
+ ))} +
+ +
window.scrollTo({ top: 0, behavior: 'smooth' })} + > + +
+
+
+ ); +} + +function UpToLineIcon(props: HTMLAttributes) { + return ( + + + + + + + + + ); +} diff --git a/src/float/demos/basic.tsx b/src/float/demos/basic.tsx new file mode 100644 index 000000000..1be9d49c7 --- /dev/null +++ b/src/float/demos/basic.tsx @@ -0,0 +1,20 @@ +import React, { useState } from 'react'; +import { Float, Image } from 'dt-react-component'; + +export default function () { + const [position, setPosition] = useState({ x: 0, y: 0 }); + return ( + setPosition({ x, y })} + > + + + ); +} diff --git a/src/float/index.md b/src/float/index.md new file mode 100644 index 000000000..c8cef7633 --- /dev/null +++ b/src/float/index.md @@ -0,0 +1,32 @@ +--- +title: Float 悬浮组件 +group: 组件 +toc: content +demo: + cols: 1 +--- + +# Float 悬浮组件 + +悬浮在页面上且支持拖拽至任意位置的组件 + +## 何时使用 + +实现全局渲染,悬浮在页面上任意位置功能 + +## 示例 + +基础使用 +返回顶部 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +| --------- | ------------------------ | --------------------------- | ------- | +| className | 类名 | `string` | - | +| style | 样式 | `CSSProperties` | - | +| draggable | 拖拽配置 | `boolean \| DraggableProps` | `false` | +| position | 位置 | `{x: number, y: number}` | 左上角 | +| onChange | 拖拽结束后触发的回调函数 | `Function` | - | + +其中 `DraggableProps` 类型具体参考 [draggable-api](https://github.com/react-grid-layout/react-draggable?tab=readme-ov-file#draggable-api)。 diff --git a/src/float/index.scss b/src/float/index.scss new file mode 100644 index 000000000..7a6fb058c --- /dev/null +++ b/src/float/index.scss @@ -0,0 +1,8 @@ +.dtc-float-container { + position: fixed; + top: 0; + left: 0; + &__dragging { + pointer-events: none; + } +} diff --git a/src/float/index.tsx b/src/float/index.tsx new file mode 100644 index 000000000..d6df6e039 --- /dev/null +++ b/src/float/index.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import Draggable, { type DraggableEventHandler, type DraggableProps } from 'react-draggable'; +import classNames from 'classnames'; + +import useMergeOption, { type MergeOption } from '../useMergeOption'; +import './index.scss'; + +export interface IFloatProps { + className?: string; + style?: React.CSSProperties; + draggable?: MergeOption>>; + position?: DraggableProps['position']; + onChange?: DraggableProps['onStop']; +} + +export default function Float({ + className, + style, + draggable = false, + position, + children, + onChange, +}: React.PropsWithChildren) { + const [dragging, setDragging] = useState(false); + const mergedDraggable = useMergeOption(draggable); + + const handleStopDrag: DraggableEventHandler = (e, data) => { + mergedDraggable.options.onStop?.(e, data); + onChange?.(e, data); + setDragging(false); + }; + + const handleDrag: DraggableEventHandler = (e, data) => { + mergedDraggable.options.onDrag?.(e, data); + setDragging(true); + }; + + return ( + +
+ {children} +
+
+ ); +} diff --git a/src/index.ts b/src/index.ts index 3c270867e..802dfcdb4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ export { default as ErrorBoundary } from './errorBoundary'; export { default as LoadError } from './errorBoundary/loadError'; export { default as FilterRules } from './filterRules'; export { default as Flex } from './flex'; +export { default as Float } from './float'; export { default as Form } from './form'; export { default as Fullscreen } from './fullscreen'; export { default as GlobalLoading } from './globalLoading';