diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-06 17:48:03 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | 12a03a9a72f7895d571dbaeeb245d92aa277a610 (patch) | |
| tree | 41b6b07928e4f5e101b7ea5d8389bb4325bbac76 /src/components/molecules/collapsible/collapsible.tsx | |
| parent | fb860884857da73ee5b5e897745301cdf1d770a2 (diff) | |
refactor(components): merge HeadingButton and Widget components
The HeadingButton component was only used inside Widget component and
it is not very useful on its own so I merge the two components in a
new Collapsible component.
Diffstat (limited to 'src/components/molecules/collapsible/collapsible.tsx')
| -rw-r--r-- | src/components/molecules/collapsible/collapsible.tsx | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/src/components/molecules/collapsible/collapsible.tsx b/src/components/molecules/collapsible/collapsible.tsx new file mode 100644 index 0000000..e61ccba --- /dev/null +++ b/src/components/molecules/collapsible/collapsible.tsx @@ -0,0 +1,108 @@ +import { + useCallback, + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, + forwardRef, + useId, + useState, +} from 'react'; +import { Button, Icon } from '../../atoms'; +import styles from './collapsible.module.scss'; + +export type CollapsibleProps = Omit< + HTMLAttributes<HTMLDivElement>, + 'children' +> & { + /** + * The collapsible body. + */ + children: ReactNode; + /** + * Should we disable padding around body? + * + * @default false + */ + disablePadding?: boolean; + /** + * Should the body be bordered? + * + * @default false + */ + hasBorders?: boolean; + /** + * The collapsible heading. + */ + heading: ReactNode; + /** + * Should the component be collapsed or expanded by default? + * + * @default false + */ + isCollapsed?: boolean; +}; + +const CollapsibleWithRef: ForwardRefRenderFunction< + HTMLDivElement, + CollapsibleProps +> = ( + { + children, + className = '', + disablePadding = false, + hasBorders = false, + heading, + isCollapsed = false, + ...props + }, + ref +) => { + const bodyId = useId(); + const [isExpanded, setIsExpanded] = useState(!isCollapsed); + const bodyClassNames = [ + styles.body, + hasBorders ? styles['body--has-borders'] : '', + styles[disablePadding ? 'body--no-padding' : 'body--has-padding'], + ]; + const wrapperClassNames = [ + styles.wrapper, + styles[isExpanded ? 'wrapper--expanded' : 'wrapper--collapsed'], + className, + ]; + + const handleState = useCallback(() => { + setIsExpanded((prevState) => !prevState); + }, []); + + return ( + <div {...props} className={wrapperClassNames.join(' ')} ref={ref}> + <Button + aria-controls={bodyId} + aria-expanded={isExpanded} + className={styles.heading} + // eslint-disable-next-line react/jsx-no-literals -- Kind allowed + kind="neutral" + onClick={handleState} + // eslint-disable-next-line react/jsx-no-literals -- Shape allowed + shape="initial" + > + {heading} + <Icon + aria-hidden + className={styles.icon} + shape={isExpanded ? 'minus' : 'plus'} + /> + </Button> + <div className={bodyClassNames.join(' ')} id={bodyId}> + {children} + </div> + </div> + ); +}; + +/** + * Collapsible component. + * + * Render a heading associated to a collapsible body. + */ +export const Collapsible = forwardRef(CollapsibleWithRef); |
