feat(core): add ScopedStylesMixin

This commit is contained in:
jorenbroekema 2022-02-28 13:28:20 +01:00
parent cc281b505a
commit 57d2c62b78
10 changed files with 206 additions and 63 deletions

View file

@ -0,0 +1,6 @@
---
'@lion/core': patch
'@lion/input-range': patch
---
Abstract input-range's scoped styles feature to a mixin for reuse. With this mixin, you can scope styles on a LightDOM component.

View file

@ -18,7 +18,8 @@ import { LitElement, html, render } from 'lit-element';
- [function to deduplicate mixins (dedupeMixin)](#deduping-of-mixins)
- Mixin to handle disabled (DisabledMixin)
- Mixin to handle disabled AND tabIndex (DisabledWithTabIndexMixin)
- Mixin to manage auto generated needed slot elements in light dom (SlotMixin)
- Mixin to manage auto-generated needed slot elements in light dom (SlotMixin)
- Mixin to create scoped styles in LightDOM-using components (ScopedStylesMixin)
> These features are not well documented - care to help out?
@ -49,7 +50,7 @@ In other words, this may happen to the protoype chain `... -> M2 -> BaseMixin ->
An example of this may be a `LocalizeMixin` used across different components and mixins. Some mixins may need it and many components need it too and can not rely on other mixins to have it by default, so must inherit from it independently.
The more generic the mixin is, the higher the chance of being appliend more than once. As a mixin author you can't control how it is used, and can't always predict it. So as a safety measure it is always recommended to create deduping mixins.
The more generic the mixin is, the higher the chance of being applied more than once. As a mixin author you can't control how it is used, and can't always predict it. So as a safety measure it is always recommended to create deduping mixins.
### Usage of dedupeMixin()

View file

@ -80,6 +80,7 @@ export { dedupeMixin } from '@open-wc/dedupe-mixin';
export { DelegateMixin } from './src/DelegateMixin.js';
export { DisabledMixin } from './src/DisabledMixin.js';
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
export { ScopedStylesMixin } from './src/ScopedStylesMixin.js';
export { SlotMixin } from './src/SlotMixin.js';
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
export { browserDetection } from './src/browserDetection.js';

View file

@ -71,6 +71,7 @@ export { dedupeMixin } from '@open-wc/dedupe-mixin';
export { DelegateMixin } from './src/DelegateMixin.js';
export { DisabledMixin } from './src/DisabledMixin.js';
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
export { ScopedStylesMixin } from './src/ScopedStylesMixin.js';
export { SlotMixin } from './src/SlotMixin.js';
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
export { browserDetection } from './src/browserDetection.js';

View file

@ -0,0 +1,55 @@
import { dedupeMixin } from '@open-wc/dedupe-mixin';
import { unsafeCSS, css } from 'lit';
/**
* @typedef {import('../types/ScopedStylesMixinTypes').ScopedStylesMixin} ScopedStylesMixin
*/
/**
* @type {ScopedStylesMixin}
* @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
*/
const ScopedStylesMixinImplementation = superclass =>
// eslint-disable-next-line no-shadow
// @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
class ScopedStylesHost extends superclass {
/**
* @param {import('lit').CSSResult} scope
* @return {import('lit').CSSResultGroup}
*/
// eslint-disable-next-line no-unused-vars
static scopedStyles(scope) {
return css``;
}
constructor() {
super();
// Perhaps use constructable stylesheets instead once Safari supports replace(Sync) methods
this.__styleTag = document.createElement('style');
this.scopedClass = `${this.localName}-${Math.floor(Math.random() * 10000)}`;
}
connectedCallback() {
super.connectedCallback();
this.classList.add(this.scopedClass);
this.__setupStyleTag();
}
disconnectedCallback() {
super.disconnectedCallback();
this.__teardownStyleTag();
}
__setupStyleTag() {
this.__styleTag.textContent = /** @type {typeof ScopedStylesHost} */ (this.constructor)
.scopedStyles(unsafeCSS(this.scopedClass))
.toString();
this.insertBefore(this.__styleTag, this.childNodes[0]);
}
__teardownStyleTag() {
this.removeChild(this.__styleTag);
}
};
export const ScopedStylesMixin = dedupeMixin(ScopedStylesMixinImplementation);

View file

@ -0,0 +1,64 @@
import { expect, fixture } from '@open-wc/testing';
// import { html as _html } from 'lit/static-html.js';
import { LitElement, css, html } from '../index.js';
import { ScopedStylesMixin } from '../src/ScopedStylesMixin.js';
describe('ScopedStylesMixin', () => {
class Scoped extends ScopedStylesMixin(LitElement) {
/**
* @param {import('lit').CSSResult} scope
* @returns {import('lit').CSSResultGroup}
*/
static scopedStyles(scope) {
return css`
.${scope} .test {
color: #fff000;
}
`;
}
render() {
return html` <p class="test">Some Text</p> `;
}
createRenderRoot() {
return this;
}
}
before(() => {
customElements.define('scoped-el', Scoped);
});
it('contains the scoped css class for the slotted input style', async () => {
const el = /** @type {Scoped} */ (await fixture(html`<scoped-el></scoped-el>`));
expect(el.classList.contains(el.scopedClass)).to.equal(true);
});
it('adds a style tag as the first child which contains a class selector to the element', async () => {
const el = /** @type {Scoped} */ (await fixture(html` <scoped-el></scoped-el> `));
expect(el.children[0].tagName).to.equal('STYLE');
expect(el.children[0].innerHTML).to.contain(el.scopedClass);
});
it('the scoped styles are applied correctly to the DOM elements', async () => {
const el = /** @type {Scoped} */ (await fixture(html`<scoped-el></scoped-el>`));
const testText = /** @type {HTMLElement} */ (el.querySelector('.test'));
const cl = Array.from(el.classList);
expect(cl.find(item => item.startsWith('scoped-el-'))).to.not.be.undefined;
expect(getComputedStyle(testText).getPropertyValue('color')).to.equal('rgb(255, 240, 0)');
});
it('does cleanup of the style tag when moving or deleting the el', async () => {
const wrapper = await fixture(`
<div></div>
`);
const wrapper2 = await fixture(`
<div></div>
`);
const el = document.createElement('scoped-el');
wrapper.appendChild(el);
wrapper2.appendChild(el);
expect(el.children[1]).to.be.undefined;
});
});

View file

@ -0,0 +1,20 @@
import { Constructor } from '@open-wc/dedupe-mixin';
import { CSSResultGroup, CSSResult, LitElement } from 'lit';
export declare class ScopedStylesHost {
static scopedStyles(scope: CSSResult): CSSResultGroup;
__styleTag: HTMLStyleElement;
scopedClass: string;
private __setupStyleTag(): void;
private __teardownStyleTag(): void;
}
export declare function ScopedStylesMixinImplementation<T extends Constructor<LitElement>>(
superclass: T,
): T & Constructor<ScopedStylesHost> & Pick<typeof ScopedStylesHost, keyof typeof ScopedStylesHost>;
export type ScopedStylesMixin = typeof ScopedStylesMixinImplementation;

View file

@ -1,5 +1,5 @@
/* eslint-disable import/no-extraneous-dependencies */
import { css, html, unsafeCSS } from '@lion/core';
import { css, html, ScopedStylesMixin } from '@lion/core';
import { LionInput } from '@lion/input';
import { formatNumber } from '@lion/localize';
@ -12,7 +12,7 @@ import { formatNumber } from '@lion/localize';
*
* @customElement `lion-input-range`
*/
export class LionInputRange extends LionInput {
export class LionInputRange extends ScopedStylesMixin(LionInput) {
/** @type {any} */
static get properties() {
return {
@ -42,7 +42,7 @@ export class LionInputRange extends LionInput {
/**
* @param {CSSResult} scope
*/
static rangeStyles(scope) {
static scopedStyles(scope) {
return css`
/* Custom input range styling comes here, be aware that this won't work for polyfilled browsers */
.${scope} .form-control {
@ -69,22 +69,6 @@ export class LionInputRange extends LionInput {
* @param {string} modelValue
*/
this.parser = modelValue => parseFloat(modelValue);
this.scopedClass = `${this.localName}-${Math.floor(Math.random() * 10000)}`;
/** @private */
this.__styleTag = document.createElement('style');
}
connectedCallback() {
super.connectedCallback();
/* eslint-disable-next-line wc/no-self-class */
this.classList.add(this.scopedClass);
this.__setupStyleTag();
}
disconnectedCallback() {
super.disconnectedCallback();
this.__teardownStyleTag();
}
/** @param {import('@lion/core').PropertyValues } changedProperties */
@ -151,17 +135,4 @@ export class LionInputRange extends LionInput {
</div>
`;
}
/** @private */
__setupStyleTag() {
this.__styleTag.textContent = /** @type {typeof LionInputRange} */ (this.constructor)
.rangeStyles(unsafeCSS(this.scopedClass))
.toString();
this.insertBefore(this.__styleTag, this.childNodes[0]);
}
/** @private */
__teardownStyleTag() {
this.removeChild(this.__styleTag);
}
}

View file

@ -14,35 +14,6 @@ describe('<lion-input-range>', () => {
expect(el._inputNode.type).to.equal('range');
});
it('contain the scoped css class for the slotted input style', async () => {
const el = await fixture(`
<lion-input-range></lion-input-range>
`);
expect(el.classList.contains(el.scopedClass)).to.equal(true);
});
it('adds a style tag as the first child which contains a class selector to the element', async () => {
const el = await fixture(`
<lion-input-range></lion-input-range>
`);
expect(el.children[0].tagName).to.equal('STYLE');
expect(el.children[0].innerHTML).to.contain(el.scopedClass);
});
it('does cleanup of the style tag when moving or deleting the el', async () => {
const wrapper = await fixture(`
<div></div>
`);
const wrapper2 = await fixture(`
<div></div>
`);
const el = document.createElement('lion-input-range');
wrapper.appendChild(el);
wrapper2.appendChild(el);
expect(el.children[1].tagName).to.not.equal('STYLE');
});
it('displays the modelValue and unit', async () => {
const el = await fixture(html`
<lion-input-range .modelValue=${75} unit="${`%`}"></lion-input-range>

View file

@ -1322,6 +1322,16 @@
dependencies:
"@lion/core" "0.18.2"
"@lion/combobox@^0.8.6":
version "0.8.7"
resolved "https://registry.yarnpkg.com/@lion/combobox/-/combobox-0.8.7.tgz#74414fddb317f8abb27409ce0cf1a6708ecd6d9d"
integrity sha512-1SfaqOW1zihuX3oXlLnawwC9m9MajZoKZ5Ow4KYCKKiBkAVGifDAHTDXz/ls3hyWICFn7/oYtqRpa7qbwZ0CXQ==
dependencies:
"@lion/core" "0.20.0"
"@lion/form-core" "0.15.5"
"@lion/listbox" "0.11.0"
"@lion/overlays" "0.30.0"
"@lion/core@0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@lion/core/-/core-0.16.0.tgz#c4c8ac81b8d5bece6d40d561a392382c7ae73533"
@ -1350,6 +1360,15 @@
"@open-wc/scoped-elements" "^2.0.1"
lit "^2.0.2"
"@lion/core@0.20.0":
version "0.20.0"
resolved "https://registry.yarnpkg.com/@lion/core/-/core-0.20.0.tgz#3982dc7a6ec5e742680b7453a684840358799759"
integrity sha512-CyJNGNMWkPI/kf9lhMnkZInrLLvmPQxt6qO7B60S2k7UNANd2R53zNgqTtllYp16Ej+4Mol9JoW2Ik6+1vq9ig==
dependencies:
"@open-wc/dedupe-mixin" "^1.3.0"
"@open-wc/scoped-elements" "^2.0.1"
lit "^2.0.2"
"@lion/form-core@0.15.4":
version "0.15.4"
resolved "https://registry.yarnpkg.com/@lion/form-core/-/form-core-0.15.4.tgz#e5fdc49199b9a491becf70370ab6de1407bc7a66"
@ -1358,6 +1377,22 @@
"@lion/core" "0.19.0"
"@lion/localize" "0.21.3"
"@lion/form-core@0.15.5":
version "0.15.5"
resolved "https://registry.yarnpkg.com/@lion/form-core/-/form-core-0.15.5.tgz#af40dab6c7ca32187322b84a6319a277fcbd4617"
integrity sha512-SM27hqupHZ2eq4enaaX/dbIH6fTJYrISA378zZjCBPcYSDq39EXMgnzgGXWfZzi1DPEffbl2g6VKR+UGc8xRZg==
dependencies:
"@lion/core" "0.20.0"
"@lion/localize" "0.22.0"
"@lion/listbox@0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@lion/listbox/-/listbox-0.11.0.tgz#f730714803f14a6d001a5e0c2cdff17146eccac7"
integrity sha512-nLwNZuARVcdT5+TNqph56+g1CnXMcRMjM0vzvSNgezGTZJ3b6onumgQwEXLGhxIbB16qFbfpaPZP7SaALfFsBA==
dependencies:
"@lion/core" "0.20.0"
"@lion/form-core" "0.15.5"
"@lion/listbox@^0.10.7":
version "0.10.7"
resolved "https://registry.yarnpkg.com/@lion/listbox/-/listbox-0.10.7.tgz#9af689615ea0964e4a5613c3e5b2df9b33fc2d54"
@ -1375,6 +1410,24 @@
"@lion/core" "0.19.0"
singleton-manager "1.4.2"
"@lion/localize@0.22.0":
version "0.22.0"
resolved "https://registry.yarnpkg.com/@lion/localize/-/localize-0.22.0.tgz#88e0755b53c699231af0df0b4302de2197484bc0"
integrity sha512-75q3Xp/5A2v39mWHTaCZXc3L6nO3DD2vU5w/WPFusW6mzMJw3nzB4ot665UrjbY/HJwQV7Trjh9c/O79oh5x8Q==
dependencies:
"@bundled-es-modules/message-format" "6.0.4"
"@lion/core" "0.20.0"
singleton-manager "1.4.3"
"@lion/overlays@0.30.0":
version "0.30.0"
resolved "https://registry.yarnpkg.com/@lion/overlays/-/overlays-0.30.0.tgz#cd30c6ee8beda2ef977eb9b049cbac3549040c50"
integrity sha512-3P/VMCkzp6c0E8Nl6Mtvtqg/7J93JM3MEPi14WE9PWYQZcrtvcWSfLXCoI3Va1HoFfRuaAcN0DN0AO6Z71iFeg==
dependencies:
"@lion/core" "0.20.0"
"@popperjs/core" "^2.5.4"
singleton-manager "1.4.3"
"@lion/overlays@^0.26.1":
version "0.26.1"
resolved "https://registry.yarnpkg.com/@lion/overlays/-/overlays-0.26.1.tgz#d1bfa4f5f97108982afa7b409ba4300f8b2d2ba5"