From e4435bcbe0f13217b1cc8ff71412146690bcd33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=88=98=20MINSOO=20KIM?= Date: Sun, 14 May 2023 17:17:19 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20=EC=97=AC=EA=B2=83=EC=A0=80?= =?UTF-8?q?=EA=B2=83=20=EC=88=98=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 12 ++++++++++++ .eslintignore | 3 ++- .eslintrc.json | 2 +- .prettierignore | 3 ++- 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ebe51d3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index b512c09..97008e5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +yarn.lock \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 37dbb1b..bc4e694 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,7 +6,7 @@ "extends": [ "eslint:recommended", "plugin:react/recommended", - "plgin:react-hooks/recommended", + "plugin:react-hooks/recommended", "plugin:storybook/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended" diff --git a/.prettierignore b/.prettierignore index b512c09..97008e5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +yarn.lock \ No newline at end of file From 8d8b6d2f70b2990c0dc0c314b15a0e981ad1c84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=88=98=20MINSOO=20KIM?= Date: Sun, 14 May 2023 17:19:04 +0900 Subject: [PATCH 2/6] docs: update readme (#89) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3768ecb..037f308 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ To use WAP UI, Install the `@wap-ui/react` package and its peer dependencies. (`@emotion/react`, `@emotion/styled`, `framer-motion`) ```sh +pnpm add @wap-ui/react @emotion/react @emotion/styled framer-motion +# or yarn add @wap-ui/react @emotion/react @emotion/styled framer-motion # or npm i @wap-ui/react @emotion/react @emotion/styled framer-motion From a783da4e7a5e064233d129fb7d8f4e0dd06dc4c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=88=98=20MINSOO=20KIM?= Date: Sun, 14 May 2023 17:20:28 +0900 Subject: [PATCH 3/6] feat: add usePortal hook (#89) --- docs/components/Portal.md | 32 ----------------- packages/react/src/components/index.ts | 1 - packages/react/src/hooks/usePortal.ts | 50 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 33 deletions(-) delete mode 100644 docs/components/Portal.md create mode 100644 packages/react/src/hooks/usePortal.ts diff --git a/docs/components/Portal.md b/docs/components/Portal.md deleted file mode 100644 index c6e85a9..0000000 --- a/docs/components/Portal.md +++ /dev/null @@ -1,32 +0,0 @@ -# `[Component] Portal` - -## `type` - -```tsx -interface Portal { - children: React.ReactNode; - - /** - * @description DOM의 id 혹은 element를 지정합니다. - * @default 'portal' - className으로 지정됨 - * - */ - target?: HTMLElement | string; -} -``` - -## `example` - -```tsx - - - - - - - - - - - -``` diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 0342e28..7e5f3f5 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -3,7 +3,6 @@ export * from './Card'; export * from './Checkbox'; export * from './Radio'; export * from './Modal'; -export * from './Portal'; export * from './Accordion'; export * from './TextInput'; export * from './Toggle'; diff --git a/packages/react/src/hooks/usePortal.ts b/packages/react/src/hooks/usePortal.ts new file mode 100644 index 0000000..aea60df --- /dev/null +++ b/packages/react/src/hooks/usePortal.ts @@ -0,0 +1,50 @@ +import { useEffect, useState } from 'react'; + +const createElement = (id: string): HTMLElement => { + const el = document.createElement('div'); + el.setAttribute('id', id); + return el; +}; + +const isBrowser = (): boolean => { + return Boolean( + typeof window !== 'undefined' && + window.document && + window.document.createElement, + ); +}; + +/** +@description +usePortal은 문서의 본문에 div 요소를 만들고 반환하는 훅입니다. +이는 모달, 드롭다운 및 툴팁을 만드는 데 유용합니다. +index.html에 div 요소를 추가하지 않아도 됩니다. +@example +const Modal = () => { + const el = usePortal('modal'); + if (!el) return null; + return createPortal(, el); +}; + */ +const usePortal = (selectId: string): HTMLElement | null => { + const id = `${selectId}`; + + const [elSnapshot, setElSnapshot] = useState( + isBrowser() ? createElement(id) : null, + ); + + useEffect(() => { + const parentElement = document.body; + const hasElement = parentElement?.querySelector(`#${id}`); + const el = hasElement || createElement(id); + + if (!hasElement) { + parentElement.appendChild(el); + } + setElSnapshot(el); + }, []); + + return elSnapshot; +}; + +export default usePortal; From 614b0c21d40b5474654ee6938d929708c3ae3102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=88=98=20MINSOO=20KIM?= Date: Sun, 14 May 2023 17:30:54 +0900 Subject: [PATCH 4/6] refactor: change portal to usePortal hook (#89) --- packages/react/.storybook/preview-head.html | 4 +- .../src/components/Dropdown/Dropdown.tsx | 9 +- packages/react/src/components/Modal/Modal.tsx | 57 +++++++------ .../src/components/Portal/Portal.stories.tsx | 60 -------------- .../react/src/components/Portal/Portal.tsx | 82 ------------------- packages/react/src/components/Portal/index.ts | 1 - .../src/components/Toast/Toaster/Toaster.tsx | 35 ++++---- packages/react/src/hooks/index.ts | 2 + 8 files changed, 59 insertions(+), 191 deletions(-) delete mode 100644 packages/react/src/components/Portal/Portal.stories.tsx delete mode 100644 packages/react/src/components/Portal/Portal.tsx delete mode 100644 packages/react/src/components/Portal/index.ts diff --git a/packages/react/.storybook/preview-head.html b/packages/react/.storybook/preview-head.html index 2e3ae13..e551040 100644 --- a/packages/react/.storybook/preview-head.html +++ b/packages/react/.storybook/preview-head.html @@ -1 +1,3 @@ - + diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index 730bd6a..9454ac7 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -1,10 +1,10 @@ import React, { useRef, useState } from 'react'; -import useOnClickOutside from '../../hooks/useOnClickOutside'; import { DropdownMenu } from './DropdownMenu'; import { DropdownMenuItem } from './DropdownMenuItem'; import { DropdownButton } from './DropdownButton/DropdownButton'; import { DropdownContext } from './DropdownContext'; -import { Portal } from '../Portal'; +import { usePortal, useOnClickOutside } from '../../hooks'; +import { createPortal } from 'react-dom'; export interface DropdownProps { children: React.ReactNode[]; @@ -27,6 +27,7 @@ export interface DropdownProps { * ``` */ export const Dropdown = ({ children }: DropdownProps) => { + const el = usePortal('dropdown'); /** triggerRef은 Context를 타고 DropdownButton으로 전달됨 */ const triggerRef = useRef(null); const contentRef = useRef(null); @@ -42,6 +43,8 @@ export const Dropdown = ({ children }: DropdownProps) => { } }); + if (!el) return null; + return ( { {/* tirgger button Actions */} {trigger} {/* content menu ... */} - {content} + {createPortal(content, el)} ); }; diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx index 5446237..1ed1c8f 100644 --- a/packages/react/src/components/Modal/Modal.tsx +++ b/packages/react/src/components/Modal/Modal.tsx @@ -1,10 +1,11 @@ import { AnimatePresence, useWillChange, type Variants } from 'framer-motion'; import React, { useLayoutEffect } from 'react'; -import { Portal } from '../Portal'; import * as S from './Modal.styles'; import { ModalBody } from './ModalBody/ModalBody'; import { ModalFooter } from './ModalFooter/ModalFooter'; import { ModalHeader } from './ModalHeader/ModalHeader'; +import { usePortal } from '../../hooks'; +import { createPortal } from 'react-dom'; export interface ModalProps { /** @@ -52,6 +53,7 @@ export const Modal = ({ children, }: ModalProps) => { const willChange = useWillChange(); + const el = usePortal('modal'); useLayoutEffect(() => { if (isOpen) { @@ -88,37 +90,38 @@ export const Modal = ({ }, }; - return ( - - - {isOpen && ( - <> - + {isOpen && ( + <> + + + - - - {children} - - - - )} - - + > + {children} + + + + )} + , + el, ); }; diff --git a/packages/react/src/components/Portal/Portal.stories.tsx b/packages/react/src/components/Portal/Portal.stories.tsx deleted file mode 100644 index 6ca77a2..0000000 --- a/packages/react/src/components/Portal/Portal.stories.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import React from 'react'; -import { Button } from '../Button'; -import styled from '@emotion/styled'; -import { Modal } from '../Modal'; -import { Portal, PortalProps } from './Portal'; -import useDisclosure from '../../hooks/useDisclosure'; - -export default { - title: 'Components/Portal', - component: Portal, -} as ComponentMeta; - -const Template: ComponentStory = (args: PortalProps) => { - const { isOpen, onOpen, onClose } = useDisclosure(); - - return ( - <> - - - - - Header - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut - enim ad minim veniam, quis nostrud exercitation ullamco laboris - nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor - in reprehenderit in voluptate velit esse cillum dolore eu fugiat - nulla pariatur. Excepteur sint occaecat cupidatat non proident, - sunt in culpa qui officia deserunt mollit anim id est laborum. - - - - - - - - - - - ); -}; - -const Container = styled.div` - height: 200vh; -`; - -export const Default = Template.bind({}); - -export const WithId = Template.bind({}); -WithId.args = { - target: 'modal', -}; - -export const WithElement = Template.bind({}); -WithElement.args = { - target: document.getElementById('modal') as HTMLElement, -}; diff --git a/packages/react/src/components/Portal/Portal.tsx b/packages/react/src/components/Portal/Portal.tsx deleted file mode 100644 index 9c1a024..0000000 --- a/packages/react/src/components/Portal/Portal.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import ReactDOM from 'react-dom'; - -export interface PortalProps { - children: React.ReactNode; - - /** - * @description DOM의 id 혹은 element를 지정합니다. - * @default 'portal' - className으로 지정됨 - * - */ - target?: HTMLElement | string; -} - -/** - * 참고 - * @see https://github.com/mantinedev/mantine/blob/master/src/mantine-core/src/Portal/Portal.tsx - * - * @example - * - * - * - * - * - * - * - * - * - * - * - */ - -export const Portal = ({ children, target }: PortalProps) => { - const portalContainer = useRef(); - - if (typeof target === 'string') { - /** - * @description target이 string일 경우, 해당 id를 가진 element를 찾는다 - */ - if (document.getElementById(target) as HTMLElement) { - portalContainer.current = document.getElementById(target) as HTMLElement; - } else { - /** - * 찾지 못할 경우 target을 className으로 갖는 element를 만든다 - */ - if (!portalContainer.current) { - portalContainer.current = document.createElement('div'); - portalContainer.current.className = target; - document.body.appendChild(portalContainer.current); - } - } - } else if (target instanceof HTMLElement) { - /** - * @description target이 HTMLElement일 경우, 해당 element를 찾는다 - */ - portalContainer.current = target; - document.body.appendChild(portalContainer.current); - } else { - /** - * @description target이 없을 경우, body에 portal라는 className를 가진 div element를 생성한다 - */ - if (!portalContainer.current) { - const div = document.createElement('div'); - div.className = 'portal'; - portalContainer.current = div; - } - document.body.appendChild(portalContainer.current); - } - - useEffect(() => { - return () => { - if (target === undefined && portalContainer.current) { - document.body.removeChild(portalContainer.current); - } - }; - }, [target]); - - return ReactDOM.createPortal( - children, - portalContainer.current as HTMLElement, - ); -}; diff --git a/packages/react/src/components/Portal/index.ts b/packages/react/src/components/Portal/index.ts deleted file mode 100644 index ca6e556..0000000 --- a/packages/react/src/components/Portal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Portal } from './Portal'; diff --git a/packages/react/src/components/Toast/Toaster/Toaster.tsx b/packages/react/src/components/Toast/Toaster/Toaster.tsx index 21866c2..10b61db 100644 --- a/packages/react/src/components/Toast/Toaster/Toaster.tsx +++ b/packages/react/src/components/Toast/Toaster/Toaster.tsx @@ -1,9 +1,10 @@ import { AnimatePresence } from 'framer-motion'; import React, { useEffect, useState } from 'react'; -import { Portal } from '../../Portal'; import { ToastBar } from '../ToastBar/ToastBar'; import { getToastList } from '../useToast'; import * as S from './Toaster.styles'; +import { usePortal } from '../../../hooks'; +import { createPortal } from 'react-dom'; type ToastPosition = | 'top-left' @@ -85,6 +86,7 @@ const getPositionStyles = (position: ToastPosition): React.CSSProperties => { */ export const Toaster = ({ position = 'bottom-center' }: ToasterProps) => { + const el = usePortal('toast'); const positionStyles = getPositionStyles(position); const [isBottom, setIsBottom] = useState(true); const toastList = getToastList(); @@ -97,21 +99,20 @@ export const Toaster = ({ position = 'bottom-center' }: ToasterProps) => { } }, [position, setIsBottom]); - return ( - <> - - - - - {toastList.map((toast) => ( - - {toast.message} - - ))} - - - - - + if (!el) return null; + + return createPortal( + + + + {toastList.map((toast) => ( + + {toast.message} + + ))} + + + , + el, ); }; diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 8aed404..23f0d09 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -3,3 +3,5 @@ export { default as useDisclosure } from './useDisclosure'; export { default as useThrottle } from './useThrottle'; export { default as useDebounce } from './useDebounce'; export { default as useIntersectionObserver } from './useIntersectionObserver'; +export { default as usePortal } from './usePortal'; +export { default as useOnClickOutside } from './useOnClickOutside'; From f292e19f3c8ba48c239207a576424cfbfd35cafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=88=98=20MINSOO=20KIM?= Date: Sun, 14 May 2023 17:32:21 +0900 Subject: [PATCH 5/6] v1.4.0 --- packages/react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/package.json b/packages/react/package.json index db6b5a9..e275ff4 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@wap-ui/react", - "version": "1.3.3", + "version": "1.4.0", "description": "WAP UI / React UI Components Library", "repository": "https://github.com/pknu-wap/2022_2_WAP_WEB_TEAM1.git", "author": "neko113 ", From 2b0c39e0b81e6e11633a9c6a6e2a8892dfec867d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EC=88=98=20MINSOO=20KIM?= Date: Sun, 14 May 2023 17:33:40 +0900 Subject: [PATCH 6/6] chore: update readme (#89) --- packages/react/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react/README.md b/packages/react/README.md index 796374a..037f308 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -30,10 +30,10 @@ - + - + @@ -45,6 +45,8 @@ To use WAP UI, Install the `@wap-ui/react` package and its peer dependencies. (`@emotion/react`, `@emotion/styled`, `framer-motion`) ```sh +pnpm add @wap-ui/react @emotion/react @emotion/styled framer-motion +# or yarn add @wap-ui/react @emotion/react @emotion/styled framer-motion # or npm i @wap-ui/react @emotion/react @emotion/styled framer-motion @@ -141,7 +143,7 @@ export default Home; ## `Links` - #### [Documents](https://github.com/pknu-wap/wap-ui/tree/main/docs) -- #### [NPM](https://www.npmjs.com/package/wap-ui) +- #### [NPM](https://www.npmjs.com/package/@wap-ui/react) - #### [Playground](https://wap-ui.vercel.app/) - #### [Presentations](https://github.com/pknu-wap/wap-ui/tree/main/ppt) - #### [Example](https://github.com/pknu-wap/wap-ui/tree/main/example)