From cf04e13e9539f4810a5fd1b95c8aae6a0451a933 Mon Sep 17 00:00:00 2001 From: Derek Persons Date: Thu, 25 Mar 2021 15:30:51 -0700 Subject: [PATCH 1/2] rewrite themer to use a react context --- src/__tests__/ensureExports.spec.js | 4 +- src/components/Link/__tests__/Link.spec.js | 10 +- src/index.d.ts | 5 +- src/index.js | 6 +- src/styles/themer/Themer.d.ts | 15 + src/styles/themer/Themer.js | 56 ++ src/styles/themer/ThemerContext.d.ts | 9 + src/styles/themer/ThemerContext.js | 4 + src/styles/themer/ThemerProvider.js | 42 ++ src/styles/themer/__tests__/themer.spec.js | 515 +++++++++--------- src/styles/themer/__tests__/withTheme.spec.js | 2 + src/styles/themer/index.d.ts | 26 +- src/styles/themer/index.js | 61 +-- src/styles/themer/withTheme.js | 8 +- 14 files changed, 417 insertions(+), 346 deletions(-) create mode 100644 src/styles/themer/Themer.d.ts create mode 100644 src/styles/themer/Themer.js create mode 100644 src/styles/themer/ThemerContext.d.ts create mode 100644 src/styles/themer/ThemerContext.js create mode 100644 src/styles/themer/ThemerProvider.js diff --git a/src/__tests__/ensureExports.spec.js b/src/__tests__/ensureExports.spec.js index 2a936b03..d5149384 100644 --- a/src/__tests__/ensureExports.spec.js +++ b/src/__tests__/ensureExports.spec.js @@ -9,7 +9,9 @@ const expectedExports = [ 'GlobalTheme', 'Normalize', 'SetStyles', - 'themer', + 'Themer', + 'ThemerContext', + 'ThemerProvider', 'withTheme', 'themePropTypes', 'responsive', diff --git a/src/components/Link/__tests__/Link.spec.js b/src/components/Link/__tests__/Link.spec.js index cd2295a6..eba6674f 100644 --- a/src/components/Link/__tests__/Link.spec.js +++ b/src/components/Link/__tests__/Link.spec.js @@ -4,10 +4,14 @@ import { StyleRoot } from '@instacart/radium' import { mount } from 'enzyme' import { spy } from 'sinon' import Link from '../Link' -import themer from '../../../styles/themer' +import { Themer, ThemerProvider } from '../../../styles/themer' import { defaultTheme } from '../../../styles/themer/utils' describe('Link', () => { + let themer + beforeEach(() => { + themer = new Themer() + }) it('renders without throwing', () => { const tree = renderer .create( @@ -64,7 +68,9 @@ describe('Link', () => { it('re-renders when the active theme changes', () => { const wrapper = mount( - HI + + HI + ) diff --git a/src/index.d.ts b/src/index.d.ts index 64eab99b..9f041a0f 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -44,7 +44,7 @@ import MenuItem from './components/Menus/MenuItem' import MenuDivider from './components/Menus/MenuDivider' import DropdownMenu from './components/Menus/DropdownMenu' import zIndex from './styles/zIndex' -import themer from './styles/themer/index' +import { Themer, ThemerContext } from './styles/themer/index' import withTheme, { WithThemeInjectedProps } from './styles/themer/withTheme' import Slide from './components/Transitions/Slide' import Grow from './components/Transitions/Grow' @@ -101,7 +101,8 @@ export { Normalize, SetStyles, // theming - themer, + Themer, + ThemerContext, withTheme, WithThemeInjectedProps, themePropTypes, diff --git a/src/index.js b/src/index.js index e34fb59d..f1822d30 100644 --- a/src/index.js +++ b/src/index.js @@ -39,7 +39,7 @@ import MenuItem from './components/Menus/MenuItem' import MenuDivider from './components/Menus/MenuDivider' import DropdownMenu from './components/Menus/DropdownMenu' import zIndex from './styles/zIndex' -import themer from './styles/themer/index' +import { Themer, ThemerContext, ThemerProvider } from './styles/themer/index' import withTheme from './styles/themer/withTheme' import Slide from './components/Transitions/Slide' import Grow from './components/Transitions/Grow' @@ -57,7 +57,9 @@ export { Normalize, SetStyles, // theming - themer, + Themer, + ThemerContext, + ThemerProvider, withTheme, themePropTypes, // grid system diff --git a/src/styles/themer/Themer.d.ts b/src/styles/themer/Themer.d.ts new file mode 100644 index 00000000..3d301ec0 --- /dev/null +++ b/src/styles/themer/Themer.d.ts @@ -0,0 +1,15 @@ +import { Theme } from './utils' + +export declare class Themer { + themeConfig: Theme + + constructor() + + set( + section: TSection, + sectionKey: TSectionKey, + themeValue: string + ): void + + subscribe(listener: (themeConfig?: Theme) => void): () => void +} diff --git a/src/styles/themer/Themer.js b/src/styles/themer/Themer.js new file mode 100644 index 00000000..7908bd97 --- /dev/null +++ b/src/styles/themer/Themer.js @@ -0,0 +1,56 @@ +/* eslint-disable no-underscore-dangle */ +import { cleanConfig, defaultTheme, themeTemplate, validConfigValue } from './utils' + +export class Themer { + constructor() { + this._themeConfig = defaultTheme + this._onChangeListeners = [] + } + + _callListeners() { + this._onChangeListeners.forEach(listener => { + listener(this._themeConfig) + }) + } + + get themeConfig() { + return this._themeConfig + } + + set themeConfig(themeConfig) { + this._themeConfig = cleanConfig(themeConfig) + this._callListeners() + } + + get(section, sectionKey) { + if (!this._themeConfig) { + console.warn( + 'Snacks theme error: No themeConfig defined. Please use Themer template: ', + themeTemplate + ) + } else if (validConfigValue(section, sectionKey)) { + return this._themeConfig[section][sectionKey] + } + } + + set(section, sectionKey, themeValue) { + if (validConfigValue(section, sectionKey)) { + this._themeConfig[section][sectionKey] = themeValue + this._callListeners() + } + } + + subscribe(listener) { + this._onChangeListeners.push(listener) + + const unsubscribe = () => { + const index = this._onChangeListeners.indexOf(listener) + if (index === -1) { + return + } + this._onChangeListeners.splice(index, 1) + } + + return unsubscribe + } +} diff --git a/src/styles/themer/ThemerContext.d.ts b/src/styles/themer/ThemerContext.d.ts new file mode 100644 index 00000000..b297ae70 --- /dev/null +++ b/src/styles/themer/ThemerContext.d.ts @@ -0,0 +1,9 @@ +import { Context } from 'react' +import { Themer } from './Themer' + +declare interface ThemerContextInterface { + tick: number // for propogating theme updates through react + themer: Themer +} + +export declare const ThemerContext: Context diff --git a/src/styles/themer/ThemerContext.js b/src/styles/themer/ThemerContext.js new file mode 100644 index 00000000..99a74edd --- /dev/null +++ b/src/styles/themer/ThemerContext.js @@ -0,0 +1,4 @@ +import { createContext } from 'react' +import { Themer } from './Themer' + +export const ThemerContext = createContext({ themer: new Themer(), tick: 0 }) diff --git a/src/styles/themer/ThemerProvider.js b/src/styles/themer/ThemerProvider.js new file mode 100644 index 00000000..52aa889e --- /dev/null +++ b/src/styles/themer/ThemerProvider.js @@ -0,0 +1,42 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Themer } from './Themer' +import { ThemerContext } from './ThemerContext' + +export class ThemerProvider extends React.Component { + static propTypes = { + themer: PropTypes.instanceOf(Themer), + children: PropTypes.node, + } + + static contextType = ThemerContext + + state = { + tick: 0, + } + + componentDidMount() { + this.unsubscribe = this.context.themer.subscribe(this.onThemeChange) + } + + componentWillUnmount() { + this.unsubscribe() + } + + onThemeChange = () => { + const { tick } = this.state + // increment tick to force an update on the context + this.setState({ tick: tick + 1 }) + } + + render() { + const { themer, children } = this.props + const { tick } = this.state + return {children} + } +} + +ThemerProvider.propTypes = { + themer: PropTypes.object, + children: PropTypes.node, +} diff --git a/src/styles/themer/__tests__/themer.spec.js b/src/styles/themer/__tests__/themer.spec.js index 3d3ef0ef..d297acec 100644 --- a/src/styles/themer/__tests__/themer.spec.js +++ b/src/styles/themer/__tests__/themer.spec.js @@ -1,298 +1,305 @@ import { spy } from 'sinon' -import themer from '../index' +import { Themer } from '../Themer' import { defaultTheme, themeTemplate } from '../utils' -it('should instantiate with default theme', () => { - expect(themer.themeConfig).toEqual(defaultTheme) -}) - -it('should warn when no config is set', () => { - const oldWarn = console.warn - console.warn = spy() - themer._themeConfig = undefined // NOTE: never actually do this - themer.get('colors', 'action') - expect( - console.warn.calledWith( - `Snacks theme error: No themeConfig defined. Please use Themer template: `, - themeTemplate - ) - ).toBeTruthy() - - console.warn = oldWarn -}) - -it('should set new, partial config', () => { - themer.themeConfig = { - colors: { - action: '#4a4gsa', - primaryBackground: '#4a4gsa', - primaryForeground: '#fff', - }, - } - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#4a4gsa', - primaryBackground: '#4a4gsa', - primaryForeground: '#fff', - }, +describe('themer', () => { + let themer + beforeEach(() => { + themer = new Themer() }) -}) -it('should set new, full config', () => { - themer.themeConfig = { - colors: { - action: '#ccc', - primaryBackground: '#ededed', - primaryForeground: '#eee', - secondaryBackground: '#fff', - secondaryForeground: '#000', - }, - } - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#ccc', - primaryBackground: '#ededed', - primaryForeground: '#eee', - secondaryBackground: '#fff', - secondaryForeground: '#000', - }, + it('should instantiate with default theme', () => { + expect(themer.themeConfig).toEqual(defaultTheme) }) -}) -it('should warn and clean config on full config set', () => { - const oldWarn = console.warn - console.warn = spy() - - themer.themeConfig = { - colors: { - action: '#ccc', - primaryBackground: '#ededed', - primaryForeground: '#eee', - secondaryBackground: '#fff', - secondaryForeground: '#000', - madness: 'pure madness', - }, - stuff: { - whatever: 'chaos', - }, - } - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#ccc', - primaryBackground: '#ededed', - primaryForeground: '#eee', - secondaryBackground: '#fff', - secondaryForeground: '#000', - }, + it('should warn when no config is set', () => { + const oldWarn = console.warn + console.warn = spy() + themer._themeConfig = undefined // NOTE: never actually do this + themer.get('colors', 'action') + expect( + console.warn.calledWith( + `Snacks theme error: No themeConfig defined. Please use Themer template: `, + themeTemplate + ) + ).toBeTruthy() + + console.warn = oldWarn }) - expect(console.warn.calledTwice).toBeTruthy() - expect( - console.warn.calledWith( - 'Snacks theme error: "madness" not a valid config key. This value will not be set. Please use Themer template: ', - themeTemplate - ) - ).toBeTruthy() - expect( - console.warn.calledWith( - 'Snacks theme error: "stuff" not a valid config section. These values will not be set. Please use Themer template: ', - themeTemplate - ) - ).toBeTruthy() - - console.warn = oldWarn -}) - -it('should set specifc value in config', () => { - themer.themeConfig = { - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, - } - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, + it('should set new, partial config', () => { + themer.themeConfig = { + colors: { + action: '#4a4gsa', + primaryBackground: '#4a4gsa', + primaryForeground: '#fff', + }, + } + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#4a4gsa', + primaryBackground: '#4a4gsa', + primaryForeground: '#fff', + }, + }) }) - themer.set('colors', 'action', '#fff') - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#fff', - primaryBackground: '#sg444h', - }, + it('should set new, full config', () => { + themer.themeConfig = { + colors: { + action: '#ccc', + primaryBackground: '#ededed', + primaryForeground: '#eee', + secondaryBackground: '#fff', + secondaryForeground: '#000', + }, + } + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#ccc', + primaryBackground: '#ededed', + primaryForeground: '#eee', + secondaryBackground: '#fff', + secondaryForeground: '#000', + }, + }) }) - themer.set('colors', 'primaryForeground', '#eee') - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#fff', - primaryBackground: '#sg444h', - primaryForeground: '#eee', - }, + it('should warn and clean config on full config set', () => { + const oldWarn = console.warn + console.warn = spy() + + themer.themeConfig = { + colors: { + action: '#ccc', + primaryBackground: '#ededed', + primaryForeground: '#eee', + secondaryBackground: '#fff', + secondaryForeground: '#000', + madness: 'pure madness', + }, + stuff: { + whatever: 'chaos', + }, + } + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#ccc', + primaryBackground: '#ededed', + primaryForeground: '#eee', + secondaryBackground: '#fff', + secondaryForeground: '#000', + }, + }) + + expect(console.warn.calledTwice).toBeTruthy() + expect( + console.warn.calledWith( + 'Snacks theme error: "madness" not a valid config key. This value will not be set. Please use Themer template: ', + themeTemplate + ) + ).toBeTruthy() + expect( + console.warn.calledWith( + 'Snacks theme error: "stuff" not a valid config section. These values will not be set. Please use Themer template: ', + themeTemplate + ) + ).toBeTruthy() + + console.warn = oldWarn }) -}) -it('should not set bad value in config and warn', () => { - const oldWarn = console.warn - console.warn = spy() - - themer.themeConfig = { - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, - } - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, + it('should set specifc value in config', () => { + themer.themeConfig = { + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + } + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + }) + + themer.set('colors', 'action', '#fff') + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#fff', + primaryBackground: '#sg444h', + }, + }) + + themer.set('colors', 'primaryForeground', '#eee') + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#fff', + primaryBackground: '#sg444h', + primaryForeground: '#eee', + }, + }) }) - themer.set('colors', 'madness', 'pure madness') - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, + it('should not set bad value in config and warn', () => { + const oldWarn = console.warn + console.warn = spy() + + themer.themeConfig = { + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + } + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + }) + + themer.set('colors', 'madness', 'pure madness') + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + }) + + expect( + console.warn.calledWith( + 'Snacks theme error: "madness" not a valid config key. This value will not be set. Please use Themer template: ', + themeTemplate + ) + ).toBeTruthy() + + themer.set('stuff', 'whatever', 'chaos') + + expect(themer.themeConfig).toEqual({ + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + }) + + expect( + console.warn.calledWith( + 'Snacks theme error: "stuff" not a valid config section. These values will not be set. Please use Themer template: ', + themeTemplate + ) + ).toBeTruthy() + + console.warn = oldWarn }) - expect( - console.warn.calledWith( - 'Snacks theme error: "madness" not a valid config key. This value will not be set. Please use Themer template: ', - themeTemplate - ) - ).toBeTruthy() + it('should get specifc value in config', () => { + themer.themeConfig = { + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + } - themer.set('stuff', 'whatever', 'chaos') - - expect(themer.themeConfig).toEqual({ - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, + expect(themer.get('colors', 'action')).toEqual('#4a4gsa') }) - expect( - console.warn.calledWith( - 'Snacks theme error: "stuff" not a valid config section. These values will not be set. Please use Themer template: ', - themeTemplate - ) - ).toBeTruthy() - - console.warn = oldWarn -}) - -it('should get specifc value in config', () => { - themer.themeConfig = { - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, - } + it('should not get bad value in config', () => { + const oldWarn = console.warn + console.warn = spy() + + themer.themeConfig = { + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + } + + expect(themer.get('stuff', 'whatever')).toEqual(undefined) + expect( + console.warn.calledWith( + 'Snacks theme error: "stuff" not a valid config section. These values will not be set. Please use Themer template: ', + themeTemplate + ) + ).toBeTruthy() + + console.warn = oldWarn + }) - expect(themer.get('colors', 'action')).toEqual('#4a4gsa') -}) + it('should add listener to list', () => { + const listener = theme => {} + themer.subscribe(listener) -it('should not get bad value in config', () => { - const oldWarn = console.warn - console.warn = spy() - - themer.themeConfig = { - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, - } - - expect(themer.get('stuff', 'whatever')).toEqual(undefined) - expect( - console.warn.calledWith( - 'Snacks theme error: "stuff" not a valid config section. These values will not be set. Please use Themer template: ', - themeTemplate - ) - ).toBeTruthy() - - console.warn = oldWarn -}) - -it('should add listener to list', () => { - const listener = theme => {} - themer.subscribe(listener) + expect(themer._onChangeListeners.length).toEqual(1) + expect(themer._onChangeListeners[0]).toEqual(listener) + }) - expect(themer._onChangeListeners.length).toEqual(1) - expect(themer._onChangeListeners[0]).toEqual(listener) -}) + it('should remove listener from list', () => { + themer._onChangeListeners.length = 0 -it('should remove listener from list', () => { - themer._onChangeListeners.length = 0 + const listener = theme => {} + const unsubscribe = themer.subscribe(listener) - const listener = theme => {} - const unsubscribe = themer.subscribe(listener) + expect(themer._onChangeListeners.length).toEqual(1) + expect(themer._onChangeListeners[0]).toEqual(listener) - expect(themer._onChangeListeners.length).toEqual(1) - expect(themer._onChangeListeners[0]).toEqual(listener) + unsubscribe() - unsubscribe() + expect(themer._onChangeListeners.includes(listener)).toBeFalsy() + expect(themer._onChangeListeners.length).toEqual(0) + }) - expect(themer._onChangeListeners.includes(listener)).toBeFalsy() - expect(themer._onChangeListeners.length).toEqual(0) -}) + it('should call listener callback on theme config change', () => { + themer._onChangeListeners.length = 0 -it('should call listener callback on theme config change', () => { - themer._onChangeListeners.length = 0 + const listener = spy() + const unsubscribe = themer.subscribe(listener) - const listener = spy() - const unsubscribe = themer.subscribe(listener) + themer.themeConfig = { + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + } - themer.themeConfig = { - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, - } + expect(themer.themeConfig).toEqual({ + colors: { + action: '#4a4gsa', + primaryBackground: '#sg444h', + }, + }) - expect(themer.themeConfig).toEqual({ - colors: { - action: '#4a4gsa', - primaryBackground: '#sg444h', - }, + expect(listener.calledWith(themer.themerConfig)) + expect(listener.calledOnce).toBeTruthy() }) - expect(listener.calledWith(themer.themerConfig)) - expect(listener.calledOnce).toBeTruthy() -}) - -it('should call listener callback on theme config set call', () => { - themer._onChangeListeners.length = 0 + it('should call listener callback on theme config set call', () => { + themer._onChangeListeners.length = 0 - const listener = spy() - const unsubscribe = themer.subscribe(listener) + const listener = spy() + const unsubscribe = themer.subscribe(listener) - themer.set('colors', 'action', '#fff') + themer.set('colors', 'action', '#fff') - expect(listener.calledWith(themer.themerConfig)) - expect(listener.calledOnce).toBeTruthy() -}) + expect(listener.calledWith(themer.themerConfig)) + expect(listener.calledOnce).toBeTruthy() + }) -it('should handle bad listener array remove', () => { - themer._onChangeListeners.length = 0 + it('should handle bad listener array remove', () => { + themer._onChangeListeners.length = 0 - const listener = spy() - const unsubscribe = themer.subscribe(listener) + const listener = spy() + const unsubscribe = themer.subscribe(listener) - themer._onChangeListeners.length = 0 // oh no! + themer._onChangeListeners.length = 0 // oh no! - unsubscribe() // should not cause an error + unsubscribe() // should not cause an error + }) }) diff --git a/src/styles/themer/__tests__/withTheme.spec.js b/src/styles/themer/__tests__/withTheme.spec.js index f6d6e17f..bf5892c0 100644 --- a/src/styles/themer/__tests__/withTheme.spec.js +++ b/src/styles/themer/__tests__/withTheme.spec.js @@ -35,6 +35,7 @@ describe('while in production mode', () => { jest.resetModules() process.env.NODE_ENV = 'production' + // eslint-disable-next-line global-require TestComponent = require('../withTheme').default(Test) }) @@ -59,6 +60,7 @@ describe('while in production mode', () => { }) it('falls back to active themer theme if props are invalid', () => { + // eslint-disable-next-line array-callback-return ;[null, undefined].map(invalidTheme => { const tree = renderer.create().toJSON() expect(tree.props.style.backgroundColor).toBe(defaultTheme.colors.primaryBackground) diff --git a/src/styles/themer/index.d.ts b/src/styles/themer/index.d.ts index a641f856..a0aa1190 100644 --- a/src/styles/themer/index.d.ts +++ b/src/styles/themer/index.d.ts @@ -1,24 +1,2 @@ -import { Theme } from './utils' - -declare class Themer { - themeConfig: Theme - - constructor() - - get( - section: TSection, - sectionKey: TSectionKey - ): Theme[TSection][TSectionKey] - - set( - section: TSection, - sectionKey: TSectionKey, - themeValue: string - ): void - - subscribe(listener: (themeConfig?: Theme) => void): () => void -} - -declare const themer: Themer - -export default themer +export { Themer } from './Themer' +export { ThemerContext } from './ThemerContext' diff --git a/src/styles/themer/index.js b/src/styles/themer/index.js index c669ba83..484f05ee 100644 --- a/src/styles/themer/index.js +++ b/src/styles/themer/index.js @@ -1,58 +1,3 @@ -/* eslint-disable no-underscore-dangle */ -import { cleanConfig, defaultTheme, themeTemplate, validConfigValue } from './utils' - -class Themer { - constructor() { - this._themeConfig = defaultTheme - this._onChangeListeners = [] - } - - _callListeners() { - this._onChangeListeners.forEach(listener => { - listener(this._themeConfig) - }) - } - - get themeConfig() { - return this._themeConfig - } - - set themeConfig(themeConfig) { - this._themeConfig = cleanConfig(themeConfig) - this._callListeners() - } - - get(section, sectionKey) { - if (!this._themeConfig) { - console.warn( - 'Snacks theme error: No themeConfig defined. Please use Themer template: ', - themeTemplate - ) - } else if (validConfigValue(section, sectionKey)) { - return this._themeConfig[section][sectionKey] - } - } - - set(section, sectionKey, themeValue) { - if (validConfigValue(section, sectionKey)) { - this._themeConfig[section][sectionKey] = themeValue - this._callListeners() - } - } - - subscribe(listener) { - this._onChangeListeners.push(listener) - - const unsubscribe = () => { - const index = this._onChangeListeners.indexOf(listener) - if (index === -1) { - return - } - this._onChangeListeners.splice(index, 1) - } - - return unsubscribe - } -} - -export default new Themer() +export { Themer } from './Themer' +export { ThemerContext } from './ThemerContext' +export { ThemerProvider } from './ThemerProvider' diff --git a/src/styles/themer/withTheme.js b/src/styles/themer/withTheme.js index ac3e4085..d5f3b0ea 100644 --- a/src/styles/themer/withTheme.js +++ b/src/styles/themer/withTheme.js @@ -3,7 +3,7 @@ import React, { Component } from 'react' import { isValidElementType } from 'react-is' import PropTypes from 'prop-types' -import themer from './index' +import { ThemerContext } from './ThemerContext' import { cleanConfig, themePropTypes } from './utils' function withTheme(options = {}) { @@ -27,8 +27,10 @@ function withTheme(options = {}) { snacksTheme: themePropTypes, } + static contextType = ThemerContext + componentDidMount() { - this.unsubscribe = themer.subscribe(this.onThemeChange) + this.unsubscribe = this.context.themer.subscribe(this.onThemeChange) this.validateSnacksTheme() } @@ -60,7 +62,7 @@ function withTheme(options = {}) { render() { const { snacksTheme, forwardedRef, ...rest } = this.props - const theme = this.themeIsValid() ? snacksTheme : themer.themeConfig + const theme = this.themeIsValid() ? snacksTheme : this.context.themer.themeConfig return } From 92c5dd12d8cf36d911bb2d73ed61b2ba142fd965 Mon Sep 17 00:00:00 2001 From: Derek Persons Date: Fri, 26 Mar 2021 07:28:50 -0700 Subject: [PATCH 2/2] remove tick and add types for ThemeProvider --- src/index.d.ts | 3 ++- src/styles/themer/ThemerProvider.d.ts | 10 ++++++++++ src/styles/themer/ThemerProvider.js | 5 +++-- src/styles/themer/index.d.ts | 1 + 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 src/styles/themer/ThemerProvider.d.ts diff --git a/src/index.d.ts b/src/index.d.ts index 9f041a0f..95b5791b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -44,7 +44,7 @@ import MenuItem from './components/Menus/MenuItem' import MenuDivider from './components/Menus/MenuDivider' import DropdownMenu from './components/Menus/DropdownMenu' import zIndex from './styles/zIndex' -import { Themer, ThemerContext } from './styles/themer/index' +import { Themer, ThemerContext, ThemerProvider } from './styles/themer/index' import withTheme, { WithThemeInjectedProps } from './styles/themer/withTheme' import Slide from './components/Transitions/Slide' import Grow from './components/Transitions/Grow' @@ -103,6 +103,7 @@ export { // theming Themer, ThemerContext, + ThemerProvider, withTheme, WithThemeInjectedProps, themePropTypes, diff --git a/src/styles/themer/ThemerProvider.d.ts b/src/styles/themer/ThemerProvider.d.ts new file mode 100644 index 00000000..217f0286 --- /dev/null +++ b/src/styles/themer/ThemerProvider.d.ts @@ -0,0 +1,10 @@ +import { Context } from 'react' +import { Themer } from './Themer' +import * as React from 'react' + +export interface ThemerProviderProps { + children?: React.ReactNode + themer: Themer +} + +export declare const ThemerProvider: React.ComponentType diff --git a/src/styles/themer/ThemerProvider.js b/src/styles/themer/ThemerProvider.js index 52aa889e..c66c7211 100644 --- a/src/styles/themer/ThemerProvider.js +++ b/src/styles/themer/ThemerProvider.js @@ -31,8 +31,9 @@ export class ThemerProvider extends React.Component { render() { const { themer, children } = this.props - const { tick } = this.state - return {children} + // this creates a new reference every time render gets called + const themerContext = { themer } + return {children} } } diff --git a/src/styles/themer/index.d.ts b/src/styles/themer/index.d.ts index a0aa1190..484f05ee 100644 --- a/src/styles/themer/index.d.ts +++ b/src/styles/themer/index.d.ts @@ -1,2 +1,3 @@ export { Themer } from './Themer' export { ThemerContext } from './ThemerContext' +export { ThemerProvider } from './ThemerProvider'