Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat #190 - Accordion Component #191

Merged
merged 5 commits into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dassana-io/web-components",
"version": "0.8.2",
"version": "0.8.3",
"publishConfig": {
"registry": "https://npm.pkg.github.com/dassana-io"
},
Expand Down
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
}
22 changes: 22 additions & 0 deletions src/components/Accordion/CollapseIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 CollapseIndicatorProps {
isCollapsed: boolean
}

export const CollapseIndicator: FC<CollapseIndicatorProps> = ({
nancy-dassana marked this conversation as resolved.
Show resolved Hide resolved
isCollapsed
}: CollapseIndicatorProps) => (
<motion.div
animate={{
rotate: isCollapsed ? 0 : 180
}}
transition={{ duration: 0.5 }}
whileHover={{ scale: 1.1 }}
>
<FontAwesomeIcon icon={faChevronDown} />
</motion.div>
)
47 changes: 47 additions & 0 deletions src/components/Accordion/PanelContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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: {
padding: spacing.m,
paddingTop: 0
}
})

interface PanelContentProps {
children: ReactNode
isActive: boolean
}

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

return (
<AnimatePresence initial={false}>
{isActive && (
<motion.section
nancy-dassana marked this conversation as resolved.
Show resolved Hide resolved
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>
)
}
108 changes: 108 additions & 0 deletions src/components/Accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { CollapseIndicator } from './CollapseIndicator'
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, spacing } = styleguide
const { dark, light } = ThemeType

const useStyles = createUseStyles({
header: {
...font.bodyLarge,
...flexSpaceBetween,
cursor: 'pointer',
padding: spacing.m
},
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>
<CollapseIndicator isCollapsed={!isActivePanel} />
</div>
<PanelContent isActive={isActivePanel}>
{content}
</PanelContent>
</div>
)
})}
</div>
)
}
15 changes: 15 additions & 0 deletions src/components/Accordion/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { themedStyles, ThemeType } from '../assets/styles'

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
}
}
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