{
40 | static defaultProps = {
41 | beforeCollapse: () => null,
42 | afterCollapse: () => null,
43 | beginClosed: false,
44 | children: null
45 | };
46 |
47 | content: ?HTMLElement;
48 |
49 | constructor(props: toggleProps) {
50 | super(props);
51 |
52 | this.state = {
53 | isCollapsed: this.props.beginClosed,
54 | contentHeight: 0
55 | };
56 | }
57 |
58 | componentDidMount() {
59 | const contentHeight = this.content ? this.content.scrollHeight : 0;
60 | this.setState({ contentHeight });
61 | }
62 |
63 | componentWillReceiveProps() {
64 | const contentHeight = this.content ? this.content.scrollHeight : 0;
65 | if (contentHeight !== this.state.contentHeight) {
66 | this.setState({ contentHeight });
67 | }
68 | }
69 |
70 | getContent = (ref: ElementRef<*>) => {
71 | if (!ref) return;
72 | this.content = ref;
73 | };
74 |
75 | toggleCollapse = () => {
76 | const contentHeight = this.content ? this.content.scrollHeight : 0;
77 | this.setState({ contentHeight, isCollapsed: !this.state.isCollapsed });
78 | };
79 |
80 | render() {
81 | let { beforeCollapse, children, afterCollapse } = this.props;
82 | let { isCollapsed, contentHeight } = this.state;
83 |
84 | return (
85 |
86 | {beforeCollapse(isCollapsed, this.toggleCollapse)}
87 |
93 | {children}
94 |
95 | {afterCollapse(isCollapsed, this.toggleCollapse)}
96 |
97 | );
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/packages/pretty-proptypes/src/PrettyConvert/converters.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | // TODO: Remove this eslint-disable
4 | /* eslint-disable */
5 | /** @jsx jsx */
6 | import { jsx, css } from '@emotion/core';
7 | import { type Node } from 'react';
8 | import convert, { resolveFromGeneric } from 'kind2string';
9 | import type { Components } from '../components';
10 | import AddBrackets from './AddBrackets';
11 | import { colors } from '../components/constants';
12 | /*::
13 | import * as K from 'extract-react-types'
14 | */
15 |
16 | export const SIMPLE_TYPES = [
17 | 'array',
18 | 'boolean',
19 | 'number',
20 | 'string',
21 | 'symbol',
22 | 'node',
23 | 'element',
24 | 'custom',
25 | 'any',
26 | 'void',
27 | 'mixed'
28 | ];
29 |
30 | function printComplexType(type, components, depth) {
31 | if (typeof type === 'object' && !SIMPLE_TYPES.includes(type.kind)) {
32 | return prettyConvert(type, components, depth);
33 | }
34 | return null;
35 | }
36 |
37 | export const TypeMinWidth = (props: { children: Node }) => (
38 |
45 | );
46 |
47 | const Arrow = () => (
48 |
53 | {' => '}
54 |
55 | );
56 |
57 | export const converters: { [string]: ?Function } = {
58 | intersection: (type: K.Intersection, components: Components) =>
59 | type.types.reduce(
60 | (acc, intersectionType, index) =>
61 | index < type.types.length - 1
62 | ? [
63 | ...acc,
64 | {prettyConvert(intersectionType, components)},
65 | &
66 | ]
67 | : [...acc, {prettyConvert(intersectionType, components)}],
68 | []
69 | ),
70 | string: (type: K.String, components: Components) => {
71 | if (type.value != null) {
72 | return {convert(type)};
73 | }
74 | return {convert(type)};
75 | },
76 | // nullable types are currently stripping infromation, as we show 'required'
77 | // when it is required. This may be incorrect, and should be reconsidered.
78 | nullable: (type: K.Nullable, components: Components, depth: number) => {
79 | return prettyConvert(type.arguments, components, depth);
80 | },
81 | generic: (type: K.Generic, components: Components, depth: number) => {
82 | if (type.value && type.typeParams) {
83 | // As Flow does not know what the keyword Array means, we're doing a
84 | // check here for generic types with a nominal value of 'Array'
85 | // If a type meets this criteria, we print out its contents as per below.
86 | return (
87 |
88 | {convert(type.value)}
89 |
90 | {type.typeParams &&
91 | type.typeParams.params.map((param, index, array) => (
92 |
93 | {prettyConvert(param, components, depth)}
94 | {type.typeParams && index === array.length - 1 ? '' : ', '}
95 |
96 | ))}
97 |
98 |
99 | );
100 | }
101 | return prettyConvert(resolveFromGeneric(type), components);
102 | },
103 | object: (type: K.Obj, components: Components, depth: number) => {
104 | if (type.members.length === 0) {
105 | return Object;
106 | }
107 | let simpleObj =
108 | type.members.filter(mem => {
109 | if (mem === null) {
110 | /** if the member is null, error out */
111 | console.warn(`null property in members of ${type.referenceIdName} of kind ${type.kind} `);
112 | return false;
113 | }
114 | return !SIMPLE_TYPES.includes(mem.kind);
115 | }).length === 0;
116 |
117 | if (simpleObj) {
118 | return {convert(type)};
119 | }
120 |
121 | return (
122 |
123 |
124 |
125 | {type.members
126 | .filter(p => p)
127 | .map(prop => {
128 | if (prop.kind === 'spread') {
129 | const nestedObj = resolveFromGeneric(prop.value);
130 | // Spreads almost always resolve to an object, but they can
131 | // also resolve to an import. We just allow it to fall through
132 | // to prettyConvert if there are no members
133 | if (nestedObj.members) {
134 | return nestedObj.members.map(newProp =>
135 | prettyConvert(newProp, components, depth)
136 | );
137 | }
138 | }
139 | return prettyConvert(prop, components, depth);
140 | })}
141 |
142 |
143 |
144 | );
145 | },
146 | arrayType: (type: K.ArrayType, components: Components, depth: number) => {
147 | return (
148 |
149 | Array
150 |
151 | {prettyConvert(type.type, components, depth)}
152 |
153 |
154 | );
155 | },
156 | property: (type: K.Property, components: Components, depth: number) => (
157 |
158 | {type.key && (
159 |
160 | {convert(type.key)}
161 |
162 | )}
163 | {type.value.kind !== 'generic' ? ` ${type.value.kind}` : ' '}
164 | {type.optional ? null : required}{' '}
165 | {printComplexType(type.value, components, depth)}
166 |
167 | ),
168 | union: (type: K.Union, components: Components, depth: number) => (
169 |
170 | One of
171 |
172 |
173 | {type.types.map((t, index, array) => (
174 |
175 | {prettyConvert(t, components, depth + 1)}
176 | {array.length - 1 === index ? '' : ', '}
177 |
178 | ))}
179 |
180 |
181 |
182 | ),
183 | function: (type: K.Func, components: Components, depth: number) => {
184 | let simpleReturn = type.returnType && SIMPLE_TYPES.includes(type.returnType.kind);
185 |
186 | let simpleParameters =
187 | type.parameters.filter(param => SIMPLE_TYPES.includes(param.value.kind)).length ===
188 | type.parameters.length;
189 |
190 | if (simpleParameters && simpleReturn) {
191 | return (
192 |
193 | {`(${type.parameters.map(convert).join(', ')})`}
194 |
195 | {`${convert(type.returnType)}`}
196 |
197 | );
198 | } else if (simpleParameters || type.parameters.length < 2) {
199 | return (
200 |
201 |
202 | {type.parameters.map((param, index, array) => [
203 | prettyConvert(param, components, depth),
204 | array.length - 1 === index ? '' : ', '
205 | ])}
206 |
207 |
208 | {type.returnType ? prettyConvert(type.returnType, components, depth) : 'undefined'}
209 |
210 | );
211 | } else {
212 | return (
213 |
214 | function
215 |
216 |
217 | {type.parameters.map((param, index, array) => (
218 |
219 | {prettyConvert(param, components, depth + 1)}
220 | {array.length - 1 === index ? '' : ', '}
221 |
222 | ))}
223 |
224 |
225 |
226 | {type.returnType ? prettyConvert(type.returnType, components, depth) : 'undefined'}
227 |
228 | );
229 | }
230 | },
231 | param: (type: K.Param, components, depth) => (
232 | {prettyConvert(type.value, components, depth)}
233 | ),
234 | typeof: (type: K.Typeof, components, depth) => prettyConvert(type.type, components, depth)
235 | };
236 |
237 | const prettyConvert = (type: K.AnyKind, components: Components, depth: number = 1) => {
238 | if (!type) {
239 | return '';
240 | }
241 |
242 | const converter = converters[type.kind];
243 | if (!converter) {
244 | const stringType = convert(type);
245 | return {stringType};
246 | }
247 | return converter(type, components, depth);
248 | };
249 |
250 | export default prettyConvert;
251 |
--------------------------------------------------------------------------------
/packages/pretty-proptypes/src/PrettyConvert/converters.test.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react';
4 | import { render } from '@testing-library/react';
5 | import { extractReactTypes } from 'extract-react-types';
6 | import components from '../components';
7 | import prettyConvert from './converters';
8 |
9 | const simpleStringKind = { kind: 'string', value: 'here it is' };
10 | let getSimplePropKind = (obj = {}) => ({
11 | kind: 'property',
12 | key: { kind: 'id', name: 'prop1' },
13 | value: simpleStringKind,
14 | ...obj
15 | });
16 |
17 | const assembleERTAST = (propTypes, defaultProps, type = 'flow') => {
18 | let file = `
19 | class Component extends React.Component<${propTypes}> {
20 | defaultProps = ${defaultProps}
21 | }`;
22 | let res = extractReactTypes(file, type);
23 | return res.component.members;
24 | };
25 |
26 | const getSingleDefault = defaultPropVal => {
27 | return assembleERTAST(`{ a: any }`, `{ a: ${defaultPropVal} }`)[0].default;
28 | };
29 |
30 | const getSingleProp = defaultPropType => {
31 | const propTypes = assembleERTAST(`{ a: ${defaultPropType} }`, `{}`)[0];
32 | return propTypes.value;
33 | };
34 |
35 | test('return an empty string when no type is given', () => {
36 | // $FlowFixMe - deliberately passing in undefined here
37 | let res = prettyConvert(undefined, components);
38 | expect(res).toBe('');
39 | });
40 |
41 | test('fallback to kind2string type when no converter is found', () => {
42 | // $FlowFixMe - we are deliberately testing a null case here
43 | const { container: wrapper } = render(prettyConvert({ kind: 'sunshine' }, components));
44 | const { container: other } = render();
45 |
46 | expect(wrapper.innerHTML).toBe(other.innerHTML);
47 | });
48 |
49 | test('prettyConvert string value type', () => {
50 | let kind = simpleStringKind;
51 | const { container: wrapper } = render(prettyConvert(kind, components));
52 | const { container: other } = render(
53 | "{kind.value}"
54 | );
55 |
56 | expect(wrapper.innerHTML).toBe(other.innerHTML);
57 | });
58 |
59 | test('prettyConvert string type type', () => {
60 | const { container: wrapper } = render(prettyConvert({ kind: 'string' }, components));
61 | const { container: other } = render(string);
62 |
63 | expect(wrapper.innerHTML).toBe(other.innerHTML);
64 | });
65 |
66 | test('prettyConvert nullable value', () => {
67 | let kind = {
68 | kind: 'nullable',
69 | arguments: { kind: 'string' }
70 | };
71 | const { container: wrapper } = render(prettyConvert(kind, components));
72 | const { container: other } = render(string);
73 | expect(wrapper.innerHTML).toBe(other.innerHTML);
74 | });
75 |
76 | test('prettyConvert simple property', () => {
77 | let simplePropKind = getSimplePropKind();
78 | const { container: wrapper } = render(prettyConvert(simplePropKind, components));
79 |
80 | expect(wrapper.textContent).toContain(simplePropKind.key.name);
81 | expect(wrapper.textContent).toContain(simplePropKind.value.kind);
82 | });
83 |
84 | test('optional property', () => {
85 | const { container: wrapper } = render(
86 | prettyConvert(getSimplePropKind({ optional: true }), components)
87 | );
88 | expect(wrapper.querySelector('[data-testid="required"]')).toBeFalsy();
89 | });
90 |
91 | test('simple object', () => {
92 | let values = getSingleDefault(`{ a: 'something', b: 'elsewhere' }`);
93 | const { container: wrapper } = render(prettyConvert(values, components));
94 |
95 | expect(wrapper.textContent).toContain('a');
96 | expect(wrapper.textContent).toContain('b');
97 | });
98 |
99 | test('object with nested object', () => {
100 | let values = getSingleDefault(`{ a: 'something', b: { c: 'elsewhere' }}`);
101 | const { container: wrapper } = render(prettyConvert(values, components));
102 | expect(wrapper.textContent).toContain('c');
103 | });
104 |
105 | test('resolve generic of array', () => {
106 | let values = getSingleProp(`Array`);
107 | const { container } = render(prettyConvert(values, components));
108 | expect(container.textContent).toContain('Array');
109 | expect(container.textContent).toContain('string');
110 | });
111 |
112 | test('objects with null members do not throw', () => {
113 | const type = {
114 | kind: 'object',
115 | members: [
116 | {
117 | kind: 'property',
118 | optional: false,
119 | key: {
120 | kind: 'id',
121 | name: 'children'
122 | },
123 | value: {
124 | kind: 'generic',
125 | value: {
126 | kind: 'id',
127 | name: 'React.ReactNode'
128 | }
129 | }
130 | },
131 | null
132 | ],
133 | referenceIdName: 'LabelTextProps'
134 | };
135 | expect(() => prettyConvert(type, components)).not.toThrow();
136 | });
137 |
--------------------------------------------------------------------------------
/packages/pretty-proptypes/src/PrettyConvert/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /** @jsx jsx */
3 | import { jsx, css } from '@emotion/core';
4 | import { Component, type Node } from 'react';
5 | import { resolveFromGeneric } from 'kind2string';
6 | import { gridSize } from '../components/constants';
7 | import allComponents, { type Components } from '../components';
8 | import Toggle from './Toggle';
9 | import prettyConvert, { SIMPLE_TYPES } from './converters';
10 |
11 | const Wrapper = (props: { children: Node }) => (
12 |
22 | );
23 |
24 | type PrettyPropTypeProps = {
25 | typeValue: Object,
26 | shouldCollapse?: boolean,
27 | components: Components
28 | };
29 |
30 | export default class PrettyPropType extends Component {
31 | static defaultProps = {
32 | components: allComponents
33 | };
34 |
35 | render() {
36 | let { shouldCollapse, typeValue: type, components } = this.props;
37 | // any instance of returning null means we are confident the information will
38 | // be displayed elsewhere so we do not need to also include it here.
39 | if (type.kind === 'generic') {
40 | type = resolveFromGeneric(type);
41 | }
42 | if (SIMPLE_TYPES.includes(type.kind)) return null;
43 | if (type.kind === 'nullable' && SIMPLE_TYPES.includes(type.arguments.kind)) {
44 | return null;
45 | }
46 |
47 | return shouldCollapse ? (
48 | (
51 |
52 |
53 | {isCollapsed ? 'Expand Prop Shape' : 'Hide Prop Shape'}
54 |
55 |
56 | )}
57 | >
58 | {prettyConvert(type, components)}
59 |
60 | ) : (
61 | {prettyConvert(type, components)}
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/packages/pretty-proptypes/src/Prop/Heading.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /** @jsx jsx */
3 | import { jsx, css } from '@emotion/core';
4 | import { type Node } from 'react';
5 | import { colors, gridSize, borderRadius } from '../components/constants';
6 |
7 | export const Heading = ({ children, ...rest }: { children: Node }) => (
8 |
19 | {children}
20 |
21 | );
22 |
23 | export const HeadingDefault = (props: { children: Node }) => (
24 |
30 | );
31 |
32 | export const HeadingRequired = (props: { children: Node }) => (
33 |
39 | );
40 |
41 | export const HeadingDeprecated = (props: { children: Node }) => (
42 |
48 | );
49 |
50 | export const HeadingType = (props: { children: Node }) => (
51 |
61 | );
62 |
63 | export const HeadingName = (props: { children: Node }) => (
64 |
75 | );
76 |
77 | const Whitespace = () => ' ';
78 |
79 | type PropTypeHeadingProps = {
80 | name: any,
81 | required: boolean,
82 | deprecated?: boolean,
83 | type: any,
84 | // This is probably giving up
85 | defaultValue?: any
86 | };
87 |
88 | const PropTypeHeading = (props: PropTypeHeadingProps) => (
89 |
90 |
91 | {props.name}
92 |
93 |
94 | {props.type}
95 | {props.defaultValue !== undefined && = {props.defaultValue}}
96 | {props.required && props.defaultValue === undefined ? (
97 | required
98 | ) : null}
99 | {props.deprecated && deprecated}
100 |
101 | );
102 |
103 | export default PropTypeHeading;
104 |
--------------------------------------------------------------------------------
/packages/pretty-proptypes/src/Prop/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /** @jsx jsx */
3 | import { jsx, css } from '@emotion/core';
4 | import { Component, type ComponentType, type Node } from 'react';
5 | import md from 'react-markings';
6 | import { gridSize } from '../components/constants';
7 | import PrettyPropType from '../PrettyConvert';
8 | import PropTypeHeading from './Heading';
9 | import type { CommonProps } from '../types';
10 |
11 | const PropTypeWrapper = (props: { children: Node }) => (
12 |
22 | );
23 |
24 | type PropProps = CommonProps & {
25 | shapeComponent: ComponentType
26 | };
27 |
28 | export default class Prop extends Component {
29 | static defaultProps = {
30 | shapeComponent: (props: CommonProps) =>
31 | };
32 |
33 | render() {
34 | let { shapeComponent: ShapeComponent, ...commonProps } = this.props;
35 |
36 | let {
37 | defaultValue,
38 | description,
39 | name,
40 | required,
41 | deprecated,
42 | type,
43 | components,
44 | componentDisplayName
45 | } = commonProps;
46 |
47 | return (
48 |
51 |
58 | {description && (
59 |
60 | {' '}
61 | {md([deprecated ? description.replace('@deprecated', '') : description])}
62 |
63 | )}
64 |
65 |
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/pretty-proptypes/src/Props/Props.test.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react';
3 | import { render, screen } from '@testing-library/react';
4 | import { extractReactTypes } from 'extract-react-types';
5 |
6 | import Props from './';
7 |
8 | const file = `
9 | const MyComponent = (props: { simpleProp: string }) => null;
10 |
11 | export default MyComponent;
12 | `;
13 |
14 | test('should visualise props from extract-types-loader', () => {
15 | const { container, getByText } = render(
16 |
17 | );
18 |
19 | const prop = getByText('simpleProp');
20 | expect(prop).toBeTruthy();
21 | expect(getByText('required')).toBeTruthy();
22 | expect(getByText('string')).toBeTruthy();
23 | });
24 |
25 | test('simple facts about two props', () => {
26 | const file2 = `
27 | const MyComponent = (props: { simpleProp: string, secondProp?: number }) => null;
28 |
29 | export default MyComponent;
30 | `;
31 |
32 | const { container, getByText } = render(
33 |
34 | );
35 |
36 | expect(getByText('simpleProp')).toBeTruthy();
37 | expect(getByText('secondProp')).toBeTruthy();
38 | });
39 |
40 | test('renders no children when passed on props', () => {
41 | const { container } = render();
42 | expect(container.children).toHaveLength(0);
43 | });
44 |
45 | describe('#typescript typeSystem', () => {
46 | test('should visualize generic props from extract-types-loader', () => {
47 | const code = `
48 | import React from 'react';
49 |
50 | export type OnSubmitHandler = (
51 | values: FormData,
52 | callback?: (errors?: Record) => void,
53 | ) => void | Object | Promise