Skip to content
191 changes: 169 additions & 22 deletions docs/docs/components/modal.mdx
Original file line number Diff line number Diff line change
@@ -1,22 +1,169 @@
# Modal (Spec)

## Notes

- use Create Portal for Markup
- Proper settings to control the trigger
- Proper way to adjust size, position, etc.
- Overlay Customization and option to not show it
- Lock scroll when modal is visible
- Close on ClickOutside
- Close on EscapeKey
- use `<dialog>` element instead of `<div>`

| Attributes | Values | Optional ? |
| :-------------------- | :------------------: | ---------: |
| `isOpen` | boolean (true/false) | No |
| `onClose` | function | Yes |
| `onOpen` | function | Yes |
| `size` | string | Yes |
| `showOverlay` | boolean | Yes |
| `closeOnOutsideClick` | boolean | Yes |
| `overlayStyles` | Style Object | Yes |
# Modal

### Quick start

Here's a quick start guide to get started with the modal component

### Importing Component

```jsx
import { Modal } from "@hover-design/react";
```

### Code Snippets and Examples

##### Simple Modal

import "@hover-design/react/dist/style.css";
import { Modal, Label, Input, Flex, Button } from "@hover-design/react";
import ModalExample from "@site/src/components/examples/ModalExample";

```jsx
import { Modal, Label, Input, Flex, Button } from "@hover-design/react";

function Demo() {
const [isOpen, setIsOpen] = useState(false);
<Button onClick={()=>{setIsOpen(true)}}>Open Modal</Button>
<Modal
title="Modal Title"
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
}}
>
{/* Modal content */}
</Modal>;
}
```

<ModalExample
title="Modal Title"
baseStyles={{ backgroundColor: "var(--ifm-navbar-background-color)" }}
>
<Flex gap="12px">
<Flex gap="4px" flexDirection="column">
<Label>
Label
<Input placeholder="Input" />
</Label>
<Label>
Label
<Input placeholder="Input" />
</Label>
<Label>
Label
<Input placeholder="Input" />
</Label>
</Flex>
<Flex gap="4px" flexDirection="column">
<Label>
Label
<Input placeholder="Input" />
</Label>
<Label>
Label
<Input placeholder="Input" />
</Label>
</Flex>
</Flex>
<Flex justifyContent="flex-end">
<Button>Submit</Button>
</Flex>
</ModalExample>

##### Modal without Heading and Icon

```jsx
<Modal isCloseIconVisible={false}>{/* Modal content */}</Modal>
```

<ModalExample baseStyles={{ backgroundColor: "var(--ifm-navbar-background-color)" }} isCloseIconVisible={false}>

<span>I am a very simple and happy modal</span>

</ModalExample>

##### Modal without Overlay

```jsx
<Modal showOverlay={false}>{/* Modal content */}</Modal>
```

<ModalExample baseStyles={{ backgroundColor: "var(--ifm-navbar-background-color)" }} showOverlay={false}>

<span>I do not have a overlay</span>

</ModalExample>

##### Prevent Clicking Outside Modal to Close

```jsx
<Modal closeOnClickOutside={false}>{/* Modal content */}</Modal>
```

<ModalExample baseStyles={{ backgroundColor: "var(--ifm-navbar-background-color)" }} closeOnClickOutside={false}>

<span>I am a persistent kinda modal</span>

</ModalExample>

##### Customizing Modal Base and overlay

You can customize the base and overlay styles of the modal by passing in the baseStyles and overlayStyles props. Refer this Spec for this:

overlayStyles

| Property | Description | Default |
| --------------- | --------------------- | ------------- |
| backgroundColor | Background of Overlay | rgba(0, 0, 0) |
| zIndex | Z Index | 1 |
| position | Position | fixed |
| top | Top | 0 |
| left | Left | 0 |
| right | Right | 0 |
| bottom | Bottom | 0 |
| filter | Filter | unset |
| opacity | opacity | 0.5 |

baseStyles

| Property | Description | Default |
| --------------- | ------------------ | --------------------------- |
| backgroundColor | Background of Base | rgba(255, 255, 255) |
| zIndex | Z Index | 10 |
| position | Position | relative |
| transform | Transform | translate(-50%, -50%) |
| top | Top | 50% |
| left | Left | unset |
| right | Right | unset |
| bottom | Bottom | unset |
| padding | Padding | 12px |
| width | Width | 440px |
| height | height | auto |
| boxShadow | Box Shadow | 0 0 10px rgba(0, 0, 0, 0.5) |

```jsx
<Modal baseStyles={{ backgroundColor: "#C1292E" }} isCloseIconVisible={false}>
{/* Modal content */}
</Modal>
```

<ModalExample overlayStyles={{backgroundColor:"red"}} baseStyles={{ backgroundColor: "#C1292E" }} isCloseIconVisible={false}>

<span>I am a very red and angry modal</span>

</ModalExample>

### Props Reference

| Key | type | Optional? |
| :------------------ | :------------------: | --------: |
| children | `React.ReactNode` | No |
| isOpen | `boolean` | No |
| onClose | `()=>void` | No |
| title | `string` | Yes |
| closeOnClickOutside | `boolean` | Yes |
| isCloseIconVisible | `boolean` | Yes |
| baseStyles | `base CSS object` | Yes |
| overlayStyles | `overlay CSS object` | Yes |
| showOverlay | `boolean` | Yes |
27 changes: 27 additions & 0 deletions docs/src/components/examples/ModalExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Modal, IModalProps, Button } from "@hover-design/react";

import React, { useState } from "react";

const ModalExample = (props: IModalProps) => {
const [isModalOpen, setModalOpen] = useState(false);
return (
<div>
<Button
onClick={() => {
setModalOpen(true);
}}
>
Open Modal
</Button>
<Modal
isOpen={isModalOpen}
onClose={() => {
setModalOpen(false);
}}
{...props}
></Modal>
</div>
);
};

export default ModalExample;
44 changes: 44 additions & 0 deletions lib/src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Story } from "@ladle/react";
import { useState } from "react";
import { Modal } from "./Modal";
import { IModalProps } from "./modal.types";

export const ModalStory: Story<IModalProps> = ({
title,
children,
closeOnClickOutside,
...props
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
<button
onClick={() => {
setIsModalOpen(true);
}}
>
Open Modal
</button>
<Modal
{...props}
baseStyles={{ backgroundColor: "var(--ladle-bg-color-secondary)" }}
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
}}
title={title}
closeOnClickOutside={closeOnClickOutside}
>
{children}
</Modal>
</>
);
};

ModalStory.args = {
title: "Modal Title",
children: <div>Modal Content</div>,
closeOnClickOutside: true,
isCloseIconVisible: true,
};
ModalStory.argTypes = {};
90 changes: 90 additions & 0 deletions lib/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { assignInlineVars } from "@vanilla-extract/dynamic";
import React, { useState } from "react";
import { eliminateUndefinedKeys } from "../../utils/object-utils";
import { useClickOutside } from "../../hooks/useClickOutside";
import { Portal } from "../Portal/Portal";
import CloseIcon from "../_internal/Icons/Close";
import {
modalCloseStyleClass,
modalHeaderStyleClass,
modalStyleClass,
modalThemeClass,
modalThemeVars,
modalTitleStyleClass,
overlayStyleClass,
} from "./modal.styles.css";
import { IModalProps } from "./modal.types";

const Modal: React.FC<IModalProps> = ({
children,
isOpen,
onClose,
title,
closeOnClickOutside = true,
isCloseIconVisible = true,
baseStyles,
overlayStyles,
showOverlay = true,
style,
className,
...nativeProps
}) => {
const modalSurfaceRef = React.useRef<HTMLDivElement>(null);
if (closeOnClickOutside) {
useClickOutside(modalSurfaceRef, onClose);
}

if (isOpen === false) {
return null;
}
const customStyles = assignInlineVars(
eliminateUndefinedKeys({
[modalThemeVars.base.backgroundColor]: baseStyles?.backgroundColor,
[modalThemeVars.base.borderRadius]: baseStyles?.borderRadius,
[modalThemeVars.base.width]: baseStyles?.width,
[modalThemeVars.base.height]: baseStyles?.height,
[modalThemeVars.base.top]: baseStyles?.top,
[modalThemeVars.base.left]: baseStyles?.left,
[modalThemeVars.base.right]: baseStyles?.right,
[modalThemeVars.base.bottom]: baseStyles?.bottom,
[modalThemeVars.base.transform]: baseStyles?.transform,
[modalThemeVars.base.position]: baseStyles?.position,
[modalThemeVars.base.padding]: baseStyles?.padding,
[modalThemeVars.base.zIndex]: baseStyles?.zIndex,
[modalThemeVars.base.boxShadow]: baseStyles?.boxShadow,
[modalThemeVars.overlay.backgroundColor]: overlayStyles?.backgroundColor,
[modalThemeVars.overlay.zIndex]: overlayStyles?.zIndex,
[modalThemeVars.overlay.opacity]: overlayStyles?.opacity,
[modalThemeVars.overlay.top]: overlayStyles?.top,
[modalThemeVars.overlay.left]: overlayStyles?.left,
[modalThemeVars.overlay.right]: overlayStyles?.right,
[modalThemeVars.overlay.bottom]: overlayStyles?.bottom,
[modalThemeVars.overlay.filter]: overlayStyles?.filter,
})
);
return (
<Portal>
<div style={customStyles} className={modalThemeClass}>
{showOverlay && <div className={overlayStyleClass} />}
<div
ref={modalSurfaceRef}
style={style}
className={`${modalStyleClass} ${className}`}
{...nativeProps}
>
<div className={modalHeaderStyleClass}>
{!!title && <p className={modalTitleStyleClass}>{title}</p>}
{isCloseIconVisible && (
<button className={modalCloseStyleClass} onClick={onClose}>
<CloseIcon height={"16"} width={"16"} />
</button>
)}
</div>
{children}
</div>
</div>
</Portal>
);
};

export { Modal };
3 changes: 3 additions & 0 deletions lib/src/components/Modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./Modal";
export * from "./modal.types";
export * from "./modal.styles.css";
18 changes: 18 additions & 0 deletions lib/src/components/Modal/modal.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const sizes = {
sm: {
width: "320px",
},
md: {
width: "440px",
},
lg: {
width: "550px",
},
xl: {
width: "720px",
},
full: {
height: "100%",
width: "100%",
},
};
Loading