Permalink
Browse files

Update ThemeProvider and withMaterialTheme to handle theme changes

  • Loading branch information...
dantman committed Oct 4, 2017
1 parent d668bd6 commit a22b9f9cb8dc770c6a63c8c843d6eab86d3c8c20
View
@@ -93,7 +93,8 @@
],
"quotes": [
"warn",
"single"
"single",
{"avoidEscape": true}
],
"rest-spread-spacing": [
"error",
View
@@ -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",
View
@@ -0,0 +1,8 @@
'use strict';
import PropTypes from 'prop-types';
export default ({
Observable: PropTypes.shape({
subscribe: PropTypes.func,
}),
});
@@ -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);
}
@@ -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 = {};
@@ -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,
@@ -49,7 +63,7 @@ function makeMock() {
};
}
test('provides a materialTheme prop', () => {
test('withMaterialTheme provides a materialTheme prop', () => {
const {MyComponent, render} = makeMock();
const result = render(
@@ -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(
@@ -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(
@@ -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);
});
@@ -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';
@@ -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;
};
@@ -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} />
);
}
}

0 comments on commit a22b9f9

Please sign in to comment.