Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native cssstylesheet #853

Merged
merged 27 commits into from
Apr 11, 2020
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
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) &&
Copy link
Member

Choose a reason for hiding this comment

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

Think we need to change this to ('adoptedStyleSheets' in window.ShadowRoot.prototype), instead of ('adoptedStyleSheets' in Document.prototype). This makes sure it'll be false when ShadyDOM is enabled.

Copy link
Member

Choose a reason for hiding this comment

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

Seems like this change should be needed to get the test running under the polyfill to run?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@justinfagnani had an issue with this. To quote him:

We need to keep this checks against Document, since we have developers that delete the properties on Document.prototype to disable the use of adopted style sheets so they can edit in DevTools.

(window.ShadyCSS === undefined) &&
samthor marked this conversation as resolved.
Show resolved Hide resolved
('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
67 changes: 46 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,53 @@ 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), in
// this case an Error is thrown
// (2) the ShadyCSS polyfill is enabled (:. supportsAdoptingStyleSheets is
// false)
this._styles = this._styles.map((s) => {
if (s instanceof CSSStyleSheet) {
if (s.ownerNode) {
throw new Error(`CSSStyleSheet instances used in 'styles' cannot ` +
`come from <style> tags.`);
}
if (!supportsAdoptingStyleSheets) {
// Flatten the cssText from the passed constructible stylesheet. The
// user might have expected to update their stylesheets over time,
// but the alternative was a crash.
const cssText = Array.prototype.slice.call(s.cssRules)
samthor marked this conversation as resolved.
Show resolved Hide resolved
.reduce((css, rule) => css + rule.cssText, '');
return unsafeCSS(cssText);
}
}
return s;
});
}

private _needsShimAdoptedStyleSheets?: boolean;
Expand Down Expand Up @@ -189,15 +214,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
114 changes: 111 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,107 @@ suite('Static get styles', () => {

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

test(
'Non-adoptible stylesheet is disallowed',
async () => {
const s = document.createElement('style');
s.textContent = `@media (max-width: 768px) { #_adoptible_test { border: 4px solid red; } }`;
container.appendChild(s);
const sheetFromStyle = s.sheet;
container.removeChild(s);

const base = generateElementName();
customElements.define(base, class extends LitElement {
static styles = (sheetFromStyle as CSSStyleSheet);
});

const el = document.createElement(base);
container.appendChild(el);

try {
await (el as LitElement).updateComplete;
assert.fail('update should fail as <style>.sheet is disallowed');
} catch (e) {
// ok
}
});

// Test this in Shadow DOM without `adoptedStyleSheets` only since it's easily
// detectable in that case. Look explicitly for no ShadyCSS.
const testNativeAdoptedStyleSheets = (window.ShadyCSS === undefined) &&
(typeof ShadowRoot === 'function') &&
('adoptedStyleSheets' in window.ShadowRoot.prototype);
(testNativeAdoptedStyleSheets ? test : test.skip)(
'Can return CSSStyleSheet where adoptedStyleSheets are natively 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');

sheet.replaceSync('div { border: 2px solid red; }');
assert.equal(
getComputedStyle(div).getPropertyValue('border-top-width').trim(),
'2px');
});

// Test that when ShadyCSS is enabled _even with_ native support, we can return
// a CSSStyleSheet that will be flattened and play nice with others.
const testShadyCSSWithAdoptedStyleSheetSupport = (window.ShadyCSS !== undefined) &&
(typeof ShadowRoot === 'function') &&
('adoptedStyleSheets' in window.ShadowRoot.prototype);
(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 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');

// 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