diff --git a/.changeset/itchy-plums-sip.md b/.changeset/itchy-plums-sip.md new file mode 100644 index 000000000..d911979d3 --- /dev/null +++ b/.changeset/itchy-plums-sip.md @@ -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. diff --git a/docs/docs/systems/core/overview.md b/docs/docs/systems/core/overview.md index f8d68ee29..4aab78388 100644 --- a/docs/docs/systems/core/overview.md +++ b/docs/docs/systems/core/overview.md @@ -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() diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts index a3590817f..c79620912 100644 --- a/packages/core/index.d.ts +++ b/packages/core/index.d.ts @@ -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'; diff --git a/packages/core/index.js b/packages/core/index.js index 9609c39a2..37c08e9d0 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -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'; diff --git a/packages/core/src/ScopedStylesMixin.js b/packages/core/src/ScopedStylesMixin.js new file mode 100644 index 000000000..72c2a18e7 --- /dev/null +++ b/packages/core/src/ScopedStylesMixin.js @@ -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} 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); diff --git a/packages/core/test/ScopedStylesMixin.test.js b/packages/core/test/ScopedStylesMixin.test.js new file mode 100644 index 000000000..fe116319a --- /dev/null +++ b/packages/core/test/ScopedStylesMixin.test.js @@ -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`

Some Text

`; + } + + 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``)); + 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` `)); + 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``)); + 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(` +
+ `); + const wrapper2 = await fixture(` +
+ `); + const el = document.createElement('scoped-el'); + wrapper.appendChild(el); + wrapper2.appendChild(el); + expect(el.children[1]).to.be.undefined; + }); +}); diff --git a/packages/core/types/ScopedStylesMixinTypes.d.ts b/packages/core/types/ScopedStylesMixinTypes.d.ts new file mode 100644 index 000000000..7756eee07 --- /dev/null +++ b/packages/core/types/ScopedStylesMixinTypes.d.ts @@ -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>( + superclass: T, +): T & Constructor & Pick; + +export type ScopedStylesMixin = typeof ScopedStylesMixinImplementation; diff --git a/packages/input-range/src/LionInputRange.js b/packages/input-range/src/LionInputRange.js index 13e02c774..88beae327 100644 --- a/packages/input-range/src/LionInputRange.js +++ b/packages/input-range/src/LionInputRange.js @@ -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 { `; } - - /** @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); - } } diff --git a/packages/input-range/test/lion-input-range.test.js b/packages/input-range/test/lion-input-range.test.js index 96b32001f..d40473406 100644 --- a/packages/input-range/test/lion-input-range.test.js +++ b/packages/input-range/test/lion-input-range.test.js @@ -14,35 +14,6 @@ describe('', () => { expect(el._inputNode.type).to.equal('range'); }); - it('contain the scoped css class for the slotted input style', async () => { - const el = await fixture(` - - `); - 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(` - - `); - 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(` -
- `); - const wrapper2 = await fixture(` -
- `); - 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` diff --git a/yarn.lock b/yarn.lock index 857af292b..bc1bee1e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"