xref: /expo/docs/ui/components/Collapsible/index.tsx (revision da751fd5)
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:first-child': {
48    marginTop: 0,
49  },
50
51  'h4 + &, p + &, li > &': {
52    marginTop: spacing[3],
53  },
54});
55
56const summaryStyle = css({
57  display: 'flex',
58  flexDirection: 'row',
59  alignItems: 'center',
60  userSelect: 'none',
61  listStyle: 'none',
62  backgroundColor: theme.background.subtle,
63  padding: spacing[1.5],
64  paddingRight: spacing[3],
65  margin: 0,
66  cursor: 'pointer',
67
68  '&:hover span': {
69    color: theme.text.secondary,
70  },
71
72  '::-webkit-details-marker': {
73    display: 'none',
74  },
75
76  h4: {
77    marginTop: 0,
78    marginBottom: 0,
79  },
80
81  code: {
82    backgroundColor: theme.background.element,
83    display: 'inline',
84    fontSize: '90%',
85  },
86});
87
88const markerWrapperStyle = css({
89  alignSelf: 'baseline',
90  marginTop: 5,
91  marginLeft: spacing[1.5],
92  marginRight: spacing[2],
93});
94
95const markerStyle = css({
96  transform: 'rotate(-90deg)',
97  transition: `transform 200ms`,
98
99  'details[open] &': { transform: 'rotate(0)' },
100});
101
102const contentStyle = css({
103  padding: `${spacing[4]}px ${spacing[5]}px 0`,
104
105  p: {
106    marginLeft: 0,
107  },
108
109  'pre > pre': {
110    marginTop: 0,
111  },
112});
113