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