diff --git a/docs/docs/components/modal.mdx b/docs/docs/components/modal.mdx index 1795364..561b4aa 100644 --- a/docs/docs/components/modal.mdx +++ b/docs/docs/components/modal.mdx @@ -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 `` element instead of `
` - -| 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); + + { + setIsOpen(false); + }} + > + {/* Modal content */} + ; +} +``` + + + + + + + + + + + + + + + + + + +##### Modal without Heading and Icon + +```jsx +{/* Modal content */} +``` + + + +I am a very simple and happy modal + + + +##### Modal without Overlay + +```jsx +{/* Modal content */} +``` + + + +I do not have a overlay + + + +##### Prevent Clicking Outside Modal to Close + +```jsx +{/* Modal content */} +``` + + + +I am a persistent kinda modal + + + +##### 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 content */} + +``` + + + +I am a very red and angry modal + + + +### 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 | diff --git a/docs/src/components/examples/ModalExample.tsx b/docs/src/components/examples/ModalExample.tsx new file mode 100644 index 0000000..bd188a0 --- /dev/null +++ b/docs/src/components/examples/ModalExample.tsx @@ -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 ( +
+ + { + setModalOpen(false); + }} + {...props} + > +
+ ); +}; + +export default ModalExample; diff --git a/lib/src/components/Modal/Modal.stories.tsx b/lib/src/components/Modal/Modal.stories.tsx new file mode 100644 index 0000000..cd6b59b --- /dev/null +++ b/lib/src/components/Modal/Modal.stories.tsx @@ -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 = ({ + title, + children, + closeOnClickOutside, + ...props +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + return ( + <> + + { + setIsModalOpen(false); + }} + title={title} + closeOnClickOutside={closeOnClickOutside} + > + {children} + + + ); +}; + +ModalStory.args = { + title: "Modal Title", + children:
Modal Content
, + closeOnClickOutside: true, + isCloseIconVisible: true, +}; +ModalStory.argTypes = {}; diff --git a/lib/src/components/Modal/Modal.tsx b/lib/src/components/Modal/Modal.tsx new file mode 100644 index 0000000..d88845d --- /dev/null +++ b/lib/src/components/Modal/Modal.tsx @@ -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 = ({ + children, + isOpen, + onClose, + title, + closeOnClickOutside = true, + isCloseIconVisible = true, + baseStyles, + overlayStyles, + showOverlay = true, + style, + className, + ...nativeProps +}) => { + const modalSurfaceRef = React.useRef(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 ( + +
+ {showOverlay &&
} +
+
+ {!!title &&

{title}

} + {isCloseIconVisible && ( + + )} +
+ {children} +
+
+ + ); +}; + +export { Modal }; diff --git a/lib/src/components/Modal/index.ts b/lib/src/components/Modal/index.ts new file mode 100644 index 0000000..b41a5a7 --- /dev/null +++ b/lib/src/components/Modal/index.ts @@ -0,0 +1,3 @@ +export * from "./Modal"; +export * from "./modal.types"; +export * from "./modal.styles.css"; diff --git a/lib/src/components/Modal/modal.constants.ts b/lib/src/components/Modal/modal.constants.ts new file mode 100644 index 0000000..b5744c9 --- /dev/null +++ b/lib/src/components/Modal/modal.constants.ts @@ -0,0 +1,18 @@ +export const sizes = { + sm: { + width: "320px", + }, + md: { + width: "440px", + }, + lg: { + width: "550px", + }, + xl: { + width: "720px", + }, + full: { + height: "100%", + width: "100%", + }, +}; diff --git a/lib/src/components/Modal/modal.styles.css.ts b/lib/src/components/Modal/modal.styles.css.ts new file mode 100644 index 0000000..2f52546 --- /dev/null +++ b/lib/src/components/Modal/modal.styles.css.ts @@ -0,0 +1,98 @@ +import { createTheme, style } from "@vanilla-extract/css"; +import { sizes } from "./modal.constants"; +import { IModalTheme } from "./modal.types"; + +export const [modalThemeClass, modalThemeVars]: IModalTheme = createTheme({ + base: { + backgroundColor: "#fff", + borderRadius: "4px", + boxShadow: "0 0 10px rgba(0, 0, 0, 0.5)", + position: "fixed", + transform: "translate(-50%, -50%)", + top: "50%", + bottom: "unset", + left: "unset", + right: "unset", + padding: "12px", + zIndex: "10", + width: sizes.md.width, + height: "auto", + }, + overlay: { + backgroundColor: "rgba(0, 0, 0)", + zIndex: "1", + position: "fixed", + top: "0", + left: "0", + right: "0", + bottom: "0", + filter: "unset", + opacity: "0.5", + }, +}); +export const overlayStyleClass = style({ + position: modalThemeVars.overlay.position as + | "static" + | "relative" + | "absolute" + | "sticky" + | "fixed", + top: modalThemeVars.overlay.top, + left: modalThemeVars.overlay.left, + right: modalThemeVars.overlay.right, + bottom: modalThemeVars.overlay.bottom, + backgroundColor: modalThemeVars.overlay.backgroundColor, + zIndex: modalThemeVars.overlay.zIndex, + filter: modalThemeVars.overlay.filter, + opacity: modalThemeVars.overlay.opacity, +}); + +export const modalStyleClass = style({ + transitionProperty: "transform, opacity", + transitionDuration: "250ms", + transitionTimingFunction: "ease", + transformOrigin: "center center", + opacity: 1, + + background: modalThemeVars.base.backgroundColor, + borderRadius: modalThemeVars.base.borderRadius, + boxShadow: modalThemeVars.base.boxShadow, + position: modalThemeVars.base.position as + | "static" + | "relative" + | "absolute" + | "sticky" + | "fixed", + transform: modalThemeVars.base.transform, + top: modalThemeVars.base.top, + bottom: modalThemeVars.base.bottom, + left: modalThemeVars.base.left, + right: modalThemeVars.base.right, + padding: modalThemeVars.base.padding, + zIndex: modalThemeVars.base.zIndex, + width: modalThemeVars.base.width, + height: modalThemeVars.base.height, +}); + +export const modalHeaderStyleClass = style({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", +}); + +export const modalTitleStyleClass = style({ + fontSize: "inherit", + textDecoration: "none", + marginRight: "16px", + textOverflow: "ellipsis", + display: "block", + wordBreak: "break-word", +}); + +export const modalCloseStyleClass = style({ + background: "none", + border: "none", + cursor: "pointer", + padding: "0", + marginLeft: "auto", +}); diff --git a/lib/src/components/Modal/modal.types.ts b/lib/src/components/Modal/modal.types.ts new file mode 100644 index 0000000..8110751 --- /dev/null +++ b/lib/src/components/Modal/modal.types.ts @@ -0,0 +1,46 @@ +export type IModalProps = JSX.IntrinsicElements["div"] & { + children: React.ReactNode; + isOpen: boolean; + onClose: () => void; + title?: string; + size?: "sm" | "md" | "lg" | "xl" | "full"; + width?: string; + height?: string; + closeOnClickOutside?: boolean; + isCloseIconVisible?: boolean; + baseStyles?: Partial; + overlayStyles?: Partial; + showOverlay?: boolean; +}; + +export type IModalTheme = [ + string, + { + base: { + backgroundColor: string; + borderRadius: string; + boxShadow: string; + position: string; + transform: string; + top: string; + bottom: string; + left: string; + right: string; + padding: string; + zIndex: string; + width: string; + height: string; + }; + overlay: { + backgroundColor: string; + zIndex: string; + position: string; + top: string; + left: string; + right: string; + bottom: string; + opacity: string; + filter: string; + }; + } +]; diff --git a/lib/src/components/_internal/Icons/Close.tsx b/lib/src/components/_internal/Icons/Close.tsx new file mode 100644 index 0000000..287f18c --- /dev/null +++ b/lib/src/components/_internal/Icons/Close.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Icon } from "../../../components/Icon"; +import { IIconProps } from "../../../components/Icon/icon.type"; + +const CloseIcon: React.FC = (props) => { + return ( + + + + + + ); +}; + +export default CloseIcon; diff --git a/lib/src/index.ts b/lib/src/index.ts index 1864cd3..2680f2f 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -14,4 +14,5 @@ export * from "./components/Icon"; export * from "./components/TextArea"; export * from "./components/Tab"; export * from "./components/Avatar"; +export * from "./components/Modal"; export * from "./components/Table"; diff --git a/lib/src/utils/object-utils.ts b/lib/src/utils/object-utils.ts new file mode 100644 index 0000000..299b13a --- /dev/null +++ b/lib/src/utils/object-utils.ts @@ -0,0 +1,11 @@ +export const eliminateUndefinedKeys = (record: Record) => { + const keys = Object.keys(record); + const eliminated: Record = {}; + for (const key of keys) { + const value = record[key]; + if (value !== undefined) { + eliminated[key] = value; + } + } + return eliminated; +};