-
Notifications
You must be signed in to change notification settings - Fork 169
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
Changes from 2 commits
3922e3e
4e29618
441c0de
64b9e6b
50f934a
afae643
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"singleQuote": true, | ||
"bracketSpacing": false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,7 +60,7 @@ export function parseExportPartsAttribute(attr) { | |
// matches native behavior). | ||
continue; | ||
} | ||
parts.push({ inner, outer }); | ||
parts.push({inner, outer}); | ||
} | ||
return parts; | ||
} | ||
|
@@ -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. | ||
|
@@ -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 || ''}; | ||
} | ||
|
||
/** | ||
|
@@ -163,5 +163,49 @@ export function formatShadyPartSelector( | |
); | ||
return `[shady-part~="${attr}"]`; | ||
}) | ||
.join(""); | ||
.join(''); | ||
} | ||
|
||
/* eslint-disable no-unused-vars */ | ||
/** | ||
* Perform any needed Shadow Parts updates after a ShadyDOM insertBefore call | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you describe in more detail what this does and why it's necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be much less stress if we didn't have to alter the part attribute. Can you describe how this would limit the fidelity of the shim? In particular, naively, a rule like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, interesting. I thought of an example that seemed sort of realistic where your suggestion would have caused an error, but then I thought of a modification that resolves it: (1) <style>
/* error, the <div> grand-child of <x-other> shouldn't match */
x-super .x-host[part~=foo] { color: red; }
/* but is OK with this modification */
x-super x-host.x-super .x-host[part~=foo] { color: red; }
</style>
<x-super>
<x-host class="x-super">
<div class="x-host" part="foo">
<x-other class="x-super">
<x-host class="x-other">
<div class="x-host" part="foo"> However, you can still construct some patterns that fail: (2) <style>
x-super x-host.x-super .x-host[part~=foo] { color: red; }
</style>
<x-super>
<x-host class="x-super">
<div class="x-host" part="foo">
<x-other class="x-super">
<x-host class="x-other">
<!-- error: should not be red -->
<div class="x-host" part="foo"> Whereas with the (3) <style>
x-super x-host [shady-part~="x-super:x-host:foo"] { color: red; }
</style>
<x-super>
<x-host>
<div class="x-host" part="foo" shady-part="x-super:x-host:foo">
<x-other>
<x-host>
<!-- OK -->
<div class="x-host" part="foo" shady-part="x-other:x-host:foo"> This is not to say that the I guess the next question is, are examples like (2) above acceptable gaps in fidelity? It seems a little tricky to workaround the (2) example unless you can control the part names. WDYT? Can you think of any other selector tricks that would work even better? The other consideration here is exported parts. In the implementation in #331, the way we support export parts is by adding additional I guess the way to do this if we used this other approach would be: each time we discover that an exported part needs to receive some style, we clone that style rule (or update its selector) with the new scope. We'd need to do per-instance styling here, though, in cases where two instances of the same element have different exportparts attributes. I already interact with per-instance styling in #333 to support custom properties within part rules, so it wouldn't be that wild. The difference here, though, would be that we'd be doing per-instance styling for more cases, even in Edge when native custom property support is available. WDYT, am I thinking about this right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for considering this so carefully. To summarize, nesting is a problem with any of our approaches, but with the current approach nesting of (A->B) inside an (A->B) is problematic whereas with the proposed approach nesting an A inside an A is problematic. I was considering this without exportparts. I think considering that argues for how you've done it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged. |
||
* has been made. | ||
* | ||
* @param {!HTMLElement} parentNode | ||
* @param {!HTMLElement} newNode | ||
* @param {?HTMLElement} referenceNode | ||
* @return {void} | ||
*/ | ||
export function onInsertBefore(parentNode, newNode, referenceNode) { | ||
/* eslint-enable no-unused-vars */ | ||
if (!parentNode.getRootNode) { | ||
// TODO(aomarks) Why is it in noPatch mode on Chrome 41 and other older | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To include support for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm having trouble getting tests to work with noPatch enabled -- it looks like there are no tests under shadycss with noPatch enabled yet. I'll need to spend some more time on it, so I've updated the TODO and filed #343 and will do noPatch support as a separate PR in next sprint, if that's ok. |
||
// browsers that getRootNode is sometimes undefined? | ||
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 parts = parentNode.querySelectorAll('[part]'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, to support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this do a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also consider doing this in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be searching There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good idea. Probably missing something obvious here and I can look into it myself, but how can I avoid not querying the template again to look for parts when the template is stamped? Would I need to add some new tracing to understand when an Also, it seems like this could cut down on the cost of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You'd need to add support for knowing when to not support the querySelector, but there's precedent for this: https://github.com/webcomponents/polyfills/blob/master/packages/shadydom/src/patches/Node.js#L309. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Done. I also needed some other changes to make this happen (since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As mentioned in other comment, will do in follow up (#343). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No benchmarks yet. I will add them in an upcoming task: #344 Will look into replacing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@sorvell any thoughts on this? I've filed #346 for future investigation. |
||
if (parts.length === 0) { | ||
return; | ||
} | ||
const host = root.host; | ||
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') | ||
) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
@@ -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) : | ||
|
@@ -396,6 +405,20 @@ class StyleTransformer { | |
return output.join(''); | ||
} | ||
|
||
_transformPartSelector(selector, scope) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doc with an example transformation. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
@@ -457,6 +480,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); | ||
} | ||
|
@@ -470,6 +495,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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -375,6 +375,12 @@ export const NodePatches = utils.getOwnPropertyDescriptors({ | |
} else if (node.ownerDocument !== this.ownerDocument) { | ||
this.ownerDocument.adoptNode(node); | ||
} | ||
if (!utils.disableShadowParts) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See previous comment about performance concerns and consider instead a tighter integration at https://github.com/webcomponents/polyfills/blob/master/packages/shadydom/src/patches/Node.js#L311-L333. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged. Added TODO and filed #345 for upcoming task. |
||
const shim = getScopingShim(); | ||
if (shim) { | ||
shim['onInsertBefore'](this, node, ref_node); | ||
} | ||
} | ||
return node; | ||
}, | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -134,4 +134,4 @@ export function treeVisitor(node, visitorFn) { | |
treeVisitor(n, visitorFn); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so, should it? If there's no |
||
|
||
settings.hasNativeShadowDOM = Boolean(Element.prototype.attachShadow && Node.prototype.getRootNode); | ||
|
||
// The user might need to pass the custom elements polyfill a flag by setting an | ||
|
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> |
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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.