Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/uniwind/src/components/native/useStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export const useStyle = (className: string | undefined, componentProps: Record<s

useLayoutEffect(() => {
if (__DEV__ || styleState.dependencies.length > 0) {
const dispose = UniwindListener.subscribe(rerender, styleState.dependencies)
const dispose = UniwindListener.subscribe(rerender, styleState.dependencies, {
shouldNotifyVariables: (theme) => theme === (uniwindContext.scopedTheme ?? UniwindStore.runtime.currentThemeName),
})

return dispose
}
Expand Down
4 changes: 1 addition & 3 deletions packages/uniwind/src/core/config/config.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase {
})
})

if (theme === this.currentTheme) {
UniwindListener.notify([StyleDependency.Variables])
}
UniwindListener.notifyVariables(theme)
}

updateInsets(insets: Insets) {
Expand Down
15 changes: 11 additions & 4 deletions packages/uniwind/src/core/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { StyleDependency } from '../../types'
import { UniwindListener } from '../listener'
import { Logger } from '../logger'
import { CSSVariables, ThemeName } from '../types'
Expand All @@ -11,6 +10,16 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase {
super()
}

getRuntimeCSSVariableValue(theme: ThemeName, varName: string): string | number | undefined {
const vars = this.runtimeCSSVariables.get(theme)

if (!vars || !Object.prototype.hasOwnProperty.call(vars, varName)) {
return undefined
}

return vars[varName]
}

updateCSSVariables(theme: ThemeName, variables: CSSVariables) {
Object.entries(variables).forEach(([varName, varValue]) => {
if (!varName.startsWith('--') && __DEV__) {
Expand All @@ -29,9 +38,7 @@ class UniwindConfigBuilder extends UniwindConfigBuilderBase {
}
})

if (theme === this.currentTheme) {
UniwindListener.notify([StyleDependency.Variables])
}
UniwindListener.notifyVariables(theme)
}

protected onThemeChange() {
Expand Down
17 changes: 17 additions & 0 deletions packages/uniwind/src/core/listener.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { StyleDependency } from '../types'
import { ThemeName } from './types'

type SubscribeOptions = {
once?: boolean
shouldNotifyVariables?: (theme: ThemeName) => boolean
}

class UniwindListenerBuilder {
private notifyingVariablesTheme: ThemeName | null = null
private listeners = {
[StyleDependency.ColorScheme]: new Set<() => void>(),
[StyleDependency.Theme]: new Set<() => void>(),
Expand All @@ -23,6 +26,12 @@ class UniwindListenerBuilder {
})
}

notifyVariables(theme: ThemeName) {
this.notifyingVariablesTheme = theme
this.listeners[StyleDependency.Variables].forEach(callback => callback())
this.notifyingVariablesTheme = null
}

notifyAll() {
Object.values(this.listeners).forEach(listenerSet => {
listenerSet.forEach(callback => callback())
Expand All @@ -41,6 +50,14 @@ class UniwindListenerBuilder {

dependencies.forEach(dep => {
const wrappedListener = () => {
if (dep === StyleDependency.Variables && this.notifyingVariablesTheme !== null) {
const shouldNotify = options?.shouldNotifyVariables?.(this.notifyingVariablesTheme) ?? true

if (!shouldNotify) {
return
}
}

listener()

if (options?.once) {
Expand Down
8 changes: 6 additions & 2 deletions packages/uniwind/src/core/native/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ class UniwindStoreBuilder {
}

const isScopedTheme = uniwindContext.scopedTheme !== null
const theme = uniwindContext.scopedTheme ?? this.runtime.currentThemeName
const cacheKey = `${className}${state?.isDisabled ?? false}${state?.isFocused ?? false}${state?.isPressed ?? false}${isScopedTheme}`
const cache = this.cache[uniwindContext.scopedTheme ?? this.runtime.currentThemeName]
const cache = this.cache[theme]

if (!cache) {
return emptyState
Expand All @@ -52,7 +53,10 @@ class UniwindStoreBuilder {
UniwindListener.subscribe(
() => cache.delete(cacheKey),
result.dependencies,
{ once: true },
{
once: true,
shouldNotifyVariables: (changedTheme) => changedTheme === theme,
},
)
}

Expand Down
12 changes: 12 additions & 0 deletions packages/uniwind/src/core/web/getWebStyles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { generateDataSet } from '../../components/web/generateDataSet'
import { Uniwind } from '../config/config'
import { RNStyle, UniwindContextType } from '../types'
import { CSSListener } from './cssListener'
import { parseCSSValue } from './parseCSSValue'
Expand Down Expand Up @@ -110,6 +111,17 @@ export const getWebVariable = (name: string, uniwindContext: UniwindContextType)
return undefined
}

const theme = uniwindContext.scopedTheme ?? Uniwind.currentTheme
const runtimeValue = Uniwind.getRuntimeCSSVariableValue(theme, name)

if (runtimeValue !== undefined) {
return parseCSSValue(
typeof runtimeValue === 'number'
? `${runtimeValue}px`
: runtimeValue,
)
}

if (uniwindContext.scopedTheme !== null) {
dummyParent.setAttribute('class', uniwindContext.scopedTheme)
} else {
Expand Down
14 changes: 10 additions & 4 deletions packages/uniwind/src/hoc/withUniwind.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const withUniwind: WithUniwind = <

const withAutoUniwind = (Component: Component<AnyObject>) => (props: AnyObject) => {
const uniwindContext = useUniwindContext()
const effectiveTheme = uniwindContext.scopedTheme ?? UniwindStore.runtime.currentThemeName
const { dependencies, generatedProps } = Object.entries(props).reduce((acc, [propName, propValue]) => {
if (isColorClassProperty(propName)) {
const colorProp = classToColor(propName)
Expand Down Expand Up @@ -73,10 +74,12 @@ const withAutoUniwind = (Component: Component<AnyObject>) => (props: AnyObject)
const [, rerender] = useReducer(() => ({}), {})

useLayoutEffect(() => {
const dispose = UniwindListener.subscribe(rerender, Array.from(new Set(dependencies)))
const dispose = UniwindListener.subscribe(rerender, Array.from(new Set(dependencies)), {
shouldNotifyVariables: (theme) => theme === effectiveTheme,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dispose
}, [dependencySum])
}, [dependencySum, effectiveTheme])

return (
<Component
Expand All @@ -88,6 +91,7 @@ const withAutoUniwind = (Component: Component<AnyObject>) => (props: AnyObject)

const withManualUniwind = (Component: Component<AnyObject>, options: Record<PropertyKey, OptionMapping>) => (props: AnyObject) => {
const uniwindContext = useUniwindContext()
const effectiveTheme = uniwindContext.scopedTheme ?? UniwindStore.runtime.currentThemeName
const { generatedProps, dependencies } = Object.entries(options).reduce((acc, [propName, option]) => {
const className = props[option.fromClassName]

Expand Down Expand Up @@ -139,10 +143,12 @@ const withManualUniwind = (Component: Component<AnyObject>, options: Record<Prop
const [, rerender] = useReducer(() => ({}), {})

useLayoutEffect(() => {
const dispose = UniwindListener.subscribe(rerender, Array.from(new Set(dependencies)))
const dispose = UniwindListener.subscribe(rerender, Array.from(new Set(dependencies)), {
shouldNotifyVariables: (theme) => theme === effectiveTheme,
})

return dispose
}, [dependencySum])
}, [dependencySum, effectiveTheme])

return (
<Component
Expand Down
4 changes: 4 additions & 0 deletions packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useLayoutEffect, useRef, useState } from 'react'
import { Uniwind } from '../../core/config'
import { useUniwindContext } from '../../core/context'
import { UniwindListener } from '../../core/listener'
import { Logger } from '../../core/logger'
Expand Down Expand Up @@ -71,6 +72,9 @@ export const useCSSVariable: UseCSSVariable = (name: string | Array<string>) =>
const dispose = UniwindListener.subscribe(
updateValue,
[StyleDependency.Theme, StyleDependency.Variables],
{
shouldNotifyVariables: (theme) => theme === (uniwindContext.scopedTheme ?? Uniwind.currentTheme),
},
)

return dispose
Expand Down
7 changes: 5 additions & 2 deletions packages/uniwind/src/hooks/useResolveClassNames.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { UniwindStore } from '../core/native'

export const useResolveClassNames = (className: string) => {
const uniwindContext = useUniwindContext()
const effectiveTheme = uniwindContext.scopedTheme ?? UniwindStore.runtime.currentThemeName
const [uniwindState, recreate] = useReducer(
() => UniwindStore.getStyles(className, undefined, undefined, uniwindContext),
undefined,
Expand All @@ -19,11 +20,13 @@ export const useResolveClassNames = (className: string) => {

useLayoutEffect(() => {
if (uniwindState.dependencies.length > 0) {
const dispose = UniwindListener.subscribe(recreate, uniwindState.dependencies)
const dispose = UniwindListener.subscribe(recreate, uniwindState.dependencies, {
shouldNotifyVariables: (theme) => theme === effectiveTheme,
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dispose
}
}, [uniwindState.dependencySum, className])
}, [uniwindState.dependencySum, className, effectiveTheme])

return uniwindState.styles
}
41 changes: 41 additions & 0 deletions packages/uniwind/tests/native/components/scoped-theme.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,47 @@ describe('ScopedTheme', () => {
expect(nestedLightInDark).toHaveBeenLastCalledWith('#ffffff')
})

test('useCSSVariable rerenders only affected custom scoped theme', () => {
const base = jest.fn()
const nestedCustom = jest.fn()

const Component = (props: { test: jest.Mock }) => {
const backgroundColor = useCSSVariable('--color-background')

props.test(backgroundColor)

return null
}

act(() => {
Uniwind.updateCSSVariables('custom', { '--color-background': '#123456' })
})

renderUniwind(
<React.Fragment>
<Component test={base} />
<ScopedTheme theme="custom">
<Component test={nestedCustom} />
</ScopedTheme>
</React.Fragment>,
)

expect(Uniwind.currentTheme).toEqual('light')
expect(base).toHaveBeenCalledTimes(1)
expect(nestedCustom).toHaveBeenCalledTimes(1)
expect(base).toHaveBeenLastCalledWith('#ffffff')
expect(nestedCustom).toHaveBeenLastCalledWith('#123456')

act(() => {
Uniwind.updateCSSVariables('custom', { '--color-background': '#112233' })
})

expect(base).toHaveBeenCalledTimes(1)
expect(nestedCustom).toHaveBeenCalledTimes(2)
expect(base).toHaveBeenLastCalledWith('#ffffff')
expect(nestedCustom).toHaveBeenLastCalledWith('#112233')
})

test('useUniwind', () => {
const base = jest.fn()
const nestedDark = jest.fn()
Expand Down
4 changes: 2 additions & 2 deletions packages/uniwind/tests/setup.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ beforeAll(async () => {
cssPath,
debug: true,
platform: Platform.iOS,
themes: ['light', 'dark'],
themes: ['light', 'dark', 'custom'],
polyfills: undefined,
})

eval(
`const { Uniwind } = require('../src/core/config/config.native');
Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']);
Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark', 'custom']);
Uniwind.updateInsets({
top: ${SAFE_AREA_INSET_TOP},
left: 0,
Expand Down
58 changes: 58 additions & 0 deletions packages/uniwind/tests/web/scoped-theme-css-variables.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { act, render } from '@testing-library/react'
import * as React from 'react'
import { ScopedTheme } from '../../src/components/ScopedTheme/ScopedTheme'
import { Uniwind } from '../../src/core/config/config'
import { getWebVariable } from '../../src/core/web/getWebStyles'
import { useCSSVariable } from '../../src/hooks/useCSSVariable'

describe('Scoped theme + CSS variables (web)', () => {
afterEach(() => {
Uniwind.setTheme('light')
document.documentElement.removeAttribute('style')
})

test('getWebVariable reads runtime update for scoped theme while global theme stays light', () => {
Uniwind.setTheme('light')
Uniwind.updateCSSVariables('custom', { '--test-scoped-web-a': '#aabbcc' })

const value = getWebVariable('--test-scoped-web-a', { scopedTheme: 'custom' })

expect(value).toMatch(/^#aabbcc$/i)
})

test('useCSSVariable rerenders only affected custom scoped theme', () => {
const base = jest.fn()
const nestedScopedTestTheme = jest.fn()

const Component = (props: { test: jest.Mock }) => {
const v = useCSSVariable('--test-scoped-web-b')

props.test(v)

return null
}

Uniwind.setTheme('light')

render(
<React.Fragment>
<Component test={base} />
<ScopedTheme theme="custom">
<Component test={nestedScopedTestTheme} />
</ScopedTheme>
</React.Fragment>,
)

expect(Uniwind.currentTheme).toEqual('light')
expect(base).toHaveBeenCalledTimes(1)
expect(nestedScopedTestTheme).toHaveBeenCalledTimes(1)

act(() => {
Uniwind.updateCSSVariables('custom', { '--test-scoped-web-b': '#ddeeff' })
})

expect(base).toHaveBeenCalledTimes(1)
expect(nestedScopedTestTheme).toHaveBeenCalledTimes(2)
expect(nestedScopedTestTheme.mock.calls[1][0]).toMatch(/^#ddeeff$/i)
})
})