Skip to content
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/float/__tests__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Float Component should match snapshot 1`] = `
<DocumentFragment>
<div
class="dtc-float-container test-class react-draggable"
style="color: red; transform: translate(0px,0px);"
>
Test
</div>
</DocumentFragment>
`;
84 changes: 84 additions & 0 deletions src/float/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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<IFloatProps['position']>,
to: NonNullable<IFloatProps['position']>
) {
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(<Float {...defaultProps}>Test</Float>);
expect(asFragment()).toMatchSnapshot();
});

it('should handle drag events', () => {
const fn = jest.fn();
const { container } = render(
<Float {...defaultProps} onChange={fn}>
Test
</Float>
);
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(
<Float {...defaultProps} draggable={false} onChange={fn}>
Test
</Float>
);
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(
<Float
{...defaultProps}
draggable={{
onDrag: fn,
}}
>
Test
</Float>
);

const floatContainer = container.firstChild as HTMLElement;
dragFromTo(floatContainer, { x: 0, y: 0 }, { x: 100, y: 100 }).mouseUp();

expect(fn).toBeCalled();
});
});
62 changes: 62 additions & 0 deletions src/float/demos/backTop.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Resize onResize={() => setSize({ width: window.innerWidth, height: window.innerHeight })}>
<section>
{Array.from({ length: 1000 }).map((_, idx) => (
<div key={idx}>{idx}. This is the segment</div>
))}
</section>
<Float draggable={false} position={{ y: size.height - 64, x: size.width - 64 }}>
<div
style={{
width: 40,
height: 40,
borderRadius: '50%',
backgroundColor: 'rgba(0,0,0,.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
>
<UpToLineIcon style={{ fontSize: 24, lineHeight: 0, color: '#fff' }} />
</div>
</Float>
</Resize>
);
}

function UpToLineIcon(props: HTMLAttributes<HTMLSpanElement>) {
return (
<span {...props}>
<svg
className="icon"
width="1em"
height="1em"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
fillRule="evenodd"
clipRule="evenodd"
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"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
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"
fill="currentColor"
/>
</g>
</svg>
</span>
);
}
20 changes: 20 additions & 0 deletions src/float/demos/basic.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Float
draggable={{ bounds: 'body' }}
position={position}
onChange={(_, { x, y }) => setPosition({ x, y })}
>
<Image
height={200}
width={200}
src="https://dtstack.github.io/dt-react-component/static/empty_overview.43b0eedf.png"
style={{ borderColor: 'red' }}
/>
</Float>
);
}
32 changes: 32 additions & 0 deletions src/float/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: Float 悬浮组件
group: 组件
toc: content
demo:
cols: 1
---

# Float 悬浮组件

悬浮在页面上且支持拖拽至任意位置的组件

## 何时使用

实现全局渲染,悬浮在页面上任意位置功能

## 示例

<code src="./demos/basic.tsx" iframe="true">基础使用</code>
<code src="./demos/backTop.tsx" iframe="true">返回顶部</code>

## 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)。
8 changes: 8 additions & 0 deletions src/float/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.dtc-float-container {
position: fixed;
top: 0;
left: 0;
&__dragging {
pointer-events: none;
}
}
58 changes: 58 additions & 0 deletions src/float/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Partial<Omit<DraggableProps, 'position'>>>;
position?: DraggableProps['position'];
onChange?: DraggableProps['onStop'];
}

export default function Float({
className,
style,
draggable = false,
position,
children,
onChange,
}: React.PropsWithChildren<IFloatProps>) {
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 (
<Draggable
disabled={mergedDraggable.disabled}
{...mergedDraggable.options}
position={position}
onDrag={handleDrag}
onStop={handleStopDrag}
>
<div
className={classNames(
'dtc-float-container',
className,
dragging && 'dtc-float-container__dragging'
)}
style={style}
>
{children}
</div>
</Draggable>
);
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading