Skip to content

Commit f8f005d

Browse files
authored
feat(float): add float component (#578)
* feat(float): add float component * feat(float): add react-draggable dep
1 parent 90741fc commit f8f005d

File tree

10 files changed

+297
-0
lines changed

10 files changed

+297
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"react-markdown": "~8.0.6",
125125
"react-syntax-highlighter": "~15.5.0",
126126
"remark-gfm": "~3.0.1",
127+
"react-draggable": "~4.4.6",
127128
"shortid": "^2.2.16",
128129
"showdown": "^1.9.0"
129130
},

pnpm-lock.yaml

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Float Component should match snapshot 1`] = `
4+
<DocumentFragment>
5+
<div
6+
class="dtc-float-container test-class react-draggable"
7+
style="color: red; transform: translate(0px,0px);"
8+
>
9+
Test
10+
</div>
11+
</DocumentFragment>
12+
`;

src/float/__tests__/index.test.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React from 'react';
2+
import { cleanup, fireEvent, render } from '@testing-library/react';
3+
import '@testing-library/jest-dom/extend-expect';
4+
5+
import Float, { IFloatProps } from '../index';
6+
7+
function dragFromTo(
8+
ele: HTMLElement,
9+
from: NonNullable<IFloatProps['position']>,
10+
to: NonNullable<IFloatProps['position']>
11+
) {
12+
fireEvent.mouseDown(ele, { clientX: from.x, clientY: from.y });
13+
fireEvent.mouseMove(document, { clientX: to.x, clientY: to.y });
14+
return {
15+
mouseUp: () => fireEvent.mouseUp(ele, { clientX: to.x, clientY: to.y }),
16+
};
17+
}
18+
19+
describe('Float Component', () => {
20+
const defaultProps: IFloatProps = {
21+
className: 'test-class',
22+
style: { color: 'red' },
23+
draggable: true,
24+
position: { x: 0, y: 0 },
25+
};
26+
27+
beforeEach(() => {
28+
cleanup();
29+
});
30+
31+
it('should match snapshot', () => {
32+
const { asFragment } = render(<Float {...defaultProps}>Test</Float>);
33+
expect(asFragment()).toMatchSnapshot();
34+
});
35+
36+
it('should handle drag events', () => {
37+
const fn = jest.fn();
38+
const { container } = render(
39+
<Float {...defaultProps} onChange={fn}>
40+
Test
41+
</Float>
42+
);
43+
const floatContainer = container.firstChild as HTMLElement;
44+
const { mouseUp } = dragFromTo(floatContainer, { x: 0, y: 0 }, { x: 100, y: 100 });
45+
expect(floatContainer).toHaveClass('dtc-float-container__dragging');
46+
47+
mouseUp();
48+
49+
expect(fn.mock.calls[0][1]).toEqual(expect.objectContaining({ x: 100, y: 100 }));
50+
expect(floatContainer).not.toHaveClass('dtc-float-container__dragging');
51+
});
52+
53+
it('should disable dragging when draggable is set to false', () => {
54+
const fn = jest.fn();
55+
const { container } = render(
56+
<Float {...defaultProps} draggable={false} onChange={fn}>
57+
Test
58+
</Float>
59+
);
60+
const floatContainer = container.firstChild as HTMLElement;
61+
dragFromTo(floatContainer, { x: 0, y: 0 }, { x: 100, y: 100 }).mouseUp();
62+
63+
expect(fn).not.toHaveBeenCalled();
64+
});
65+
66+
it('should support pass through draggable options', () => {
67+
const fn = jest.fn();
68+
const { container } = render(
69+
<Float
70+
{...defaultProps}
71+
draggable={{
72+
onDrag: fn,
73+
}}
74+
>
75+
Test
76+
</Float>
77+
);
78+
79+
const floatContainer = container.firstChild as HTMLElement;
80+
dragFromTo(floatContainer, { x: 0, y: 0 }, { x: 100, y: 100 }).mouseUp();
81+
82+
expect(fn).toBeCalled();
83+
});
84+
});

src/float/demos/backTop.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { HTMLAttributes, useState } from 'react';
2+
import { Float, Resize } from 'dt-react-component';
3+
4+
export default function () {
5+
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
6+
7+
return (
8+
<Resize onResize={() => setSize({ width: window.innerWidth, height: window.innerHeight })}>
9+
<section>
10+
{Array.from({ length: 1000 }).map((_, idx) => (
11+
<div key={idx}>{idx}. This is the segment</div>
12+
))}
13+
</section>
14+
<Float draggable={false} position={{ y: size.height - 64, x: size.width - 64 }}>
15+
<div
16+
style={{
17+
width: 40,
18+
height: 40,
19+
borderRadius: '50%',
20+
backgroundColor: 'rgba(0,0,0,.2)',
21+
display: 'flex',
22+
alignItems: 'center',
23+
justifyContent: 'center',
24+
}}
25+
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
26+
>
27+
<UpToLineIcon style={{ fontSize: 24, lineHeight: 0, color: '#fff' }} />
28+
</div>
29+
</Float>
30+
</Resize>
31+
);
32+
}
33+
34+
function UpToLineIcon(props: HTMLAttributes<HTMLSpanElement>) {
35+
return (
36+
<span {...props}>
37+
<svg
38+
className="icon"
39+
width="1em"
40+
height="1em"
41+
viewBox="0 0 24 24"
42+
fill="currentColor"
43+
xmlns="http://www.w3.org/2000/svg"
44+
>
45+
<g>
46+
<path
47+
fillRule="evenodd"
48+
clipRule="evenodd"
49+
d="M4 3.88672C4 4.30093 4.34112 4.63672 4.7619 4.63672H19.2381C19.6589 4.63672 20 4.30093 20 3.88672C20 3.47251 19.6589 3.13672 19.2381 3.13672H4.7619C4.34112 3.13672 4 3.47251 4 3.88672Z"
50+
fill="currentColor"
51+
/>
52+
<path
53+
fillRule="evenodd"
54+
clipRule="evenodd"
55+
d="M12.0026 7.82525L7.18185 13.5648H10.6324V20.1141H13.3752V13.5648H16.8233L12.0026 7.82525ZM11.1411 6.51864C11.5907 5.98337 12.4144 5.98337 12.864 6.51864L18.4894 13.2162C19.1042 13.9481 18.5838 15.0648 17.628 15.0648H14.8752V20.1141C14.8752 20.9426 14.2036 21.6141 13.3752 21.6141H10.6324C9.80398 21.6141 9.13241 20.9426 9.13241 20.1141V15.0648H6.37715C5.42129 15.0648 4.90094 13.9481 5.5157 13.2162L11.1411 6.51864Z"
56+
fill="currentColor"
57+
/>
58+
</g>
59+
</svg>
60+
</span>
61+
);
62+
}

src/float/demos/basic.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React, { useState } from 'react';
2+
import { Float, Image } from 'dt-react-component';
3+
4+
export default function () {
5+
const [position, setPosition] = useState({ x: 0, y: 0 });
6+
return (
7+
<Float
8+
draggable={{ bounds: 'body' }}
9+
position={position}
10+
onChange={(_, { x, y }) => setPosition({ x, y })}
11+
>
12+
<Image
13+
height={200}
14+
width={200}
15+
src="https://dtstack.github.io/dt-react-component/static/empty_overview.43b0eedf.png"
16+
style={{ borderColor: 'red' }}
17+
/>
18+
</Float>
19+
);
20+
}

src/float/index.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
title: Float 悬浮组件
3+
group: 组件
4+
toc: content
5+
demo:
6+
cols: 1
7+
---
8+
9+
# Float 悬浮组件
10+
11+
悬浮在页面上且支持拖拽至任意位置的组件
12+
13+
## 何时使用
14+
15+
实现全局渲染,悬浮在页面上任意位置功能
16+
17+
## 示例
18+
19+
<code src="./demos/basic.tsx" iframe="true">基础使用</code>
20+
<code src="./demos/backTop.tsx" iframe="true">返回顶部</code>
21+
22+
## API
23+
24+
| 参数 | 说明 | 类型 | 默认值 |
25+
| --------- | ------------------------ | --------------------------- | ------- |
26+
| className | 类名 | `string` | - |
27+
| style | 样式 | `CSSProperties` | - |
28+
| draggable | 拖拽配置 | `boolean \| DraggableProps` | `false` |
29+
| position | 位置 | `{x: number, y: number}` | 左上角 |
30+
| onChange | 拖拽结束后触发的回调函数 | `Function` | - |
31+
32+
其中 `DraggableProps` 类型具体参考 [draggable-api](https://github.com/react-grid-layout/react-draggable?tab=readme-ov-file#draggable-api)

src/float/index.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.dtc-float-container {
2+
position: fixed;
3+
top: 0;
4+
left: 0;
5+
&__dragging {
6+
pointer-events: none;
7+
}
8+
}

src/float/index.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { useState } from 'react';
2+
import Draggable, { type DraggableEventHandler, type DraggableProps } from 'react-draggable';
3+
import classNames from 'classnames';
4+
5+
import useMergeOption, { type MergeOption } from '../useMergeOption';
6+
import './index.scss';
7+
8+
export interface IFloatProps {
9+
className?: string;
10+
style?: React.CSSProperties;
11+
draggable?: MergeOption<Partial<Omit<DraggableProps, 'position'>>>;
12+
position?: DraggableProps['position'];
13+
onChange?: DraggableProps['onStop'];
14+
}
15+
16+
export default function Float({
17+
className,
18+
style,
19+
draggable = false,
20+
position,
21+
children,
22+
onChange,
23+
}: React.PropsWithChildren<IFloatProps>) {
24+
const [dragging, setDragging] = useState(false);
25+
const mergedDraggable = useMergeOption(draggable);
26+
27+
const handleStopDrag: DraggableEventHandler = (e, data) => {
28+
mergedDraggable.options.onStop?.(e, data);
29+
onChange?.(e, data);
30+
setDragging(false);
31+
};
32+
33+
const handleDrag: DraggableEventHandler = (e, data) => {
34+
mergedDraggable.options.onDrag?.(e, data);
35+
setDragging(true);
36+
};
37+
38+
return (
39+
<Draggable
40+
disabled={mergedDraggable.disabled}
41+
{...mergedDraggable.options}
42+
position={position}
43+
onDrag={handleDrag}
44+
onStop={handleStopDrag}
45+
>
46+
<div
47+
className={classNames(
48+
'dtc-float-container',
49+
className,
50+
dragging && 'dtc-float-container__dragging'
51+
)}
52+
style={style}
53+
>
54+
{children}
55+
</div>
56+
</Draggable>
57+
);
58+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { default as ErrorBoundary } from './errorBoundary';
1414
export { default as LoadError } from './errorBoundary/loadError';
1515
export { default as FilterRules } from './filterRules';
1616
export { default as Flex } from './flex';
17+
export { default as Float } from './float';
1718
export { default as Form } from './form';
1819
export { default as Fullscreen } from './fullscreen';
1920
export { default as GlobalLoading } from './globalLoading';

0 commit comments

Comments
 (0)