Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 6 additions & 2 deletions packages/uniwind/src/hoc/withUniwind.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ 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 === (uniwindContext.scopedTheme ?? UniwindStore.runtime.currentThemeName),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dispose
}, [dependencySum])
Expand Down Expand Up @@ -139,7 +141,9 @@ 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 === (uniwindContext.scopedTheme ?? UniwindStore.runtime.currentThemeName),
})

return dispose
}, [dependencySum])
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
4 changes: 3 additions & 1 deletion packages/uniwind/src/hooks/useResolveClassNames.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ 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 === (uniwindContext.scopedTheme ?? UniwindStore.runtime.currentThemeName),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dispose
}
Expand Down
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)
})
})