Skip to content

Commit

Permalink
Merge pull request #853 from samthor/native-cssstylesheet
Browse files Browse the repository at this point in the history
Native cssstylesheet
  • Loading branch information
Steve Orvell authored Apr 11, 2020
2 parents ddab8b5 + c172c04 commit c8e182d
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 27 deletions.
8 changes: 5 additions & 3 deletions src/lib/css-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ found at http://polymer.github.io/PATENTS.txt
/**
* Whether the current browser supports `adoptedStyleSheets`.
*/
export const supportsAdoptingStyleSheets =
export const supportsAdoptingStyleSheets = (window.ShadowRoot) &&
(window.ShadyCSS === undefined || window.ShadyCSS.nativeShadow) &&
('adoptedStyleSheets' in Document.prototype) &&
('replace' in CSSStyleSheet.prototype);

Expand All @@ -28,15 +29,16 @@ export class CSSResult {
throw new Error(
'CSSResult is not constructable. Use `unsafeCSS` or `css` instead.');
}

this.cssText = cssText;
}

// Note, 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|null {
if (this._styleSheet === undefined) {
// Note, if `adoptedStyleSheets` is supported then we assume CSSStyleSheet
// is constructable.
// Note, if `supportsAdoptingStyleSheets` is true then we assume
// CSSStyleSheet is constructable.
if (supportsAdoptingStyleSheets) {
this._styleSheet = new CSSStyleSheet();
this._styleSheet.replaceSync(this.cssText);
Expand Down
62 changes: 41 additions & 21 deletions src/lit-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {PropertyValues, UpdatingElement} from './lib/updating-element.js';
export * from './lib/updating-element.js';
export * from './lib/decorators.js';
export {html, svg, TemplateResult, SVGTemplateResult} from 'lit-html/lit-html.js';
import {supportsAdoptingStyleSheets, CSSResult} from './lib/css-tag.js';
import {supportsAdoptingStyleSheets, CSSResult, unsafeCSS} from './lib/css-tag.js';
export * from './lib/css-tag.js';

declare global {
Expand All @@ -33,7 +33,10 @@ declare global {
(window['litElementVersions'] || (window['litElementVersions'] = []))
.push('2.3.1');

export interface CSSResultArray extends Array<CSSResult|CSSResultArray> {}
export type CSSResultOrNative = CSSResult|CSSStyleSheet;

export interface CSSResultArray extends
Array<CSSResultOrNative|CSSResultArray> {}

/**
* Sentinal value used to avoid calling lit-html's render function when
Expand Down Expand Up @@ -80,19 +83,19 @@ export class LitElement extends UpdatingElement {

/**
* Array of styles to apply to the element. The styles should be defined
* using the [[`css`]] tag function.
* using the [[`css`]] tag function or via constructible stylesheets.
*/
static styles?: CSSResult|CSSResultArray;
static styles?: CSSResultOrNative|CSSResultArray;

private static _styles: CSSResult[]|undefined;
private static _styles: Array<CSSResultOrNative|CSSResult>|undefined;

/**
* Return the array of styles to apply to the element.
* Override this method to integrate into a style management system.
*
* @nocollapse
*/
static getStyles(): CSSResult|CSSResultArray|undefined {
static getStyles(): CSSResultOrNative|CSSResultArray|undefined {
return this.styles;
}

Expand All @@ -109,31 +112,48 @@ export class LitElement extends UpdatingElement {
// This should be addressed when a browser ships constructable
// stylesheets.
const userStyles = this.getStyles();
if (userStyles === undefined) {
this._styles = [];
} else if (Array.isArray(userStyles)) {

if (Array.isArray(userStyles)) {
// De-duplicate styles preserving the _last_ instance in the set.
// This is a performance optimization to avoid duplicated styles that can
// occur especially when composing via subclassing.
// The last item is kept to try to preserve the cascade order with the
// assumption that it's most important that last added styles override
// previous styles.
const addStyles =
(styles: CSSResultArray, set: Set<CSSResult>): Set<CSSResult> =>
styles.reduceRight(
(set: Set<CSSResult>, s) =>
// Note: On IE set.add() does not return the set
Array.isArray(s) ? addStyles(s, set) : (set.add(s), set),
set);
const addStyles = (styles: CSSResultArray, set: Set<CSSResultOrNative>):
Set<CSSResultOrNative> => styles.reduceRight(
(set: Set<CSSResultOrNative>, s) =>
// Note: On IE set.add() does not return the set
Array.isArray(s) ? addStyles(s, set) : (set.add(s), set),
set);
// Array.from does not work on Set in IE, otherwise return
// Array.from(addStyles(userStyles, new Set<CSSResult>())).reverse()
const set = addStyles(userStyles, new Set<CSSResult>());
const styles: CSSResult[] = [];
const set = addStyles(userStyles, new Set<CSSResultOrNative>());
const styles: CSSResultOrNative[] = [];
set.forEach((v) => styles.unshift(v));
this._styles = styles;
} else {
this._styles = [userStyles];
this._styles = userStyles === undefined ? [] : [userStyles];
}

// Ensure that there are no invalid CSSStyleSheet instances here. They are
// invalid in two conditions.
// (1) the sheet is non-constructible (`sheet` of a HTMLStyleElement), but
// this is impossible to check except via .replaceSync or use
// (2) the ShadyCSS polyfill is enabled (:. supportsAdoptingStyleSheets is
// false)
this._styles = this._styles.map((s) => {
if (s instanceof CSSStyleSheet && !supportsAdoptingStyleSheets) {
// Flatten the cssText from the passed constructible stylesheet (or
// undetectable non-constructible stylesheet). The user might have
// expected to update their stylesheets over time, but the alternative
// is a crash.
const cssText = Array.prototype.slice.call(s.cssRules)
.reduce((css, rule) => css + rule.cssText, '');
return unsafeCSS(cssText);
}
return s;
});
}

private _needsShimAdoptedStyleSheets?: boolean;
Expand Down Expand Up @@ -189,15 +209,15 @@ export class LitElement extends UpdatingElement {
}
// There are three separate cases here based on Shadow DOM support.
// (1) shadowRoot polyfilled: use ShadyCSS
// (2) shadowRoot.adoptedStyleSheets available: use it.
// (2) shadowRoot.adoptedStyleSheets available: use it
// (3) shadowRoot.adoptedStyleSheets polyfilled: append styles after
// rendering
if (window.ShadyCSS !== undefined && !window.ShadyCSS.nativeShadow) {
window.ShadyCSS.ScopingShim!.prepareAdoptedCssText(
styles.map((s) => s.cssText), this.localName);
} else if (supportsAdoptingStyleSheets) {
(this.renderRoot as ShadowRoot).adoptedStyleSheets =
styles.map((s) => s.styleSheet!);
styles.map((s) => s instanceof CSSStyleSheet ? s : s.styleSheet!);
} else {
// This must be done after rendering so the actual style insertion is done
// in `update`.
Expand Down
95 changes: 92 additions & 3 deletions src/test/lit-element_styling_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,10 +792,16 @@ suite('Static get styles', () => {

test('element class only gathers styles once', async () => {
const base = generateElementName();
let styleCounter = 0;
let getStylesCounter = 0;
let stylesCounter = 0;
customElements.define(base, class extends LitElement {
static getStyles() {
getStylesCounter++;
return super.getStyles();
}

static get styles() {
styleCounter++;
stylesCounter++;
return css`:host {
border: 10px solid black;
}`;
Expand All @@ -821,7 +827,8 @@ suite('Static get styles', () => {
'10px',
'el2 styled correctly');
assert.equal(
styleCounter, 1, 'styles property should only be accessed once');
stylesCounter, 1, 'styles property should only be accessed once');
assert.equal(getStylesCounter, 1, 'getStyles() should be called once');
});

test(
Expand Down Expand Up @@ -890,6 +897,88 @@ suite('Static get styles', () => {

document.body.removeChild(element);
});

const testAdoptedStyleSheets =
(window.ShadowRoot) &&
('replace' in CSSStyleSheet.prototype);
(testAdoptedStyleSheets ? test : test.skip)(
'Can return CSSStyleSheet where adoptedStyleSheets are natively supported',
async () => {
const sheet = new CSSStyleSheet();
sheet.replaceSync('div { border: 4px solid red; }');
const normal = css`span { border: 4px solid blue; }`;

const base = generateElementName();
customElements.define(base, class extends LitElement {
static styles = [sheet, normal];

render() {
return htmlWithStyles`<div></div><span></span>`;
}
});

const el = document.createElement(base);
container.appendChild(el);
await (el as LitElement).updateComplete;
const div = el.shadowRoot!.querySelector('div')!;
assert.equal(
getComputedStyle(div).getPropertyValue('border-top-width').trim(),
'4px');

const span = el.shadowRoot!.querySelector('span')!;
assert.equal(
getComputedStyle(span).getPropertyValue('border-top-width').trim(),
'4px');

// When the WC polyfills are included, calling .replaceSync is a noop to
// our styles as they're already flattened (so expect 4px). Otherwise,
// look for the updated value.
const usesAdoptedStyleSheet = (window.ShadyCSS === undefined || window.ShadyCSS.nativeShadow);
const expectedValue = usesAdoptedStyleSheet ? '2px' : '4px';
sheet.replaceSync('div { border: 2px solid red; }');

assert.equal(
getComputedStyle(div).getPropertyValue('border-top-width').trim(),
expectedValue);
});

// Test that when ShadyCSS is enabled (while still having native support for
// adoptedStyleSheets), we can return a CSSStyleSheet that will be flattened
// and play nice with others.
const testShadyCSSWithAdoptedStyleSheetSupport =
(window.ShadowRoot) &&
('replace' in CSSStyleSheet.prototype) &&
(window.ShadyCSS !== undefined && !window.ShadyCSS.nativeShadow);
(testShadyCSSWithAdoptedStyleSheetSupport ? test : test.skip)(
'CSSStyleSheet is flattened where ShadyCSS is enabled yet adoptedStyleSheets are supported',
async () => {
const sheet = new CSSStyleSheet();
sheet.replaceSync('div { border: 4px solid red; }');

const base = generateElementName();
customElements.define(base, class extends LitElement {
static styles = sheet;

render() {
return htmlWithStyles`<div></div>`;
}
});

const el = document.createElement(base);
container.appendChild(el);
await (el as LitElement).updateComplete;

const div = el.shadowRoot!.querySelector('div')!;
assert.equal(
getComputedStyle(div).getPropertyValue('border-top-width').trim(),
'4px');

// CSSStyleSheet update should fail, as the styles will be flattened.
sheet.replaceSync('div { border: 2px solid red; }');
assert.equal(
getComputedStyle(div).getPropertyValue('border-top-width').trim(),
'4px', 'CSS should not reflect CSSStyleSheet as it was flattened');
});
});

suite('ShadyDOM', () => {
Expand Down

0 comments on commit c8e182d

Please sign in to comment.