Skip to content

Commit

Permalink
Merge pull request #18 from Atnic/feature/modal
Browse files Browse the repository at this point in the history
feat: modal component
  • Loading branch information
muhamien authored Feb 23, 2024
2 parents e750893 + 44f8107 commit 96d8f55
Show file tree
Hide file tree
Showing 36 changed files with 1,954 additions and 4 deletions.
6 changes: 3 additions & 3 deletions packages/components/accordion/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @nextui-org/accordion
# @jala-banyu/accordion

Accordion display a list of high-level options that can expand/collapse to reveal more information.

Expand All @@ -7,9 +7,9 @@ Please refer to the [documentation](https://nextui.org//docs/components/accordio
## Installation

```sh
yarn add @nextui-org/accordion
yarn add @jala-banyu/accordion
# or
npm i @nextui-org/accordion
npm i @jala-banyu/accordion
```

## Contribution
Expand Down
24 changes: 24 additions & 0 deletions packages/components/modal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# @jala-banyu/modal

A Quick description of the component

> This is an internal utility, not intended for public usage.
## Installation

```sh
yarn add @jala-banyu/modal
# or
npm i @jala-banyu/modal
```

## Contribution

Yes please! See the
[contributing guidelines](https://github.com/Atnic/banyu/blob/master/CONTRIBUTING.md)
for details.

## Licence

This project is licensed under the terms of the
[MIT license](https://github.com/Atnic/banyu/blob/master/LICENSE).
28 changes: 28 additions & 0 deletions packages/components/modal/__tests__/modal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react";
import {render} from "@testing-library/react";

import {Modal} from "../src";

describe("Modal", () => {
it("should render correctly", () => {
// eslint-disable-next-line react/no-children-prop
const wrapper = render(
<Modal>
<span>test</span>
</Modal>,
);

expect(() => wrapper.unmount()).not.toThrow();
});

it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();

render(
<Modal ref={ref}>
<span>test</span>
</Modal>,
);
// expect(ref.current).not.toBeNull();
});
});
73 changes: 73 additions & 0 deletions packages/components/modal/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "@jala-banyu/modal",
"version": "0.0.0",
"description": "Displays a dialog with a custom content that requires attention or provides additional information.",
"keywords": [
"modal"
],
"author": "Muhammad Amien <[email protected]>",
"homepage": "#",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Atnic/banyu.git",
"directory": "packages/components/modal"
},
"bugs": {
"url": "https://github.com/Atnic/banyu/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"@jala-banyu/theme": ">=1.2.0",
"@jala-banyu/system": ">=1.0.0"
},
"dependencies": {
"@jala-banyu/use-disclosure": "workspace:*",
"@jala-banyu/use-aria-button": "workspace:*",
"@jala-banyu/framer-transitions": "workspace:*",
"@jala-banyu/shared-utils": "workspace:*",
"@jala-banyu/react-utils": "workspace:*",
"@jala-banyu/shared-icons": "workspace:*",
"@jala-banyu/use-aria-modal-overlay": "workspace:*",
"@react-aria/dialog": "^3.5.7",
"@react-aria/interactions": "^3.19.1",
"@react-aria/overlays": "^3.18.1",
"@react-aria/utils": "^3.21.1",
"@react-stately/overlays": "^3.6.3",
"@react-aria/focus": "^3.14.3",
"@react-types/overlays": "^3.8.3",
"react-remove-scroll": "^2.5.6"
},
"devDependencies": {
"@jala-banyu/theme": "workspace:*",
"@jala-banyu/system": "workspace:*",
"@jala-banyu/input": "workspace:*",
"@jala-banyu/checkbox": "workspace:*",
"@jala-banyu/button": "workspace:*",
"@jala-banyu/link": "workspace:*",
"react-lorem-component": "0.13.0",
"framer-motion": "^10.16.4",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json"
}
23 changes: 23 additions & 0 deletions packages/components/modal/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Modal from "./modal";
import ModalContent from "./modal-content";
import ModalHeader from "./modal-header";
import ModalBody from "./modal-body";
import ModalFooter from "./modal-footer";

// export types
export type {ModalProps} from "./modal";
export type {ModalContentProps} from "./modal-content";
export type {ModalHeaderProps} from "./modal-header";
export type {ModalBodyProps} from "./modal-body";
export type {ModalFooterProps} from "./modal-footer";
export type {UseDisclosureProps} from "@jala-banyu/use-disclosure";

// export hooks
export {useModal} from "./use-modal";
export {useDisclosure} from "@jala-banyu/use-disclosure";

// export context
export {ModalProvider, useModalContext} from "./modal-context";

// export components
export {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter};
43 changes: 43 additions & 0 deletions packages/components/modal/src/modal-body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {useEffect} from "react";
import {forwardRef, HTMLBanyuProps} from "@jala-banyu/system";
import {useDOMRef} from "@jala-banyu/react-utils";
import {clsx} from "@jala-banyu/shared-utils";

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

export interface ModalBodyProps extends HTMLBanyuProps<"div"> {}

const ModalBody = forwardRef<"div", ModalBodyProps>((props, ref) => {
const {as, children, className, ...otherProps} = props;

const {slots, classNames, bodyId, setBodyMounted} = useModalContext();

const domRef = useDOMRef(ref);

const Component = as || "div";

/**
* Notify us if this component was rendered or used,
* so we can append `aria-labelledby` automatically
*/
useEffect(() => {
setBodyMounted(true);

return () => setBodyMounted(false);
}, [setBodyMounted]);

return (
<Component
ref={domRef}
className={slots.body({class: clsx(classNames?.body, className)})}
id={bodyId}
{...otherProps}
>
{children}
</Component>
);
});

ModalBody.displayName = "Banyu.ModalBody";

export default ModalBody;
116 changes: 116 additions & 0 deletions packages/components/modal/src/modal-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type {AriaDialogProps} from "@react-aria/dialog";
import type {HTMLMotionProps} from "framer-motion";

import {cloneElement, isValidElement, ReactNode, useMemo} from "react";
import {forwardRef} from "@jala-banyu/system";
import {DismissButton} from "@react-aria/overlays";
import {TRANSITION_VARIANTS} from "@jala-banyu/framer-transitions";
import {CloseIcon} from "@jala-banyu/shared-icons";
import {RemoveScroll} from "react-remove-scroll";
import {motion} from "framer-motion";
import {useDialog} from "@react-aria/dialog";
import {mergeProps} from "@react-aria/utils";
import {HTMLBanyuProps} from "@jala-banyu/system";

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

type KeysToOmit = "children" | "role";

export interface ModalContentProps extends AriaDialogProps, HTMLBanyuProps<"div", KeysToOmit> {
children: ReactNode | ((onClose: () => void) => ReactNode);
}

const ModalContent = forwardRef<"div", ModalContentProps, KeysToOmit>((props, _) => {
const {as, children, role = "dialog", ...otherProps} = props;

const {
Component: DialogComponent,
domRef,
slots,
isOpen,
classNames,
motionProps,
backdrop,
closeButton,
hideCloseButton,
disableAnimation,
shouldBlockScroll,
getDialogProps,
getBackdropProps,
getCloseButtonProps,
onClose,
} = useModalContext();

const Component = as || DialogComponent || "div";

const {dialogProps} = useDialog(
{
role,
},
domRef,
);

const closeButtonContent = isValidElement(closeButton) ? (
cloneElement(closeButton, getCloseButtonProps())
) : (
<button {...getCloseButtonProps()}>
<CloseIcon />
</button>
);

const content = (
<Component {...getDialogProps(mergeProps(dialogProps, otherProps))}>
<DismissButton onDismiss={onClose} />
{!hideCloseButton && closeButtonContent}
{typeof children === "function" ? children(onClose) : children}
<DismissButton onDismiss={onClose} />
</Component>
);

const backdropContent = useMemo(() => {
if (backdrop === "transparent") {
return null;
}

if (disableAnimation) {
return <div {...getBackdropProps()} />;
}

return (
<motion.div
animate="enter"
exit="exit"
initial="exit"
variants={TRANSITION_VARIANTS.fade}
{...(getBackdropProps() as HTMLMotionProps<"div">)}
/>
);
}, [backdrop, disableAnimation, getBackdropProps]);

return (
<div tabIndex={-1}>
{backdropContent}
<RemoveScroll forwardProps enabled={shouldBlockScroll && isOpen} removeScrollBar={false}>
{disableAnimation ? (
<div className={slots.wrapper({class: classNames?.wrapper})}>{content}</div>
) : (
<motion.div
animate="enter"
className={slots.wrapper({class: classNames?.wrapper})}
exit="exit"
initial="exit"
variants={scaleInOut}
{...motionProps}
>
{content}
</motion.div>
)}
</RemoveScroll>
</div>
);
});

ModalContent.displayName = "Banyu.ModalContent";

export default ModalContent;
9 changes: 9 additions & 0 deletions packages/components/modal/src/modal-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {createContext} from "@jala-banyu/react-utils";

import {UseModalReturn} from "./use-modal";

export const [ModalProvider, useModalContext] = createContext<UseModalReturn>({
name: "ModalContext",
errorMessage:
"useModalContext: `context` is undefined. Seems you forgot to wrap all popover components within `<Modal />`",
});
31 changes: 31 additions & 0 deletions packages/components/modal/src/modal-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {forwardRef, HTMLBanyuProps} from "@jala-banyu/system";
import {useDOMRef} from "@jala-banyu/react-utils";
import {clsx} from "@jala-banyu/shared-utils";

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

export interface ModalFooterProps extends HTMLBanyuProps<"footer"> {}

const ModalFooter = forwardRef<"footer", ModalFooterProps>((props, ref) => {
const {as, children, className, ...otherProps} = props;

const {slots, classNames} = useModalContext();

const domRef = useDOMRef(ref);

const Component = as || "footer";

return (
<Component
ref={domRef}
className={slots.footer({class: clsx(classNames?.footer, className)})}
{...otherProps}
>
{children}
</Component>
);
});

ModalFooter.displayName = "Banyu.ModalFooter";

export default ModalFooter;
Loading

0 comments on commit 96d8f55

Please sign in to comment.