Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Web runtime:

- Web keeps styles in CSS and passes `{ $$css: true, tailwind: className }` through RNW style arrays.
- `getWebStyles` uses a hidden DOM element to compute style values when a JS value is needed, such as color extraction or `useResolveClassNames`.
- `CSSListener` tracks active CSS rules and media queries, then notifies subscribers when class-dependent media rules change.
- `CSSListener` tracks active CSS rules and media queries, then notifies subscribers when class-dependent media rules change. After scanning newly discovered stylesheets, it emits a variables notification so JS-resolved styles refresh when CSS arrives after module initialization, including Metro web development startup.
- `ScopedTheme` renders a `div` with the theme class and `display: contents` on web.
- `LayoutDirection` renders a contents-style wrapper with `direction`/`dir` semantics so RTL/LTR variants can be scoped to a subtree.
- `ScopedVariables` renders a `display: contents` wrapper and sets its variables as inline custom properties on that wrapper, so the real DOM cascade resolves `var(--name)` to the scoped value for every descendant (numbers become px). During JS reads (`getWebVariable` / `useResolveClassNames`) it also applies the variables to the hidden `dummyParent`, then clears them.
Expand Down
6 changes: 6 additions & 0 deletions packages/uniwind/src/core/web/cssListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class CSSListenerBuilder {
private initialize() {
this.pendingInitialization = undefined
this.pruneStaleRules()
let added = false

for (const sheet of Array.from(document.styleSheets)) {
// Skip already processed stylesheets
Expand All @@ -149,6 +150,11 @@ class CSSListenerBuilder {
this.processedStyleSheets.add(sheet)

this.addMediaQueriesDeep(rules)
added = true
}

if (added) {
UniwindListener.notify([StyleDependency.Variables])
Comment thread
Brentlok marked this conversation as resolved.
}
}

Expand Down
45 changes: 45 additions & 0 deletions packages/uniwind/tests/web/core/css-listener.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { afterEach, expect, test, vi } from 'vitest'
import { CSSListener } from '../../../src/core/web/cssListener'

afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
})

test('notifies class subscribers after discovering a stylesheet injected later', async () => {
vi.useFakeTimers()
vi.stubGlobal('requestIdleCallback', undefined)

const style = document.createElement('style')
style.textContent = '.accent-late { accent-color: rgb(255, 0, 0); }'

const listener = vi.fn(() => {
const rule = Array.from(CSSListener.activeRules).find(rule => rule.selectorText === '.accent-late')
return rule?.style.getPropertyValue('accent-color')
})
const dispose = CSSListener.subscribeToClassName('accent-late', listener)
Comment thread
Brentlok marked this conversation as resolved.

try {
document.head.appendChild(style)
await Promise.resolve()

expect(listener).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(50)

expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveLastReturnedWith('rgb(255, 0, 0)')

// A scan with no new stylesheet should not invalidate resolved styles.
const meta = document.createElement('meta')
document.head.appendChild(meta)
await Promise.resolve()
await vi.advanceTimersByTimeAsync(50)
expect(listener).toHaveBeenCalledTimes(1)
meta.remove()
} finally {
dispose()
style.remove()
await Promise.resolve()
await vi.advanceTimersByTimeAsync(50)
}
})
Loading