diff --git a/src/components/Accordion/Accordion.stories.tsx b/src/components/Accordion/Accordion.stories.tsx new file mode 100644 index 00000000..c477f17f --- /dev/null +++ b/src/components/Accordion/Accordion.stories.tsx @@ -0,0 +1,319 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { default as Accordion } from './index'; +export default { + title: 'Accordion', + component: Accordion, +}; + +export const DefaultAccordion = () => { + return ( + + +

+ Default Accordion +

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ ); +}; + +export const MultipleAccordions = () => { + return ( + <> + + +

Accordion 1

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + +

Accordion 2

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + +

Accordion 3

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + ); +}; + +export const DisabledAccordion = () => { + return ( + <> + + +

Accordion 1

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + +

Accordion 2

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + +

Accordion 3

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + ); +}; + +export const ControlledAccordion: React.FC = () => { + const [expanded, setExpanded] = React.useState(''); + + const handleChange = (panel: string) => { + return (open?: boolean) => setExpanded(open ? panel : ''); + }; + return ( + <> + + +

Accordion 1

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + +

Accordion 2

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + +

Accordion 3

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + ); +}; + +export const StyledAccordion = () => { + return ( + <> + + +

Styled Accordion

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+ + +

Nested Accordion

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Vestibulum dapibus justo erat. Lorem ipsum dolor sit amet, + consectetur adipiscing elit. Donec ut turpis cursus, tempus ex + in, dictum ante. Vivamus consequat, justo imperdiet ultricies + cursus, neque nisl elementum ligula, a pharetra velit nibh vel + purus. Nam ornare leo non purus fringilla, id lacinia libero + rhoncus. Cras dolor nulla, porta sed neque nec, auctor dapibus + ligula. Etiam at interdum neque. Suspendisse imperdiet odio + nisi, lobortis lacinia turpis porttitor eu. Nam fermentum neque + nulla, ut dapibus ante dignissim at. Nam sodales, sem sed + pulvinar scelerisque, arcu orci luctus diam, ut luctus ipsum ex + eu nunc. Curabitur at purus cursus, fermentum nisi nec, varius + orci. Quisque in eros dictum, imperdiet sapien eu, vulputate + sapien. +

+
+
+
+
+ + + +

+ Styled Accordion +

+

+ Secondary Text +

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + + +

Click Me

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + ); +}; + +export const DiffTypeAccordion = () => { + return ( +
+ + +

Primary Accordion

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + + +

Secondary Accordion

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + + +

Danger Accordion

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+ + +

Info Accordion

+
+ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. +

+
+
+
+ ); +}; diff --git a/src/components/Accordion/Accordion.test.tsx b/src/components/Accordion/Accordion.test.tsx new file mode 100644 index 00000000..cb1be523 --- /dev/null +++ b/src/components/Accordion/Accordion.test.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { render, fireEvent,screen } from '@testing-library/react'; +import Accordion from './index' + +describe('Accordion', () => { + + it('should match snapshot', () => { + const { asFragment } = render( + + +

hello

+
+ +

New Accoirdon detail helloooo

+
+
) + expect(asFragment()).toMatchSnapshot(); + }); + + it('should render items', () => { + const headerText = 'HelloFromHeader' + const detailText = 'HelloFromDetail' + render( + + +

{headerText}

+
+ +

{detailText}

+
+
) + const headerEl = screen.queryByText(headerText) + const detailEl = screen.queryByText(detailText) + expect(headerEl).toBeInTheDocument(); + expect(detailEl).toBeInTheDocument(); + }); + + it('should open when the expanded props passed', () => { + const headerText = 'HelloFromHeader' + const detailText = 'HelloFromDetail' + render( + + +

{headerText}

+
+ +

{detailText}

+
+
) + const activeDiv = document.querySelector('.accordion-container.active') + expect(activeDiv).toHaveClass('accordion-container active') + }) + + it('should open when the header clicked', () => { + const headerText = 'HelloFromHeader' + const detailText = 'HelloFromDetail' + render( + + +

{headerText}

+
+ +

{detailText}

+

{detailText}

+
+
) + const headerEl = screen.queryByText(headerText) as HTMLDivElement + const inActiveDiv = document.querySelector('.accordion-container') + expect(inActiveDiv).not.toHaveClass('accordion-container active') + fireEvent.click(headerEl) + const activeDiv = document.querySelector('.accordion-container.active') + expect(activeDiv).toHaveClass('accordion-container active') + }) + it('should close when the accordion already open', () => { + const headerText = 'HelloFromHeader' + const detailText = 'HelloFromDetail' + render( + + +

{headerText}

+
+ +

{detailText}

+
+
) + const headerEl = screen.queryByText(headerText) as HTMLDivElement + const currentDiv = headerEl.closest('.accordion-container') + + fireEvent.click(headerEl) + fireEvent.click(headerEl) + expect(currentDiv).not.toHaveClass('active') + }) + it('should have a style when props passed', () => { + const headerText = 'HelloFromHeader' + const detailText = 'HelloFromDetail' + render( + + +

{headerText}

+
+ +

{detailText}

+
+
) + + const accordionDiv = document.querySelector('.accordion-container') + expect(accordionDiv).toHaveStyle('background-color : red;') + }) + it('should have a render multiple child elements', () => { + const headerText = 'HelloFromHeader' + const headerText2 = 'HelloFromHeader2' + const detailText = 'HelloFromDetail' + render( + + +

{headerText}

+ +
+ +

{detailText}

+

{headerText2}

+

{headerText2}

+

{headerText2}

+
+
) + + const accordionHeaderDiv = document.querySelector('.accordion-detail-container-content') as HTMLDivElement + + expect(accordionHeaderDiv.children).toHaveLength(4) + }) + +}) diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx new file mode 100644 index 00000000..216158d2 --- /dev/null +++ b/src/components/Accordion/Accordion.tsx @@ -0,0 +1,87 @@ +import React, { + useState, + InputHTMLAttributes, + useEffect, + ReactElement, +} from 'react'; +import AccordionDetail from './AccordionDetail'; +import AccordionHeader from './AccordionHeader'; +import { classNames } from '../../utils/classNames'; +import { AccordionDetailProps } from './AccordionDetail'; +import { AccordionHeaderProps } from './AccordionHeader'; + +export type AccordionType = + | 'primary' + | 'secondary' + | 'danger' + | 'info' + | 'default'; + +export interface IAccordionProps { + children: [ + ReactElement, + ReactElement + ]; + disabled?: boolean; + expanded?: boolean; + sx?: {}; + rounded?: boolean; + onClick?: (a?: boolean) => void; + key?: number | string; + accordionType?: AccordionType; +} + +export type bhdrProps = InputHTMLAttributes; +type InputArgs = IAccordionProps & Omit; +function Accordion(props: InputArgs) { + const { children, disabled, accordionType, onClick, expanded, sx, key } = + props; + + const [open, setOpen] = useState(false); + useEffect(() => { + setOpen(expanded as boolean); + }, [expanded]); + + const btnOnClick = (a: boolean) => { + if (!disabled) { + setOpen(!a); + } + }; + + const checkOpenorNot = () => { + if (typeof onClick === 'function') { + onClick?.(!open); + } + }; + let styleClasses = classNames('accordion-container', { + [`acc-${accordionType}`]: true, + disabled: !!disabled, + }); + + return ( +
+ btnOnClick(open)} + key={`header${key}`} + expandedStyle={children[0]?.props['expandedStyle']} + > + {children[0].props.children} + + + + {children[1].props.children} + +
+ ); +} + +export default Accordion; diff --git a/src/components/Accordion/AccordionDetail.tsx b/src/components/Accordion/AccordionDetail.tsx new file mode 100644 index 00000000..bf8759b2 --- /dev/null +++ b/src/components/Accordion/AccordionDetail.tsx @@ -0,0 +1,29 @@ +import React, { ReactElement, Children, cloneElement } from 'react'; +import { IAccordionProps } from './Accordion'; +export type AccordionDetailProps = { + children: ReactElement | ReactElement[]; + isOpen?: boolean; + expanded?: boolean; + sx?: {}; +}; +function AccordionDetail(props: AccordionDetailProps) { + return ( +
+
+ {props.children + ? Children.map( + props.children, + (child: ReactElement) => { + return cloneElement(child); + } + ) + : props.children} +
+
+ ); +} + +export default AccordionDetail; diff --git a/src/components/Accordion/AccordionHeader.tsx b/src/components/Accordion/AccordionHeader.tsx new file mode 100644 index 00000000..5dc24829 --- /dev/null +++ b/src/components/Accordion/AccordionHeader.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +export type AccordionHeaderProps = { + children: JSX.Element | JSX.Element[]; + btnOnClick?: () => void; + isOpen?: boolean; + sx?: {}; + expandedStyle?: {}; +}; +const AccordionHeader = (props: AccordionHeaderProps) => { + return ( +
+
+ {props.children} +
+
+ ); +}; + +export default AccordionHeader; diff --git a/src/components/Accordion/_Accordion.scss b/src/components/Accordion/_Accordion.scss new file mode 100644 index 00000000..9d7e9b6a --- /dev/null +++ b/src/components/Accordion/_Accordion.scss @@ -0,0 +1,93 @@ +.accordion-container { + border-top: 1px solid $gray-300; + border-bottom: 1px solid $gray-300; + cursor: pointer; + user-select: none; + background-color: $white; + &.disabled, + &[disabled] { + cursor: not-allowed; + opacity: $btn-disabled-opacity; + box-shadow: none; + background-color: $gray-600; + } + &.acc-primary { + background-color: $primary; + color: white; + } + &.acc-secondary { + background-color: $secondary; + color: white; + } + &.acc-danger { + background-color: $danger; + color: white; + } + &.acc-info { + background-color: $info; + color: white; + } +} + +.accordion-container.active { + margin: 16px 0; + .accordion-container:nth-of-type(n + 2) { + border-top: 1px solid $cyan; + } + + &.acc-primary { + border: 1px solid $cyan; + &.acc-primary:last-of-type { + border-bottom: 1px solid $cyan; + } + &.acc-primary:nth-of-type(n + 2) { + border-top: 1px solid $cyan; + } + } +} + +.accordion-header__btn { + width: 100%; + overflow: hidden; + display: flex; + align-items: center; + font-size: 22px; + font-weight: 500; +} + +.accordion-header__btn > p { + padding-left: 20px; + padding-top: 15px; + max-width: 70%; + word-wrap: break-word; +} + +.accordion-header__btn::after { + content: ''; + flex-shrink: 1; + width: 20px; + height: 20px; + margin-left: auto; + margin-right: 10px; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-size: 22px; + transition: transform 0.4s ease-in-out; +} + +.accordion-header.active .accordion-header__btn::after { + transform: rotate(-180deg); +} + +.accordion-detail-container { + transition: max-height 1.8s all; + overflow: hidden; + max-height: 0px; + &.active { + transition: max-height 1.8s all; + max-height: 1000px; + } +} +.accordion-detail-container-content { + padding: 15px 20px; +} diff --git a/src/components/Accordion/__snapshots__/Accordion.test.tsx.snap b/src/components/Accordion/__snapshots__/Accordion.test.tsx.snap new file mode 100644 index 00000000..2d63aeeb --- /dev/null +++ b/src/components/Accordion/__snapshots__/Accordion.test.tsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Accordion should match snapshot 1`] = ` + +
+
+
+

+ hello +

+
+
+
+
+

+ New Accoirdon detail helloooo +

+
+
+
+
+`; diff --git a/src/components/Accordion/index.tsx b/src/components/Accordion/index.tsx new file mode 100644 index 00000000..adee4907 --- /dev/null +++ b/src/components/Accordion/index.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; +import Accordion, { IAccordionProps } from './Accordion'; +import AccordionDetail, { AccordionDetailProps } from './AccordionDetail'; +import AccordionHeader, { AccordionHeaderProps } from './AccordionHeader'; + +export type PatAccordionComponent = FC & { + Header: FC; + Detail: FC; +}; + +const TransAccordion = Accordion as PatAccordionComponent; + +TransAccordion.Header = AccordionHeader; +TransAccordion.Detail = AccordionDetail; + +export default TransAccordion; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 609ecbf0..ee853e77 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -89,4 +89,4 @@ Button.defaultProps = { disabled: false, }; -export default Button; \ No newline at end of file +export default Button; diff --git a/src/index.tsx b/src/index.tsx index 70aeb63c..963872d9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,3 +5,4 @@ export { default as Message } from './components/Message'; export { default as Card } from './components/Card'; export { default as Dropdown } from './components/Dropdown'; export { default as Progress } from './components/Progress'; +export {default as Accordion} from './components/Accordion'; diff --git a/src/styles/index.scss b/src/styles/index.scss index 33fa969f..a4cb3bac 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -12,3 +12,4 @@ @import '../components/Input/Input'; @import '../components/Card/Card'; @import '../components/Progress/Progress'; +@import '../components/Accordion/Accordion'