Skip to content

Commit

Permalink
feat(react): accordion component
Browse files Browse the repository at this point in the history
  • Loading branch information
itupix committed Jun 20, 2023
1 parent c0e7baf commit c468f01
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 0 deletions.
106 changes: 106 additions & 0 deletions packages/core/src/components/accordion/accordion.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
@use '../../helpers';
@use '../../mixins';

@mixin Accordion() {
@if not mixins.includes('Accordion') {
@include _Accordion();
}
}

@mixin _Accordion() {
.ods-accordion-wrapper:not(.separated) {
background-color: helpers.color('background-surface');
border: 1px solid helpers.color('border-separator');
border-radius: helpers.border-radius('large');
overflow: hidden;
}

.ods-accordion {
overflow: hidden;

&:not(:last-child) {
border-bottom: 1px solid helpers.color('border-separator');
}
}

.separated .ods-accordion {
background-color: helpers.color('background-surface');
border: 1px solid helpers.color('border-separator');
border-radius: helpers.border-radius('large');

&:not(:last-child) {
margin-bottom: helpers.space(2);
}
}

.ods-accordion-toggler {
@include helpers.font('400-regular');
align-items: center;
background-color: transparent;
border: 0;
cursor: pointer;
display: flex;
gap: helpers.space(1);
justify-content: flex-start;
padding: var(--padding);
transition: background-color linear 0.2s;
width: 100%;

&:hover {
background-color: helpers.color('background-action-subtle');
}
}

.ods-accordion-secondary-content > * {
margin: 0;
}

.ods-accordion-chevron {
transition: transform linear 0.2s;
}

.ods-accordion-title {
margin-right: auto;
}

.ods-accordion-content-wrapper {
max-height: 0;
overflow: hidden;
padding: 0 var(--padding);
transition: padding ease-in-out 0.3s, max-height ease-in-out 0.3s;
}

.separator {
position: relative;

&::before {
border-bottom: 1px solid helpers.color('border-separator');
content: '';
left: var(--padding);
position: absolute;
right: var(--padding);
top: 0;
}
}

.open {
.ods-accordion-content-wrapper {
max-height: 100vh;
padding-bottom: var(--padding);
padding-top: var(--padding);
}

.ods-accordion-content {
opacity: 1;
}

.ods-accordion-chevron {
transform: rotateZ(180deg);
}
}

.ods-accordion-content {
opacity: 0;
transition: opacity linear 0.4s;
}
}
3 changes: 3 additions & 0 deletions packages/core/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
@forward './tooltip/tooltip';
@use './validation/validation';
@forward './validation/validation';
@use './accordion/accordion.scss';
@forward './accordion/accordion.scss';

@mixin components() {
@include asterisk.Asterisk();
Expand All @@ -66,4 +68,5 @@
@include textarea.Textarea();
@include tooltip.Tooltip();
@include validation.Validation();
@include accordion.Accordion();
}
24 changes: 24 additions & 0 deletions packages/react/src/components/accordion/accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Accordion, type AccordionProps } from '@onfido/castor-react';
import React from 'react';
import { Meta, Story } from '../../../../../docs';

export default {
title: 'React/Accordion',
component: Accordion,
args: {
list: [
{
title: 'A title with an icon',
iconName: 'bolt',
content: <div>Content example.</div>,
},
{
title: 'Another title without icon',
content: <div>Another content example.</div>,
},
],
},
parameters: {},
} as Meta<AccordionProps>;

export const Playground: Story<AccordionProps> = {};
112 changes: 112 additions & 0 deletions packages/react/src/components/accordion/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { c, classy, cssVars, space } from '@onfido/castor';
import type { IconName } from '@onfido/castor-icons';
import { Icon } from '@onfido/castor-react';
import React, { useEffect, useState, type FC } from 'react';

export type AccordionItem = {
isOpen?: boolean;
title: string;
iconName?: IconName;
content: React.ReactElement;
secondaryContent?: React.ReactElement;
};

export type AccordionStyles = {
padding?: number;
};

export interface AccordionProps {
list: AccordionItem[];
onChange?: (newList: AccordionItem[]) => void;
onlyOneOpen: boolean;
separated: boolean;
withSeparator: boolean;
styles?: AccordionStyles;
}

const defaultStyles = {
padding: 3,
};

export const Accordion: FC<AccordionProps> = ({
list: listProps,
onChange,
onlyOneOpen = false,
separated = false,
withSeparator = false,
styles: stylesProps = defaultStyles,
}) => {
const [list, setList] = useState<AccordionItem[]>(listProps);

const _styles = {
...defaultStyles,
...stylesProps,
};

useEffect(() => {
let newList = [...list];

newList = newList.map((item) => ({
...item,
isOpen: false,
}));

if (onChange) onChange(newList);
}, [onlyOneOpen]);

const handleClick = (index: number) => () => {
let newList = [...list];

if (onlyOneOpen) {
newList = newList.map((item, newIndex) => ({
...item,
isOpen: newIndex !== index ? false : !item.isOpen,
}));
} else {
newList[index].isOpen = !list[index].isOpen;
}
setList(newList);
if (onChange) onChange(newList);
};

const style = cssVars({ padding: space(_styles.padding) });

return (
<div
style={style}
className={classy(c('accordion-wrapper'), { separated })}
>
{list.map((item, index) => (
<div
key={index}
className={classy(c('accordion'), { open: item.isOpen })}
>
<button
className={classy(c('accordion-toggler'))}
onClick={handleClick(index)}
>
{item.iconName && <Icon name={item.iconName} aria-hidden="true" />}
<span className={classy(c('accordion-title'))}>{item.title}</span>
{item.secondaryContent && (
<span className={classy(c('accordion-secondary-content'))}>
{item.secondaryContent}
</span>
)}
<Icon
className={classy(c('accordion-chevron'))}
name="chevron-down"
aria-hidden="true"
/>
</button>
<div
className={classy(c('accordion-content-wrapper'), {
separator: withSeparator,
})}
>
<div className={classy(c('accordion-content'))}>{item.content}</div>
</div>
</div>
))}
</div>
);
};
1 change: 1 addition & 0 deletions packages/react/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './accordion/accordion';
export * from './asterisk/asterisk';
export * from './button/button';
export * from './checkbox/checkbox';
Expand Down
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12735,6 +12735,14 @@ react-dom@^17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"

react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.0"

react-element-to-jsx-string@^14.3.4:
version "14.3.4"
resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8"
Expand Down Expand Up @@ -13393,6 +13401,13 @@ scheduler@^0.20.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"

scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
dependencies:
loose-envify "^1.1.0"

schema-utils@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
Expand Down

0 comments on commit c468f01

Please sign in to comment.