From c468f0134662af5ec2850d88f23ec82f582b3b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Boukorras?= Date: Mon, 12 Jun 2023 13:36:55 +0200 Subject: [PATCH] feat(react): accordion component --- .../src/components/accordion/accordion.scss | 106 +++++++++++++++++ packages/core/src/components/index.scss | 3 + .../accordion/accordion.stories.tsx | 24 ++++ .../src/components/accordion/accordion.tsx | 112 ++++++++++++++++++ packages/react/src/components/index.ts | 1 + yarn.lock | 15 +++ 6 files changed, 261 insertions(+) create mode 100644 packages/core/src/components/accordion/accordion.scss create mode 100644 packages/react/src/components/accordion/accordion.stories.tsx create mode 100644 packages/react/src/components/accordion/accordion.tsx diff --git a/packages/core/src/components/accordion/accordion.scss b/packages/core/src/components/accordion/accordion.scss new file mode 100644 index 000000000..f2c0fbbff --- /dev/null +++ b/packages/core/src/components/accordion/accordion.scss @@ -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; + } +} diff --git a/packages/core/src/components/index.scss b/packages/core/src/components/index.scss index f1a7e78c2..fc1dd84aa 100644 --- a/packages/core/src/components/index.scss +++ b/packages/core/src/components/index.scss @@ -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(); @@ -66,4 +68,5 @@ @include textarea.Textarea(); @include tooltip.Tooltip(); @include validation.Validation(); + @include accordion.Accordion(); } diff --git a/packages/react/src/components/accordion/accordion.stories.tsx b/packages/react/src/components/accordion/accordion.stories.tsx new file mode 100644 index 000000000..0ff4f40d5 --- /dev/null +++ b/packages/react/src/components/accordion/accordion.stories.tsx @@ -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:
Content example.
, + }, + { + title: 'Another title without icon', + content:
Another content example.
, + }, + ], + }, + parameters: {}, +} as Meta; + +export const Playground: Story = {}; diff --git a/packages/react/src/components/accordion/accordion.tsx b/packages/react/src/components/accordion/accordion.tsx new file mode 100644 index 000000000..34ad8cb0d --- /dev/null +++ b/packages/react/src/components/accordion/accordion.tsx @@ -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 = ({ + list: listProps, + onChange, + onlyOneOpen = false, + separated = false, + withSeparator = false, + styles: stylesProps = defaultStyles, +}) => { + const [list, setList] = useState(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 ( +
+ {list.map((item, index) => ( +
+ +
+
{item.content}
+
+
+ ))} +
+ ); +}; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 34a4d39ab..b19eb3447 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -1,3 +1,4 @@ +export * from './accordion/accordion'; export * from './asterisk/asterisk'; export * from './button/button'; export * from './checkbox/checkbox'; diff --git a/yarn.lock b/yarn.lock index 73b9f4cee..c811aefe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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"