Skip to content

Commit

Permalink
fix(slotted): fix applying polyfilled slotted css to nested slot
Browse files Browse the repository at this point in the history
Closes #2183
  • Loading branch information
adamdbradley committed Feb 5, 2020
1 parent e9c36fd commit e4229db
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 9 deletions.
52 changes: 43 additions & 9 deletions src/utils/shadow-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,22 +200,46 @@ const colonHostContextPartReplacer = (host: string, part: string, suffix: string
}
};

const convertColonSlotted = (cssText: string, slotAttr: string) => {
const regExp = _cssColonSlottedRe;
const convertColonSlotted = (cssText: string, slotScopeId: string) => {
const slotClass = '.' + slotScopeId + ' > ';
const selectors: {orgSelector: string; updatedSelector: string}[] = [];

return cssText.replace(regExp, (...m: string[]) => {
cssText = cssText.replace(_cssColonSlottedRe, (...m: string[]) => {
if (m[2]) {
const compound = m[2].trim();
const suffix = m[3];
const slottedSelector = slotClass + compound + suffix;

let prefixSelector = '';
for (let i: number = m[4] as any - 1; i >= 0; i--) {
const char = m[5][i];
if (char === '}' || char === ',') {
break;
}
prefixSelector = char + prefixSelector;
}

const sel = '.' + slotAttr + ' > ' + compound + suffix;
const orgSelector = prefixSelector + slottedSelector;
const addedSelector = `${prefixSelector.trimRight()}${slottedSelector.trim()}`;
if (orgSelector.trim() !== addedSelector.trim()) {
const updatedSelector = `${addedSelector}, ${orgSelector}`;
selectors.push({
orgSelector,
updatedSelector,
});
}

return sel;
return slottedSelector;

} else {
return _polyfillHostNoCombinator + m[3];
}
});

return {
selectors,
cssText
};
};

const convertColonHostContext = (cssText: string) => {
Expand Down Expand Up @@ -368,7 +392,9 @@ const scopeCssText = (cssText: string, scopeId: string, hostScopeId: string, slo
cssText = insertPolyfillHostInCssText(cssText);
cssText = convertColonHost(cssText);
cssText = convertColonHostContext(cssText);
cssText = convertColonSlotted(cssText, slotScopeId);

const slotted = convertColonSlotted(cssText, slotScopeId);
cssText = slotted.cssText;
cssText = convertShadowDOMSelectors(cssText);

if (scopeId) {
Expand All @@ -377,7 +403,11 @@ const scopeCssText = (cssText: string, scopeId: string, hostScopeId: string, slo

cssText = cssText.replace(/-shadowcsshost-no-combinator/g, `.${hostScopeId}`);
cssText = cssText.replace(/>\s*\*\s+([^{, ]+)/gm, ' $1 ');
return cssText.trim();

return {
cssText: cssText.trim(),
slottedSelectors: slotted.selectors,
};
};


Expand Down Expand Up @@ -416,14 +446,18 @@ export const scopeCss = (cssText: string, scopeId: string, commentOriginalSelect
});
}

const scopedCssText = scopeCssText(cssText, scopeId, hostScopeId, slotScopeId, commentOriginalSelector);
cssText = [scopedCssText, ...commentsWithHash].join('\n');
const scoped = scopeCssText(cssText, scopeId, hostScopeId, slotScopeId, commentOriginalSelector);
cssText = [scoped.cssText, ...commentsWithHash].join('\n');

if (commentOriginalSelector) {
orgSelectors.forEach(({placeholder, comment}) => {
cssText = cssText.replace(placeholder, comment);
});
}

scoped.slottedSelectors.forEach(slottedSelector => {
cssText = cssText.replace(slottedSelector.orgSelector, slottedSelector.updatedSelector);
});

return cssText;
};
20 changes: 20 additions & 0 deletions src/utils/test/scope-css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,26 @@ describe('ShadowCss', function() {
expect(r).toEqual('.sc-ion-tag-s > ul, .sc-ion-tag-s > li {}');
});

it('should combine parent selector', () => {
const r = s('div{} .a .b .c ::slotted(*) {}', 'sc-ion-tag');
expect(r).toEqual('div.sc-ion-tag{} .a .b .c.sc-ion-tag-s > *, .a .b .c .sc-ion-tag-s > * {}');
});

it('same selectors', () => {
const r = s('::slotted(*) {}, ::slotted(*) {}, ::slotted(*) {}', 'sc-ion-tag');
expect(r).toEqual('.sc-ion-tag-s > * {}, .sc-ion-tag-s > * {}, .sc-ion-tag-s > * {}');
});

it('same selectors, commentOriginalSelector', () => {
const r = s('::slotted(*) {}, ::slotted(*) {}, ::slotted(*) {}', 'sc-ion-tag', true);
expect(r).toEqual('/*!@::slotted(*)*/.sc-ion-tag-s > * {}/*!@, ::slotted(*)*/.sc-ion-tag, .sc-ion-tag-s > * {}/*!@, ::slotted(*)*/.sc-ion-tag, .sc-ion-tag-s > * {}');
});

it('should combine parent selector when comma', () => {
const r = s('.a .b, .c ::slotted(*) {}', 'sc-ion-tag');
expect(r).toEqual('.a.sc-ion-tag .b.sc-ion-tag, .c.sc-ion-tag-s > *, .c .sc-ion-tag-s > * {}');
});

it('should handle multiple selector, commentOriginalSelector', () => {
const r = s('::slotted(ul), ::slotted(li) {}', 'sc-ion-tag', true);
expect(r).toEqual('/*!@::slotted(ul), ::slotted(li)*/.sc-ion-tag-s > ul, .sc-ion-tag-s > li {}');
Expand Down
11 changes: 11 additions & 0 deletions test/karma/test-app/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export namespace Components {
'href'?: string;
}
interface SlotReplaceWrapperRoot {}
interface SlottedCss {}
interface StylusCmp {}
interface SvgAttr {}
interface SvgClass {}
Expand Down Expand Up @@ -839,6 +840,12 @@ declare global {
new (): HTMLSlotReplaceWrapperRootElement;
};

interface HTMLSlottedCssElement extends Components.SlottedCss, HTMLStencilElement {}
var HTMLSlottedCssElement: {
prototype: HTMLSlottedCssElement;
new (): HTMLSlottedCssElement;
};

interface HTMLStylusCmpElement extends Components.StylusCmp, HTMLStencilElement {}
var HTMLStylusCmpElement: {
prototype: HTMLStylusCmpElement;
Expand Down Expand Up @@ -973,6 +980,7 @@ declare global {
'slot-reorder-root': HTMLSlotReorderRootElement;
'slot-replace-wrapper': HTMLSlotReplaceWrapperElement;
'slot-replace-wrapper-root': HTMLSlotReplaceWrapperRootElement;
'slotted-css': HTMLSlottedCssElement;
'stylus-cmp': HTMLStylusCmpElement;
'svg-attr': HTMLSvgAttrElement;
'svg-class': HTMLSvgClassElement;
Expand Down Expand Up @@ -1176,6 +1184,7 @@ declare namespace LocalJSX {
'href'?: string;
}
interface SlotReplaceWrapperRoot {}
interface SlottedCss {}
interface StylusCmp {}
interface SvgAttr {}
interface SvgClass {}
Expand Down Expand Up @@ -1287,6 +1296,7 @@ declare namespace LocalJSX {
'slot-reorder-root': SlotReorderRoot;
'slot-replace-wrapper': SlotReplaceWrapper;
'slot-replace-wrapper-root': SlotReplaceWrapperRoot;
'slotted-css': SlottedCss;
'stylus-cmp': StylusCmp;
'svg-attr': SvgAttr;
'svg-class': SvgClass;
Expand Down Expand Up @@ -1405,6 +1415,7 @@ declare module "@stencil/core" {
'slot-reorder-root': LocalJSX.SlotReorderRoot & JSXBase.HTMLAttributes<HTMLSlotReorderRootElement>;
'slot-replace-wrapper': LocalJSX.SlotReplaceWrapper & JSXBase.HTMLAttributes<HTMLSlotReplaceWrapperElement>;
'slot-replace-wrapper-root': LocalJSX.SlotReplaceWrapperRoot & JSXBase.HTMLAttributes<HTMLSlotReplaceWrapperRootElement>;
'slotted-css': LocalJSX.SlottedCss & JSXBase.HTMLAttributes<HTMLSlottedCssElement>;
'stylus-cmp': LocalJSX.StylusCmp & JSXBase.HTMLAttributes<HTMLStylusCmpElement>;
'svg-attr': LocalJSX.SvgAttr & JSXBase.HTMLAttributes<HTMLSvgAttrElement>;
'svg-class': LocalJSX.SvgClass & JSXBase.HTMLAttributes<HTMLSvgClassElement>;
Expand Down
28 changes: 28 additions & 0 deletions test/karma/test-app/slotted-css/cmp.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
:host {
display: inline-flex;
border: 2px dashed gray;
padding: 2px;
}

/* 1. [Edge] no border on default slot */
.content ::slotted(*) {
background-color: rgb(0, 255, 0);
}

/* 2. [Edge] extra border shows up on host */
::slotted(:not([slot="header-slot-name"])) {
border: 4px solid rgb(0, 0, 255);
color: rgb(0, 0, 255);
font-weight: bold;
}

::slotted([slot="header-slot-name"]) {
border: 4px solid rgb(255, 0, 0);
color: rgb(255, 0, 0);
font-weight: bold;
}

::slotted(*) {
margin: 8px;
padding: 8px;
}
27 changes: 27 additions & 0 deletions test/karma/test-app/slotted-css/cmp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Component, h, Host } from '@stencil/core';


@Component({
tag: 'slotted-css',
styleUrl: 'cmp.css',
shadow: true,
})
export class SlottedCss {
render() {
return (
<Host>
<section>
<header>
<slot name="header-slot-name" />
</header>
<section class="content">
<slot />
</section>
<footer>
<slot name="footer-slot-name" />
</footer>
</section>
</Host>
);
}
}
10 changes: 10 additions & 0 deletions test/karma/test-app/slotted-css/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<meta charset="utf8">
<script src="/build/testapp.esm.js" type="module"></script>
<script src="/build/testapp.js" nomodule></script>

<slotted-css>
<div class="red" slot="header-slot-name">header-slot-name: red color and border</div>
<div class="green">default slot: green background, blue border and color</div>
<div class="blue" slot="footer-slot-name">footer-slot-name: blue color and border</div>
</slotted-css>
33 changes: 33 additions & 0 deletions test/karma/test-app/slotted-css/karma.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { setupDomTests } from '../util';


describe('slotted css', function() {
const { setupDom, tearDownDom } = setupDomTests(document);
let app: HTMLElement;

beforeEach(async () => {
app = await setupDom('/slotted-css/index.html');
});
afterEach(tearDownDom);


it('assign slotted css', async () => {
const elm = app.querySelector('slotted-css');

const redElm = elm.querySelector('.red');
const redStyles = window.getComputedStyle(redElm);
expect(redStyles.color).toEqual('rgb(255, 0, 0)');

// green background, blue border and color
const greenElm = elm.querySelector('.green');
const greenStyles = window.getComputedStyle(greenElm);
expect(greenStyles.backgroundColor).toEqual('rgb(0, 255, 0)');
expect(greenStyles.color).toEqual('rgb(0, 0, 255)');

const blueElm = elm.querySelector('.blue');
const blueStyles = window.getComputedStyle(blueElm);
expect(blueStyles.color).toEqual('rgb(0, 0, 255)');

});

});

0 comments on commit e4229db

Please sign in to comment.