Merge pull request #1613 from ing-bank/scoped-styles-mixin

feat(core): add ScopedStylesMixin
This commit is contained in:
Joren Broekema 2022-03-01 09:39:45 +01:00 committed by GitHub
commit 77790ac269
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 203 additions and 71 deletions

View file

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

View file

@ -88,15 +88,16 @@ The pseudo-elements associated with the slider track/thumb are not tree-abiding,
```
This means you will need to style the slotted native input from the LightDOM,
and for this we added a helper method to `LionInputRange` which inserts a `<style>` element
and for this we added added our ScopedStylesController as a controller to `LionInputRange`.
This controller inserts a `<style>` element
that emulates scoping by generating a uniquely generated class on the LionInputRange component.
This prevents the styling from conflicting with other elements on the page.
To use it when extending, override `static rangeStyles(scope)`:
To use it when extending, override `static scopedStyles(scope)`:
```js
class MyInputRange extends LionInputRange {
static rangeStyles(scope) {
static scopedStyles(scope) {
return css`
.${scope} .form-control::-webkit-slider-runnable-track {
background-color: lightgreen;

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 { ScopedStylesController } from './src/ScopedStylesController.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 { ScopedStylesController } from './src/ScopedStylesController.js';
export { SlotMixin } from './src/SlotMixin.js';
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
export { browserDetection } from './src/browserDetection.js';

View file

@ -0,0 +1,49 @@
import { unsafeCSS, css } from 'lit';
/**
* @typedef {import('lit').ReactiveControllerHost} ReactiveControllerHost
* @typedef {import('lit').ReactiveController} ReactiveController
* @implements {ReactiveController}
*/
export class ScopedStylesController {
/**
* @param {import('lit').CSSResult} scope
* @return {import('lit').CSSResultGroup}
*/
// eslint-disable-next-line no-unused-vars
static scopedStyles(scope) {
return css``;
}
/**
* @param {ReactiveControllerHost & import('lit').LitElement} host
*/
constructor(host) {
(this.host = host).addController(this);
// Perhaps use constructable stylesheets instead once Safari supports replace(Sync) methods
this.__styleTag = document.createElement('style');
this.scopedClass = `${this.host.localName}-${Math.floor(Math.random() * 10000)}`;
}
hostConnected() {
this.host.classList.add(this.scopedClass);
this.__setupStyleTag();
}
hostDisconnected() {
this.__teardownStyleTag();
}
__setupStyleTag() {
this.__styleTag.textContent = /** @type {typeof ScopedStylesController} */ (
this.host.constructor
)
.scopedStyles(unsafeCSS(this.scopedClass))
.toString();
this.host.insertBefore(this.__styleTag, this.host.childNodes[0]);
}
__teardownStyleTag() {
this.host.removeChild(this.__styleTag);
}
}

View file

@ -0,0 +1,75 @@
import { expect, fixture } from '@open-wc/testing';
// import { html as _html } from 'lit/static-html.js';
import { LitElement, css, html } from '../index.js';
import { ScopedStylesController } from '../src/ScopedStylesController.js';
describe('ScopedStylesMixin', () => {
class Scoped extends LitElement {
/**
* @param {import('lit').CSSResult} scope
* @returns {import('lit').CSSResultGroup}
*/
static scopedStyles(scope) {
return css`
.${scope} .test {
color: #fff000;
}
`;
}
constructor() {
super();
this.scopedStylesController = new ScopedStylesController(this);
}
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 & ScopedStylesController} */ (
await fixture(html`<scoped-el></scoped-el>`)
);
expect(el.classList.contains(el.scopedStylesController.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 & ScopedStylesController} */ (
await fixture(html` <scoped-el></scoped-el> `)
);
expect(el.children[0].tagName).to.equal('STYLE');
expect(el.children[0].innerHTML).to.contain(el.scopedStylesController.scopedClass);
});
it('the scoped styles are applied correctly to the DOM elements', async () => {
const el = /** @type {Scoped & ScopedStylesController} */ (
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

@ -1,4 +1,4 @@
import { ifDefined, LitElement } from '@lion/core';
import { LitElement } from '@lion/core';
import { IsNumber, LionField, Validator } from '@lion/form-core';
import '@lion/form-core/define';
import { localizeTearDown } from '@lion/localize/test-helpers';
@ -1387,8 +1387,8 @@ export function runFormGroupMixinSuite(cfg = {}) {
render() {
return html`
<${tag}
.modelValue=${ifDefined(this.modelValue)}
.serializedValue=${ifDefined(this.serializedValue)}>
.modelValue=${this.modelValue}
.serializedValue=${this.serializedValue}>
${this.fields.map(field => {
if (typeof field === 'object') {
return html`<${childTag} name="${field.name}" .modelValue="${field.value}"></${childTag}>`;

View file

@ -1,5 +1,5 @@
/* eslint-disable import/no-extraneous-dependencies */
import { css, html, unsafeCSS } from '@lion/core';
import { css, html, ScopedStylesController } from '@lion/core';
import { LionInput } from '@lion/input';
import { formatNumber } from '@lion/localize';
@ -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 {
@ -59,6 +59,8 @@ export class LionInputRange extends LionInput {
constructor() {
super();
/** @type {ScopedStylesController} */
this.scopedStylesController = new ScopedStylesController(this);
this.min = Infinity;
this.max = Infinity;
this.step = 1;
@ -69,22 +71,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 +137,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

@ -64,8 +64,8 @@ export class LionInput extends NativeTextFieldMixin(LionField) {
}
/**
* @param {PropertyKey} name
* @param {?} oldValue
* @param {PropertyKey} [name]
* @param {?} [oldValue]
*/
requestUpdate(name, oldValue) {
super.requestUpdate(name, oldValue);

View file

@ -12,7 +12,8 @@
"alwaysStrict": true,
"types": ["node", "mocha", "sinon"],
"esModuleInterop": true,
"suppressImplicitAnyIndexErrors": true
"suppressImplicitAnyIndexErrors": true,
"skipLibCheck": true
},
"include": ["packages/**/*.js", "packages-node/**/*.js"],
"exclude": [

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"