aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/collapsible/collapsible.tsx
blob: e61ccba6ab1a835dd9bf60cc5d6543e901e81651 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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);