diff --git a/package.json b/package.json index 5570da3..0c58a6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wap-ui", - "version": "1.1.6", + "version": "1.1.8", "repository": "https://github.com/pknu-wap/2022_2_WAP_WEB_TEAM1.git", "author": "neko113 ", "license": "MIT", diff --git a/packages/components/Modal/Modal.stories.tsx b/packages/components/Modal/Modal.stories.tsx index 241ba8c..aec6954 100644 --- a/packages/components/Modal/Modal.stories.tsx +++ b/packages/components/Modal/Modal.stories.tsx @@ -1,8 +1,9 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; import { Modal } from './Modal'; -import React, { useState } from 'react'; +import React from 'react'; import { Button } from '../Button'; import styled from '@emotion/styled'; +import useDisclosure from '../../hooks/useDisclosure'; export default { title: 'Components/Modal', @@ -10,21 +11,13 @@ export default { } as ComponentMeta; const Template: ComponentStory = () => { - const [visible, setVisible] = useState(false); - - const openModal = () => { - setVisible(true); - }; - - const closeModal = () => { - setVisible(false); - }; + const { isOpen, onOpen, onClose } = useDisclosure(); return ( - - - Header + + + Header Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do @@ -38,7 +31,7 @@ const Template: ComponentStory = () => { - + diff --git a/packages/components/Portal/Portal.stories.tsx b/packages/components/Portal/Portal.stories.tsx index c9a3047..d8360c7 100644 --- a/packages/components/Portal/Portal.stories.tsx +++ b/packages/components/Portal/Portal.stories.tsx @@ -1,9 +1,10 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; -import React, { useState } from 'react'; +import React from 'react'; import { Button } from '../Button'; import styled from '@emotion/styled'; import { Modal } from '../Modal'; import { Portal, Props } from './Portal'; +import useDisclosure from '../../hooks/useDisclosure'; export default { title: 'Components/Portal', @@ -11,23 +12,15 @@ export default { } as ComponentMeta; const Template: ComponentStory = (args: Props) => { - const [visible, setVisible] = useState(false); - - const openModal = () => { - setVisible(true); - }; - - const closeModal = () => { - setVisible(false); - }; + const { isOpen, onOpen, onClose } = useDisclosure(); return ( <> - + - - Header + + Header Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do @@ -41,7 +34,7 @@ const Template: ComponentStory = (args: Props) => { - + diff --git a/packages/hooks/index.ts b/packages/hooks/index.ts new file mode 100644 index 0000000..8aed404 --- /dev/null +++ b/packages/hooks/index.ts @@ -0,0 +1,5 @@ +export { default as useDidMountEffect } from './useDidMountEffect'; +export { default as useDisclosure } from './useDisclosure'; +export { default as useThrottle } from './useThrottle'; +export { default as useDebounce } from './useDebounce'; +export { default as useIntersectionObserver } from './useIntersectionObserver'; diff --git a/packages/hooks/useDebounce.ts b/packages/hooks/useDebounce.ts new file mode 100644 index 0000000..3e31ad0 --- /dev/null +++ b/packages/hooks/useDebounce.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from 'react'; + +interface Props { + value: T; + delay?: number; +} + +/** + * useDebounce hook은 delay 안에 값 변경 시 값을 유지한다. + * delay 안에 함수가 한번더 실행되면 앞의 작업을 취소하고 delay이 + * 지날 때까지 다시 호출 되지 않으면 callback을 실행하는 형식으로 되어있다. + * + * @example + * const [searchText, setSearchText] = useState(''); + * const debouncedText = useDebounce({ value: searchText }); + * setSearchText(e.target.value)} + * /> + */ +const useDebounce = ({ value, delay = 500 }: Props): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}; + +export default useDebounce; diff --git a/packages/hooks/useDisclosure.ts b/packages/hooks/useDisclosure.ts new file mode 100644 index 0000000..816cfd1 --- /dev/null +++ b/packages/hooks/useDisclosure.ts @@ -0,0 +1,32 @@ +import { useCallback, useState } from 'react'; + +interface Props { + defaultIsOpen?: boolean; +} + +/** + * {열린상태, 열기, 닫기, 토글} 의 기능을 가진다. + * @example + * const { isOpen, onOpen, onClose, onToggle } = useDisclosure(); + */ + +const useDisclosure = ({ defaultIsOpen = false }: Props = {}) => { + const [isOpen, setIsOpen] = useState(defaultIsOpen); + + const onClose = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const onOpen = useCallback(() => { + setIsOpen(true); + }, [setIsOpen]); + + const onToggle = useCallback(() => { + const action = isOpen ? onClose : onOpen; + action(); + }, [isOpen, onClose, onOpen]); + + return { isOpen, onOpen, onClose, onToggle }; +}; + +export default useDisclosure; diff --git a/packages/hooks/useIntersectionObserver.ts b/packages/hooks/useIntersectionObserver.ts new file mode 100644 index 0000000..6eb3e3e --- /dev/null +++ b/packages/hooks/useIntersectionObserver.ts @@ -0,0 +1,43 @@ +import { useEffect, useRef } from 'react'; + +interface useIntersectionObserverProps { + onIntersect: () => void; +} +/** + * IntersectionObserver(교차 관찰자 API)는 타겟 엘레멘트와 + * 타겟의 부모 혹은 상위 엘레멘트의 뷰포트가 교차되는 부분을 비동기적으로 관찰하는 API이다. + * + * @example + * const loadMore = () => { + if (hasNextPage) fetchNextPage(); + }; + const targetElement = useIntersectionObserver({ onIntersect: loadMore }); + */ +const useIntersectionObserver = ({ + onIntersect, +}: useIntersectionObserverProps) => { + const targetElement = useRef(null); + + useEffect(() => { + if (!targetElement || !targetElement.current) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => entry.isIntersecting && onIntersect()); + }, + { + threshold: 0.5, + }, + ); + + observer.observe(targetElement && targetElement.current); + + return () => { + observer.disconnect(); + }; + }, [onIntersect]); + + return targetElement; +}; + +export default useIntersectionObserver; diff --git a/packages/hooks/useThrottle.ts b/packages/hooks/useThrottle.ts new file mode 100644 index 0000000..495f263 --- /dev/null +++ b/packages/hooks/useThrottle.ts @@ -0,0 +1,29 @@ +import { useEffect, useRef, useState } from 'react'; + +interface Props { + value: T; + delay?: number; +} + +/** + * useThrottle hook은 delay마다 값의 변경이 이루어 진다. + * delay 뒤에 callback이 실행되고 delay이 지나기전 다시 호출될 경우 아직 + * delay이 지나지 않았기 때문에 callback을 실행하지 않고 함수를 종료시키는 형태로 되어있다. + */ +const useTrottle = ({ value, delay = 500 }: Props) => { + const [throttleValue, setTrottleValue] = useState(value); + const throttling = useRef(false); + useEffect(() => { + if (throttling.current === false) { + setTrottleValue(value); + throttling.current = true; + } + setTimeout(() => { + if (throttling?.current) throttling.current = false; + }, delay); + }, [value, delay]); + + return throttleValue; +}; + +export default useTrottle; diff --git a/packages/index.ts b/packages/index.ts index 51c04f8..76b4436 100644 --- a/packages/index.ts +++ b/packages/index.ts @@ -1,4 +1,5 @@ export * from './components'; export * from './layouts'; +export * from './hooks'; export { WapUIProvider } from './theme/theme-provider';