chore: refactor to reactive controller

This commit is contained in:
jorenbroekema 2022-02-28 16:49:44 +01:00
parent 006a47e530
commit 17ae20df86
9 changed files with 79 additions and 91 deletions

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

@ -80,7 +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 { 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,7 +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 { 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

@ -1,55 +0,0 @@
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

@ -1,10 +1,10 @@
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';
import { ScopedStylesController } from '../src/ScopedStylesController.js';
describe('ScopedStylesMixin', () => {
class Scoped extends ScopedStylesMixin(LitElement) {
class Scoped extends LitElement {
/**
* @param {import('lit').CSSResult} scope
* @returns {import('lit').CSSResultGroup}
@ -17,6 +17,11 @@ describe('ScopedStylesMixin', () => {
`;
}
constructor() {
super();
this.scopedStylesController = new ScopedStylesController(this);
}
render() {
return html` <p class="test">Some Text</p> `;
}
@ -31,18 +36,24 @@ describe('ScopedStylesMixin', () => {
});
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);
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} */ (await fixture(html` <scoped-el></scoped-el> `));
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.scopedClass);
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} */ (await fixture(html`<scoped-el></scoped-el>`));
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;

View file

@ -1,20 +0,0 @@
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, ScopedStylesMixin } from '@lion/core';
import { css, html, ScopedStylesController } 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 ScopedStylesMixin(LionInput) {
export class LionInputRange extends LionInput {
/** @type {any} */
static get properties() {
return {
@ -59,6 +59,8 @@ export class LionInputRange extends ScopedStylesMixin(LionInput) {
constructor() {
super();
/** @type {ScopedStylesController} */
this.scopedStylesController = new ScopedStylesController(this);
this.min = Infinity;
this.max = Infinity;
this.step = 1;

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);