Skip to content

Instantly share code, notes, and snippets.

@tzynwang
Created October 1, 2022 02:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tzynwang/d0edecb49bf283a6e4a0b8fe468f0882 to your computer and use it in GitHub Desktop.
Save tzynwang/d0edecb49bf283a6e4a0b8fe468f0882 to your computer and use it in GitHub Desktop.
import React, { memo, useState, useCallback, useEffect, useMemo } from 'react';
import { css } from '@emotion/css';
import cn from 'classnames';
import type { AccordionProps } from './types';
const defaultWrapperStyle = css({ width: '100%', border: '1px solid #333' });
const defaultBodyStyle = css({
padding: '24px',
borderTop: '1px solid #333',
backgroundColor: '#fffffe',
});
function Accordion(props: AccordionProps): React.ReactElement {
/* States */
const {
children,
classes = { wrapper: '', title: '', body: '' },
open,
...rest
} = props;
delete rest.className;
const [localOpen, setLocalOpen] = useState<boolean>(false);
const [titleElement, setTitleElement] = useState<JSX.Element>(
<React.Fragment />
);
const [bodyElement, setBodyElement] = useState<JSX.Element>(
<React.Fragment />
);
const hasOpenFromProps = useMemo(
() => Object.keys(props).includes('open'),
[props]
);
const openToUse = useMemo(
() => (hasOpenFromProps ? open : localOpen),
[hasOpenFromProps, open, localOpen]
);
/* Functions */
const toggleAccordion = useCallback((): void => {
setLocalOpen((prev) => !prev);
}, []);
/* Hooks */
useEffect(() => {
React.Children.forEach(children, (child, index) => {
const childElement = child as JSX.Element;
if (index === 0) {
setTitleElement(
React.cloneElement(childElement, {
open: openToUse,
onClick: hasOpenFromProps ? undefined : toggleAccordion,
accordionTitleClass: classes.title,
})
);
}
if (index === 1) {
setBodyElement(
React.cloneElement(childElement, {
open: openToUse,
accordionBodyClass: cn(openToUse && defaultBodyStyle, classes.body),
})
);
}
});
}, [
children,
openToUse,
hasOpenFromProps,
classes.title,
classes.body,
toggleAccordion,
]);
/* Main */
return (
<div className={cn(defaultWrapperStyle, classes.wrapper)} {...rest}>
{titleElement}
{bodyElement}
</div>
);
}
export default memo(Accordion);
import React from 'react';
export interface AccordionProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
open?: boolean;
classes?: Partial<AccordionClass>;
}
interface AccordionClass {
wrapper: string;
title: string;
body: string;
}
import React, { memo, useMemo } from 'react';
import { css } from '@emotion/css';
import cn from 'classnames';
import type { AccordionBodyProps } from './types';
function AccordionBody(props: AccordionBodyProps): React.ReactElement {
/* States */
const { open, children, accordionBodyClass = '', ...rest } = props;
delete rest.className;
const bodyStyle = useMemo(
() =>
css({
height: open ? 'auto' : '0px',
visibility: open ? 'unset' : 'hidden',
overflow: open ? 'visible' : 'hidden',
transform: `scaleY(${open ? '100%' : '0%'})`,
transformOrigin: 'top left',
transitionDuration: '.2s',
transitionProperty: 'transform, height'
}),
[open]
);
/* Main */
return (
<div className={cn(bodyStyle, accordionBodyClass)} {...rest}>
{children}
</div>
);
}
AccordionBody.displayName = 'AccordionBody';
export default memo(AccordionBody);
import React from 'react';
export interface AccordionBodyProps
extends React.HTMLAttributes<HTMLDivElement> {
open?: boolean;
children: React.ReactNode;
accordionBodyClass?: string;
}
import React, { memo } from 'react';
import { css } from '@emotion/css';
import cn from 'classnames';
import { ExpendLessIcon } from '@Assets/icons';
import ButtonBase from '@Components/Base/ButtonBase';
import type { AccordionTitleProps } from './types';
const title = css({
width: '100%',
minHeight: '48px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
border: 'none',
backgroundColor: 'transparent',
fontSize: 'inherit',
});
const iconStyle = css({
transition: 'transform .2s ease',
});
const transformIcon = css({
transform: 'rotate(180deg)',
});
function AccordionTitle(props: AccordionTitleProps): React.ReactElement {
/* States */
const { children, accordionTitleClass = '', open, onClick, ...rest } = props;
delete rest.className;
/* Main */
return (
<ButtonBase className={cn(title, accordionTitleClass)} onClick={onClick}>
{children}
<ExpendLessIcon className={cn(iconStyle, open && transformIcon)} />
</ButtonBase>
);
}
AccordionTitle.displayName = 'AccordionTitle';
export default memo(AccordionTitle);
import type { ButtonBaseProps } from '@Components/Base/ButtonBase/types';
export interface AccordionTitleProps extends ButtonBaseProps {
open?: boolean;
accordionTitleClass?: string;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment