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