diff --git a/ui-kit/package.json b/ui-kit/package.json index 7c03d38d..1a9d8ecc 100644 --- a/ui-kit/package.json +++ b/ui-kit/package.json @@ -23,7 +23,8 @@ "dependencies": { "classnames": "^2.2.6", "ionicons": "^5.2.3", - "react-spring": "^8.0.27" + "react-spring": "^8.0.27", + "resize-observer-polyfill": "^1.5.1" }, "scripts": { "start": "start-storybook -p 6006 --no-dll", diff --git a/ui-kit/src/components/Accordion/index.tsx b/ui-kit/src/components/Accordion/index.tsx new file mode 100644 index 00000000..d909ad69 --- /dev/null +++ b/ui-kit/src/components/Accordion/index.tsx @@ -0,0 +1,86 @@ +import React, { forwardRef, HTMLAttributes, useEffect, useRef, useState } from 'react'; +import { Combine } from 'src/types/utils'; +import classnames from 'classnames'; +import Icon from '../Icon'; +import { chevronDown } from 'ionicons/icons'; +import Text from '../Text'; +import { useResizeObserver } from 'src/hooks/useResizeObserver'; +import { colors } from 'src/constants/colors'; + +type Props = Combine< + { + label: string; + defaultOpen?: boolean; + onChange?: (state: boolean) => void; + onOpen?: () => void; + onClose?: () => void; + }, + HTMLAttributes +>; +const Accordion = forwardRef(function Accordion( + { label, className, children, defaultOpen = false, onChange, onOpen, onClose, ...props }, + ref +) { + const [open, setOpen] = useState(defaultOpen); + const contentRef = useRef(null); + const [bodyHeight, setBodyHeight] = useState(0); + + const toggleContentOpen = () => { + setOpen((state) => !state); + }; + + const updateContentHeight = () => + setBodyHeight(contentRef.current?.getBoundingClientRect().height ?? 0); + + useResizeObserver(contentRef, updateContentHeight); + + useEffect(() => { + onChange?.(open); + }, [open]); + + useEffect(() => { + if (open === true) { + onOpen?.(); + } else { + onClose?.(); + } + }, [open]); + + return ( +
+
+ + + {label} + +
+
+
+ {children} +
+
+
+ ); +}); + +export default Accordion; diff --git a/ui-kit/src/hooks/useResizeObserver.ts b/ui-kit/src/hooks/useResizeObserver.ts new file mode 100644 index 00000000..c24bbbb5 --- /dev/null +++ b/ui-kit/src/hooks/useResizeObserver.ts @@ -0,0 +1,23 @@ +import { RefObject, useEffect, useRef } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; + +export function useResizeObserver( + ref: RefObject, + resizeCallback: (arg: ResizeObserverEntry['contentRect']) => void +) { + const resizeObsesrverRef = useRef(null); + const onResize = useRef(resizeCallback); + + useEffect(() => { + if (ref.current === null) { + return; + } + + resizeObsesrverRef.current = new ResizeObserver((entries) => { + onResize.current(entries[0].contentRect); + }); + resizeObsesrverRef.current.observe(ref.current); + + return () => resizeObsesrverRef.current?.disconnect(); + }, [ref]); +} diff --git a/ui-kit/src/index.ts b/ui-kit/src/index.ts index d77d0f70..97e73b19 100644 --- a/ui-kit/src/index.ts +++ b/ui-kit/src/index.ts @@ -21,6 +21,7 @@ export { export { default as Snackbar } from './components/Snackbar'; export { default as List, ListItem } from './components/List'; export { default as Input } from './components/Input'; +export { default as Accordion } from './components/Accordion'; export { Portal } from './contexts/Portal'; export { useToast } from './contexts/Toast'; export { useSnackbar } from './contexts/Snackbar'; diff --git a/ui-kit/src/sass/components/_Accordion.scss b/ui-kit/src/sass/components/_Accordion.scss new file mode 100644 index 00000000..b31ac4f3 --- /dev/null +++ b/ui-kit/src/sass/components/_Accordion.scss @@ -0,0 +1,42 @@ +$animation-duration: 0.3s ease-in-out; + +.lubycon-accordion { + border: { + top: 1px solid get-color('gray20'); + bottom: 1px solid get-color('gray20'); + } + & + & { + border-top: none; + } + &--opened { + .lubycon-accordion__label__icon { + transform: rotate(180deg); + } + } + &__label { + display: flex; + align-items: center; + padding: 16px; + user-select: none; + cursor: pointer; + transition: background-color 0.1s ease-in-out; + &:hover { + background-color: get-color('gray10'); + } + &__icon { + margin-right: 16px; + transition: transform $animation-duration; + } + &__text { + color: get-color('gray90'); + } + } + &__cover { + transition: height $animation-duration; + overflow: hidden; + &__content { + padding: 8px 16px 16px 50px; + transition: opacity $animation-duration; + } + } +} diff --git a/ui-kit/src/sass/components/_Input.scss b/ui-kit/src/sass/components/_Input.scss index 0b7019f7..2e9e2598 100644 --- a/ui-kit/src/sass/components/_Input.scss +++ b/ui-kit/src/sass/components/_Input.scss @@ -7,7 +7,7 @@ $description-position: 30px; background-color: get-color('gray20'); padding: 10px; border-radius: 8px; - border: 2px solid transparent; + border: 1px solid transparent; transition: border 0.1s ease-in-out, background-color 0.1s ease-in-out; box-sizing: border-box; @@ -25,7 +25,7 @@ $description-position: 30px; } &--focused { - border-color: get-color('gray50'); + border-color: get-color('gray100'); background-color: get-color('gray10'); } diff --git a/ui-kit/src/sass/components/_index.scss b/ui-kit/src/sass/components/_index.scss index 7e78b96f..14d8dd4c 100644 --- a/ui-kit/src/sass/components/_index.scss +++ b/ui-kit/src/sass/components/_index.scss @@ -1,3 +1,4 @@ +@import './Accordion'; @import './Alert'; @import './Button'; @import './Text'; diff --git a/ui-kit/src/stories/Accordion.stories.tsx b/ui-kit/src/stories/Accordion.stories.tsx new file mode 100644 index 00000000..5dbfd3fb --- /dev/null +++ b/ui-kit/src/stories/Accordion.stories.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Accordion } from 'src'; +import { Meta } from '@storybook/react/types-6-0'; + +export default { + title: 'Lubycon UI Kit/Accordion', +} as Meta; + +export const Default = () => { + return ( + <> + console.log(`onChange: ${v}`)} + onOpen={() => console.log('handleOpen')} + onClose={() => console.log('handleClose')} + > + 아코디언이 νŽΌμ³μ§€λ©΄ μ•„λž˜μ— λ‚΄μš©μ΄ λ‚˜μ˜΅λ‹ˆλ‹€. +
+ 아코디언이 νŽΌμ³μ§€λ©΄ μ•„λž˜μ— λ‚΄μš©μ΄ λ‚˜μ˜΅λ‹ˆλ‹€. +
+ 아코디언이 νŽΌμ³μ§€λ©΄ μ•„λž˜μ— λ‚΄μš©μ΄ λ‚˜μ˜΅λ‹ˆλ‹€. +
+
+ + κ·€μ—¬μš΄ 에비츄 + + + 아코디언이 νŽΌμ³μ§€λ©΄ μ•„λž˜μ— λ‚΄μš©μ΄ λ‚˜μ˜΅λ‹ˆλ‹€. +
+ 아코디언이 νŽΌμ³μ§€λ©΄ μ•„λž˜μ— λ‚΄μš©μ΄ λ‚˜μ˜΅λ‹ˆλ‹€. +
+ 아코디언이 νŽΌμ³μ§€λ©΄ μ•„λž˜μ— λ‚΄μš©μ΄ λ‚˜μ˜΅λ‹ˆλ‹€. +
+
+ + ); +}; diff --git a/yarn.lock b/yarn.lock index a4a9fb76..c91cac5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14974,6 +14974,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"