diff --git a/docs/docs/components/modal.mdx b/docs/docs/components/modal.mdx new file mode 100644 index 0000000..1795364 --- /dev/null +++ b/docs/docs/components/modal.mdx @@ -0,0 +1,22 @@ +# 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 | diff --git a/lib/src/components/Portal/Portal.tsx b/lib/src/components/Portal/Portal.tsx new file mode 100644 index 0000000..62b272d --- /dev/null +++ b/lib/src/components/Portal/Portal.tsx @@ -0,0 +1,20 @@ +import { useEffect, useMemo } from "react"; +import { createPortal } from "react-dom"; +import { PortalType } from "./portal.types"; + +const Portal = ({ children }: PortalType): JSX.Element => { + const portalRoot = document.body; + const mountElement = useMemo(() => document.createElement("dialog"), []); + mountElement.setAttribute("open", "true"); + mountElement.style.cssText = "padding:0;border:0"; + + useEffect(() => { + portalRoot.appendChild(mountElement); + return () => { + portalRoot.removeChild(mountElement); + }; + }, [mountElement, portalRoot]); + return createPortal(children, mountElement); +}; + +export { Portal }; diff --git a/lib/src/components/Portal/portal.types.ts b/lib/src/components/Portal/portal.types.ts new file mode 100644 index 0000000..84469c5 --- /dev/null +++ b/lib/src/components/Portal/portal.types.ts @@ -0,0 +1,3 @@ +export interface PortalType { + children: React.ReactNode; +} diff --git a/lib/src/hooks/useBodyScrollLock.tsx b/lib/src/hooks/useBodyScrollLock.tsx new file mode 100644 index 0000000..b6062e2 --- /dev/null +++ b/lib/src/hooks/useBodyScrollLock.tsx @@ -0,0 +1,13 @@ +import { useLayoutEffect } from "react"; + +const useLockBodyScroll = (value: boolean): void => { + useLayoutEffect((): (() => void) => { + value + ? (document.body.style.overflow = "hidden") + : (document.body.style.overflow = "auto"); + + return () => (document.body.style.overflow = "auto"); + }, []); +}; + +export { useLockBodyScroll }; diff --git a/lib/src/hooks/useClickOutside.tsx b/lib/src/hooks/useClickOutside.tsx new file mode 100644 index 0000000..13ecebd --- /dev/null +++ b/lib/src/hooks/useClickOutside.tsx @@ -0,0 +1,33 @@ +import { RefObject, useEffect } from "react"; + +type AnyEvent = MouseEvent | TouchEvent; + +function useClickOutside( + ref: RefObject, + handler: (event: AnyEvent) => void +): void { + useEffect(() => { + const listener = (event: AnyEvent) => { + const el = ref?.current; + + // Do nothing if clicking ref's element or descendent elements + if (!el || el.contains(event.target as Node)) { + return; + } + + handler(event); + }; + + document.addEventListener(`mousedown`, listener); + document.addEventListener(`touchstart`, listener); + + return () => { + document.removeEventListener(`mousedown`, listener); + document.removeEventListener(`touchstart`, listener); + }; + + // Reload only if ref or handler changes + }, [ref, handler]); +} + +export { useClickOutside };