xref: /expo/docs/ui/components/Collapsible/index.tsx (revision 79f4e8ce)
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
41export function ExpoKitCollapsible({ children }: CollapsibleProps) {
42  return (
43    <div css={configDetailsStyle}>
44      <Collapsible summary="ExpoKit">{children}</Collapsible>
45    </div>
46  );
47}
48
49export function BareWorkflowCollapsible({ children }: CollapsibleProps) {
50  return (
51    <div css={configDetailsStyle}>
52      <Collapsible summary="Bare Workflow">{children}</Collapsible>
53    </div>
54  );
55}
56
57const detailsStyle = css({
58  overflow: 'hidden',
59  background: theme.background.default,
60  border: `1px solid ${theme.border.default}`,
61  borderRadius: borderRadius.medium,
62  padding: 0,
63  marginBottom: spacing[3],
64
65  '&[open]': {
66    boxShadow: shadows.micro,
67  },
68
69  'h4 + &, li > &': {
70    marginTop: spacing[3],
71  },
72});
73
74const summaryStyle = css({
75  display: 'flex',
76  flexDirection: 'row',
77  alignItems: 'center',
78  userSelect: 'none',
79  listStyle: 'none',
80  backgroundColor: theme.background.secondary,
81  padding: spacing[1.5],
82  paddingRight: spacing[3],
83  margin: 0,
84  cursor: 'pointer',
85
86  '&:hover span': {
87    color: theme.text.secondary,
88  },
89});
90
91const markerWrapperStyle = css({
92  alignSelf: 'baseline',
93  marginTop: 5,
94  marginLeft: spacing[1.5],
95  marginRight: spacing[2],
96});
97
98const markerStyle = css({
99  transform: 'rotate(-90deg)',
100  transition: `transform 200ms`,
101
102  'details[open] &': { transform: 'rotate(0)' },
103});
104
105const contentStyle = css(typography.body.paragraph, {
106  padding: `${spacing[4]}px ${spacing[5]}px 0`,
107
108  p: {
109    marginLeft: 0,
110  },
111
112  'pre > pre': {
113    marginTop: 0,
114  },
115});
116
117const configDetailsStyle = css({
118  marginTop: spacing[3],
119
120  '& details[open]': {
121    paddingBottom: spacing[4],
122  },
123});
124