Skip to content

Commit e022349

Browse files
committed
feat: use new Context API for options
1 parent ebb9b7e commit e022349

File tree

9 files changed

+163
-150
lines changed

9 files changed

+163
-150
lines changed

package.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@
6767
"prettier-eslint": "^8.8.1",
6868
"puppeteer": "^1.2.0",
6969
"raf": "^3.4.0",
70-
"react": "^16.2.0",
71-
"react-dom": "^16.2.0",
70+
"react": "^16.3.0-alpha.2",
71+
"react-dom": "^16.3.0-alpha.2",
7272
"rimraf": "^2.6.2",
7373
"shelljs": "^0.8.1",
7474
"source-map-loader": "^0.2.1",
@@ -158,8 +158,14 @@
158158
"collectCoverageFrom": [
159159
"src/**/*.{ts,tsx}"
160160
],
161-
"coverageReporters": ["json", "lcov", "text-summary"],
162-
"coveragePathIgnorePatterns": ["\\.d\\.ts$"]
161+
"coverageReporters": [
162+
"json",
163+
"lcov",
164+
"text-summary"
165+
],
166+
"coveragePathIgnorePatterns": [
167+
"\\.d\\.ts$"
168+
]
163169
},
164170
"prettier": {
165171
"singleQuote": true,

src/components/Endpoint/Endpoint.tsx

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { ShelfIcon } from '../../common-elements';
33
import { OperationModel } from '../../services';
4-
import { ComponentWithOptions } from '../OptionsProvider';
4+
import { OptionsContext } from '../OptionsProvider';
55
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
66

77
import {
@@ -25,7 +25,7 @@ export interface EndpointState {
2525
expanded: boolean;
2626
}
2727

28-
export class Endpoint extends ComponentWithOptions<EndpointProps, EndpointState> {
28+
export class Endpoint extends React.Component<EndpointProps, EndpointState> {
2929
constructor(props) {
3030
super(props);
3131
this.state = {
@@ -38,39 +38,41 @@ export class Endpoint extends ComponentWithOptions<EndpointProps, EndpointState>
3838
};
3939

4040
render() {
41-
const { operation, inverted } = this.props;
41+
const { operation, inverted, hideHostname } = this.props;
4242
const { expanded } = this.state;
4343

44-
const hideHostname = this.props.hideHostname || this.options.hideHostname;
45-
4644
// TODO: highlight server variables, e.g. https://{user}.test.com
4745
return (
48-
<OperationEndpointWrap>
49-
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
50-
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '}
51-
<ServerRelativeURL>{operation.path}</ServerRelativeURL>
52-
<ShelfIcon
53-
float={'right'}
54-
color={inverted ? 'black' : 'white'}
55-
size={'20px'}
56-
direction={expanded ? 'up' : 'down'}
57-
style={{ marginRight: '-25px' }}
58-
/>
59-
</EndpointInfo>
60-
<ServersOverlay expanded={expanded}>
61-
{operation.servers.map(server => (
62-
<ServerItem key={server.url}>
63-
<div>{server.description}</div>
64-
<SelectOnClick>
65-
<ServerUrl>
66-
{!hideHostname && <span>{server.url}</span>}
67-
{operation.path}
68-
</ServerUrl>
69-
</SelectOnClick>
70-
</ServerItem>
71-
))}
72-
</ServersOverlay>
73-
</OperationEndpointWrap>
46+
<OptionsContext.Consumer>
47+
{options => (
48+
<OperationEndpointWrap>
49+
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
50+
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '}
51+
<ServerRelativeURL>{operation.path}</ServerRelativeURL>
52+
<ShelfIcon
53+
float={'right'}
54+
color={inverted ? 'black' : 'white'}
55+
size={'20px'}
56+
direction={expanded ? 'up' : 'down'}
57+
style={{ marginRight: '-25px' }}
58+
/>
59+
</EndpointInfo>
60+
<ServersOverlay expanded={expanded}>
61+
{operation.servers.map(server => (
62+
<ServerItem key={server.url}>
63+
<div>{server.description}</div>
64+
<SelectOnClick>
65+
<ServerUrl>
66+
{!(hideHostname || options.hideHostname) && <span>{server.url}</span>}
67+
{operation.path}
68+
</ServerUrl>
69+
</SelectOnClick>
70+
</ServerItem>
71+
))}
72+
</ServersOverlay>
73+
</OperationEndpointWrap>
74+
)}
75+
</OptionsContext.Consumer>
7476
);
7577
}
7678
}

src/components/Markdown/Markdown.tsx

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import styled from '../../styled-components';
33

44
import * as DOMPurify from 'dompurify';
55
import { AppStore, MarkdownRenderer } from '../../services';
6-
import { ComponentWithOptions } from '../OptionsProvider';
6+
import { OptionsContext } from '../OptionsProvider';
77

88
import { markdownCss } from './styles';
99

@@ -24,7 +24,7 @@ export interface MarkdownProps {
2424
store?: AppStore;
2525
}
2626

27-
class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
27+
class InternalMarkdown extends React.Component<MarkdownProps> {
2828
constructor(props: MarkdownProps) {
2929
super(props);
3030

@@ -40,10 +40,7 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
4040
throw new Error('When using components with Markdwon in ReDoc, store prop must be provided');
4141
}
4242

43-
const sanitize =
44-
this.props.sanitize || this.options.untrustedSpec
45-
? (html: string) => DOMPurify.sanitize(html)
46-
: (html: string) => html;
43+
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);
4744

4845
const renderer = new MarkdownRenderer();
4946
const parts = components
@@ -62,26 +59,36 @@ class InternalMarkdown extends ComponentWithOptions<MarkdownProps> {
6259
appendClass += ' -inline';
6360
}
6461

65-
if (inline) {
66-
return (
67-
<span
68-
className={className + appendClass}
69-
dangerouslySetInnerHTML={{ __html: sanitize(parts[0] as string) }}
70-
/>
71-
);
72-
}
73-
7462
return (
75-
<div className={className + appendClass}>
76-
{parts.map(
77-
(part, idx) =>
78-
typeof part === 'string' ? (
79-
<div key={idx} dangerouslySetInnerHTML={{ __html: sanitize(part) }} />
80-
) : (
81-
<part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />
82-
),
83-
)}
84-
</div>
63+
<OptionsContext.Consumer>
64+
{options =>
65+
inline ? (
66+
<span
67+
className={className + appendClass}
68+
dangerouslySetInnerHTML={{
69+
__html: sanitize(options.untrustedSpec, parts[0] as string),
70+
}}
71+
/>
72+
) : (
73+
<div className={className + appendClass}>
74+
{parts.map(
75+
(part, idx) =>
76+
typeof part === 'string' ? (
77+
<div
78+
key={idx}
79+
dangerouslySetInnerHTML={{ __html: sanitize(options.untrustedSpec, part) }}
80+
/>
81+
) : (
82+
<part.component
83+
key={idx}
84+
{...{ ...part.attrs, ...part.propsSelector(store) }}
85+
/>
86+
),
87+
)}
88+
</div>
89+
)
90+
}
91+
</OptionsContext.Consumer>
8592
);
8693
}
8794
}

src/components/Operation/Operation.tsx

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { observer } from 'mobx-react';
66

77
import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements';
88

9-
import { ComponentWithOptions } from '../OptionsProvider';
9+
import { OptionsContext } from '../OptionsProvider';
1010

1111
import { ShareLink } from '../../common-elements/linkify';
1212
import { Endpoint } from '../Endpoint/Endpoint';
@@ -40,31 +40,34 @@ interface OperationProps {
4040
}
4141

4242
@observer
43-
export class Operation extends ComponentWithOptions<OperationProps> {
43+
export class Operation extends React.Component<OperationProps> {
4444
render() {
4545
const { operation } = this.props;
4646

4747
const { name: summary, description, deprecated } = operation;
48-
const pathInMiddle = this.options.pathInMiddlePanel;
4948
return (
50-
<OperationRow>
51-
<MiddlePanel>
52-
<H2>
53-
<ShareLink href={'#' + operation.getHash()} />
54-
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
55-
</H2>
56-
{pathInMiddle && <Endpoint operation={operation} inverted={true} />}
57-
{description !== undefined && <Markdown source={description} />}
58-
<SecurityRequirements securities={operation.security} />
59-
<Parameters parameters={operation.parameters} body={operation.requestBody} />
60-
<ResponsesList responses={operation.responses} />
61-
</MiddlePanel>
62-
<DarkRightPanel>
63-
{!pathInMiddle && <Endpoint operation={operation} />}
64-
<RequestSamples operation={operation} />
65-
<ResponseSamples operation={operation} />
66-
</DarkRightPanel>
67-
</OperationRow>
49+
<OptionsContext.Consumer>
50+
{options => (
51+
<OperationRow>
52+
<MiddlePanel>
53+
<H2>
54+
<ShareLink href={'#' + operation.getHash()} />
55+
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
56+
</H2>
57+
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />}
58+
{description !== undefined && <Markdown source={description} />}
59+
<SecurityRequirements securities={operation.security} />
60+
<Parameters parameters={operation.parameters} body={operation.requestBody} />
61+
<ResponsesList responses={operation.responses} />
62+
</MiddlePanel>
63+
<DarkRightPanel>
64+
{!options.pathInMiddlePanel && <Endpoint operation={operation} />}
65+
<RequestSamples operation={operation} />
66+
<ResponseSamples operation={operation} />
67+
</DarkRightPanel>
68+
</OperationRow>
69+
)}
70+
</OptionsContext.Consumer>
6871
);
6972
}
7073
}

src/components/OptionsProvider.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,19 @@ import * as React from 'react';
33

44
import { RedocNormalizedOptions } from '../services/RedocNormalizedOptions';
55

6-
export interface OptionsProviderProps {
7-
options: RedocNormalizedOptions;
8-
}
9-
10-
export class OptionsProvider extends React.Component<OptionsProviderProps> {
11-
static childContextTypes = {
12-
redocOptions: PropTypes.object.isRequired,
13-
};
14-
15-
getChildContext() {
16-
return {
17-
redocOptions: this.props.options,
18-
};
19-
}
6+
// TODO: contribute declarations to @types/react once 16.3 is released
7+
type ReactProviderComponent<T> = React.ComponentType<{ value: T }>;
8+
type ReactConsumerComponent<T> = React.ComponentType<{ children: ((value: T) => React.ReactNode) }>;
209

21-
render() {
22-
return React.Children.only(this.props.children);
23-
}
10+
interface ReactContext<T> {
11+
Provider: ReactProviderComponent<T>;
12+
Consumer: ReactConsumerComponent<T>;
2413
}
2514

26-
export class ComponentWithOptions<P = {}, S = {}> extends React.Component<P, S> {
27-
static contextTypes = {
28-
redocOptions: PropTypes.object,
29-
};
30-
31-
get options(): RedocNormalizedOptions {
32-
return this.context.redocOptions || {};
33-
}
15+
declare module 'react' {
16+
function createContext<T>(defatulValue: T): ReactContext<T>;
3417
}
18+
19+
export const OptionsContext = React.createContext(new RedocNormalizedOptions({}));
20+
export const OptionsProvider = OptionsContext.Provider;
21+
export const OptionsConsumer = OptionsContext.Consumer;

src/components/Redoc/Redoc.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class Redoc extends React.Component<RedocProps> {
3636
const store = this.props.store;
3737
return (
3838
<ThemeProvider theme={options.theme}>
39-
<OptionsProvider options={options}>
39+
<OptionsProvider value={options}>
4040
<RedocWrap className="redoc-wrap">
4141
<StickyResponsiveSidebar menu={menu} className="menu-content">
4242
<ApiLogo info={spec.info} />

src/components/SideMenu/SideMenu.tsx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
import { observer } from 'mobx-react';
22
import * as React from 'react';
3-
import { ComponentWithOptions } from '../OptionsProvider';
3+
import { OptionsContext } from '../OptionsProvider';
44

55
import { IMenuItem, MenuStore } from '../../services/MenuStore';
66
import { MenuItems } from './MenuItems';
77

88
import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar';
99

1010
@observer
11-
export class SideMenu extends ComponentWithOptions<{ menu: MenuStore }> {
11+
export class SideMenu extends React.Component<{ menu: MenuStore }> {
1212
private _updateScroll?: () => void;
1313

1414
render() {
1515
const store = this.props.menu;
16-
const nativeScrollbars = this.options.nativeScrollbars;
17-
return nativeScrollbars ? (
18-
<MenuItems
19-
style={{
20-
overflow: 'auto',
21-
msOverflowStyle: '-ms-autohiding-scrollbar',
22-
}}
23-
items={store.items}
24-
onActivate={this.activate}
25-
/>
26-
) : (
27-
<PerfectScrollbar updateFn={this.saveScrollUpdate}>
28-
<MenuItems items={store.items} onActivate={this.activate} />
29-
</PerfectScrollbar>
16+
return (
17+
<OptionsContext.Consumer>
18+
{options =>
19+
options.nativeScrollbars ? (
20+
<MenuItems
21+
style={{
22+
overflow: 'auto',
23+
msOverflowStyle: '-ms-autohiding-scrollbar',
24+
}}
25+
items={store.items}
26+
onActivate={this.activate}
27+
/>
28+
) : (
29+
<PerfectScrollbar updateFn={this.saveScrollUpdate}>
30+
<MenuItems items={store.items} onActivate={this.activate} />
31+
</PerfectScrollbar>
32+
)
33+
}
34+
</OptionsContext.Consumer>
3035
);
3136
}
3237

0 commit comments

Comments
 (0)