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
103 changes: 12 additions & 91 deletions packages/reactive-element/src/css-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,6 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

const NODE_MODE = false;

// Allows minifiers to rename references to globalThis
const global = globalThis;

/**
* Whether the current browser supports `adoptedStyleSheets`.
*/
export const supportsAdoptingStyleSheets: boolean =
global.ShadowRoot &&
(global.ShadyCSS === undefined || global.ShadyCSS.nativeShadow) &&
'adoptedStyleSheets' in Document.prototype &&
'replace' in CSSStyleSheet.prototype;

/**
* A CSSResult or native CSSStyleSheet.
*
Expand All @@ -36,8 +22,6 @@ export type CSSResultGroup = CSSResultOrNative | CSSResultArray;

const constructionToken = Symbol();

const cssTagCache = new WeakMap<TemplateStringsArray, CSSStyleSheet>();

/**
* A container for a string of CSS text, that may be used to create a CSSStyleSheet.
*
Expand All @@ -47,60 +31,36 @@ const cssTagCache = new WeakMap<TemplateStringsArray, CSSStyleSheet>();
*/
export class CSSResult {
// This property needs to remain unminified.
['_$cssResult$'] = true;
_$cssResult$ = true;
readonly cssText: string;
private _styleSheet?: CSSStyleSheet;
private _strings: TemplateStringsArray | undefined;

private constructor(
cssText: string,
strings: TemplateStringsArray | undefined,
safeToken: symbol
) {
private constructor(cssText: string, safeToken: symbol) {
if (safeToken !== constructionToken) {
throw new Error(
'CSSResult is not constructable. Use `unsafeCSS` or `css` instead.'
);
}
this.cssText = cssText;
this._strings = strings;
}

// This is a getter so that it's lazy. In practice, this means stylesheets
// are not created until the first element instance is made.
get styleSheet(): CSSStyleSheet | undefined {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be CSSStyleSheet, we can drop the | undefined here.

// If `supportsAdoptingStyleSheets` is true then we assume CSSStyleSheet is
// constructable.
let styleSheet = this._styleSheet;
const strings = this._strings;
if (supportsAdoptingStyleSheets && styleSheet === undefined) {
const cacheable = strings !== undefined && strings.length === 1;
if (cacheable) {
styleSheet = cssTagCache.get(strings);
}
if (styleSheet === undefined) {
(this._styleSheet = styleSheet = new CSSStyleSheet()).replaceSync(
this.cssText
);
if (cacheable) {
cssTagCache.set(strings, styleSheet);
}
}
if (this._styleSheet === undefined) {
(this._styleSheet = new CSSStyleSheet()).replaceSync(this.cssText);
}
return styleSheet;
return this._styleSheet;
}

toString(): string {
// TODO (justinfagnani): Do we need this?
return this.cssText;
}
}

type ConstructableCSSResult = CSSResult & {
new (
cssText: string,
strings: TemplateStringsArray | undefined,
safeToken: symbol
): CSSResult;
new (cssText: string, safeToken: symbol): CSSResult;
};

const textFromCSSResult = (value: CSSResultGroup | number) => {
Expand Down Expand Up @@ -128,7 +88,6 @@ const textFromCSSResult = (value: CSSResultGroup | number) => {
export const unsafeCSS = (value: unknown) =>
new (CSSResult as ConstructableCSSResult)(
typeof value === 'string' ? value : String(value),
undefined,
constructionToken
);

Expand All @@ -151,55 +110,17 @@ export const css = (
(acc, v, idx) => acc + textFromCSSResult(v) + strings[idx + 1],
strings[0]
);
return new (CSSResult as ConstructableCSSResult)(
cssText,
strings,
constructionToken
);
return new (CSSResult as ConstructableCSSResult)(cssText, constructionToken);
};

/**
* Applies the given styles to a `shadowRoot`. When Shadow DOM is
* available but `adoptedStyleSheets` is not, styles are appended to the
* `shadowRoot` to [mimic the native feature](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/adoptedStyleSheets).
* Note, when shimming is used, any styles that are subsequently placed into
* the shadowRoot should be placed *before* any shimmed adopted styles. This
* will match spec behavior that gives adopted sheets precedence over styles in
* shadowRoot.
* Applies the given styles to a `shadowRoot`.
*/
export const adoptStyles = (
renderRoot: ShadowRoot,
styles: Array<CSSResultOrNative>
) => {
if (supportsAdoptingStyleSheets) {
(renderRoot as ShadowRoot).adoptedStyleSheets = styles.map((s) =>
s instanceof CSSStyleSheet ? s : s.styleSheet!
);
} else {
for (const s of styles) {
const style = document.createElement('style');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nonce = (global as any)['litNonce'];
if (nonce !== undefined) {
style.setAttribute('nonce', nonce);
}
style.textContent = (s as CSSResult).cssText;
renderRoot.appendChild(style);
}
}
};

const cssResultFromStyleSheet = (sheet: CSSStyleSheet) => {
let cssText = '';
for (const rule of sheet.cssRules) {
cssText += rule.cssText;
}
return unsafeCSS(cssText);
renderRoot.adoptedStyleSheets = styles.map((s) =>
s instanceof CSSStyleSheet ? s : s.styleSheet!
);
};

export const getCompatibleStyle =
supportsAdoptingStyleSheets ||
(NODE_MODE && global.CSSStyleSheet === undefined)
? (s: CSSResultOrNative) => s
: (s: CSSResultOrNative) =>
s instanceof CSSStyleSheet ? cssResultFromStyleSheet(s) : s;
19 changes: 9 additions & 10 deletions packages/reactive-element/src/reactive-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@
* @packageDocumentation
*/

import {
getCompatibleStyle,
adoptStyles,
CSSResultGroup,
CSSResultOrNative,
} from './css-tag.js';
import {adoptStyles, CSSResultGroup, CSSResultOrNative} from './css-tag.js';
import type {
ReactiveController,
ReactiveControllerHost,
Expand Down Expand Up @@ -945,18 +940,22 @@ export abstract class ReactiveElement
protected static finalizeStyles(
styles?: CSSResultGroup
): Array<CSSResultOrNative> {
const elementStyles = [];
const elementStyles: Array<CSSResultOrNative> = [];
if (Array.isArray(styles)) {
// Dedupe the flattened array in reverse order to preserve the last items.
// Casting to Array<unknown> works around TS error that
// appears to come from trying to flatten a type CSSResultArray.
const set = new Set((styles as Array<unknown>).flat(Infinity).reverse());
const set = new Set(
(styles as Array<unknown>)
.flat(Infinity)
.reverse() as Array<CSSResultOrNative>
);
// Then preserve original order by adding the set items in reverse order.
for (const s of set) {
elementStyles.unshift(getCompatibleStyle(s as CSSResultOrNative));
elementStyles.unshift(s);
}
} else if (styles !== undefined) {
elementStyles.push(getCompatibleStyle(styles));
elementStyles.push(styles);
}
return elementStyles;
}
Expand Down
50 changes: 5 additions & 45 deletions packages/reactive-element/src/test/css-tag_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,18 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import {
css,
CSSResult,
unsafeCSS,
supportsAdoptingStyleSheets,
} from '@lit/reactive-element/css-tag.js';
import {css, CSSResult, unsafeCSS} from '@lit/reactive-element/css-tag.js';
import {assert} from 'chai';

suite('Styling', () => {
suite('css tag', () => {
test('stylesheet from same template literal without expressions are cached', () => {
// Alias avoids syntax highlighting issues in editors
const cssValue = css;
const makeStyle = () => cssValue`foo`;
const style1 = makeStyle();
if (supportsAdoptingStyleSheets) {
assert.isDefined(style1.styleSheet);
assert.strictEqual(style1.styleSheet, style1.styleSheet);
const style2 = makeStyle();
// Equal because we cache stylesheets based on TemplateStringArrays
assert.strictEqual(style1.styleSheet, style2.styleSheet);
} else {
assert.isUndefined(style1.styleSheet);
}
});

test('stylesheet from same template literal with expressions are not cached', () => {
// Alias avoids syntax highlighting issues in editors
const cssValue = css;
const makeStyle = () => cssValue`background: ${cssValue`blue`}`;
const style1 = makeStyle();
if (supportsAdoptingStyleSheets) {
assert.isDefined(style1.styleSheet);
assert.strictEqual(style1.styleSheet, style1.styleSheet);
const style2 = makeStyle();
assert.notStrictEqual(style1.styleSheet, style2.styleSheet);
} else {
assert.isUndefined(style1.styleSheet);
}
});

test('unsafeCSS() always produces a new stylesheet', () => {
const makeStyle = () => unsafeCSS(`foo`);
const style1 = makeStyle();
if (supportsAdoptingStyleSheets) {
assert.isDefined(style1.styleSheet);
assert.strictEqual(style1.styleSheet, style1.styleSheet);
const style2 = makeStyle();
assert.notStrictEqual(style1.styleSheet, style2.styleSheet);
} else {
assert.isUndefined(style1.styleSheet);
}
assert.isDefined(style1.styleSheet);
assert.strictEqual(style1.styleSheet, style1.styleSheet);
const style2 = makeStyle();
assert.notStrictEqual(style1.styleSheet, style2.styleSheet);
});

test('`css` get styles throws when unsafe values are used', async () => {
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-size.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as fs from 'fs';
// it's likely that we'll ask you to investigate ways to reduce the size.
//
// In either case, update the size here and push a new commit to your PR.
const expectedLitCoreSize = 15734;
const expectedLitCoreSize = 15084;
const expectedLitHtmlSize = 7309;

const litCoreSrc = fs.readFileSync('packages/lit/lit-core.min.js', 'utf8');
Expand Down
Loading