xref: /expo/docs/ui/components/Collapsible/index.tsx (revision cedf99eb)
1import { css } from '@emotion/react';
2import { shadows, theme } from '@expo/styleguide';
3import { borderRadius, spacing } from '@expo/styleguide-base';
4import { TriangleDownIcon } from '@expo/styleguide-icons';
5import type { PropsWithChildren, ReactNode } from 'react';
6
7import { DEMI } from '~/ui/components/Text';
8
9type CollapsibleProps = PropsWithChildren<{
10  /**
11   * The content of the collapsible summary.
12   */
13  summary: ReactNode;
14  /**
15   * If the collapsible should be rendered "open" by default.
16   */
17  open?: boolean;
18  testID?: string;
19}>;
20
21export function Collapsible({ summary, open, testID, children }: CollapsibleProps) {
22  return (
23    <details css={detailsStyle} open={open} data-testid={testID}>
24      <summary css={summaryStyle}>
25        <div css={markerWrapperStyle}>
26          <TriangleDownIcon className="icon-sm text-icon-default" css={markerStyle} />
27        </div>
28        <DEMI>{summary}</DEMI>
29      </summary>
30      <div css={contentStyle}>{children}</div>
31    </details>
32  );
33}
34
35const detailsStyle = css({
36  overflow: 'hidden',
37  background: theme.background.default,
38  border: `1px solid ${theme.border.default}`,
39  borderRadius: borderRadius.md,
40  padding: 0,
41  marginBottom: spacing[3],
42
43  '&[open]': {
44    boxShadow: shadows.xs,
45  },
46
47  'h4 + &, p + &, li > &': {
48    marginTop: spacing[3],
49  },
50});
51
52const summaryStyle = css({
53  display: 'flex',
54  flexDirection: 'row',
55  alignItems: 'center',
56  userSelect: 'none',
57  listStyle: 'none',
58  backgroundColor: theme.background.subtle,
59  padding: spacing[1.5],
60  paddingRight: spacing[3],
61  margin: 0,
62  cursor: 'pointer',
63
64  '&:hover span': {
65    color: theme.text.secondary,
66  },
67
68  '::-webkit-details-marker': {
69    display: 'none',
70  },
71
72  h4: {
73    marginTop: 0,
74    marginBottom: 0,
75  },
76
77  code: {
78    backgroundColor: theme.background.element,
79    display: 'inline',
80    fontSize: '90%',
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({
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