Skip to content

Primitive shadow parts support #329

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

Merged
merged 6 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"bracketSpacing": false
}
5 changes: 4 additions & 1 deletion packages/shadycss/externs/shadycss-externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
* styleDocument: function(Object<string, string>=),
* flushCustomStyles: function(),
* getComputedStyleValue: function(!Element, string): string,
* onInsertBefore: function(!HTMLElement, !HTMLElement, ?HTMLElement): void,
* ScopingShim: (Object|undefined),
* ApplyShim: (Object|undefined),
* CustomStyleInterface: (Object|undefined),
* nativeCss: boolean,
* nativeShadow: boolean,
* cssBuild: (string | undefined),
* disableRuntime: boolean,
* disableShadowParts: (boolean | undefined),
* }}
*/
let ShadyCSSInterface; //eslint-disable-line no-unused-vars
Expand All @@ -26,6 +28,7 @@ let ShadyCSSInterface; //eslint-disable-line no-unused-vars
* shimshadow: (boolean | undefined),
* cssBuild: (string | undefined),
* disableRuntime: (boolean | undefined),
* disableShadowParts: (boolean | undefined),
* }}
*/
let ShadyCSSOptions; //eslint-disable-line no-unused-vars
Expand Down Expand Up @@ -63,4 +66,4 @@ HTMLTemplateElement.prototype._style;
/**
* @type {string | undefined}
*/
DOMTokenList.prototype.value;
DOMTokenList.prototype.value;
17 changes: 17 additions & 0 deletions packages/shadycss/src/scoping-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import templateMap from './template-map.js';
import * as ApplyShimUtils from './apply-shim-utils.js';
import {updateNativeProperties, detectMixin} from './common-utils.js';
import {CustomStyleInterfaceInterface, CustomStyleProvider} from './custom-style-interface.js'; // eslint-disable-line no-unused-vars
import * as shadowParts from './shadow-parts.js';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you consider whether shadow parts should be a part of the scoping shim or given separate opt-in support in ShadyCSS, like @apply or custom style interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good thought, hadn't considered that. What are the main advantages you are thinking of?

The main one I can think of is that there would be more options for shipping smaller bundles to users who don't need shadow parts. We could create more permutations of the bundles, and update the loader to check the disableShadowParts flag, and only load shadow parts support when not true.

Does this suggestion imply you think shadow parts should be opt-in rather than opt-out? I've been thinking we should do opt-out, because 1) shadow parts are so widely supported now that it feels like they are part of the core set of web components APIs, and 2) I think the performance cost to applications that don't end up using shadow parts is very small (since we will do almost nothing until a ::part style is first detected in at least one template).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you've evaluated it correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Filed #339, will think about this a little more and refactor in a separate PR.

import {disableShadowParts} from './style-settings.js';

/** @type {!Object<string, string>} */
const adoptedCssTextMap = {};
Expand Down Expand Up @@ -275,6 +277,21 @@ export default class ScopingShim {
// sort ast ordering for document
this._documentOwnerStyleInfo.styleRules['rules'] = styles.map(s => StyleUtil.rulesForStyle(s));
}

/**
* Hook for performing ShadyCSS behavior for each ShadyDOM insertBefore call.
*
* @param {!HTMLElement} parentNode
* @param {!HTMLElement} newNode
* @param {?HTMLElement} referenceNode
* @return {void}
*/
onInsertBefore(parentNode, newNode, referenceNode) {
if (!disableShadowParts) {
shadowParts.onInsertBefore(parentNode, newNode, referenceNode);
}
}

/**
* Apply styles for the given element
*
Expand Down
70 changes: 66 additions & 4 deletions packages/shadycss/src/shadow-parts.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function parseExportPartsAttribute(attr) {
// matches native behavior).
continue;
}
parts.push({ inner, outer });
parts.push({inner, outer});
}
return parts;
}
Expand Down Expand Up @@ -90,7 +90,7 @@ const PART_REGEX = /(.*?)([a-z]+-\w+)([^\s]*?)::part\((.*)?\)(::?.*)?/;
* spec-compliant.
*
* Example:
* [0 ][1 ][2 ] [3 ] [4 ]
* [0 ][1 ][2 ] [3 ] [4 ]
* #parent > my-button.fancy::part(foo bar):hover
*
* @param {!string} selector The selector.
Expand All @@ -108,7 +108,7 @@ export function parsePartSelector(selector) {
return null;
}
const [, combinators, elementName, selectors, parts, pseudos] = match;
return { combinators, elementName, selectors, parts, pseudos: pseudos || "" };
return {combinators, elementName, selectors, parts, pseudos: pseudos || ''};
}

/**
Expand Down Expand Up @@ -163,5 +163,67 @@ export function formatShadyPartSelector(
);
return `[shady-part~="${attr}"]`;
})
.join("");
.join('');
}

/* eslint-disable no-unused-vars */
/**
* Add "shady-part" attributes to new nodes on insertion.
*
* This function will be called by ShadyDOM during any insertBefore call,
* before the native insert has occured.
*
* @param {!HTMLElement} parentNode
* @param {!HTMLElement} newNode
* @param {?HTMLElement} referenceNode
* @return {void}
*/
export function onInsertBefore(parentNode, newNode, referenceNode) {
/* eslint-enable no-unused-vars */
if (newNode instanceof Text) {
// No parts in text.
return;
}
if (!parentNode.getRootNode) {
// TODO(aomarks) We're in noPatch mode. Wrap where needed and add tests.
// https://github.com/webcomponents/polyfills/issues/343
return;
}
const root = parentNode.getRootNode();
if (root === document) {
// Parts in the document scope would never have any effect. Return early so
// we don't waste time querying it.
return;
}
const host = root.host;
if (!host) {
// If there's no host, we're not connected, so no part styles could apply
// here.
return;
}
let parts = newNode.querySelectorAll('[part]');
// TODO(aomarks) We should be able to get much better performance over the
// querySelectorAll calls here by integrating the part check into the walk
// that ShadyDOM already does to find slots.
// https://github.com/webcomponents/polyfills/issues/345
if (newNode instanceof HTMLElement && newNode.hasAttribute('part')) {
parts = [newNode, ...parts];
}
if (parts.length === 0) {
return;
}
const receiverScope = host.localName;
const superRoot = host.getRootNode();
const providerScope =
superRoot === document ? 'document' : superRoot.host.localName;
for (const part of parts) {
part.setAttribute(
'shady-part',
formatShadyPartAttribute(
providerScope,
receiverScope,
part.getAttribute('part')
)
);
}
}
6 changes: 5 additions & 1 deletion packages/shadycss/src/style-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ if (window.ShadyCSS && window.ShadyCSS.cssBuild !== undefined) {
/** @type {boolean} */
export const disableRuntime = Boolean(window.ShadyCSS && window.ShadyCSS.disableRuntime);

/** @type {boolean} */
export const disableShadowParts =
Boolean(window.ShadyCSS && window.ShadyCSS.disableShadowParts);

if (window.ShadyCSS && window.ShadyCSS.nativeCss !== undefined) {
nativeCssVariables_ = window.ShadyCSS.nativeCss;
} else if (window.ShadyCSS) {
Expand All @@ -53,4 +57,4 @@ if (window.ShadyCSS && window.ShadyCSS.nativeCss !== undefined) {
// Hack for type error under new type inference which doesn't like that
// nativeCssVariables is updated in a function and assigns the type
// `function(): ?` instead of `boolean`.
export const nativeCssVariables = /** @type {boolean} */(nativeCssVariables_);
export const nativeCssVariables = /** @type {boolean} */(nativeCssVariables_);
42 changes: 40 additions & 2 deletions packages/shadycss/src/style-transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN

'use strict';

import {StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars
import {StyleNode, parse} from './css-parse.js'; // eslint-disable-line no-unused-vars
import * as StyleUtil from './style-util.js';
import {nativeShadow} from './style-settings.js';
import {nativeShadow, disableShadowParts} from './style-settings.js';
import {parsePartSelector, formatShadyPartSelector} from './shadow-parts.js';

/* Transforms ShadowDOM styling into ShadyDOM styling

Expand Down Expand Up @@ -315,6 +316,12 @@ class StyleTransformer {
NTH, (m, type, inner) => `:${type}(${inner.replace(/\s/g, '')})`);
selector = this._twiddleNthPlus(selector);
}
if (!disableShadowParts && PART.test(selector)) {
// Hacky transform "::part(foo bar)" to "::part(foo,bar)" so that
// SIMPLE_SELECTOR_SEP isn't confused by the spaces.
// TODO(aomarks) Can we make SIMPLE_SELECTOR_SEP smarter instead?
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be nice to work this out, but acknowledge that it may be non-trivial.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged. Leaving for now with TODO.

selector = selector.replace(PART, (m) => m.replace(' ', ','));
}
// Preserve selectors like `:-webkit-any` so that SIMPLE_SELECTOR_SEP does
// not get confused by spaces inside the pseudo selector
const isMatches = MATCHES.test(selector);
Expand Down Expand Up @@ -350,6 +357,8 @@ class StyleTransformer {
let slottedIndex = selector.indexOf(SLOTTED);
if (selector.indexOf(HOST) >= 0) {
selector = this._transformHostSelector(selector, hostScope);
} else if (!disableShadowParts && selector.match(PART)) {
selector = this._transformPartSelector(selector, hostScope);
// replace other selectors with scoping class
} else if (slottedIndex !== 0) {
selector = scope ? this._transformSimpleSelector(selector, scope) :
Expand Down Expand Up @@ -396,6 +405,32 @@ class StyleTransformer {
return output.join('');
}

/**
* Transform a `::part` selector into a `shady-part` attribute selector.
*
* Example:
* Given: 'parent > x-b.fancy::part(foo):hover' andd scope 'x-a'
* Returns: 'parent > x-b.fancy [shady-part~="x-a:x-b:foo"]:hover'
*
* @param {!string} selector The `::part` selector.
* @param {!string} scope Lowercase custom-element name of the scope that
* defined this `::part` rule.
* @return {!string} Transformed `shady-part` attribute selector.
*/
_transformPartSelector(selector, scope) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doc with an example transformation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

const parsed = parsePartSelector(selector);
if (parsed === null) {
return selector;
}
const {combinators, elementName, selectors, parts, pseudos} = parsed;
// Earlier we did a hacky transform from "part(foo bar)" to "part(foo,bar)"
// so that the SIMPLE_SELECTOR regex didn't get confused by spaces.
const partSelector =
formatShadyPartSelector(scope, elementName, parts.replace(',', ' '));
return (scope === 'document' ? '' : scope + ' ') +
`${combinators}${elementName}${selectors} ${partSelector}${pseudos}`;
}

// :host(...) -> scopeName...
_transformHostSelector(selector, hostScope) {
let m = selector.match(HOST_PAREN);
Expand Down Expand Up @@ -457,6 +492,8 @@ class StyleTransformer {
return '';
} else if (selector.match(SLOTTED)) {
return this._transformComplexSelector(selector, SCOPE_DOC_SELECTOR);
} else if (!disableShadowParts && selector.match(PART)) {
return this._transformPartSelector(selector, 'document');
} else {
return this._transformSimpleSelector(selector.trim(), SCOPE_DOC_SELECTOR);
}
Expand All @@ -470,6 +507,7 @@ const SIMPLE_SELECTOR_SEP = /(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=[])+)/g;
const SIMPLE_SELECTOR_PREFIX = /[[.:#*]/;
const HOST = ':host';
const ROOT = ':root';
const PART = /::part\([^)]*\)/;
const SLOTTED = '::slotted';
const SLOTTED_START = new RegExp(`^(${SLOTTED})`);
// NOTE: this supports 1 nested () pair for things like
Expand Down
9 changes: 9 additions & 0 deletions packages/shadydom/src/patches/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,15 @@ export const NodePatches = utils.getOwnPropertyDescriptors({
ownerRoot._asyncRender();
}
}
if (!utils.disableShadowParts) {
const shim = getScopingShim();
if (shim) {
// Note that we do want to call this before the actual native insert,
// because in the case that we're inserting from a DocumentFragment,
// we want to know exactly which new child nodes are being inserted.
shim['onInsertBefore'](this, node, ref_node);
}
}
if (allowNativeInsert) {
// if adding to a shadyRoot, add to host instead
let container = utils.isShadyRoot(this) ?
Expand Down
2 changes: 1 addition & 1 deletion packages/shadydom/src/style-scoping.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,4 @@ export function treeVisitor(node, visitorFn) {
treeVisitor(n, visitorFn);
}
}
}
}
4 changes: 4 additions & 0 deletions packages/shadydom/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {shadyDataForNode} from './shady-data.js';
/** @type {!Object} */
export const settings = window['ShadyDOM'] || {};

/** @type {boolean} */
export const disableShadowParts =
Boolean(window['ShadyCSS'] && window['ShadyCSS']['disableShadowParts']);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be true if ShadyCSS does not exist?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think so, should it? If there's no ShadyCSS, then the user hasn't set any initialization flags, so the default should hold, which would be disableShadowParts set to false.


settings.hasNativeShadowDOM = Boolean(Element.prototype.attachShadow && Node.prototype.getRootNode);

// The user might need to pass the custom elements polyfill a flag by setting an
Expand Down
22 changes: 17 additions & 5 deletions packages/tests/shadycss/runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@

<script src="../node_modules/wct-browser-legacy/browser.js"></script>

<script>
(function(){
<script type="module">
import {suites as shadowPartsSuites} from './shadow-parts/suites.js';

var suites = [
'css-parse.html',
'apply-shim.html',
Expand All @@ -38,7 +39,8 @@
'wc-1.html',
'scoping-api.html',
'mixin-fallbacks.html',
'interface.html'
'interface.html',
...shadowPartsSuites.map((file) => `shadow-parts/${file}`),
];

// http://eddmann.com/posts/cartesian-product-in-javascript/
Expand Down Expand Up @@ -71,7 +73,9 @@
webcomponents = 'wc-register=true';
}
// if native is available, make sure to test polyfill
if (Element.prototype.attachShadow && document.documentElement.getRootNode) {
const hasNativeShadow =
Element.prototype.attachShadow && document.documentElement.getRootNode;
if (hasNativeShadow) {
webcomponents = addUrlOption(webcomponents, 'wc-shadydom=true');
}
// ce + sd becomes a single test iteration.
Expand Down Expand Up @@ -110,6 +114,14 @@
]);
}

// Skip shadow part tests if the browser supports shadow DOM but not shadow
// parts, unless we are forcing the polyfill on. ShadyCSS doesn't polyfill
// just shadow parts on top of native shadow DOM, so these would fail.
const hasNativeParts = 'part' in HTMLElement.prototype;
if (hasNativeShadow && !hasNativeParts) {
suites = suites.filter((url) =>
!(url.startsWith('shadow-parts/') && !url.includes('wc-shadydom')));
}

WCT.loadSuites(suites);
})();
</script>
21 changes: 21 additions & 0 deletions packages/tests/shadycss/shadow-parts/all-native.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<!--
@license
Copyright (c) 2020 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<title>shadycss/shadow-parts/all-native</title>

<script src="../../node_modules/wct-browser-legacy/browser.js"></script>

<!-- This suite is just for more convenient local test execution. -->
<script type="module">
import {suites} from './suites';

WCT.loadSuites(suites);
</script>
26 changes: 26 additions & 0 deletions packages/tests/shadycss/shadow-parts/all-polyfilled.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<!--
@license
Copyright (c) 2020 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<title>shadycss/shadow-parts/all-polyfilled</title>

<script src="../../node_modules/wct-browser-legacy/browser.js"></script>

<!-- This suite is just for more convenient local test execution. -->
<script type="module">
import {suites} from './suites';

WCT.loadSuites(
suites.map(
(url) =>
url + "?wc-register=true&wc-shadydom=true&wc-shimcssproperties=true"
)
);
</script>
Loading