Skip to content

Commit

Permalink
feat #190 - Accordion Component
Browse files Browse the repository at this point in the history
Closes #190
  • Loading branch information
github-actions committed Jan 19, 2021
1 parent f6a3be7 commit f5e716b
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 2 deletions.
37 changes: 37 additions & 0 deletions src/components/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import { Accordion, AccordionProps, Panel } from './index'
import { Meta, Story } from '@storybook/react/types-6-0'

const mockPanels: Panel[] = [
{ content: 'Content1', key: 1, title: 'Title 1' },
{ content: 'Content2', key: 2, title: 'Title 2' },
{ content: 'Content3', key: 3, title: 'Title 3' }
]

export default {
argTypes: {
panels: { control: { disable: true } }
},
component: Accordion,
parameters: {
// disabled because shallow rendering doesn't work with decorator and hook inside decorator.
storyshots: { disable: true }
},
title: 'Accordion'
} as Meta

const Template: Story<AccordionProps> = args => (
<Accordion {...args} panels={mockPanels} />
)

export const Default = Template.bind({})

export const Exclusive = Template.bind({})
Exclusive.args = {
exclusive: true
}

export const ExpandAll = Template.bind({})
ExpandAll.args = {
expandAllOnMount: true
}
21 changes: 21 additions & 0 deletions src/components/Accordion/CollapseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { motion } from 'framer-motion'
import React, { FC } from 'react'

interface CollapseButtonProps {
isCollapsed: boolean
}

export const CollapseButton: FC<CollapseButtonProps> = ({
isCollapsed
}: CollapseButtonProps) => (
<motion.div
animate={{
rotate: isCollapsed ? 0 : 180
}}
whileHover={{ scale: 1.1 }}
>
<FontAwesomeIcon icon={faChevronDown} />
</motion.div>
)
46 changes: 46 additions & 0 deletions src/components/Accordion/PanelContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createUseStyles } from 'react-jss'
import { styleguide } from '../assets/styles'
import { AnimatePresence, motion } from 'framer-motion'
import React, { FC, ReactNode } from 'react'

const { spacing } = styleguide

const useStyles = createUseStyles({
content: {
paddingTop: spacing.s
}
})

interface PanelContentProps {
children: ReactNode
isActive: boolean
}

export const PanelContent: FC<PanelContentProps> = ({
children,
isActive
}: PanelContentProps) => {
const classes = useStyles()

return (
<AnimatePresence initial={false}>
{isActive && (
<motion.section
animate='open'
exit='collapsed'
initial='collapsed'
transition={{
duration: 0.8,
ease: [0.04, 0.62, 0.23, 0.98]
}}
variants={{
collapsed: { height: 0, opacity: 0 },
open: { height: 'auto', opacity: 1 }
}}
>
<div className={classes.content}>{children}</div>
</motion.section>
)}
</AnimatePresence>
)
}
107 changes: 107 additions & 0 deletions src/components/Accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { CollapseButton } from './CollapseButton'
import { createUseStyles } from 'react-jss'
import { generateAccordionPanelStyles } from './utils'
import { PanelContent } from './PanelContent'
import React, { FC, Key, ReactNode, useState } from 'react'
import { styleguide, themes, ThemeType } from '../assets/styles'

const { flexSpaceBetween, font } = styleguide
const { dark, light } = ThemeType

const useStyles = createUseStyles({
header: {
...font.bodyLarge,
...flexSpaceBetween,
cursor: 'pointer'
},
panel: generateAccordionPanelStyles(light),
title: {
color: themes[light].primary
},
// eslint-disable-next-line sort-keys
'@global': {
[`.${dark}`]: {
'& $panel': generateAccordionPanelStyles(dark),
'& $title': { color: themes[dark].state.hover }
}
}
})

export interface Panel {
content: ReactNode
key: Key
title: string
}

interface SharedAccordionProps {
defaultActiveKey?: Key
panels: Panel[]
}

interface ExclusiveAccordionProps extends SharedAccordionProps {
exclusive: true
expandAllOnMount?: never
}

interface NonExclusiveAccordionProps extends SharedAccordionProps {
exclusive: false
expandAllOnMount?: boolean
}

export type AccordionProps =
| ExclusiveAccordionProps
| NonExclusiveAccordionProps

export const Accordion: FC<AccordionProps> = ({
defaultActiveKey,
exclusive = false,
expandAllOnMount = false,
panels
}: AccordionProps) => {
const getInitialActiveKeys = () => {
const defaultActiveKeys: Key[] = [panels[0].key]

if (defaultActiveKey) return [defaultActiveKey]
else if (expandAllOnMount) return panels.map(({ key }) => key)

return defaultActiveKeys
}

const [activeKeys, setActiveKeys] = useState<Key[]>(getInitialActiveKeys())
const classes = useStyles()

const togglePanel = (panelKey: Key) => {
let newActiveKeys = [...activeKeys, panelKey]

// Close panel if it is open
if (activeKeys.includes(panelKey))
newActiveKeys = activeKeys.filter(key => panelKey !== key)
// If accordion is exclusive, only one panel can be open at a time
else if (exclusive) newActiveKeys = [panelKey]

setActiveKeys(newActiveKeys)
}

return (
<div>
{panels.map(({ content, key, title }) => {
const isActivePanel = activeKeys.includes(key)

return (
<div className={classes.panel} key={key}>
<div
className={classes.header}
onClick={() => togglePanel(key)}
>
<div className={classes.title}>{title}</div>
<CollapseButton isCollapsed={!isActivePanel} />
</div>
<PanelContent isActive={isActivePanel}>
{content}
</PanelContent>
</div>
)
})}
</div>
)
}
18 changes: 18 additions & 0 deletions src/components/Accordion/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { styleguide, themedStyles, ThemeType } from '../assets/styles'

const { spacing } = styleguide

export const generateAccordionPanelStyles = (themeType: ThemeType) => {
const { base } = themedStyles[themeType]

const borderStyles = `1px solid ${base.borderColor}`

return {
'&:first-of-type': {
borderTop: borderStyles
},
borderBottom: borderStyles,
color: base.color,
padding: spacing.m
}
}
3 changes: 1 addition & 2 deletions src/components/NotificationV2/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { ev as NotificationTypes } from '@dassana-io/web-utils'
import omit from 'lodash/omit'
import { styleguide } from 'components/assets/styles'
import { v4 as uuidV4 } from 'uuid'
import {
faCheckCircle,
faExclamationCircle,
faExclamationTriangle,
faTimesCircle
} from '@fortawesome/free-solid-svg-icons'
import { themedStyles, themes, ThemeType } from '../assets/styles/themes'
import { styleguide, themedStyles, themes, ThemeType } from '../assets/styles'
import { useCallback, useState } from 'react'

const { borderRadius, flexSpaceBetween, spacing } = styleguide
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Accordion'
export * from './Avatar'
export * from './assets/styles'
export * from './Button'
Expand Down

0 comments on commit f5e716b

Please sign in to comment.