chore: refactor to reactive controller
This commit is contained in:
parent
006a47e530
commit
17ae20df86
9 changed files with 79 additions and 91 deletions
|
|
@ -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,
|
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.
|
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.
|
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
|
```js
|
||||||
class MyInputRange extends LionInputRange {
|
class MyInputRange extends LionInputRange {
|
||||||
static rangeStyles(scope) {
|
static scopedStyles(scope) {
|
||||||
return css`
|
return css`
|
||||||
.${scope} .form-control::-webkit-slider-runnable-track {
|
.${scope} .form-control::-webkit-slider-runnable-track {
|
||||||
background-color: lightgreen;
|
background-color: lightgreen;
|
||||||
|
|
|
||||||
2
packages/core/index.d.ts
vendored
2
packages/core/index.d.ts
vendored
|
|
@ -80,7 +80,7 @@ export { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
export { DelegateMixin } from './src/DelegateMixin.js';
|
export { DelegateMixin } from './src/DelegateMixin.js';
|
||||||
export { DisabledMixin } from './src/DisabledMixin.js';
|
export { DisabledMixin } from './src/DisabledMixin.js';
|
||||||
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.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 { SlotMixin } from './src/SlotMixin.js';
|
||||||
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
|
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
|
||||||
export { browserDetection } from './src/browserDetection.js';
|
export { browserDetection } from './src/browserDetection.js';
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
export { DelegateMixin } from './src/DelegateMixin.js';
|
export { DelegateMixin } from './src/DelegateMixin.js';
|
||||||
export { DisabledMixin } from './src/DisabledMixin.js';
|
export { DisabledMixin } from './src/DisabledMixin.js';
|
||||||
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.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 { SlotMixin } from './src/SlotMixin.js';
|
||||||
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
|
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
|
||||||
export { browserDetection } from './src/browserDetection.js';
|
export { browserDetection } from './src/browserDetection.js';
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { expect, fixture } from '@open-wc/testing';
|
import { expect, fixture } from '@open-wc/testing';
|
||||||
// import { html as _html } from 'lit/static-html.js';
|
// import { html as _html } from 'lit/static-html.js';
|
||||||
import { LitElement, css, html } from '../index.js';
|
import { LitElement, css, html } from '../index.js';
|
||||||
import { ScopedStylesMixin } from '../src/ScopedStylesMixin.js';
|
import { ScopedStylesController } from '../src/ScopedStylesController.js';
|
||||||
|
|
||||||
describe('ScopedStylesMixin', () => {
|
describe('ScopedStylesMixin', () => {
|
||||||
class Scoped extends ScopedStylesMixin(LitElement) {
|
class Scoped extends LitElement {
|
||||||
/**
|
/**
|
||||||
* @param {import('lit').CSSResult} scope
|
* @param {import('lit').CSSResult} scope
|
||||||
* @returns {import('lit').CSSResultGroup}
|
* @returns {import('lit').CSSResultGroup}
|
||||||
|
|
@ -17,6 +17,11 @@ describe('ScopedStylesMixin', () => {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.scopedStylesController = new ScopedStylesController(this);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html` <p class="test">Some Text</p> `;
|
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 () => {
|
it('contains the scoped css class for the slotted input style', async () => {
|
||||||
const el = /** @type {Scoped} */ (await fixture(html`<scoped-el></scoped-el>`));
|
const el = /** @type {Scoped & ScopedStylesController} */ (
|
||||||
expect(el.classList.contains(el.scopedClass)).to.equal(true);
|
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 () => {
|
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].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 () => {
|
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 testText = /** @type {HTMLElement} */ (el.querySelector('.test'));
|
||||||
const cl = Array.from(el.classList);
|
const cl = Array.from(el.classList);
|
||||||
expect(cl.find(item => item.startsWith('scoped-el-'))).to.not.be.undefined;
|
expect(cl.find(item => item.startsWith('scoped-el-'))).to.not.be.undefined;
|
||||||
20
packages/core/types/ScopedStylesMixinTypes.d.ts
vendored
20
packages/core/types/ScopedStylesMixinTypes.d.ts
vendored
|
|
@ -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;
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable import/no-extraneous-dependencies */
|
/* 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 { LionInput } from '@lion/input';
|
||||||
import { formatNumber } from '@lion/localize';
|
import { formatNumber } from '@lion/localize';
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { formatNumber } from '@lion/localize';
|
||||||
*
|
*
|
||||||
* @customElement `lion-input-range`
|
* @customElement `lion-input-range`
|
||||||
*/
|
*/
|
||||||
export class LionInputRange extends ScopedStylesMixin(LionInput) {
|
export class LionInputRange extends LionInput {
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -59,6 +59,8 @@ export class LionInputRange extends ScopedStylesMixin(LionInput) {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
/** @type {ScopedStylesController} */
|
||||||
|
this.scopedStylesController = new ScopedStylesController(this);
|
||||||
this.min = Infinity;
|
this.min = Infinity;
|
||||||
this.max = Infinity;
|
this.max = Infinity;
|
||||||
this.step = 1;
|
this.step = 1;
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,8 @@ export class LionInput extends NativeTextFieldMixin(LionField) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PropertyKey} name
|
* @param {PropertyKey} [name]
|
||||||
* @param {?} oldValue
|
* @param {?} [oldValue]
|
||||||
*/
|
*/
|
||||||
requestUpdate(name, oldValue) {
|
requestUpdate(name, oldValue) {
|
||||||
super.requestUpdate(name, oldValue);
|
super.requestUpdate(name, oldValue);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue