xref: /expo/docs/ui/components/Callout/index.tsx (revision dfd15ebd)
1import { css } from '@emotion/react';
2import {
3  borderRadius,
4  iconSize,
5  theme,
6  typography,
7  spacing,
8  ErrorIcon,
9  InfoIcon,
10  WarningIcon,
11} from '@expo/styleguide';
12import { IconProps } from '@expo/styleguide/dist/types';
13import { Children, ComponentType, PropsWithChildren, isValidElement, ReactNode } from 'react';
14
15type CalloutType = 'default' | 'warning' | 'error' | 'info';
16
17type CalloutProps = PropsWithChildren<{
18  type?: CalloutType;
19  icon?: ComponentType<IconProps> | string;
20}>;
21
22const extractType = (childrenArray: ReactNode[]) => {
23  const firstChild = Children.toArray(childrenArray[0])[0];
24
25  if (isValidElement(firstChild)) {
26    if (typeof firstChild.props.children === 'string') {
27      return firstChild.props.children.toLowerCase();
28    }
29    if (Array.isArray(firstChild.props.children)) {
30      return firstChild.props.children[0].toLowerCase();
31    }
32  }
33
34  return false;
35};
36
37export const Callout = ({ type = 'default', icon, children }: CalloutProps) => {
38  const content = Children.toArray(children).filter(child => isValidElement(child))[0];
39  const contentChildren = Children.toArray(isValidElement(content) && content?.props?.children);
40
41  const extractedType = extractType(contentChildren);
42  const finalType = ['warning', 'error', 'info'].includes(extractedType) ? extractedType : type;
43  const Icon = icon || getCalloutIcon(finalType);
44
45  return (
46    <blockquote css={[containerStyle, getCalloutColor(finalType)]} data-testid="callout-container">
47      <div css={iconStyle}>
48        {typeof icon === 'string' ? (
49          icon
50        ) : (
51          <Icon size={iconSize.small} color={getCalloutIconColor(finalType)} />
52        )}
53      </div>
54      <div css={contentStyle}>
55        {type === finalType ? children : contentChildren.filter((_, i) => i !== 0)}
56      </div>
57    </blockquote>
58  );
59};
60
61function getCalloutColor(type: CalloutType) {
62  switch (type) {
63    case 'warning':
64      return warningColorStyle;
65    case 'error':
66      return errorColorStyle;
67    case 'info':
68      return infoColorStyle;
69    default:
70      return null;
71  }
72}
73
74function getCalloutIcon(type: CalloutType) {
75  switch (type) {
76    case 'warning':
77      return WarningIcon;
78    case 'error':
79      return ErrorIcon;
80    default:
81      return InfoIcon;
82  }
83}
84
85function getCalloutIconColor(type: CalloutType) {
86  switch (type) {
87    case 'warning':
88      return theme.text.warning;
89    case 'error':
90      return theme.text.error;
91    case 'info':
92      return theme.text.info;
93    default:
94      return theme.icon.default;
95  }
96}
97
98const containerStyle = css({
99  backgroundColor: theme.background.secondary,
100  border: `1px solid ${theme.border.default}`,
101  borderRadius: borderRadius.medium,
102  display: 'flex',
103  padding: `${spacing[3]}px ${spacing[4]}px`,
104  marginBottom: spacing[4],
105
106  'table &': {
107    ':last-of-type': {
108      marginBottom: 0,
109    },
110  },
111
112  code: {
113    backgroundColor: theme.background.tertiary,
114  },
115});
116
117const iconStyle = css({
118  fontStyle: 'normal',
119  marginRight: spacing[2],
120  marginTop: spacing[1],
121  userSelect: 'none',
122});
123
124const contentStyle = css({
125  ...typography.body.paragraph,
126  color: theme.text.default,
127
128  '*:last-child': {
129    marginBottom: 0,
130  },
131});
132
133const warningColorStyle = css({
134  backgroundColor: theme.background.warning,
135  borderColor: theme.border.warning,
136
137  code: {
138    backgroundColor: theme.palette.yellow['000'],
139    borderColor: theme.palette.yellow[300],
140  },
141
142  '[data-expo-theme="dark"] & code': {
143    backgroundColor: theme.palette.yellow[100],
144    borderColor: theme.palette.yellow[200],
145  },
146});
147
148const errorColorStyle = css({
149  backgroundColor: theme.background.error,
150  borderColor: theme.border.error,
151
152  code: {
153    backgroundColor: theme.palette.red['000'],
154    borderColor: theme.palette.red[200],
155  },
156
157  '[data-expo-theme="dark"] & code': {
158    backgroundColor: theme.palette.red[100],
159  },
160});
161
162const infoColorStyle = css({
163  backgroundColor: theme.background.info,
164  borderColor: theme.border.info,
165
166  code: {
167    backgroundColor: theme.palette.blue['000'],
168    borderColor: theme.palette.blue[200],
169  },
170
171  '[data-expo-theme="dark"] & code': {
172    backgroundColor: theme.palette.blue[100],
173  },
174});
175