Skip to content

Commit

Permalink
rewrite themer to use a react context
Browse files Browse the repository at this point in the history
  • Loading branch information
NinjaBanjo committed Mar 25, 2021
1 parent 64a27f8 commit cf04e13
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 346 deletions.
4 changes: 3 additions & 1 deletion src/__tests__/ensureExports.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const expectedExports = [
'GlobalTheme',
'Normalize',
'SetStyles',
'themer',
'Themer',
'ThemerContext',
'ThemerProvider',
'withTheme',
'themePropTypes',
'responsive',
Expand Down
10 changes: 8 additions & 2 deletions src/components/Link/__tests__/Link.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -64,7 +68,9 @@ describe('Link', () => {
it('re-renders when the active theme changes', () => {
const wrapper = mount(
<StyleRoot>
<Link elementAttributes={{ 'aria-label': 'foo' }}>HI</Link>
<ThemerProvider themer={themer}>
<Link elementAttributes={{ 'aria-label': 'foo' }}>HI</Link>
</ThemerProvider>
</StyleRoot>
)

Expand Down
5 changes: 3 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -101,7 +101,8 @@ export {
Normalize,
SetStyles,
// theming
themer,
Themer,
ThemerContext,
withTheme,
WithThemeInjectedProps,
themePropTypes,
Expand Down
6 changes: 4 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -57,7 +57,9 @@ export {
Normalize,
SetStyles,
// theming
themer,
Themer,
ThemerContext,
ThemerProvider,
withTheme,
themePropTypes,
// grid system
Expand Down
15 changes: 15 additions & 0 deletions src/styles/themer/Themer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Theme } from './utils'

export declare class Themer {
themeConfig: Theme

constructor()

set<TSection extends keyof Theme, TSectionKey extends keyof Theme[TSection]>(
section: TSection,
sectionKey: TSectionKey,
themeValue: string
): void

subscribe(listener: (themeConfig?: Theme) => void): () => void
}
56 changes: 56 additions & 0 deletions src/styles/themer/Themer.js
Original file line number Diff line number Diff line change
@@ -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
}
}
9 changes: 9 additions & 0 deletions src/styles/themer/ThemerContext.d.ts
Original file line number Diff line number Diff line change
@@ -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<ThemerContextInterface>
4 changes: 4 additions & 0 deletions src/styles/themer/ThemerContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContext } from 'react'
import { Themer } from './Themer'

export const ThemerContext = createContext({ themer: new Themer(), tick: 0 })
42 changes: 42 additions & 0 deletions src/styles/themer/ThemerProvider.js
Original file line number Diff line number Diff line change
@@ -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 <ThemerContext.Provider value={{ themer, tick }}>{children}</ThemerContext.Provider>
}
}

ThemerProvider.propTypes = {
themer: PropTypes.object,
children: PropTypes.node,
}
Loading

0 comments on commit cf04e13

Please sign in to comment.