Skip to content

WIP: feat(modal): modal support draggable #544

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"@types/jest": "^29.2.3",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.0.0",
"@types/react-resizable": "^3.0.8",
"@types/shortid": "^0.0.31",
"@types/showdown": "^1.9.0",
"@types/testing-library__jest-dom": "^5.14.5",
Expand Down Expand Up @@ -118,6 +119,8 @@
"lodash-es": "^4.17.21",
"rc-drawer": "~5.1.0",
"rc-virtual-list": "^3.4.13",
"react-draggable": "~4.4.6",
"react-resizable": "^3.0.5",
"shortid": "^2.2.16",
"showdown": "^1.9.0",
"use-clippy": "^1.0.9"
Expand Down
1,828 changes: 933 additions & 895 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

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, { DraggableEventHandler, type DraggableProps } from 'react-draggable';
import classNames from 'classnames';

import useMergeOption, { 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>
);
}
26 changes: 26 additions & 0 deletions src/float/useMergeOption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMemo } from 'react';

export type MergeOption<T extends Record<string, any>> = boolean | T;

export type ReturnMergeOption<T extends Record<string, any>> = {
disabled: boolean;
options: T;
};

export default function useMergeOption<T extends Record<string, any>>(
opt: MergeOption<T>,
defaultOpt?: T
): ReturnMergeOption<T> {
return useMemo(() => {
if (typeof opt === 'object' && !!opt) {
return {
disabled: false,
options: { ...defaultOpt, ...opt },
};
}
return {
disabled: !opt,
options: <T>{ ...defaultOpt },
};
}, [opt]);
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { default as Empty } from './empty';
export { default as ErrorBoundary } from './errorBoundary';
export { default as LoadError } from './errorBoundary/loadError';
export { default as FilterRules } from './filterRules';
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
10 changes: 10 additions & 0 deletions src/modal/__tests__/__snapshots__/handle.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Handler Component should match snapshot 1`] = `
<DocumentFragment>
<div
class="dt-modal-resize-handle handle-x"
data-testid="handler"
/>
</DocumentFragment>
`;
Loading
Loading