Skip to content
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

Feat/add draggable modal #3983

Merged
merged 39 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
03b0fc5
feat(hooks): add use-draggable hook
wzc520pyfm Apr 20, 2024
a19945f
feat(components): [modal] export use-draggable
wzc520pyfm Apr 20, 2024
24aa1ce
docs(components): [modal] add draggable modal
wzc520pyfm Apr 20, 2024
34e2f53
feat(components): [modal] add ref prop for modal-header
wzc520pyfm Apr 20, 2024
56d6cf3
chore(components): [modal] add draggable modal for storybook
wzc520pyfm Apr 20, 2024
bf9ca51
chore: add changeset for draggable modal
wzc520pyfm Apr 20, 2024
9bb1a89
docs(hooks): [use-draggable] fix typo
wzc520pyfm Apr 20, 2024
2ba3742
chore: upper changeset
wzc520pyfm Apr 20, 2024
0e9caa6
chore(components): [modal] add overflow draggable modal to sb
wzc520pyfm Apr 21, 2024
a64ec22
test(components): [modal] add draggable modal tests
wzc520pyfm Apr 21, 2024
f9463c5
build: update pnpm-lock
wzc520pyfm Apr 21, 2024
ec5789d
Merge branch 'main' into pr/2818
wingkwong Apr 22, 2024
b6198b6
chore(changeset): include issue number
wingkwong Apr 22, 2024
c9b6ac6
Merge branch 'nextui-org:main' into feat/add-draggable-modal
wzc520pyfm Apr 22, 2024
e0a8dea
feat(hooks): [use-draggable] set user-select to none when during the …
wzc520pyfm Apr 22, 2024
7c27351
docs(components): [modal] update code demo title
wzc520pyfm Apr 22, 2024
90699f2
docs(components): [modal] condense description for draggable overflow
wzc520pyfm Apr 22, 2024
6680886
feat(hooks): [use-draggable] change version to 0.1.0
wzc520pyfm Apr 22, 2024
a9cb28b
refactor(hooks): [use-draggable] use use-move implement use-draggable
wzc520pyfm Apr 24, 2024
a9e9cac
feat(hooks): [use-draggable] remove repeated user-select
wzc520pyfm Apr 25, 2024
a0e2b98
test(components): [modal] update test case to use-draggable base use-…
wzc520pyfm Apr 25, 2024
f0bddfa
docs(components): [modal] update draggable examples
wzc520pyfm Apr 25, 2024
663aec4
fix(hooks): [use-draggable] fix mobile device touchmove event conflict
wzc520pyfm Apr 29, 2024
b46fd73
refactor(hooks): [use-draggable] remove drag ref prop
wzc520pyfm May 4, 2024
142b3ff
refactor(hooks): [use-draggable] draggable2is-disabled overflow2can-o…
wzc520pyfm May 4, 2024
28b114b
test(components): [modal] add draggble disable test
wzc520pyfm May 4, 2024
b85e591
chore(hooks): [use-draggable] add commant for body touchmove
wzc520pyfm May 4, 2024
cd96af7
Update packages/hooks/use-draggable/src/index.ts
wzc520pyfm May 8, 2024
2aff343
fix(hooks): [use-draggable] import use-callback
wzc520pyfm May 8, 2024
678d3e7
test(components): [modal] add mobile-sized test for draggable
wzc520pyfm May 8, 2024
50ee586
chore(hooks): [use-draggable] add use-callback for func
wzc520pyfm Jul 24, 2024
c45f9be
Merge branch 'canary' into feat/add-draggable-modal
wzc520pyfm Sep 25, 2024
0c282dd
chore(hooks): [use-draggable] update version to 2.0.0
wzc520pyfm Sep 25, 2024
09b5b8d
chore: fix typo
wzc520pyfm Sep 25, 2024
f3f4eab
Update .changeset/soft-apricots-sleep.md
jrgarciadev Nov 4, 2024
f6c51f1
Merge branch 'canary' of github.com:nextui-org/nextui into feat/add-d…
jrgarciadev Nov 4, 2024
d8795d2
fix: pnpm lock
jrgarciadev Nov 4, 2024
5721340
fix: build
jrgarciadev Nov 4, 2024
bf4affb
chore: add updated moadl
jrgarciadev Nov 4, 2024
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
6 changes: 6 additions & 0 deletions .changeset/soft-apricots-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nextui-org/modal": patch
"@nextui-org/use-draggable": patch
---

Add draggable modal (#2647)
Comment on lines +1 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using minor version bumps instead of patches.

According to semantic versioning guidelines, adding new functionality (like the draggable modal feature) typically warrants a minor version bump rather than a patch. Patch versions are reserved for backward-compatible bug fixes.

Apply this diff to update the version types:

---
-"@nextui-org/modal": patch
-"@nextui-org/use-draggable": patch
+"@nextui-org/modal": minor
+"@nextui-org/use-draggable": minor
---
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
---
"@nextui-org/modal": patch
"@nextui-org/use-draggable": patch
---
Add draggable modal (#2647)
---
"@nextui-org/modal": minor
"@nextui-org/use-draggable": minor
---
Add draggable modal (#2647)

3 changes: 2 additions & 1 deletion apps/docs/config/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,8 @@
"key": "modal",
"title": "Modal",
"keywords": "modal, dialog box, popup, overlay, content focus",
"path": "/docs/components/modal.mdx"
"path": "/docs/components/modal.mdx",
"updated": true
},
{
"key": "navbar",
Expand Down
45 changes: 45 additions & 0 deletions apps/docs/content/components/modal/draggable-overflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const App = `import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure, useDraggable} from "@nextui-org/react";

export default function App() {
const {isOpen, onOpen, onOpenChange} = useDisclosure();
const targetRef = React.useRef(null);
const {moveProps} = useDraggable({targetRef, canOverflow: true});

return (
<>
<Button onPress={onOpen}>Open Modal</Button>
<Modal ref={targetRef} isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader {...moveProps} className="flex flex-col gap-1">Modal Title</ModalHeader>
<ModalBody>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam pulvinar risus non risus hendrerit venenatis.
Pellentesque sit amet hendrerit risus, sed porttitor quam.
</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
<Button color="primary" onPress={onClose}>
Action
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
45 changes: 45 additions & 0 deletions apps/docs/content/components/modal/draggable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const App = `import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure, useDraggable} from "@nextui-org/react";

export default function App() {
const {isOpen, onOpen, onOpenChange} = useDisclosure();
const targetRef = React.useRef(null);
const {moveProps} = useDraggable({ targetRef });

return (
<>
<Button onPress={onOpen}>Open Modal</Button>
<Modal ref={targetRef} isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader {...moveProps} className="flex flex-col gap-1">Modal Title</ModalHeader>
<ModalBody>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam pulvinar risus non risus hendrerit venenatis.
Pellentesque sit amet hendrerit risus, sed porttitor quam.
</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
<Button color="primary" onPress={onClose}>
Action
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
4 changes: 4 additions & 0 deletions apps/docs/content/components/modal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import backdrop from "./backdrop";
import customBackdrop from "./custom-backdrop";
import customMotion from "./custom-motion";
import customStyles from "./custom-styles";
import draggable from "./draggable";
import draggableOverflow from "./draggable-overflow";

export const modalContent = {
usage,
Expand All @@ -20,4 +22,6 @@ export const modalContent = {
customBackdrop,
customMotion,
customStyles,
draggable,
draggableOverflow,
};
32 changes: 22 additions & 10 deletions apps/docs/content/docs/components/modal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ NextUI exports 5 modal-related components:
<ImportTabs
commands={{
main: `import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter
} from "@nextui-org/react";`,
individual:
`import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter
} from "@nextui-org/modal";`,
}}
Expand All @@ -72,9 +72,9 @@ When the modal opens:

<CodeDemo title="Sizes" files={modalContent.sizes} />

### Non-dissmissable
### Non-dismissible

By default, the modal can be closed by clicking on the overlay or pressing the <Kbd>Esc</Kbd> key.
By default, the modal can be closed by clicking on the overlay or pressing the <Kbd>Esc</Kbd> key.
You can disable this behavior by setting the following properties:

- Set the `isDismissable` property to `false` to prevent the modal from closing when clicking on the overlay.
Expand Down Expand Up @@ -138,6 +138,18 @@ Modal offers a `motionProps` property to customize the `enter` / `exit` animatio

> Learn more about Framer motion variants [here](https://www.framer.com/motion/animation/#variants).

### Draggable

Try to drag the header part.

<CodeDemo title="Draggable" files={modalContent.draggable} />

### Draggable Overflow

Set overflow to true can drag overflow the viewport.

<CodeDemo title="Draggable Overflow" files={modalContent.draggableOverflow} />

## Slots

- **wrapper**: The wrapper slot of the modal. It wraps the `base` and the `backdrop` slots.
Expand Down
107 changes: 106 additions & 1 deletion packages/components/modal/__tests__/modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,28 @@ import * as React from "react";
import {render, fireEvent} from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import {Modal, ModalContent, ModalBody, ModalHeader, ModalFooter} from "../src";
import {Modal, ModalContent, ModalBody, ModalHeader, ModalFooter, useDraggable} from "../src";

// e.g. console.error Warning: Function components cannot be given refs.
// Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
const spy = jest.spyOn(console, "error").mockImplementation(() => {});

const ModalDraggable = ({canOverflow = false, isDisabled = false}) => {
const targetRef = React.useRef(null);

const {moveProps} = useDraggable({targetRef, canOverflow, isDisabled});

return (
<Modal ref={targetRef} isOpen>
<ModalContent>
<ModalHeader {...moveProps}>Modal header</ModalHeader>
<ModalBody>Modal body</ModalBody>
<ModalFooter>Modal footer</ModalFooter>
</ModalContent>
</Modal>
);
};

describe("Modal", () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -109,4 +125,93 @@ describe("Modal", () => {
fireEvent.keyDown(modal, {key: "Escape"});
expect(onClose).toHaveBeenCalledTimes(1);
});

it("should be rendered a draggable modal", () => {
// mock viewport size to 1920x1080
jest.spyOn(document.documentElement, "clientWidth", "get").mockImplementation(() => 1920);
jest.spyOn(document.documentElement, "clientHeight", "get").mockImplementation(() => 1080);

const wrapper = render(<ModalDraggable />);

const modal = wrapper.getByRole("dialog");
const modalHeader = wrapper.getByText("Modal header");

fireEvent.touchStart(modalHeader, {changedTouches: [{pageX: 0, pageY: 0}]});
fireEvent.touchMove(modalHeader, {changedTouches: [{pageX: 100, pageY: 50}]});
fireEvent.touchEnd(modalHeader, {changedTouches: [{pageX: 100, pageY: 50}]});

expect(() => wrapper.unmount()).not.toThrow();
expect(document.documentElement.clientWidth).toBe(1920);
expect(document.documentElement.clientHeight).toBe(1080);
expect(modalHeader.style.cursor).toBe("move");
expect(modal.style.transform).toBe("translate(100px, 50px)");
});

it("should be rendered a draggable modal on mobile", () => {
// mock viewport size to 375x667
jest.spyOn(document.documentElement, "clientWidth", "get").mockImplementation(() => 375);
jest.spyOn(document.documentElement, "clientHeight", "get").mockImplementation(() => 667);

const wrapper = render(<ModalDraggable />);

const modal = wrapper.getByRole("dialog");
const modalHeader = wrapper.getByText("Modal header");

fireEvent.touchStart(modalHeader, {changedTouches: [{pageX: 0, pageY: 0}]});
fireEvent.touchMove(modalHeader, {changedTouches: [{pageX: 0, pageY: 50}]});
fireEvent.touchEnd(modalHeader, {changedTouches: [{pageX: 0, pageY: 50}]});

expect(document.documentElement.clientWidth).toBe(375);
expect(document.documentElement.clientHeight).toBe(667);
expect(modal.style.transform).toBe("translate(0px, 50px)");
});

it("should not drag overflow viewport", () => {
// mock viewport size to 1920x1080
jest.spyOn(document.documentElement, "clientWidth", "get").mockImplementation(() => 1920);
jest.spyOn(document.documentElement, "clientHeight", "get").mockImplementation(() => 1080);
const wrapper = render(<ModalDraggable />);
const modal = wrapper.getByRole("dialog");
const modalHeader = wrapper.getByText("Modal header");

fireEvent.touchStart(modalHeader, {changedTouches: [{pageX: 100, pageY: 50}]});
fireEvent.touchMove(modalHeader, {changedTouches: [{pageX: 10000, pageY: 5000}]});
fireEvent.touchEnd(modalHeader, {changedTouches: [{pageX: 10000, pageY: 5000}]});

expect(modal.style.transform).toBe("translate(1920px, 1080px)");
});

it("should not drag when disabled", () => {
// mock viewport size to 1920x1080
jest.spyOn(document.documentElement, "clientWidth", "get").mockImplementation(() => 1920);
jest.spyOn(document.documentElement, "clientHeight", "get").mockImplementation(() => 1080);
const wrapper = render(<ModalDraggable isDisabled />);
const modal = wrapper.getByRole("dialog");
const modalHeader = wrapper.getByText("Modal header");

fireEvent.touchStart(modalHeader, {changedTouches: [{pageX: 100, pageY: 50}]});
fireEvent.touchMove(modalHeader, {changedTouches: [{pageX: 200, pageY: 100}]});
fireEvent.touchEnd(modalHeader, {changedTouches: [{pageX: 200, pageY: 100}]});

expect(modal.style.transform).toBe("");
});

test("should be rendered a draggable modal with overflow", () => {
// mock viewport size to 1920x1080
jest.spyOn(document.documentElement, "clientWidth", "get").mockImplementation(() => 1920);
jest.spyOn(document.documentElement, "clientHeight", "get").mockImplementation(() => 1080);

const wrapper = render(<ModalDraggable canOverflow />);

const modal = wrapper.getByRole("dialog");
const modalHeader = wrapper.getByText("Modal header");

fireEvent.touchStart(modalHeader, {changedTouches: [{pageX: 0, pageY: 0}]});
fireEvent.touchMove(modalHeader, {changedTouches: [{pageX: 2000, pageY: 1500}]});
fireEvent.touchEnd(modalHeader, {changedTouches: [{pageX: 2000, pageY: 1500}]});

expect(document.documentElement.clientWidth).toBe(1920);
expect(document.documentElement.clientHeight).toBe(1080);
expect(modal.style.transform).toBe("translate(2000px, 1500px)");
});
});
2 changes: 2 additions & 0 deletions packages/components/modal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
},
"dependencies": {
"@nextui-org/use-disclosure": "workspace:*",
"@nextui-org/use-draggable": "workspace:*",
"@nextui-org/use-aria-button": "workspace:*",
"@nextui-org/framer-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
Expand All @@ -63,6 +64,7 @@
"@nextui-org/checkbox": "workspace:*",
"@nextui-org/button": "workspace:*",
"@nextui-org/link": "workspace:*",
"@nextui-org/switch": "workspace:*",
"react-lorem-component": "0.13.0",
"framer-motion": "^11.0.22",
"clean-package": "2.2.0",
Expand Down
1 change: 1 addition & 0 deletions packages/components/modal/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type {UseDisclosureProps} from "@nextui-org/use-disclosure";
// export hooks
export {useModal} from "./use-modal";
export {useDisclosure} from "@nextui-org/use-disclosure";
export {useDraggable} from "@nextui-org/use-draggable";

// export context
export {ModalProvider, useModalContext} from "./modal-context";
Expand Down
9 changes: 7 additions & 2 deletions packages/components/modal/src/modal-header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {useEffect} from "react";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/react-utils";
import {ReactRef, useDOMRef} from "@nextui-org/react-utils";
import {clsx} from "@nextui-org/shared-utils";

import {useModalContext} from "./modal-context";

export interface ModalHeaderProps extends HTMLNextUIProps<"header"> {}
export interface ModalHeaderProps extends HTMLNextUIProps<"header"> {
/**
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLElement | null>;
}

const ModalHeader = forwardRef<"header", ModalHeaderProps>((props, ref) => {
const {as, children, className, ...otherProps} = props;
Expand Down
Loading