diff --git a/src/components/Accordion/Accordion.examples.story.tsx b/src/components/Accordion/Accordion.examples.story.tsx new file mode 100644 index 00000000..d2dea90d --- /dev/null +++ b/src/components/Accordion/Accordion.examples.story.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { Accordion } from './Accordion'; +import { Layout } from '../../storybook'; +import styled from 'styled-components'; +import { Gear } from '../../icons'; +import { rem } from 'polished'; +import { core } from '../../tokens'; + +export default { + title: 'components/Accordion/examples', + component: Accordion, + argTypes: { + allowMultiple: { control: { disable: true } }, + defaultIndex: { control: { disable: true } }, + format: { control: { disable: true } }, + }, +}; + +export function AccordionWithSubcopy({ args }) { + return ( + + + + Accordion content + + + + ); +} + +AccordionWithSubcopy.storyName = 'Accordion - subcopy'; + +export function AccordionWithIcon({ args }) { + return ( + + + } + > + Accordion content + + + + ); +} + +const GearIcon = styled(Gear)` + width: ${rem(22)}; + margin-right: ${rem(10)}; + path { + fill: ${core.color.text.primary}; + } +`; + +AccordionWithIcon.storyName = 'Accordion - icon'; + +export function DisabledAccordion({ args }) { + return ( + + + + Accordion content + + + + ); +} + +DisabledAccordion.storyName = 'Accordion - disabled'; + +export function AccordionWithError({ args }) { + return ( + + + + Accordion content + + + + ); +} + +AccordionWithError.storyName = 'Accordion - error'; + +export function AccordionWithAllowMultipleFalse({ args }) { + return ( + + + + Accordion content + + + Accordion content + + + Accordion content + + + + ); +} + +AccordionWithAllowMultipleFalse.storyName = + 'Accordion - allowMultiple is false'; + +export function AccordionAllowMultiple({ args }) { + return ( + + + + Accordion content + + + Accordion content + + + Accordion content + + + + ); +} + +AccordionAllowMultiple.storyName = + 'Accordion - allowMultiple is true (default)'; diff --git a/src/components/Accordion/Accordion.minors.tsx b/src/components/Accordion/Accordion.minors.tsx new file mode 100644 index 00000000..ec653b26 --- /dev/null +++ b/src/components/Accordion/Accordion.minors.tsx @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; + +import { + Content, + ChevronUp, + CircleWarningIcon, + Header, + StyledChevronDown, + Subcopy, + Title, + TitleContainer, + TriggerContainer, + Wrapper, +} from './Accordion.style'; +import { MinorComponent } from '../../utils'; +import { AccordionItemProps } from './Accordion.types'; + +export interface Minors { + Item: MinorComponent; +} + +export function Item({ + children, + title, + format, + index, + allowMultiple, + defaultActive, + setActiveIndex, + itemActive, + subcopy = '', + icon, + hasError = false, + disabled = false, +}: AccordionItemProps) { + const [active, setActive] = useState(defaultActive); + const isActive = allowMultiple + ? active && !disabled + : itemActive && !disabled; + + return ( + + { + if (allowMultiple) { + setActive(!active); + } else { + setActiveIndex({ index }); + } + }} + tabIndex={0} + format={format} + active={isActive} + > +
+ {hasError && } + {!hasError && icon && icon} + + {title} + {subcopy && {subcopy}} + +
+ {isActive ? ( + + ) : ( + + )} +
+ {isActive && {children}} +
+ ); +} diff --git a/src/components/Accordion/Accordion.props.story.tsx b/src/components/Accordion/Accordion.props.story.tsx new file mode 100644 index 00000000..85f377fc --- /dev/null +++ b/src/components/Accordion/Accordion.props.story.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import { rem } from 'polished'; + +import { Accordion } from './Accordion'; + +export default { + title: 'components/Accordion/props', + component: Accordion, + argTypes: { + allowMultiple: { control: { disable: true } }, + defaultIndex: { control: { disable: true } }, + format: { control: { disable: true } }, + }, +}; + +const formats = ['basic', 'secondary']; + +export function Formats({ args }) { + return ( + + {formats.map((format) => ( + + + Accordion content + + + ))} + + ); +} + +const Container = styled.div` + width: 50%; + display: flex; + flex-direction: column; + gap: ${rem(8)}; +`; diff --git a/src/components/Accordion/Accordion.story.tsx b/src/components/Accordion/Accordion.story.tsx new file mode 100644 index 00000000..0dd4a137 --- /dev/null +++ b/src/components/Accordion/Accordion.story.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Story } from '@storybook/react'; + +import { Accordion } from './Accordion'; +import { Props } from './Accordion.types'; +import styled from 'styled-components'; +import { Gear } from '../../icons'; +import { rem } from 'polished'; + +export default { + title: 'components/Accordion', + component: Accordion, +}; + +const Template: Story = (args) => { + return ( + + + + Accordion content + + + Accordion content + + + + ); +}; + +const Container = styled.div` + width: 50%; +`; + +export const Controls = Template.bind({}); +Controls.storyName = 'Accordion'; diff --git a/src/components/Accordion/Accordion.style.ts b/src/components/Accordion/Accordion.style.ts new file mode 100644 index 00000000..c92382d5 --- /dev/null +++ b/src/components/Accordion/Accordion.style.ts @@ -0,0 +1,118 @@ +import { rem } from 'polished'; +import styled, { css } from 'styled-components'; + +import { ChevronDown, CircleWarning } from '../../icons'; +import { Paragraph } from '../../typography'; + +import { grayscale } from '../../color'; +import { core } from '../../tokens'; + +export const AccordionStyled = styled.div` + display: flex; + flex-direction: column; + gap: ${rem(8)}; +`; + +export const Wrapper = styled.div<{ + format: 'basic' | 'secondary'; + active: boolean; + disabled: boolean; +}>` + ${({ disabled }) => + disabled && + css` + pointer-events: none; + opacity: 0.4; + `} + ${({ active, theme, format }) => + active && + css` + background-color: ${format === 'basic' + ? theme.name === 'dark' + ? grayscale(800) + : grayscale(50) + : 'none'}; + `} + border-radius: ${rem(10)}; + color: ${core.color.text.primary}; + ${({ active, theme, format }) => + active && + css` + border: ${format === 'secondary' + ? theme.name === 'dark' + ? `${rem(1)} solid ${grayscale(800)}` + : `${rem(1)} solid ${grayscale(50)}` + : 'none'}; + `} +`; + +export const TriggerContainer = styled.div<{ + format: 'basic' | 'secondary'; + active: boolean; +}>` + display: flex; + justify-content: space-between; + cursor: pointer; + padding: ${rem(12)} ${rem(15)}; + border-radius: ${rem(10)}; + &:hover { + background-color: ${({ theme, format }) => + format === 'basic' + ? theme.name === 'dark' + ? grayscale(800) + : grayscale(50) + : 'none'}; + border: ${({ active, theme, format }) => + !active && format === 'secondary' + ? theme.name === 'dark' + ? `${rem(1)} solid ${grayscale(800)}` + : `${rem(1)} solid ${grayscale(50)}` + : 'none'}; + } +`; + +export const Header = styled.div` + display: flex; + margin-right: ${rem(10)}; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; +`; + +export const Title = styled(Paragraph)` + font-size: ${rem(16)}; + font-weight: 700; + margin-bottom: 0; +`; + +export const Subcopy = styled(Paragraph)` + font-size: ${rem(14)}; + font-weight: 400; + margin-bottom: -${rem(0.2)}; +`; + +export const CircleWarningIcon = styled(CircleWarning)` + width: ${rem(22)}; + margin-right: ${rem(10)}; + path { + fill: ${core.color.status.negative}; + } +`; + +export const StyledChevronDown = styled(ChevronDown)` + path { + fill: ${core.color.text.primary}; + } +`; + +export const ChevronUp = styled(StyledChevronDown)` + transform: rotate(180deg); +`; + +export const Content = styled.div<{ active: boolean }>` + padding: ${rem(0)} ${rem(15)} ${rem(20)}; + max-height: ${({ active }) => (active ? '100%' : '0')}; + overflow: hidden; +`; diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx new file mode 100644 index 00000000..16663220 --- /dev/null +++ b/src/components/Accordion/Accordion.tsx @@ -0,0 +1,42 @@ +import React, { cloneElement, useState } from 'react'; + +import { withIris } from '../../utils'; +import { Item, Minors } from './Accordion.minors'; +import { Props } from './Accordion.types'; +import { AccordionStyled } from './Accordion.style'; + +export const Accordion = withIris( + AccordionComponent +); + +Accordion.Item = Item; + +function AccordionComponent({ + children, + defaultIndex, + allowMultiple = true, + format = 'basic', +}: Props) { + const [activeIndex, setActiveIndex] = useState(defaultIndex); + + function childClone(child, i) { + return cloneElement(child, { + format, + index: i, + allowMultiple, + defaultActive: defaultIndex === i, + setActiveIndex: ({ index }) => { + setActiveIndex(index); + }, + itemActive: !allowMultiple && activeIndex === i, + }); + } + + return ( + + {children.length > 1 + ? children.map(childClone) + : childClone(children, 0)} + + ); +} diff --git a/src/components/Accordion/Accordion.types.ts b/src/components/Accordion/Accordion.types.ts new file mode 100644 index 00000000..bf3af9bb --- /dev/null +++ b/src/components/Accordion/Accordion.types.ts @@ -0,0 +1,26 @@ +import { IrisProps } from '../../utils'; + +export type Props = IrisProps< + { + children: any; + allowMultiple?: boolean; + defaultIndex?: number; + format?: 'basic' | 'secondary'; + }, + HTMLDivElement +>; + +export type AccordionItemProps = IrisProps<{ + children: React.ReactElement; + title: string; + format: 'basic' | 'secondary'; + index: number; + allowMultiple: boolean; + defaultActive: boolean; + setActiveIndex: ({ index }) => void; + itemActive: boolean; + subcopy?: string; + icon?: string; + hasError?: boolean; + disabled?: boolean; +}>;