diff --git a/package.json b/package.json index 049db82..6c16c89 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,9 @@ "fbjs": "^0.8.12", "jsdom": "^9.8.3", "mocha": "^3.3.0", - "prop-types": "^15.5.9", - "react": "^15.5.4", - "react-dom": "^15.5.4", + "prop-types": "^15.7.2", + "react": "^16.3.0", + "react-dom": "^16.3.0", "rimraf": "^2.6.1", "sinon": "^1.17.6" }, @@ -64,7 +64,7 @@ }, "license": "MIT", "peerDependencies": { - "react": "^0.14.0 || ^15.0.0-0" + "react": "^16.3.0-0" }, "typings": "./index.d.ts" } diff --git a/src/components/ThemeProvider.js b/src/components/ThemeProvider.js index c6e539c..4192eb2 100644 --- a/src/components/ThemeProvider.js +++ b/src/components/ThemeProvider.js @@ -1,30 +1,10 @@ -import { Children, Component } from 'react' -import PropTypes from 'prop-types' -import themrShape from '../utils/themr-shape' - -export default class ThemeProvider extends Component { - static propTypes = { - children: PropTypes.element.isRequired, - theme: PropTypes.object.isRequired - } - - static defaultProps = { - theme: {} - } - - static childContextTypes = { - themr: themrShape.isRequired - } - - getChildContext() { - return { - themr: { - theme: this.props.theme - } - } - } - - render() { - return Children.only(this.props.children) - } +import React, { Children } from 'react' +import { ThemeContext } from './themr' + +export default function ThemeProvider(props) { + return ( + + {Children.only(props.children)} + + ) } diff --git a/src/components/themr.js b/src/components/themr.js index e19711f..868d7e5 100644 --- a/src/components/themr.js +++ b/src/components/themr.js @@ -3,6 +3,8 @@ import PropTypes from 'prop-types' import hoistNonReactStatics from 'hoist-non-react-statics' import invariant from 'invariant' +export const ThemeContext = React.createContext({ theme: {} }) + /** * @typedef {Object.} TReactCSSThemrTheme */ @@ -50,16 +52,56 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) => localTheme } + function getNamespacedTheme(props) { + const { themeNamespace, theme } = props + if (!themeNamespace) return theme + if (themeNamespace && !theme) throw new Error('Invalid themeNamespace use in react-css-themr. ' + + 'themeNamespace prop should be used only with theme prop.') + + return Object.keys(theme) + .filter(key => key.startsWith(themeNamespace)) + .reduce((result, key) => ({ ...result, [removeNamespace(key, themeNamespace)]: theme[key] }), {}) + + } + + function getContextTheme(props) { + return props.contextTheme + ? props.contextTheme.theme[config.componentName] + : {} + } + + function getThemeNotComposed(props) { + if (props.theme) return getNamespacedTheme(props) + if (config.localTheme) return config.localTheme + return getContextTheme(props) + } + + function getTheme(props) { + return props.composeTheme === COMPOSE_SOFTLY + ? { + ...getContextTheme(props), + ...config.localTheme, + ...getNamespacedTheme(props) + } + : themeable( + themeable(getContextTheme(props), config.localTheme), + getNamespacedTheme(props) + ) + } + + function calcTheme(props) { + const { composeTheme } = props + return composeTheme + ? getTheme(props) + : getThemeNotComposed(props) + } + /** * @property {{wrappedInstance: *}} refs */ class Themed extends Component { static displayName = `Themed${ThemedComponent.name}`; - static contextTypes = { - themr: PropTypes.object - } - static propTypes = { ...ThemedComponent.propTypes, composeTheme: PropTypes.oneOf([ COMPOSE_DEEPLY, COMPOSE_SOFTLY, DONT_COMPOSE ]), @@ -75,84 +117,46 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) => mapThemrProps: optionMapThemrProps } - constructor(...args) { - super(...args) - this.theme_ = this.calcTheme(this.props) - } - - getWrappedInstance() { - invariant(true, - 'DEPRECATED: To access the wrapped instance, you have to pass ' + - '{ innerRef: fn } and retrieve with a callback ref style.' - ) - - return this.refs.wrappedInstance - } - - getNamespacedTheme(props) { - const { themeNamespace, theme } = props - if (!themeNamespace) return theme - if (themeNamespace && !theme) throw new Error('Invalid themeNamespace use in react-css-themr. ' + - 'themeNamespace prop should be used only with theme prop.') - - return Object.keys(theme) - .filter(key => key.startsWith(themeNamespace)) - .reduce((result, key) => ({ ...result, [removeNamespace(key, themeNamespace)]: theme[key] }), {}) - } - - getThemeNotComposed(props) { - if (props.theme) return this.getNamespacedTheme(props) - if (config.localTheme) return config.localTheme - return this.getContextTheme() - } - - getContextTheme() { - return this.context.themr - ? this.context.themr.theme[config.componentName] - : {} - } - - getTheme(props) { - return props.composeTheme === COMPOSE_SOFTLY - ? { - ...this.getContextTheme(), - ...config.localTheme, - ...this.getNamespacedTheme(props) - } - : themeable( - themeable(this.getContextTheme(), config.localTheme), - this.getNamespacedTheme(props) - ) - } + getWrappedInstance() { + invariant(true, + 'DEPRECATED: To access the wrapped instance, you have to pass ' + + '{ innerRef: fn } and retrieve with a callback ref style.' + ) - calcTheme(props) { - const { composeTheme } = props - return composeTheme - ? this.getTheme(props) - : this.getThemeNotComposed(props) + return this.refs.wrappedInstance } - componentWillReceiveProps(nextProps) { + lastProps = {}; + calcTheme = (props) => { if ( - nextProps.composeTheme !== this.props.composeTheme || - nextProps.theme !== this.props.theme || - nextProps.themeNamespace !== this.props.themeNamespace + props.composeTheme !== this.lastProps.composeTheme || + props.theme !== this.lastProps.theme || + props.themeNamespace !== this.lastProps.themeNamespace || + props.contextTheme !== this.lastProps.contextTheme ) { - this.theme_ = this.calcTheme(nextProps) + this.lastProps = props + this.theme_ = calcTheme(props) } + return this.theme_ } render() { - return React.createElement( - ThemedComponent, - this.props.mapThemrProps(this.props, this.theme_) + return ( + + {contextTheme => ( + + )} + ) } } Themed[THEMR_CONFIG] = config - return hoistNonReactStatics(Themed, ThemedComponent) + return hoistNonReactStatics( + Themed, + ThemedComponent + ) } /** diff --git a/test/components/ThemeProvider.spec.js b/test/components/ThemeProvider.spec.js index 478400e..9ee8fe7 100644 --- a/test/components/ThemeProvider.spec.js +++ b/test/components/ThemeProvider.spec.js @@ -1,20 +1,21 @@ import React, { Component } from 'react' +import { render } from 'react-dom' import expect from 'expect' -import PropTypes from 'prop-types' import TestUtils from 'react-dom/test-utils' import { ThemeProvider } from '../../src/index' +import { ThemeContext } from '../../src/components/themr' describe('ThemeProvider', () => { class Child extends Component { render() { - return
+ return ( + + {data => JSON.stringify(data)} + + ) } } - Child.contextTypes = { - themr: PropTypes.object.isRequired - } - it('enforces a single child', () => { const theme = {} @@ -46,7 +47,7 @@ describe('ThemeProvider', () => { }) it('should add the theme to the child context', () => { - const theme = {} + const theme = { foo: 'bar' } TestUtils.renderIntoDocument( @@ -55,15 +56,16 @@ describe('ThemeProvider', () => { ) const spy = expect.spyOn(console, 'error') - const tree = TestUtils.renderIntoDocument( + const node = document.createElement('div') + render( - + , + node ) spy.destroy() expect(spy.calls.length).toBe(0) - const child = TestUtils.findRenderedComponentWithType(tree, Child) - expect(child.context.themr.theme).toBe(theme) + expect(JSON.parse(node.innerHTML)).toEqual({ theme }) }) }) diff --git a/test/components/themr.spec.js b/test/components/themr.spec.js index 430bf01..15a3935 100644 --- a/test/components/themr.spec.js +++ b/test/components/themr.spec.js @@ -1,11 +1,12 @@ import expect from 'expect' -import React, { Children, Component } from 'react' +import React, { Component } from 'react' import PropTypes from 'prop-types' import TestUtils from 'react-dom/test-utils' import sinon from 'sinon' import { render } from 'react-dom' import shallowEqual from 'fbjs/lib/shallowEqual' import { themr, themeable } from '../../src/index' +import { ThemeProvider } from '../../src/index' describe('Themr decorator function', () => { class Passthrough extends Component { @@ -15,40 +16,13 @@ describe('Themr decorator function', () => { } } - class ProviderMock extends Component { - static childContextTypes = { - themr: PropTypes.object.isRequired - } - - getChildContext() { - return { themr: { theme: this.props.theme } } - } + class ProviderMock extends Component { render() { - return Children.only(this.props.children) + return } } - it('passes a context theme object using the component\'s context', () => { - const theme = { Container: { foo: 'foo_1234' } } - - @themr('Container') - class Container extends Component { - render() { - return - } - } - - const tree = TestUtils.renderIntoDocument( - - - - ) - - const container = TestUtils.findRenderedComponentWithType(tree, Container) - expect(container.context.themr.theme).toBe(theme) - }) - it('passes a context theme object using the component\'s theme prop', () => { const containerTheme = { foo: 'foo_1234' } const theme = { Container: containerTheme } diff --git a/yarn.lock b/yarn.lock index 483df9f..09f0e22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1384,7 +1384,7 @@ fast-levenshtein@~2.0.4: version "2.0.5" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz#bd33145744519ab1c36c3ee9f31f08e9079b67f2" -fbjs@^0.8.12, fbjs@^0.8.9: +fbjs@^0.8.12: version "0.8.12" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" dependencies: @@ -1904,6 +1904,11 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@^3.5.1: version "3.7.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" @@ -2066,12 +2071,19 @@ loose-envify@^1.0.0: dependencies: js-tokens "^2.0.0" -loose-envify@^1.1.0, loose-envify@^1.3.1: +loose-envify@^1.1.0: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: js-tokens "^3.0.0" +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + micromatch@^2.1.5: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" @@ -2212,6 +2224,11 @@ object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + object-inspect@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.2.1.tgz#3b62226eb8f6d441751c7d8f22a20ff80ac9dc3f" @@ -2344,12 +2361,14 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.7, prop-types@^15.5.9, prop-types@~15.5.7: - version "15.5.9" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.9.tgz#d478eef0e761396942f70c78e772f76e8be747c9" +prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" punycode@^1.4.1: version "1.4.1" @@ -2375,23 +2394,30 @@ rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~1.0.4" -react-dom@^15.5.4: - version "15.5.4" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da" +react-dom@^16.3.0: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" + integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== dependencies: - fbjs "^0.8.9" loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "~15.5.7" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.13.6" + +react-is@^16.8.1: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" + integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== -react@^15.5.4: - version "15.5.4" - resolved "https://registry.yarnpkg.com/react/-/react-15.5.4.tgz#fa83eb01506ab237cdc1c8c3b1cea8de012bf047" +react@^16.3.0: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" + integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== dependencies: - fbjs "^0.8.9" loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.7" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.13.6" "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.2.2: version "2.2.9" @@ -2572,6 +2598,14 @@ sax@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" +scheduler@^0.13.6: + version "0.13.6" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" + integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"