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