1import { mergeClasses } from '@expo/styleguide';
2
3import {
4  DefaultPropsDefinitionData,
5  PropData,
6  PropsDefinitionData,
7  TypeDefinitionData,
8} from '~/components/plugins/api/APIDataTypes';
9import { APISectionDeprecationNote } from '~/components/plugins/api/APISectionDeprecationNote';
10import { APISectionPlatformTags } from '~/components/plugins/api/APISectionPlatformTags';
11import {
12  CommentTextBlock,
13  getCommentContent,
14  getCommentOrSignatureComment,
15  getTagData,
16  getTagNamesList,
17  H3Code,
18  H4Code,
19  renderTypeOrSignatureType,
20  resolveTypeName,
21  STYLES_APIBOX,
22  STYLES_APIBOX_NESTED,
23  ELEMENT_SPACING,
24  STYLES_NESTED_SECTION_HEADER,
25  STYLES_NOT_EXPOSED_HEADER,
26  STYLES_SECONDARY,
27  TypeDocKind,
28} from '~/components/plugins/api/APISectionUtils';
29import { CODE, H2, H3, H4, LI, MONOSPACE, P, UL } from '~/ui/components/Text';
30
31export type APISectionPropsProps = {
32  data: PropsDefinitionData[];
33  defaultProps?: DefaultPropsDefinitionData;
34  header?: string;
35};
36
37const UNKNOWN_VALUE = '...';
38
39const extractDefaultPropValue = (
40  { comment, name }: PropData,
41  defaultProps?: DefaultPropsDefinitionData
42): string | undefined => {
43  const annotationDefault = getTagData('default', comment);
44  if (annotationDefault) {
45    return getCommentContent(annotationDefault.content);
46  }
47  return defaultProps?.type?.declaration?.children?.filter(
48    (defaultProp: PropData) => defaultProp.name === name
49  )[0]?.defaultValue;
50};
51
52const renderInheritedProp = (ip: TypeDefinitionData) => {
53  return (
54    <LI key={`inherited-prop-${ip.name}-${ip.type}`}>
55      <CODE>{resolveTypeName(ip)}</CODE>
56    </LI>
57  );
58};
59
60const renderInheritedProps = (
61  data: PropsDefinitionData | undefined,
62  exposeInSidebar?: boolean
63): JSX.Element | undefined => {
64  const inheritedData = data?.type?.types ?? data?.extendedTypes ?? [];
65  const inheritedProps =
66    inheritedData.filter((ip: TypeDefinitionData) => ip.type === 'reference') ?? [];
67  if (inheritedProps.length) {
68    return (
69      <>
70        {exposeInSidebar ? <H3>Inherited Props</H3> : <H4>Inherited Props</H4>}
71        <UL>{inheritedProps.map(renderInheritedProp)}</UL>
72      </>
73    );
74  }
75  return undefined;
76};
77
78const getPropsBaseTypes = (def: PropsDefinitionData) => {
79  if (def.kind === TypeDocKind.TypeAlias) {
80    const baseTypes = def?.type?.types
81      ? def.type.types?.filter((t: TypeDefinitionData) => t.declaration)
82      : [def.type];
83    return baseTypes.map(def => def?.declaration?.children);
84  } else if (def.kind === TypeDocKind.Interface) {
85    return def.children?.filter(child => !child.inheritedFrom) ?? [];
86  }
87  return [];
88};
89
90const renderProps = (
91  def: PropsDefinitionData,
92  defaultValues?: DefaultPropsDefinitionData,
93  exposeInSidebar?: boolean
94): JSX.Element => {
95  const propsDeclarations = getPropsBaseTypes(def)
96    .flat()
97    .filter((dec, i, arr) => arr.findIndex(t => t?.name === dec?.name) === i);
98
99  return (
100    <div key={`props-definition-${def.name}`}>
101      {propsDeclarations?.map(prop =>
102        prop
103          ? renderProp(prop, extractDefaultPropValue(prop, defaultValues), exposeInSidebar)
104          : null
105      )}
106      {renderInheritedProps(def, exposeInSidebar)}
107    </div>
108  );
109};
110
111export const renderProp = (
112  { comment, name, type, flags, signatures }: PropData,
113  defaultValue?: string,
114  exposeInSidebar?: boolean
115) => {
116  const HeaderComponent = exposeInSidebar ? H3Code : H4Code;
117  const extractedSignatures = signatures || type?.declaration?.signatures;
118  const extractedComment = getCommentOrSignatureComment(comment, extractedSignatures);
119
120  return (
121    <div key={`prop-entry-${name}`} css={[STYLES_APIBOX, STYLES_APIBOX_NESTED]}>
122      <APISectionDeprecationNote comment={extractedComment} />
123      <APISectionPlatformTags comment={comment} prefix="Only for:" />
124      <HeaderComponent tags={getTagNamesList(comment)}>
125        <MONOSPACE weight="medium" css={!exposeInSidebar && STYLES_NOT_EXPOSED_HEADER}>
126          {name}
127        </MONOSPACE>
128      </HeaderComponent>
129      <P className={mergeClasses(extractedComment && ELEMENT_SPACING)}>
130        {flags?.isOptional && <span css={STYLES_SECONDARY}>Optional&emsp;&bull;&emsp;</span>}
131        <span css={STYLES_SECONDARY}>Type:</span>{' '}
132        {renderTypeOrSignatureType(type, extractedSignatures)}
133        {defaultValue && defaultValue !== UNKNOWN_VALUE ? (
134          <span>
135            <span css={STYLES_SECONDARY}>&emsp;&bull;&emsp;Default:</span>{' '}
136            <CODE>{defaultValue}</CODE>
137          </span>
138        ) : null}
139      </P>
140      <CommentTextBlock comment={extractedComment} includePlatforms={false} />
141      {!extractedComment && <br />}
142    </div>
143  );
144};
145
146const APISectionProps = ({ data, defaultProps, header = 'Props' }: APISectionPropsProps) => {
147  const baseProp = data.find(prop => prop.name === header);
148  return data?.length > 0 ? (
149    <>
150      {data?.length === 1 || header === 'Props' ? (
151        <H2 key="props-header">{header}</H2>
152      ) : (
153        <div>
154          {baseProp && <APISectionDeprecationNote comment={baseProp.comment} />}
155          <div css={STYLES_NESTED_SECTION_HEADER}>
156            <H4 key={`${header}-props-header`}>{header}</H4>
157          </div>
158          {baseProp && baseProp.comment ? <CommentTextBlock comment={baseProp.comment} /> : null}
159        </div>
160      )}
161      {data.map((propsDefinition: PropsDefinitionData) =>
162        renderProps(propsDefinition, defaultProps, data?.length === 1 || header === 'Props')
163      )}
164    </>
165  ) : null;
166};
167
168export default APISectionProps;
169