xref: /expo/docs/ui/components/Collapsible/index.tsx (revision dfd15ebd)
1import { css } from '@emotion/react';
2import {
3  borderRadius,
4  iconSize,
5  shadows,
6  spacing,
7  theme,
8  typography,
9  TriangleDownIcon,
10} from '@expo/styleguide';
11import React, { PropsWithChildren, ReactNode } from 'react';
12
13import { HEADLINE } from '~/ui/components/Text';
14
15type CollapsibleProps = PropsWithChildren<{
16  /**
17   * The content of the collapsible summary.
18   */
19  summary: ReactNode;
20  /**
21   * If the collapsible should be rendered "open" by default.
22   */
23  open?: boolean;
24  testID?: string;
25}>;
26
27export function Collapsible({ summary, open, testID, children }: CollapsibleProps) {
28  return (
29    <details css={detailsStyle} open={open} data-testid={testID}>
30      <summary css={summaryStyle}>
31        <div css={markerWrapperStyle}>
32          <TriangleDownIcon css={markerStyle} size={iconSize.small} />
33        </div>
34        <HEADLINE tag="span">{summary}</HEADLINE>
35      </summary>
36      <div css={contentStyle}>{children}</div>
37    </details>
38  );
39}
40
41const detailsStyle = css({
42  overflow: 'hidden',
43  background: theme.background.default,
44  border: `1px solid ${theme.border.default}`,
45  borderRadius: borderRadius.medium,
46  padding: 0,
47  marginBottom: spacing[3],
48
49  '&[open]': {
50    boxShadow: shadows.micro,
51  },
52
53  'h4 + &, li > &': {
54    marginTop: spacing[3],
55  },
56});
57
58const summaryStyle = css({
59  display: 'flex',
60  flexDirection: 'row',
61  alignItems: 'center',
62  userSelect: 'none',
63  listStyle: 'none',
64  backgroundColor: theme.background.secondary,
65  padding: spacing[1.5],
66  paddingRight: spacing[3],
67  margin: 0,
68  cursor: 'pointer',
69
70  '&:hover span': {
71    color: theme.text.secondary,
72  },
73
74  '::-webkit-details-marker': {
75    display: 'none',
76  },
77
78  h4: {
79    marginTop: 0,
80    marginBottom: 0,
81  },
82});
83
84const markerWrapperStyle = css({
85  alignSelf: 'baseline',
86  marginTop: 5,
87  marginLeft: spacing[1.5],
88  marginRight: spacing[2],
89});
90
91const markerStyle = css({
92  transform: 'rotate(-90deg)',
93  transition: `transform 200ms`,
94
95  'details[open] &': { transform: 'rotate(0)' },
96});
97
98const contentStyle = css(typography.body.paragraph, {
99  padding: `${spacing[4]}px ${spacing[5]}px 0`,
100
101  p: {
102    marginLeft: 0,
103  },
104
105  'pre > pre': {
106    marginTop: 0,
107  },
108});
109