feat(apollo-wind): add shadow DOM CSS injection utilities#830
feat(apollo-wind): add shadow DOM CSS injection utilities#830david-rios-uipath wants to merge 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Dependency License Review
License distribution
Excluded packages
|
There was a problem hiding this comment.
⚠️ Not ready to approve
The new helper claims idempotency but the unit tests don’t currently assert the idempotent behavior, leaving a key behavior unverified.
Pull request overview
Adds a new @uipath/apollo-wind utility to support Tailwind v4 inside Shadow DOM by extracting @property at-rules from a CSS string and registering them globally via a single marker <style> in document.head.
Changes:
- Introduces
registerCssPropertyRules(css)to extract Tailwind@propertyrules and inject them once at the document level. - Adds unit tests verifying extraction and filtering behavior.
- Exports the new utility from both the internal lib barrel and the public package entry point.
File summaries
| File | Description |
|---|---|
| packages/apollo-wind/src/lib/register-shadow-dom-properties.ts | Implements registerCssPropertyRules with regex-based extraction and idempotent DOM marker injection. |
| packages/apollo-wind/src/lib/register-shadow-dom-properties.test.ts | Adds tests for extraction, filtering, and (needs adjustment) idempotency coverage. |
| packages/apollo-wind/src/lib/index.ts | Re-exports the helper from the lib barrel. |
| packages/apollo-wind/src/index.ts | Re-exports the helper from the public API. |
Copilot's findings
- Files reviewed: 4/4 changed files
- Comments generated: 2
Note
Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.
| it('does nothing when CSS has no @property rules', () => { | ||
| registerCssPropertyRules('.flex { display: flex; }'); | ||
|
|
||
| expect(document.querySelector(SELECTOR)).toBeNull(); | ||
| }); | ||
| }); |
📦 Dev Packages
|
📊 Coverage + size by packagePer-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.
"Coverage" is each package's own |
9d18f76 to
074dd34
Compare
Browsers silently ignore @Property rules inside shadow DOM <style> elements, breaking Tailwind v4 composable utilities (border, shadow, ring, translate). Every shadow DOM consumer was reimplementing the same inject + register dance. New exports: - injectTailwindIntoShadowRoot(root, css) — injects stylesheet + registers @Property rules - hasApolloWindCss(root) — two-level detection (tag check + computed-style probes) - TAILWIND_INJECT_ATTR — sentinel attribute constant
074dd34 to
93f0ca3
Compare
| export function hasApolloWindCss(root: Document | ShadowRoot): boolean { | ||
| if (root.querySelector(`style[${TAILWIND_INJECT_ATTR}]`)) { | ||
| return true; | ||
| } | ||
|
|
||
| const probeParent = root instanceof Document ? root.body : root; | ||
| const probe = document.createElement('div'); | ||
| probeParent.appendChild(probe); | ||
|
|
||
| try { | ||
| const computed = getComputedStyle(probe); | ||
| return PROBES.every((p) => { | ||
| probe.className = p.className; | ||
| return computed.getPropertyValue(p.property) === p.expected; | ||
| }); | ||
| } finally { | ||
| probe.remove(); | ||
| } | ||
| } |
| if (typeof document === 'undefined') return; | ||
| if (document.querySelector(`style[${MARKER}]`)) return; | ||
|
|
4948996 to
72a6254
Compare
| const probeParent = root instanceof Document ? root.body : root; | ||
| const probe = document.createElement('div'); | ||
| probeParent.appendChild(probe); | ||
|
|
||
| try { | ||
| const computed = getComputedStyle(probe); | ||
| return PROBES.every((p) => { | ||
| probe.className = p.className; | ||
| return computed.getPropertyValue(p.property) === p.expected; | ||
| }); |
| const style = document.createElement('style'); | ||
| style.setAttribute(TAILWIND_INJECT_ATTR, ''); | ||
| style.textContent = css; | ||
| root.prepend(style); | ||
| registerCssPropertyRules(css); | ||
| return style; |
| export function registerCssPropertyRules(css: string): void { | ||
| if (typeof document === 'undefined') return; | ||
| if (document.querySelector(`style[${MARKER}]`)) return; | ||
|
|
||
| const rules = extractPropertyRules(css); | ||
| if (rules.length === 0) return; | ||
|
|
||
| const style = document.createElement('style'); | ||
| style.setAttribute(MARKER, ''); | ||
| style.textContent = rules.join('\n'); | ||
| document.head.appendChild(style); | ||
| } |
There was a problem hiding this comment.
Both points are intentional:
-
Single-call idempotency: There's one CSS bundle (
tailwind.canvas.css). This function is@internal— onlyinjectTailwindIntoShadowRootcalls it, always with the same CSS. Merge logic would be dead code. -
Global
document: The consumer (TraceView web component) always runs in the host page's document, never in iframes. Adding an optionalDocumentparameter complicates the API for a nonexistent scenario.
0f0885d to
96d87ea
Compare
| const probeParent = root instanceof Document ? root.body : root; | ||
| const probe = document.createElement('div'); | ||
| probeParent.appendChild(probe); |
| export function registerCssPropertyRules(css: string): void { | ||
| if (typeof document === 'undefined') return; | ||
| if (document.querySelector(`style[${MARKER}]`)) return; | ||
|
|
||
| const rules = extractPropertyRules(css); | ||
| if (rules.length === 0) return; | ||
|
|
||
| const style = document.createElement('style'); | ||
| style.setAttribute(MARKER, ''); | ||
| style.textContent = rules.join('\n'); | ||
| document.head.appendChild(style); | ||
| } |
| afterEach(() => { | ||
| document.querySelectorAll(SELECTOR).forEach((el) => { el.remove(); }); | ||
| document.querySelectorAll(PROPERTY_SELECTOR).forEach((el) => { el.remove(); }); | ||
| document.querySelectorAll('div').forEach((el) => { el.parentNode?.removeChild(el); }); | ||
| }); |
| const probeParent = root instanceof Document ? root.body : root; | ||
| const probe = document.createElement('div'); | ||
| probeParent.appendChild(probe); |
| const computed = getComputedStyle(probe); | ||
| return PROBES.every((p) => { | ||
| probe.className = p.className; | ||
| return computed.getPropertyValue(p.property) === p.expected; | ||
| }); |
Summary
Adds reusable utilities for injecting apollo-wind Tailwind CSS into shadow DOM. Browsers silently ignore
@propertyrules inside shadow root<style>elements, breaking Tailwind v4 composable utilities (border,shadow-*,ring-*,translate-*). Every shadow DOM consumer was reimplementing the same inject + register dance — this PR consolidates it.See upstream discussion: tailwindlabs/tailwindcss#16772
Immediate consumer:
@uipath/traceview— migrating shared components from MUI to apollo-wind (Radix), which portal inside a shadow root.New exports
injectTailwindIntoShadowRoot(root, css)— injects a<style data-tailwind-inject>into the shadow root and registers@propertyrules at the document level. Idempotent. Returns the createdHTMLStyleElement(ornullif skipped) so callers can clean up on unmount.hasApolloWindCss(root)— two-level detection: fast-path tag check + computed-style probes (grid/flex/hidden/relative). Deliberately avoids probing custom properties (they inherit across the shadow boundary and would false-positive).TAILWIND_INJECT_ATTR— sentinel attribute constant (data-tailwind-inject).Usage
Changes
src/lib/register-shadow-dom-properties.ts— internal helper that extracts@propertyrules via regex and injects intodocument.head. Idempotent, SSR-safe.src/lib/shadow-dom-css.ts— the public API:injectTailwindIntoShadowRoot,hasApolloWindCss,TAILWIND_INJECT_ATTR. CallsregisterCssPropertyRulesinternally.src/lib/index.tsandsrc/index.ts— export the public API.Testing
pnpm --filter @uipath/apollo-wind testpasses (923 tests)pnpm --filter @uipath/apollo-wind build— new exports indist/index.d.ts🤖 Generated with Claude Code