-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added Modal and Portal components, useOnClickOutside hook and e…
…xternal types
- Loading branch information
1 parent
778d8b2
commit 6b24803
Showing
18 changed files
with
374 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Meta } from '@storybook/addon-docs' | ||
|
||
<Meta title='Components/Portal' /> | ||
|
||
# Poralt | ||
|
||
This component renders the children in a root div after all app nodes. see [the docs](https://reactjs.org/docs/portals.html) for more information. | ||
|
||
This component depends on ThemeProvider to run correctly. | ||
|
||
## Usage | ||
|
||
```tsx | ||
<Portal>Your components to render in a React Portal here...</Portal> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* eslint-disable react/jsx-no-useless-fragment */ | ||
|
||
import React, { useEffect, useState, memo, PropsWithChildren } from 'react' | ||
import { createPortal } from 'react-dom' | ||
|
||
type PortalProps = { | ||
enabled?: boolean | ||
} | ||
|
||
const PortalBase = ({ children, enabled }: PropsWithChildren<PortalProps>) => { | ||
const [mounted, setMounted] = useState(false) | ||
|
||
useEffect(() => { | ||
setMounted(true) | ||
|
||
return () => setMounted(false) | ||
}, []) | ||
|
||
if (!enabled) { | ||
return <>{children}</> | ||
} | ||
|
||
return mounted | ||
? createPortal( | ||
children, | ||
document.querySelector('#root-design-system-portals') as Element | ||
) | ||
: null | ||
} | ||
|
||
PortalBase.defaultProps = { | ||
enabled: true, | ||
} | ||
|
||
const Portal = memo(PortalBase) | ||
|
||
export { Portal } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Portal' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Portal' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './atoms' | ||
export * from './hocs' | ||
export * from './molecules' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { Meta } from '@storybook/addon-docs' | ||
|
||
import { Canvas } from 'core/storybookWrappers' | ||
|
||
import ModalStory from './ModalStory' | ||
|
||
<Meta title='Components/Modal' /> | ||
|
||
# Modal | ||
|
||
This is a Modal component with xstyled super powers. | ||
|
||
## Props & Children | ||
|
||
Obs.: Types used in this Doc: | ||
|
||
```ts | ||
import React, { ReactNode, Dispatch, SetStateAction } from 'react' | ||
|
||
type ModalFunctionNotation = { | ||
isOpen: boolean | ||
setIsOpen: Dispatch<SetStateAction<boolean>> | ||
} | ||
``` | ||
- render: (params: ModalFunctionNotation) => ReactNode | ||
A function that rentuns the components to render inside Modal. | ||
- externalIsOpen?: boolean | ||
A boolean that indicates if the Modal is open or not. When used, internal state is ignored, and setIsOpen does nothing. | ||
- children?: (params: ModalFunctionNotation) => ReactNode | null | ||
A function that returns the components to render in the place of your JSX that you putted the Modal component. | ||
## Usage | ||
```tsx | ||
<Modal | ||
render={({ isOpen, setIsOpen }) => ( | ||
<div> | ||
<button type='button' onClick={() => setIsOpen(false)}> | ||
close | ||
</button> | ||
<p>I'm a Modal and i'm {isOpen ? 'opened' : 'closed'}</p> | ||
</div> | ||
)} | ||
> | ||
{({ setIsOpen }) => ( | ||
<button type='button' onClick={() => setIsOpen(true)}> | ||
Open | ||
</button> | ||
)} | ||
</Modal> | ||
``` | ||
<Canvas> | ||
<ModalStory /> // The source code is above | ||
</Canvas> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
|
||
import React, { | ||
useRef, | ||
useState, | ||
useCallback, | ||
Dispatch, | ||
SetStateAction, | ||
memo, | ||
RefObject, | ||
ReactNode, | ||
} from 'react' | ||
|
||
import { | ||
ModalPortalDedupleContext, | ||
ModalPortalDedupleProvider, | ||
} from 'contexts/ModalPortalDedupleContext' | ||
import { motion } from 'framer-motion' | ||
|
||
import { FlexProps } from 'components/atoms' | ||
import { Portal } from 'components/hocs/Portal' | ||
import { CardProps } from 'components/molecules' | ||
|
||
import { useOnClickOutside } from 'hooks' | ||
|
||
import { ModalBackground, ModalCard } from './styled' | ||
|
||
type ModalFunctionNotation = { | ||
isOpen: boolean | ||
setIsOpen: Dispatch<SetStateAction<boolean>> | ||
excludeRef: RefObject<any> | ||
} | ||
|
||
export type ModalProps = { | ||
render: (params: ModalFunctionNotation) => ReactNode | ||
children?: (params: ModalFunctionNotation) => ReactNode | ||
externalIsOpen?: boolean | ||
backgroundProps?: FlexProps | ||
disableUseOnClickOutside?: boolean | ||
} & Omit<CardProps, 'children'> | ||
|
||
const motionBackgroundVariants = { | ||
hidden: { | ||
opacity: 0, | ||
transitionEnd: { | ||
display: 'none', | ||
}, | ||
}, | ||
visible: { | ||
opacity: 1, | ||
display: 'block', | ||
}, | ||
} | ||
|
||
const ModalBase = ({ | ||
render, | ||
externalIsOpen, | ||
children, | ||
backgroundProps, | ||
disableUseOnClickOutside, | ||
...props | ||
}: ModalProps) => { | ||
const [isOpen, setIsOpen] = useState<boolean>(false) | ||
|
||
const modalIsOpen = typeof externalIsOpen === 'boolean' ? externalIsOpen : isOpen | ||
|
||
const modalMainRef = useRef(null) | ||
|
||
const excludeRef = useRef(null) | ||
|
||
const hasParentModal = ModalPortalDedupleContext.useSelector( | ||
state => state.parentModalAlreadyExistsUsingPortal | ||
) | ||
|
||
const clickOutsideHandler = useCallback(() => { | ||
if (disableUseOnClickOutside) { | ||
return | ||
} | ||
|
||
setIsOpen(false) | ||
}, [disableUseOnClickOutside]) | ||
|
||
useOnClickOutside(modalMainRef, clickOutsideHandler, excludeRef) | ||
|
||
return ( | ||
<> | ||
{children && children({ isOpen: modalIsOpen, setIsOpen, excludeRef })} | ||
|
||
<ModalPortalDedupleProvider> | ||
<Portal enabled={!hasParentModal}> | ||
<motion.div | ||
variants={motionBackgroundVariants} | ||
initial='hidden' | ||
animate={modalIsOpen ? 'visible' : 'hidden'} | ||
transition={{ | ||
default: { duration: 0.3 }, | ||
}} | ||
> | ||
<ModalBackground variant='center' {...backgroundProps}> | ||
<ModalCard ref={modalMainRef} {...props}> | ||
{render({ isOpen: modalIsOpen, setIsOpen, excludeRef })} | ||
</ModalCard> | ||
</ModalBackground> | ||
</motion.div> | ||
</Portal> | ||
</ModalPortalDedupleProvider> | ||
</> | ||
) | ||
} | ||
|
||
ModalBase.defaultProps = { | ||
children: undefined, | ||
externalIsOpen: undefined, | ||
backgroundProps: undefined, | ||
disableUseOnClickOutside: false, | ||
} | ||
|
||
const Modal = memo(ModalBase) | ||
|
||
export { Modal } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React from 'react' | ||
|
||
import { Modal } from './Modal' | ||
|
||
const ModalStory = () => ( | ||
<div> | ||
<Modal | ||
render={({ isOpen, setIsOpen }) => ( | ||
<div> | ||
<button type='button' onClick={() => setIsOpen(false)}> | ||
close | ||
</button> | ||
<p>I'm a Modal and i'm {isOpen ? 'opened' : 'closed'}</p> | ||
</div> | ||
)} | ||
> | ||
{({ setIsOpen }) => ( | ||
<div> | ||
<br /> | ||
<br /> | ||
<br /> | ||
<br /> | ||
|
||
<button type='button' onClick={() => setIsOpen(true)}> | ||
Open | ||
</button> | ||
|
||
<br /> | ||
<br /> | ||
<br /> | ||
<br /> | ||
</div> | ||
)} | ||
</Modal> | ||
</div> | ||
) | ||
|
||
export default ModalStory |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Modal' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import styled from '@xstyled/styled-components' | ||
|
||
import { Flex } from 'components/atoms/Flex' | ||
import { Card } from 'components/molecules/Card' | ||
|
||
export const ModalBackground = styled(Flex)` | ||
position: absolute; | ||
top: 0; | ||
bottom: 0; | ||
left: 0; | ||
right: 0; | ||
background-color: systemTranslucidBlack; | ||
` | ||
|
||
export const ModalCard = styled(Card)` | ||
width: fit-content; | ||
max-width: 80vw; | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export * from './Fields' | ||
export * from './InfiniteBarLoader' | ||
export * from './Card' | ||
export * from './Container' | ||
export * from './Modal' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from 'react' | ||
|
||
import { createContext } from '@nexpy/react-easy-context-api' | ||
|
||
import { WithChildren } from 'types' | ||
|
||
type ModalPortalDedupleContextValue = { | ||
parentModalAlreadyExistsUsingPortal: boolean | ||
} | ||
|
||
export const ModalPortalDedupleContext = createContext<ModalPortalDedupleContextValue>({ | ||
parentModalAlreadyExistsUsingPortal: false, | ||
}) | ||
|
||
export const ModalPortalDedupleProvider = ({ children }: WithChildren) => ( | ||
<ModalPortalDedupleContext.Provider | ||
value={{ parentModalAlreadyExistsUsingPortal: true }} | ||
> | ||
{children} | ||
</ModalPortalDedupleContext.Provider> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './useOnClickOutside' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { useEffect, RefObject } from 'react' | ||
|
||
type Event = MouseEvent | TouchEvent | ||
|
||
export const useOnClickOutside = <T extends HTMLElement = HTMLElement>( | ||
ref: RefObject<T>, | ||
handler: (event: Event) => void, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
excludeRef?: RefObject<any> | ||
) => { | ||
useEffect(() => { | ||
const listener = (event: Event) => { | ||
const el = ref?.current | ||
if (!el || el.contains((event?.target as Node) || null)) { | ||
return | ||
} | ||
|
||
const elex = excludeRef?.current | ||
if (elex) { | ||
if (elex.contains((event?.target as Node) || null)) { | ||
return | ||
} | ||
} | ||
|
||
handler(event) | ||
} | ||
|
||
document.addEventListener('mousedown', listener) | ||
document.addEventListener('touchstart', listener) | ||
|
||
return () => { | ||
document.removeEventListener('mousedown', listener) | ||
document.removeEventListener('touchstart', listener) | ||
} | ||
}, [ref, handler, excludeRef]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './styled' | ||
export * from './react-definitions' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { ReactNode } from 'react' | ||
|
||
export type WithChildren<T = unknown> = T & { children?: ReactNode } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import slugifyCore from 'slugify' | ||
|
||
export const slugify = (value: string) => { | ||
return slugifyCore(value, { lower: true }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './formatters' | ||
export * from './styled' | ||
export * from './theme' |