Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/apollo-wind/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Utilities
// -----------------------------------------------------------------------------
export { cn } from './lib/utils';
export { registerCssPropertyRules } from './lib/register-shadow-dom-properties';

// -----------------------------------------------------------------------------
// Layout Components
Expand Down
1 change: 1 addition & 0 deletions packages/apollo-wind/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { cn, get, deepEqual } from './utils';
export { registerCssPropertyRules } from './register-shadow-dom-properties';
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { registerCssPropertyRules } from './register-shadow-dom-properties';

const SELECTOR = 'style[data-tw-property-rules]';

const SAMPLE_CSS = `
@layer base { .border { border-style: var(--tw-border-style); border-width: 1px; } }
@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}
@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}
.bg-popover { background-color: var(--popover); }
`;

beforeEach(() => {
document.querySelectorAll(SELECTOR).forEach((el) => el.remove());
});

afterEach(() => {
document.querySelectorAll(SELECTOR).forEach((el) => el.remove());
});

describe('registerCssPropertyRules', () => {
it('extracts @property rules from CSS and injects them into document.head', () => {
registerCssPropertyRules(SAMPLE_CSS);

const style = document.querySelector(SELECTOR);
expect(style).not.toBeNull();

const text = style?.textContent ?? '';
expect(text).toContain('@property --tw-border-style');
expect(text).toContain('@property --tw-shadow');
expect(text).toContain('@property --tw-translate-x');
});

it('does not include non-@property rules', () => {
registerCssPropertyRules(SAMPLE_CSS);

const text = document.querySelector(SELECTOR)?.textContent ?? '';
expect(text).not.toContain('.border');
expect(text).not.toContain('.bg-popover');
expect(text).not.toContain('@layer');
});

it('injects only one style element when called multiple times', () => {
registerCssPropertyRules(SAMPLE_CSS);
registerCssPropertyRules(SAMPLE_CSS);
registerCssPropertyRules(SAMPLE_CSS);

const elements = document.querySelectorAll(SELECTOR);
expect(elements).toHaveLength(1);
});

it('does nothing when CSS has no @property rules', () => {
registerCssPropertyRules('.flex { display: flex; }');

expect(document.querySelector(SELECTOR)).toBeNull();
});
});
Comment on lines +53 to +58
50 changes: 50 additions & 0 deletions packages/apollo-wind/src/lib/register-shadow-dom-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Register Tailwind v4 `@property` CSS declarations at the document level.
*
* Tailwind v4 emits `@property` at-rules (e.g. `@property --tw-border-style`)
* that define initial values for intermediate custom properties used by
* composable utilities (border, shadow, ring, transform). Browsers register
* `@property` rules globally — but silently **ignore** them when they appear
* inside a shadow root's `<style>` element.
*
* This means any Tailwind utility that chains through `--tw-*` variables
* breaks inside shadow DOM: `border`, `shadow-*`, `ring-*`, `translate-*`,
* etc. produce no visible effect because the intermediate properties are
* unregistered and fall back to the CSS "guaranteed-invalid" value.
*
* This helper extracts `@property` declarations from a CSS string and injects
* them into `document.head` so they register at the global scope where shadow
* DOM can see them.
*
* @see https://github.com/tailwindlabs/tailwindcss/discussions/16772
*
* @example
* ```ts
* import { registerCssPropertyRules } from '@uipath/apollo-wind';
* import css from '@uipath/apollo-react/canvas/styles/tailwind.canvas.css?raw';
*
* // Call once at app startup — idempotent, safe to call multiple times.
* registerCssPropertyRules(css);
* ```
*/

const PROPERTY_RE = /@property\s+--[\w-]+\s*\{[^}]*\}/g;
const MARKER = 'data-tw-property-rules';

export function registerCssPropertyRules(css: string): void {
if (typeof document === 'undefined') return;
if (document.querySelector(`style[${MARKER}]`)) return;

const rules: string[] = [];
let match: RegExpExecArray | null;
PROPERTY_RE.lastIndex = 0;
while ((match = PROPERTY_RE.exec(css)) !== null) {

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings starting with '@property ---{' and with many repetitions of '@property ---{'.
rules.push(match[0]);
}
if (rules.length === 0) return;

const style = document.createElement('style');
style.setAttribute(MARKER, '');
style.textContent = rules.join('\n');
document.head.appendChild(style);
}
Loading