Skip to content

Commit

Permalink
Update ThemeProvider and withMaterialTheme to handle theme changes
Browse files Browse the repository at this point in the history
  • Loading branch information
dantman committed Oct 4, 2017
1 parent d668bd6 commit a22b9f9
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 14 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Expand Up @@ -93,7 +93,8 @@
],
"quotes": [
"warn",
"single"
"single",
{"avoidEscape": true}
],
"rest-spread-spacing": [
"error",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -40,12 +40,14 @@
"lint": "eslint . --fix"
},
"dependencies": {
"any-observable": "^0.2.0",
"color-composite": "^0.1.0",
"color-parse": "^1.3.2",
"create-react-class": "^15.6.0",
"hoist-non-react-methods": "^1.0.2",
"prop-types": "^15.5.10",
"wcag-contrast": "^0.1.0"
"wcag-contrast": "^0.1.0",
"zen-observable": "^0.6.0"
},
"peerDependencies": {
"react-native": ">= 0.40",
Expand Down
8 changes: 8 additions & 0 deletions src/PropTypes.js
@@ -0,0 +1,8 @@
'use strict';
import PropTypes from 'prop-types';

export default ({
Observable: PropTypes.shape({
subscribe: PropTypes.func,
}),
});
65 changes: 61 additions & 4 deletions src/styles/ThemeProvider.js
@@ -1,23 +1,80 @@
'use strict';
import PropTypes from 'prop-types';
import Observable from 'any-observable';
import PropTypes from '../PropTypes';
import React, {PureComponent} from 'react';
import MaterialTheme from './MaterialTheme';

const defaultTheme = new MaterialTheme();

export default class ThemeProvider extends PureComponent {
static childContextTypes = {
materialTheme: PropTypes.instanceOf(MaterialTheme).isRequired,
materialThemeObservable: PropTypes.Observable.isRequired,
};

state = {};
static contextTypes = {
materialThemeObservable: PropTypes.Observable,
};

getChildContext() {
return {
materialTheme: this.props.theme || this.state.theme || this.context.materialTheme || defaultTheme,
materialThemeObservable: this.observable,
};
}

componentWillMount() {
if ( this.context.materialThemeObservable ) {
this._parentSubscription = this.context.materialThemeObservable.subscribe({
next: (theme) => {
this.parentMaterialTheme = theme;
this.handleThemeChange(this.props);
},
complete: () => {
delete this._parentSubscription;
},
});
} else {
this.handleThemeChange(this.props);
}
}

componentWillReceiveProps(nextProps) {
this.handleThemeChange(nextProps);
}

componentWillUnmount() {
if ( this._parentSubscription ) {
this._parentSubscription.unsubscribe();
}
}

observable = new Observable((observer) => {
if ( this.materialTheme ) {
observer.next(this.materialTheme);
}

this.observers.add(observer);

return () => {
this.observers.delete(observer);
};
});

observers = new Set();

materialTheme = undefined;
parentMaterialTheme = undefined;

handleThemeChange(props) {
const baseTheme = props.theme || this.parentMaterialTheme || defaultTheme;

// @todo Handle extend prop here

if ( baseTheme !== this.materialTheme ) {
this.materialTheme = baseTheme;
this.observers.forEach((observer) => observer.next(this.materialTheme));
}
}

render() {
return React.Children.only(this.props.children);
}
Expand Down
@@ -1,12 +1,21 @@
/**
* Covers ThemeProvider.js and withMaterialTheme.js
*/
'use strict';
import React, {PureComponent} from 'react';
import MaterialTheme from '../MaterialTheme';
import ThemeProvider from '../../styles/ThemeProvider';
import ThemeProvider from '../ThemeProvider';
import withMaterialTheme from '../withMaterialTheme';

import ReactTestRenderer from 'react-test-renderer';

const theme = new MaterialTheme({});
const theme1 = new MaterialTheme({
primary: '#111111',
});
const theme2 = new MaterialTheme({
primary: '#222222',
});

function makeMock() {
const id = {};
Expand Down Expand Up @@ -35,12 +44,17 @@ function makeMock() {

const MyComponent = withMaterialTheme(MySourceComponent);

let host;
return {
id,
MySourceComponent,
MyComponent,
render(tree) {
ReactTestRenderer.create(tree);
if ( host ) {
host.update(tree);
} else {
host = ReactTestRenderer.create(tree);
}

return {
props: renderProps,
Expand All @@ -49,7 +63,7 @@ function makeMock() {
};
}

test('provides a materialTheme prop', () => {
test('withMaterialTheme provides a materialTheme prop', () => {
const {MyComponent, render} = makeMock();

const result = render(
Expand All @@ -63,7 +77,7 @@ test('provides a materialTheme prop', () => {
expect(result.props.materialTheme).toBeInstanceOf(MaterialTheme);
});

test('materialTheme matches the theme passed to ThemeProvider', () => {
test('materialTheme prop matches the theme passed to ThemeProvider', () => {
const {MyComponent, render} = makeMock();

const result = render(
Expand All @@ -75,7 +89,7 @@ test('materialTheme matches the theme passed to ThemeProvider', () => {
expect(result.props.materialTheme).toBe(theme);
});

test('provides a default MaterialTheme when ThemeProvider is not used', () => {
test('withMaterialTheme provides a default MaterialTheme when ThemeProvider is not used', () => {
const {MyComponent, render} = makeMock();

const result = render(
Expand All @@ -86,3 +100,29 @@ test('provides a default MaterialTheme when ThemeProvider is not used', () => {
expect(result.props.materialTheme).not.toBeUndefined();
expect(result.props.materialTheme).toBeInstanceOf(MaterialTheme);
});

test("withMaterialTheme's materialTheme prop updates when ThemeProvider's theme prop is changed", () => {
const {MyComponent, render} = makeMock();
let result;

result = render(
<ThemeProvider theme={theme1}>
<MyComponent />
</ThemeProvider>
);

expect(result.props).toHaveProperty('materialTheme');
expect(result.props.materialTheme).not.toBeUndefined();
expect(result.props.materialTheme).toBe(theme1);

result = render(
<ThemeProvider theme={theme2}>
<MyComponent />
</ThemeProvider>
);

expect(result.props).toHaveProperty('materialTheme');
expect(result.props.materialTheme).not.toBeUndefined();
expect(result.props.materialTheme).toBe(theme2);
});

29 changes: 26 additions & 3 deletions src/styles/withMaterialTheme.js
@@ -1,5 +1,5 @@
'use strict';
import PropTypes from 'prop-types';
import PropTypes from '../PropTypes';
import React, {Component} from 'react';
import hoistNonReactMethods from 'hoist-non-react-methods';
import MaterialTheme from './MaterialTheme';
Expand All @@ -9,9 +9,32 @@ const defaultTheme = new MaterialTheme();
export default function withMaterialTheme(BaseComponent) {
class Wrapper extends Component { // eslint-disable-line react/require-optimization
static contextTypes = {
materialTheme: PropTypes.instanceOf(MaterialTheme),
materialThemeObservable: PropTypes.Observable,
};

state = {
materialTheme: undefined,
};

componentWillMount() {
if ( this.context.materialThemeObservable ) {
this._parentSubscription = this.context.materialThemeObservable.subscribe({
next: (materialTheme) => {
this.setState({materialTheme});
},
complete: () => {
delete this._parentSubscription;
},
});
}
}

componentWillUnmount() {
if ( this._parentSubscription ) {
this._parentSubscription.unsubscribe();
}
}

_setMainRef = (ref) => {
this._mainRef = ref;
};
Expand All @@ -21,7 +44,7 @@ export default function withMaterialTheme(BaseComponent) {
<BaseComponent
ref={this._setMainRef}
{...this.props}
materialTheme={this.context.materialTheme || defaultTheme} />
materialTheme={this.state.materialTheme || defaultTheme} />
);
}
}
Expand Down

0 comments on commit a22b9f9

Please sign in to comment.