Skip to content

Commit bc6557e

Browse files
committed
Primitive shadow parts support
This supports basic patterns of styling parts whenever a node is inserted into a shadow root. It also adds and tests a disableShadowParts ShadyCSS setting. Support for more kinds of mutations, exportparts, CSS custom properties are coming in follow up PRs.
1 parent 35a4018 commit bc6557e

File tree

14 files changed

+401
-6
lines changed

14 files changed

+401
-6
lines changed

packages/shadycss/externs/shadycss-externs.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
* styleDocument: function(Object<string, string>=),
1010
* flushCustomStyles: function(),
1111
* getComputedStyleValue: function(!Element, string): string,
12+
* onInsertBefore: function(!HTMLElement, !HTMLElement, ?HTMLElement): void,
1213
* ScopingShim: (Object|undefined),
1314
* ApplyShim: (Object|undefined),
1415
* CustomStyleInterface: (Object|undefined),
1516
* nativeCss: boolean,
1617
* nativeShadow: boolean,
1718
* cssBuild: (string | undefined),
1819
* disableRuntime: boolean,
20+
* disableShadowParts: (boolean | undefined),
1921
* }}
2022
*/
2123
let ShadyCSSInterface; //eslint-disable-line no-unused-vars
@@ -26,6 +28,7 @@ let ShadyCSSInterface; //eslint-disable-line no-unused-vars
2628
* shimshadow: (boolean | undefined),
2729
* cssBuild: (string | undefined),
2830
* disableRuntime: (boolean | undefined),
31+
* disableShadowParts: (boolean | undefined),
2932
* }}
3033
*/
3134
let ShadyCSSOptions; //eslint-disable-line no-unused-vars
@@ -63,4 +66,4 @@ HTMLTemplateElement.prototype._style;
6366
/**
6467
* @type {string | undefined}
6568
*/
66-
DOMTokenList.prototype.value;
69+
DOMTokenList.prototype.value;

packages/shadycss/src/scoping-shim.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import templateMap from './template-map.js';
2323
import * as ApplyShimUtils from './apply-shim-utils.js';
2424
import {updateNativeProperties, detectMixin} from './common-utils.js';
2525
import {CustomStyleInterfaceInterface, CustomStyleProvider} from './custom-style-interface.js'; // eslint-disable-line no-unused-vars
26+
import * as shadowParts from './shadow-parts.js';
27+
import {disableShadowParts} from './style-settings.js';
2628

2729
/** @type {!Object<string, string>} */
2830
const adoptedCssTextMap = {};
@@ -275,6 +277,21 @@ export default class ScopingShim {
275277
// sort ast ordering for document
276278
this._documentOwnerStyleInfo.styleRules['rules'] = styles.map(s => StyleUtil.rulesForStyle(s));
277279
}
280+
281+
/**
282+
* Hook for performing ShadyCSS behavior for each ShadyDOM insertBefore call.
283+
*
284+
* @param {!HTMLElement} parentNode
285+
* @param {!HTMLElement} newNode
286+
* @param {?HTMLElement} referenceNode
287+
* @return {void}
288+
*/
289+
onInsertBefore(parentNode, newNode, referenceNode) {
290+
if (!disableShadowParts) {
291+
shadowParts.onInsertBefore(parentNode, newNode, referenceNode);
292+
}
293+
}
294+
278295
/**
279296
* Apply styles for the given element
280297
*

packages/shadycss/src/shadow-parts.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,47 @@ export function formatShadyPartSelector(
165165
})
166166
.join('');
167167
}
168+
169+
/* eslint-disable no-unused-vars */
170+
/**
171+
* Perform any needed Shadow Parts updates after a ShadyDOM insertBefore call
172+
* has been made.
173+
*
174+
* @param {!HTMLElement} parentNode
175+
* @param {!HTMLElement} newNode
176+
* @param {?HTMLElement} referenceNode
177+
* @return {void}
178+
*/
179+
export function onInsertBefore(parentNode, newNode, referenceNode) {
180+
/* eslint-enable no-unused-vars */
181+
if (!parentNode.getRootNode) {
182+
// TODO(aomarks) Why is it in noPatch mode on Chrome 41 and other older
183+
// browsers that getRootNode is sometimes undefined?
184+
return;
185+
}
186+
const root = parentNode.getRootNode();
187+
if (root === document) {
188+
// Parts in the document scope would never have any effect. Return early so
189+
// we don't waste time querying it.
190+
return;
191+
}
192+
const parts = parentNode.querySelectorAll('[part]');
193+
if (parts.length === 0) {
194+
return;
195+
}
196+
const host = root.host;
197+
const receiverScope = host.localName;
198+
const superRoot = host.getRootNode();
199+
const providerScope =
200+
superRoot === document ? 'document' : superRoot.host.localName;
201+
for (const part of parts) {
202+
part.setAttribute(
203+
'shady-part',
204+
formatShadyPartAttribute(
205+
providerScope,
206+
receiverScope,
207+
part.getAttribute('part')
208+
)
209+
);
210+
}
211+
}

packages/shadycss/src/style-settings.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ if (window.ShadyCSS && window.ShadyCSS.cssBuild !== undefined) {
4040
/** @type {boolean} */
4141
export const disableRuntime = Boolean(window.ShadyCSS && window.ShadyCSS.disableRuntime);
4242

43+
/** @type {boolean} */
44+
export const disableShadowParts =
45+
Boolean(window.ShadyCSS && window.ShadyCSS.disableShadowParts);
46+
4347
if (window.ShadyCSS && window.ShadyCSS.nativeCss !== undefined) {
4448
nativeCssVariables_ = window.ShadyCSS.nativeCss;
4549
} else if (window.ShadyCSS) {
@@ -53,4 +57,4 @@ if (window.ShadyCSS && window.ShadyCSS.nativeCss !== undefined) {
5357
// Hack for type error under new type inference which doesn't like that
5458
// nativeCssVariables is updated in a function and assigns the type
5559
// `function(): ?` instead of `boolean`.
56-
export const nativeCssVariables = /** @type {boolean} */(nativeCssVariables_);
60+
export const nativeCssVariables = /** @type {boolean} */(nativeCssVariables_);

packages/shadycss/src/style-transformer.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
1010

1111
'use strict';
1212

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

1718
/* Transforms ShadowDOM styling into ShadyDOM styling
1819
@@ -315,6 +316,12 @@ class StyleTransformer {
315316
NTH, (m, type, inner) => `:${type}(${inner.replace(/\s/g, '')})`);
316317
selector = this._twiddleNthPlus(selector);
317318
}
319+
if (!disableShadowParts && PART.test(selector)) {
320+
// Hacky transform "::part(foo bar)" to "::part(foo,bar)" so that
321+
// SIMPLE_SELECTOR_SEP isn't confused by the spaces.
322+
// TODO(aomarks) Can we make SIMPLE_SELECTOR_SEP smarter instead?
323+
selector = selector.replace(PART, (m) => m.replace(' ', ','));
324+
}
318325
// Preserve selectors like `:-webkit-any` so that SIMPLE_SELECTOR_SEP does
319326
// not get confused by spaces inside the pseudo selector
320327
const isMatches = MATCHES.test(selector);
@@ -350,6 +357,8 @@ class StyleTransformer {
350357
let slottedIndex = selector.indexOf(SLOTTED);
351358
if (selector.indexOf(HOST) >= 0) {
352359
selector = this._transformHostSelector(selector, hostScope);
360+
} else if (!disableShadowParts && selector.match(PART)) {
361+
selector = this._transformPartSelector(selector, hostScope);
353362
// replace other selectors with scoping class
354363
} else if (slottedIndex !== 0) {
355364
selector = scope ? this._transformSimpleSelector(selector, scope) :
@@ -396,6 +405,20 @@ class StyleTransformer {
396405
return output.join('');
397406
}
398407

408+
_transformPartSelector(selector, scope) {
409+
const parsed = parsePartSelector(selector);
410+
if (parsed === null) {
411+
return selector;
412+
}
413+
const {combinators, elementName, selectors, parts, pseudos} = parsed;
414+
// Earlier we did a hacky transform from "part(foo bar)" to "part(foo,bar)"
415+
// so that the SIMPLE_SELECTOR regex didn't get confused by spaces.
416+
const partSelector =
417+
formatShadyPartSelector(scope, elementName, parts.replace(',', ' '));
418+
return (scope === 'document' ? '' : scope + ' ') +
419+
`${combinators}${elementName}${selectors} ${partSelector}${pseudos}`;
420+
}
421+
399422
// :host(...) -> scopeName...
400423
_transformHostSelector(selector, hostScope) {
401424
let m = selector.match(HOST_PAREN);
@@ -457,6 +480,8 @@ class StyleTransformer {
457480
return '';
458481
} else if (selector.match(SLOTTED)) {
459482
return this._transformComplexSelector(selector, SCOPE_DOC_SELECTOR);
483+
} else if (!disableShadowParts && selector.match(PART)) {
484+
return this._transformPartSelector(selector, 'document');
460485
} else {
461486
return this._transformSimpleSelector(selector.trim(), SCOPE_DOC_SELECTOR);
462487
}
@@ -470,6 +495,7 @@ const SIMPLE_SELECTOR_SEP = /(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=[])+)/g;
470495
const SIMPLE_SELECTOR_PREFIX = /[[.:#*]/;
471496
const HOST = ':host';
472497
const ROOT = ':root';
498+
const PART = /::part\([^)]*\)/;
473499
const SLOTTED = '::slotted';
474500
const SLOTTED_START = new RegExp(`^(${SLOTTED})`);
475501
// NOTE: this supports 1 nested () pair for things like

packages/shadydom/src/patches/Node.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,12 @@ export const NodePatches = utils.getOwnPropertyDescriptors({
375375
} else if (node.ownerDocument !== this.ownerDocument) {
376376
this.ownerDocument.adoptNode(node);
377377
}
378+
if (!utils.disableShadowParts) {
379+
const shim = getScopingShim();
380+
if (shim) {
381+
shim['onInsertBefore'](this, node, ref_node);
382+
}
383+
}
378384
return node;
379385
},
380386

packages/shadydom/src/style-scoping.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,4 @@ export function treeVisitor(node, visitorFn) {
134134
treeVisitor(n, visitorFn);
135135
}
136136
}
137-
}
137+
}

packages/shadydom/src/utils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {shadyDataForNode} from './shady-data.js';
1212
/** @type {!Object} */
1313
export const settings = window['ShadyDOM'] || {};
1414

15+
/** @type {boolean} */
16+
export const disableShadowParts =
17+
Boolean(window['ShadyCSS'] && window['ShadyCSS']['disableShadowParts']);
18+
1519
settings.hasNativeShadowDOM = Boolean(Element.prototype.attachShadow && Node.prototype.getRootNode);
1620

1721
// The user might need to pass the custom elements polyfill a flag by setting an

packages/tests/shadycss/runner.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@
7373
webcomponents = 'wc-register=true';
7474
}
7575
// if native is available, make sure to test polyfill
76-
if (Element.prototype.attachShadow && document.documentElement.getRootNode) {
76+
const hasNativeShadow =
77+
Element.prototype.attachShadow && document.documentElement.getRootNode;
78+
if (hasNativeShadow) {
7779
webcomponents = addUrlOption(webcomponents, 'wc-shadydom=true');
7880
}
7981
// ce + sd becomes a single test iteration.
@@ -112,5 +114,14 @@
112114
]);
113115
}
114116

117+
// Skip shadow part tests if the browser supports shadow DOM but not shadow
118+
// parts, unless we are forcing the polyfill on. ShadyCSS doesn't polyfill
119+
// just shadow parts on top of native shadow DOM, so these would fail.
120+
const hasNativeParts = 'part' in HTMLElement.prototype;
121+
if (hasNativeShadow && !hasNativeParts) {
122+
suites = suites.filter((url) =>
123+
!(url.startsWith('shadow-parts/') && !url.includes('wc-shadydom')));
124+
}
125+
115126
WCT.loadSuites(suites);
116127
</script>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<!DOCTYPE html>
2+
<!--
3+
@license
4+
Copyright (c) 2020 The Polymer Project Authors. All rights reserved.
5+
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
6+
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
7+
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
8+
Code distributed by Google as part of the polymer project is also
9+
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
10+
-->
11+
12+
<title>shadycss/shadow-parts/basic</title>
13+
14+
<script src="../test-flags.js"></script>
15+
<script src="../../node_modules/wct-browser-legacy/browser.js"></script>
16+
<script src="../../node_modules/@webcomponents/webcomponents-platform/webcomponents-platform.js"></script>
17+
<script src="../../node_modules/es6-promise/dist/es6-promise.auto.min.js"></script>
18+
<script src="../../node_modules/@webcomponents/template/template.js"></script>
19+
<script src="../../node_modules/@webcomponents/shadydom/shadydom.min.js"></script>
20+
<script src="../../node_modules/@webcomponents/custom-elements/custom-elements.min.js"></script>
21+
<script src="../../node_modules/@webcomponents/shadycss/scoping-shim.min.js"></script>
22+
<script src="../module/generated/make-element.js"></script>
23+
24+
<template id="x-a">
25+
<style>
26+
x-b::part(pa) {
27+
color: red;
28+
}
29+
</style>
30+
<x-b></x-b>
31+
</template>
32+
33+
<template id="x-b">
34+
<div part="pa">pa</div>
35+
<div part="pb">pb</div>
36+
</template>
37+
38+
<script type="module">
39+
import {pierce, color, black, red} from './test-utils.js';
40+
41+
suite('basic part styles', () => {
42+
suiteSetup(() => {
43+
makeElement('x-a');
44+
makeElement('x-b');
45+
});
46+
47+
let xa, pa, pb;
48+
49+
setup(() => {
50+
xa = document.createElement('x-a');
51+
document.body.appendChild(xa);
52+
pa = pierce('x-a', 'x-b', '[part=pa]');
53+
pb = pierce('x-a', 'x-b', '[part=pb]');
54+
});
55+
56+
teardown(() => {
57+
document.body.removeChild(xa);
58+
});
59+
60+
test('selected part receives style', () => {
61+
assert.equal(color(pa), red);
62+
});
63+
64+
test('not selected part does not receive style', () => {
65+
assert.equal(color(pb), black);
66+
});
67+
68+
test('newly added part receives style', () => {
69+
const xb2 = document.createElement('x-b');
70+
xa.shadowRoot.appendChild(xb2);
71+
const pa2 = pierce(xb2, '[part=pa]');
72+
assert.equal(color(pa2), red);
73+
});
74+
75+
if (!window.ShadyCSS.nativeShadow) {
76+
test('parts get shady-part attributes', () => {
77+
assert.equal(pa.getAttribute('shady-part'), 'x-a:x-b:pa');
78+
assert.equal(pb.getAttribute('shady-part'), 'x-a:x-b:pb');
79+
});
80+
81+
test('style transformed with shady-part attribute', () => {
82+
const style = document.querySelector('style[scope=x-a]');
83+
assert.equal(
84+
style.textContent.replace(/\n/gm, ' ').replace(/\s+/g, ' ').trim(),
85+
`x-a x-b [shady-part~="x-a:x-b:pa"] { color: red; }`
86+
);
87+
});
88+
}
89+
});
90+
</script>

0 commit comments

Comments
 (0)