From 568ed449b2075e6094fa84ec7cb577aa33c3a66a Mon Sep 17 00:00:00 2001 From: yogjin Date: Sun, 1 Oct 2023 21:45:44 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=AC=BC=EA=B2=B0=EC=9D=B4=20?= =?UTF-8?q?=ED=8D=BC=EC=A7=80=EB=8A=94=20=ED=9A=A8=EA=B3=BC=EB=A5=BC=20?= =?UTF-8?q?=EC=A3=BC=EB=8A=94=20`Ripple`=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/@common/Ripple/Ripple.tsx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 frontend/src/components/@common/Ripple/Ripple.tsx diff --git a/frontend/src/components/@common/Ripple/Ripple.tsx b/frontend/src/components/@common/Ripple/Ripple.tsx new file mode 100644 index 000000000..24919aa20 --- /dev/null +++ b/frontend/src/components/@common/Ripple/Ripple.tsx @@ -0,0 +1,128 @@ +import React, { useState, useLayoutEffect, CSSProperties } from 'react'; +import PropTypes from 'prop-types'; + +const useDebouncedRippleCleanUp = ( + rippleCount: number, + duration: number, + cleanUpFunction: Function, +) => { + useLayoutEffect(() => { + let bounce: any = null; + if (rippleCount > 0) { + clearTimeout(bounce); + + bounce = setTimeout(() => { + cleanUpFunction(); + clearTimeout(bounce); + }, duration * 4); + } + + return () => clearTimeout(bounce); + }, [rippleCount, duration, cleanUpFunction]); +}; + +type Props = { + duration?: number; + color?: CSSProperties['color']; +}; + +type Ripple = { + x: number; + y: number; + size: number; +}; + +/** + * + * @Ripple + * + * 물결이 퍼지는 효과를 추가하는 컴포넌트 + * + * 사용방법: 효과를 적용하고 싶은 컴포넌트에 ``을 children으로 선언한다. + * + * ` + * ... + * + * ` + * + * `Component` style에 + * `{ + * position: relative; + * overflow: hidden; + * }` 을 추가해야한다. + */ +const Ripple = ({ duration = 500, color = '#fff' }: Props) => { + const [rippleArray, setRippleArray] = useState([]); + + useDebouncedRippleCleanUp(rippleArray.length, duration, () => { + setRippleArray([]); + }); + + const addRipple = (event: any) => { + const rippleContainer = event.currentTarget.getBoundingClientRect(); + const size = + rippleContainer.width > rippleContainer.height + ? rippleContainer.width + : rippleContainer.height; + const x = event.pageX - rippleContainer.x - size / 2; + const y = event.pageY - rippleContainer.y - size / 2; + const newRipple = { + x, + y, + size, + }; + + setRippleArray([...rippleArray, newRipple]); + }; + + return ( + + {rippleArray.length > 0 && + rippleArray.map((ripple, index) => { + return ( + + ); + })} + + ); +}; + +export default Ripple; + +import styled from 'styled-components'; +import { Color } from 'styles/theme'; + +const S = { + RippleContainer: styled.div<{ duration: number }>` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + span { + transform: scale(0); + border-radius: 100%; + position: absolute; + opacity: 0.75; + background-color: ${(props) => props.color}; + animation-name: ripple; + animation-duration: ${(props) => props.duration}ms; + } + + @keyframes ripple { + to { + opacity: 0; + transform: scale(2); + } + } + `, +}; From d76e1f919057895c8e54b3328e16ab7937e0b0ac Mon Sep 17 00:00:00 2001 From: yogjin Date: Sun, 1 Oct 2023 21:45:59 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20`Ripple`=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@common/Ripple/Ripple.stories.tsx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 frontend/src/components/@common/Ripple/Ripple.stories.tsx diff --git a/frontend/src/components/@common/Ripple/Ripple.stories.tsx b/frontend/src/components/@common/Ripple/Ripple.stories.tsx new file mode 100644 index 000000000..ec3cf56b6 --- /dev/null +++ b/frontend/src/components/@common/Ripple/Ripple.stories.tsx @@ -0,0 +1,45 @@ +import { Meta, StoryObj } from '@storybook/react'; +import Ripple from './Ripple'; + +/** + * 아무곳이나 클릭해보세요. + * + * 물결이 퍼지는 `Ripple` 효과를 볼 수 있어요. + */ +const meta = { + title: 'common/Ripple', + component: Ripple, + args: { + color: 'gray', + }, + argTypes: {}, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = {}; + +/** + * 버튼에 Ripple 을 적용한 예시입니다. + */ +export const Button: Story = { + render: ({}) => { + return ( + + ); + }, +};