diff --git a/packages/core/index.js b/packages/core/index.js index 0985137e7..5b06d917f 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -1,8 +1,3 @@ -/** - * Info for TypeScript users: - * For now please import types from lit-element and lit-html directly. - */ - // lit-element export { css, diff --git a/packages/core/package.json b/packages/core/package.json index 8f19358eb..c0a367df0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,11 +13,14 @@ "main": "index.js", "module": "index.js", "files": [ + "*.d.ts", "*.js", "docs", "src", + "test", "test-helpers", - "translations" + "translations", + "types" ], "scripts": { "prepublishOnly": "../../scripts/npm-prepublish.js", @@ -26,7 +29,7 @@ }, "sideEffects": false, "dependencies": { - "@open-wc/dedupe-mixin": "^1.2.1", + "@open-wc/dedupe-mixin": "^1.2.18", "@open-wc/scoped-elements": "^1.0.3", "lit-element": "^2.2.1", "lit-html": "^1.0.0" diff --git a/packages/core/src/DelegateMixin.js b/packages/core/src/DelegateMixin.js index 04f5ee9a1..3c4dba16c 100644 --- a/packages/core/src/DelegateMixin.js +++ b/packages/core/src/DelegateMixin.js @@ -1,203 +1,177 @@ /* eslint-disable class-methods-use-this */ - import { dedupeMixin } from '@open-wc/dedupe-mixin'; /** - * # DelegateMixin - * Forwards defined events, methods, properties and attributes to the defined target. - * - * @example - * get delegations() { - * return { - * ...super.delegations, - * target: () => this.shadowRoot.getElementById('button1'), - * events: ['click'], - * methods: ['click'], - * properties: ['disabled'], - * attributes: ['disabled'], - * }; - * } - * - * render() { - * return html` - * - * `; - * } - * - * @type {function()} - * @polymerMixin - * @mixinFunction + * @typedef {import('../types/DelegateMixinTypes').DelegateMixin} DelegateMixin */ -export const DelegateMixin = dedupeMixin( - superclass => - // eslint-disable-next-line - class DelegateMixin extends superclass { - constructor() { - super(); - this.__eventsQueue = []; - this.__propertiesQueue = {}; - this.__setupPropertyDelegation(); - } - /** - * @returns {{target: null, events: Array, methods: Array, properties: Array, attributes: Array}} - */ - get delegations() { - return { - target: null, - events: [], - methods: [], - properties: [], - attributes: [], - }; - } +/** + * @typedef DelegateEvent + * @property {string} type - Type of event + * @property {Array} args - Event arguments + */ - connectedCallback() { - if (super.connectedCallback) { - super.connectedCallback(); - } - this._connectDelegateMixin(); - } +/** @type {DelegateMixin} */ +const DelegateMixinImplementation = superclass => + // eslint-disable-next-line + class DelegateMixin extends superclass { + constructor() { + super(); - updated(...args) { - super.updated(...args); - this._connectDelegateMixin(); - } + /** @type {DelegateEvent[]} */ + this.__eventsQueue = []; - /** - * @param {string} type - * @param {Object} args - */ - addEventListener(type, ...args) { - const delegatedEvents = this.delegations.events; - if (delegatedEvents.indexOf(type) > -1) { - if (this.delegationTarget) { - this.delegationTarget.addEventListener(type, ...args); - } else { - this.__eventsQueue.push({ type, args }); - } + /** @type {Object.} */ + this.__propertiesQueue = {}; + this.__setupPropertyDelegation(); + } + + /** + * @returns {{target: Function, events: string[], methods: string[], properties: string[], attributes: string[]}} + */ + get delegations() { + return { + target: () => {}, + events: [], + methods: [], + properties: [], + attributes: [], + }; + } + + connectedCallback() { + if (super.connectedCallback) { + super.connectedCallback(); + } + this._connectDelegateMixin(); + } + + /** @param {import('lit-element').PropertyValues } changedProperties */ + updated(changedProperties) { + super.updated(changedProperties); + this._connectDelegateMixin(); + } + + /** + * @param {string} type + * @param {...Object} args + */ + addEventListener(type, ...args) { + const delegatedEvents = this.delegations.events; + if (delegatedEvents.indexOf(type) > -1) { + if (this.delegationTarget) { + this.delegationTarget.addEventListener(type, ...args); } else { - super.addEventListener(type, ...args); + this.__eventsQueue.push({ type, args }); } + } else { + super.addEventListener(type, ...args); } + } - /** - * @param {string} name - * @param {string} value - */ - setAttribute(name, value) { - const attributeNames = this.delegations.attributes; - if (attributeNames.indexOf(name) > -1) { - if (this.delegationTarget) { - this.delegationTarget.setAttribute(name, value); - } - super.removeAttribute(name); - } else { - super.setAttribute(name, value); - } - } - - /** - * @param {string} name - */ - removeAttribute(name) { - const attributeNames = this.delegations.attributes; - if (attributeNames.indexOf(name) > -1) { - if (this.delegationTarget) { - this.delegationTarget.removeAttribute(name); - } + /** + * @param {string} name + * @param {string} value + */ + setAttribute(name, value) { + const attributeNames = this.delegations.attributes; + if (attributeNames.indexOf(name) > -1) { + if (this.delegationTarget) { + this.delegationTarget.setAttribute(name, value); } super.removeAttribute(name); + } else { + super.setAttribute(name, value); } + } - /** - * @protected - */ - _connectDelegateMixin() { - if (this.__connectedDelegateMixin) return; - - if (!this.delegationTarget) { - this.delegationTarget = this.delegations.target(); - } - + /** + * @param {string} name + */ + removeAttribute(name) { + const attributeNames = this.delegations.attributes; + if (attributeNames.indexOf(name) > -1) { if (this.delegationTarget) { - this.__emptyEventListenerQueue(); - this.__emptyPropertiesQueue(); - this.__initialAttributeDelegation(); - - this.__connectedDelegateMixin = true; + this.delegationTarget.removeAttribute(name); } } + super.removeAttribute(name); + } - /** - * @private - */ - __setupPropertyDelegation() { - const propertyNames = this.delegations.properties.concat(this.delegations.methods); - propertyNames.forEach(propertyName => { - Object.defineProperty(this, propertyName, { - get() { - const target = this.delegationTarget; - if (target) { - if (typeof target[propertyName] === 'function') { - return target[propertyName].bind(target); - } - return target[propertyName]; + _connectDelegateMixin() { + if (this.__connectedDelegateMixin) return; + + if (!this.delegationTarget) { + this.delegationTarget = this.delegations.target(); + } + + if (this.delegationTarget) { + this.__emptyEventListenerQueue(); + this.__emptyPropertiesQueue(); + this.__initialAttributeDelegation(); + + this.__connectedDelegateMixin = true; + } + } + + __setupPropertyDelegation() { + const propertyNames = this.delegations.properties.concat(this.delegations.methods); + propertyNames.forEach(propertyName => { + Object.defineProperty(this, propertyName, { + get() { + const target = this.delegationTarget; + if (target) { + if (typeof target[propertyName] === 'function') { + return target[propertyName].bind(target); } - if (this.__propertiesQueue[propertyName]) { - return this.__propertiesQueue[propertyName]; + return target[propertyName]; + } + if (this.__propertiesQueue[propertyName]) { + return this.__propertiesQueue[propertyName]; + } + // This is the moment the attribute is not delegated (and thus removed) yet. + // and the property is not set, but the attribute is (it serves as a fallback for + // __propertiesQueue). + return this.getAttribute(propertyName); + }, + set(newValue) { + if (this.delegationTarget) { + const oldValue = this.delegationTarget[propertyName]; + this.delegationTarget[propertyName] = newValue; + // connect with observer system if available + if (typeof this.triggerObserversFor === 'function') { + this.triggerObserversFor(propertyName, newValue, oldValue); } - // This is the moment the attribute is not delegated (and thus removed) yet. - // and the property is not set, but the attribute is (it serves as a fallback for - // __propertiesQueue). - return this.getAttribute(propertyName); - }, - set(newValue) { - if (this.delegationTarget) { - const oldValue = this.delegationTarget[propertyName]; - this.delegationTarget[propertyName] = newValue; - // connect with observer system if available - if (typeof this.triggerObserversFor === 'function') { - this.triggerObserversFor(propertyName, newValue, oldValue); - } - } else { - this.__propertiesQueue[propertyName] = newValue; - } - }, - }); + } else { + this.__propertiesQueue[propertyName] = newValue; + } + }, }); - } + }); + } - /** - * @private - */ - __initialAttributeDelegation() { - const attributeNames = this.delegations.attributes; - attributeNames.forEach(attributeName => { - const attributeValue = this.getAttribute(attributeName); - if (typeof attributeValue === 'string') { - this.delegationTarget.setAttribute(attributeName, attributeValue); - super.removeAttribute(attributeName); - } - }); - } + __initialAttributeDelegation() { + const attributeNames = this.delegations.attributes; + attributeNames.forEach(attributeName => { + const attributeValue = this.getAttribute(attributeName); + if (typeof attributeValue === 'string') { + this.delegationTarget.setAttribute(attributeName, attributeValue); + super.removeAttribute(attributeName); + } + }); + } - /** - * @private - */ - __emptyEventListenerQueue() { - this.__eventsQueue.forEach(ev => { - this.delegationTarget.addEventListener(ev.type, ...ev.args); - }); - } + __emptyEventListenerQueue() { + this.__eventsQueue.forEach(ev => { + this.delegationTarget.addEventListener(ev.type, ...ev.args); + }); + } - /** - * @private - */ - __emptyPropertiesQueue() { - Object.keys(this.__propertiesQueue).forEach(propName => { - this.delegationTarget[propName] = this.__propertiesQueue[propName]; - }); - } - }, -); + __emptyPropertiesQueue() { + Object.keys(this.__propertiesQueue).forEach(propName => { + this.delegationTarget[propName] = this.__propertiesQueue[propName]; + }); + } + }; + +export const DelegateMixin = dedupeMixin(DelegateMixinImplementation); diff --git a/packages/core/src/DisabledMixin.js b/packages/core/src/DisabledMixin.js index 3119670f6..1dea190e8 100644 --- a/packages/core/src/DisabledMixin.js +++ b/packages/core/src/DisabledMixin.js @@ -1,64 +1,67 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin'; /** - * #DisabledMixin - * - * @polymerMixin - * @mixinFunction + * @typedef {import('../types/DisabledMixinTypes').DisabledMixin} DisabledMixin */ -export const DisabledMixin = dedupeMixin( - superclass => - // eslint-disable-next-line no-shadow - class DisabledMixin extends superclass { - static get properties() { - return { - disabled: { - type: Boolean, - reflect: true, - }, - }; - } - constructor() { - super(); +/** @type {DisabledMixin} */ +const DisabledMixinImplementation = superclass => + // eslint-disable-next-line no-shadow + class DisabledMixinHost extends superclass { + static get properties() { + return { + disabled: { + type: Boolean, + reflect: true, + }, + }; + } + + constructor() { + super(); + this.__requestedToBeDisabled = false; + this.__isUserSettingDisabled = true; + this.__restoreDisabledTo = false; + this.disabled = false; + } + + makeRequestToBeDisabled() { + if (this.__requestedToBeDisabled === false) { + this.__requestedToBeDisabled = true; + this.__restoreDisabledTo = this.disabled; + this.__internalSetDisabled(true); + } + } + + retractRequestToBeDisabled() { + if (this.__requestedToBeDisabled === true) { this.__requestedToBeDisabled = false; - this.__isUserSettingDisabled = true; - - this.__restoreDisabledTo = false; - this.disabled = false; + this.__internalSetDisabled(this.__restoreDisabledTo); } + } - makeRequestToBeDisabled() { - if (this.__requestedToBeDisabled === false) { - this.__requestedToBeDisabled = true; + /** @param {boolean} value */ + __internalSetDisabled(value) { + this.__isUserSettingDisabled = false; + this.disabled = value; + this.__isUserSettingDisabled = true; + } + + /** + * @param {PropertyKey} name + * @param {?} oldValue + */ + _requestUpdate(name, oldValue) { + super._requestUpdate(name, oldValue); + if (name === 'disabled') { + if (this.__isUserSettingDisabled) { this.__restoreDisabledTo = this.disabled; + } + if (this.disabled === false && this.__requestedToBeDisabled === true) { this.__internalSetDisabled(true); } } + } + }; - retractRequestToBeDisabled() { - if (this.__requestedToBeDisabled === true) { - this.__requestedToBeDisabled = false; - this.__internalSetDisabled(this.__restoreDisabledTo); - } - } - - __internalSetDisabled(value) { - this.__isUserSettingDisabled = false; - this.disabled = value; - this.__isUserSettingDisabled = true; - } - - _requestUpdate(name, oldValue) { - super._requestUpdate(name, oldValue); - if (name === 'disabled') { - if (this.__isUserSettingDisabled) { - this.__restoreDisabledTo = this.disabled; - } - if (this.disabled === false && this.__requestedToBeDisabled === true) { - this.__internalSetDisabled(true); - } - } - } - }, -); +export const DisabledMixin = dedupeMixin(DisabledMixinImplementation); diff --git a/packages/core/src/DisabledWithTabIndexMixin.js b/packages/core/src/DisabledWithTabIndexMixin.js index e0570bacf..0ecea056e 100644 --- a/packages/core/src/DisabledWithTabIndexMixin.js +++ b/packages/core/src/DisabledWithTabIndexMixin.js @@ -2,85 +2,91 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin'; import { DisabledMixin } from './DisabledMixin.js'; /** - * #DisabledWithTabIndexMixin - * - * @polymerMixin - * @mixinFunction + * @typedef {import('../types/DisabledWithTabIndexMixinTypes').DisabledWithTabIndexMixin} DisabledWithTabIndexMixin */ -export const DisabledWithTabIndexMixin = dedupeMixin( - superclass => - // eslint-disable-next-line no-shadow - class DisabledWithTabIndexMixin extends DisabledMixin(superclass) { - static get properties() { - return { - // we use a property here as if we use the native tabIndex we can not set a default value - // in the constructor as it synchronously sets the attribute which is not allowed in the - // constructor phase - tabIndex: { - type: Number, - reflect: true, - attribute: 'tabindex', - }, - }; + +/** @type {DisabledWithTabIndexMixin} */ +const DisabledWithTabIndexMixinImplementation = superclass => + // eslint-disable-next-line no-shadow + class DisabledWithTabIndexMixinHost extends DisabledMixin(superclass) { + static get properties() { + return { + // we use a property here as if we use the native tabIndex we can not set a default value + // in the constructor as it synchronously sets the attribute which is not allowed in the + // constructor phase + tabIndex: { + type: Number, + reflect: true, + attribute: 'tabindex', + }, + }; + } + + constructor() { + super(); + this.__isUserSettingTabIndex = true; + this.__restoreTabIndexTo = 0; + this.__internalSetTabIndex(0); + } + + makeRequestToBeDisabled() { + super.makeRequestToBeDisabled(); + if (this.__requestedToBeDisabled === false && this.tabIndex != null) { + this.__restoreTabIndexTo = this.tabIndex; } + } - constructor() { - super(); - this.__isUserSettingTabIndex = true; - - this.__restoreTabIndexTo = 0; - this.__internalSetTabIndex(0); + retractRequestToBeDisabled() { + super.retractRequestToBeDisabled(); + if (this.__requestedToBeDisabled === true) { + this.__internalSetTabIndex(this.__restoreTabIndexTo); } + } - makeRequestToBeDisabled() { - super.makeRequestToBeDisabled(); - if (this.__requestedToBeDisabled === false) { - this.__restoreTabIndexTo = this.tabIndex; - } - } + /** + * @param {number} value + */ + __internalSetTabIndex(value) { + this.__isUserSettingTabIndex = false; + this.tabIndex = value; + this.__isUserSettingTabIndex = true; + } - retractRequestToBeDisabled() { - super.retractRequestToBeDisabled(); - if (this.__requestedToBeDisabled === true) { + /** + * @param {PropertyKey} name + * @param {?} oldValue + */ + _requestUpdate(name, oldValue) { + super._requestUpdate(name, oldValue); + + if (name === 'disabled') { + if (this.disabled) { + this.__internalSetTabIndex(-1); + } else { this.__internalSetTabIndex(this.__restoreTabIndexTo); } } - __internalSetTabIndex(value) { - this.__isUserSettingTabIndex = false; - this.tabIndex = value; - this.__isUserSettingTabIndex = true; - } - - _requestUpdate(name, oldValue) { - super._requestUpdate(name, oldValue); - - if (name === 'disabled') { - if (this.disabled) { - this.__internalSetTabIndex(-1); - } else { - this.__internalSetTabIndex(this.__restoreTabIndexTo); - } + if (name === 'tabIndex') { + if (this.__isUserSettingTabIndex && this.tabIndex != null) { + this.__restoreTabIndexTo = this.tabIndex; } - if (name === 'tabIndex') { - if (this.__isUserSettingTabIndex) { - this.__restoreTabIndexTo = this.tabIndex; - } - - if (this.tabIndex !== -1 && this.__requestedToBeDisabled === true) { - this.__internalSetTabIndex(-1); - } - } - } - - firstUpdated(changedProperties) { - super.firstUpdated(changedProperties); - // for ShadyDom the timing is a little different and we need to make sure - // the tabindex gets correctly updated here - if (this.disabled) { + if (this.tabIndex !== -1 && this.__requestedToBeDisabled === true) { this.__internalSetTabIndex(-1); } } - }, -); + } + + /** @param {import('lit-element').PropertyValues } changedProperties */ + firstUpdated(changedProperties) { + super.firstUpdated(changedProperties); + // for ShadyDom the timing is a little different and we need to make sure + // the tabindex gets correctly updated here + if (this.disabled) { + this.__internalSetTabIndex(-1); + } + } + }; + +export const DisabledWithTabIndexMixin = dedupeMixin(DisabledWithTabIndexMixinImplementation); diff --git a/packages/core/src/LionSingleton.js b/packages/core/src/LionSingleton.js index 5353ff8c5..de864ab3f 100644 --- a/packages/core/src/LionSingleton.js +++ b/packages/core/src/LionSingleton.js @@ -6,10 +6,11 @@ */ export class LionSingleton { /** - * @param {function()} mixin + * @param {function} mixin */ static addInstanceMixin(mixin) { if (!this.__instanceMixins) { + /** @type {function[]} */ this.__instanceMixins = []; } this.__instanceMixins.push(mixin); @@ -26,6 +27,8 @@ export class LionSingleton { Klass = mixin(Klass); }); } + // Ignoring, because it's up to the extension layer to accept arguments in its constructor + // @ts-ignore-next-line return new Klass(...args); } diff --git a/packages/core/src/SlotMixin.js b/packages/core/src/SlotMixin.js index 5d1e81f76..7ccc31b34 100644 --- a/packages/core/src/SlotMixin.js +++ b/packages/core/src/SlotMixin.js @@ -1,78 +1,59 @@ /* eslint-disable class-methods-use-this */ - import { dedupeMixin } from '@open-wc/dedupe-mixin'; + /** - * # SlotMixin - * `SlotMixin`, when attached to the DOM it creates content for defined slots in the Light DOM. - * The content element is created using a factory function and is assigned a slot name from the key. - * Existing slot content is not overridden. - * - * The purpose is to have the default content in the Light DOM rather than hidden in Shadow DOM - * like default slot content works natively. - * - * @example - * get slots() { - * return { - * ...super.slots, - * // appends
to the Light DOM of this element - * foo: () => document.createElement('div'), - * }; - * } - * - * @type {function()} - * @polymerMixin - * @mixinFunction + * @typedef {import('../types/SlotMixinTypes').SlotMixin} SlotMixin + * @typedef {import('../types/SlotMixinTypes').SlotsMap} SlotsMap */ -export const SlotMixin = dedupeMixin( - superclass => - // eslint-disable-next-line no-unused-vars, no-shadow - class SlotMixin extends superclass { - /** - * @returns {{}} - */ - get slots() { - return {}; - } - constructor() { - super(); - this.__privateSlots = new Set(null); - } +/** @type {SlotMixin} */ +const SlotMixinImplementation = superclass => + // eslint-disable-next-line no-unused-vars, no-shadow + class SlotMixinHost extends superclass { + /** + * @return {SlotsMap} + */ + get slots() { + return {}; + } - connectedCallback() { - if (super.connectedCallback) { - super.connectedCallback(); - } - this._connectSlotMixin(); - } + constructor() { + super(); + this.__privateSlots = new Set(null); + } - /** - * @protected - */ - _connectSlotMixin() { - if (!this.__isConnectedSlotMixin) { - Object.keys(this.slots).forEach(slotName => { - if (!this.querySelector(`[slot=${slotName}]`)) { - const slotFactory = this.slots[slotName]; - const slotContent = slotFactory(); - if (slotContent instanceof Element) { - slotContent.setAttribute('slot', slotName); - this.appendChild(slotContent); - this.__privateSlots.add(slotName); - } // ignore non-elements to enable conditional slots + connectedCallback() { + if (super.connectedCallback) { + super.connectedCallback(); + } + this._connectSlotMixin(); + } + + _connectSlotMixin() { + if (!this.__isConnectedSlotMixin) { + Object.keys(this.slots).forEach(slotName => { + if (!this.querySelector(`[slot=${slotName}]`)) { + const slotFactory = this.slots[slotName]; + const slotContent = slotFactory(); + // ignore non-elements to enable conditional slots + if (slotContent instanceof Element) { + slotContent.setAttribute('slot', slotName); + this.appendChild(slotContent); + this.__privateSlots.add(slotName); } - }); - this.__isConnectedSlotMixin = true; - } + } + }); + this.__isConnectedSlotMixin = true; } + } - /** - * @protected - * @param {string} slotName Name of the slot - * @return {boolean} true if given slot name been created by SlotMixin - */ - _isPrivateSlot(slotName) { - return this.__privateSlots.has(slotName); - } - }, -); + /** + * @param {string} slotName Name of the slot + * @return {boolean} true if given slot name been created by SlotMixin + */ + _isPrivateSlot(slotName) { + return this.__privateSlots.has(slotName); + } + }; + +export const SlotMixin = dedupeMixin(SlotMixinImplementation); diff --git a/packages/core/src/UpdateStylesMixin.js b/packages/core/src/UpdateStylesMixin.js index 3ad4f37e3..753760dd6 100644 --- a/packages/core/src/UpdateStylesMixin.js +++ b/packages/core/src/UpdateStylesMixin.js @@ -1,59 +1,78 @@ /* global ShadyCSS */ import { dedupeMixin } from '@open-wc/dedupe-mixin'; -export const UpdateStylesMixin = dedupeMixin( - superclass => - // eslint-disable-next-line no-shadow - class UpdateStylesMixin extends superclass { - /** - * @example - * - * - * - * - * $0.updateStyles({'background': 'orange', '--foo': '#fff'}) - * Chrome, Firefox: - * IE: - * => to head: - * - * @param {Object} updateStyles - */ - updateStyles(updateStyles) { - const styleString = this.getAttribute('style') || this.getAttribute('data-style') || ''; - const currentStyles = styleString.split(';').reduce((acc, stylePair) => { - const parts = stylePair.split(':'); - if (parts.length === 2) { - /* eslint-disable-next-line prefer-destructuring */ - acc[parts[0]] = parts[1]; - } - return acc; - }, {}); +/** + * @typedef {import('../types/UpdateStylesMixinTypes').UpdateStylesMixin} UpdateStylesMixin + * @typedef {import('../types/UpdateStylesMixinTypes').StylesMap} StylesMap + */ - const newStyles = { ...currentStyles, ...updateStyles }; - let newStylesString = ''; - if (typeof ShadyCSS === 'object' && !ShadyCSS.nativeShadow) { - // No ShadowDOM => IE, Edge - const newCssVariablesObj = {}; - Object.keys(newStyles).forEach(key => { - if (key.indexOf('--') === -1) { - newStylesString += `${key}:${newStyles[key]};`; - } else { - newCssVariablesObj[key] = newStyles[key]; - } - }); - this.setAttribute('style', newStylesString); - ShadyCSS.styleSubtree(this, newCssVariablesObj); - } else { - // has shadowdom => Chrome, Firefox, Safari - Object.keys(newStyles).forEach(key => { - newStylesString += `${key}: ${newStyles[key]};`; - }); - this.setAttribute('style', newStylesString); +/** @type {UpdateStylesMixin} */ +const UpdateStylesMixinImplementation = superclass => + // eslint-disable-next-line no-shadow + class UpdateStylesMixinHost extends superclass { + /** + * @example + * + * + * + * + * $0.updateStyles({'background': 'orange', '--foo': '#fff'}) + * Chrome, Firefox: + * IE: + * => to head: + * + * @param {StylesMap} updateStyles + */ + updateStyles(updateStyles) { + const styleString = this.getAttribute('style') || this.getAttribute('data-style') || ''; + + /** + * reducer function + * @param {Object.} acc + * @param {string} stylePair + */ + const reducer = (acc, stylePair) => { + /** @type {Array.} */ + const parts = stylePair.split(':'); + if (parts.length === 2) { + // eslint-disable-next-line prefer-destructuring + acc[parts[0]] = parts[1]; } + return acc; + }; + const currentStyles = styleString.split(';').reduce(reducer, {}); + + const newStyles = { ...currentStyles, ...updateStyles }; + let newStylesString = ''; + // @ts-ignore not sure how to type ShadyCSS.. + if (typeof ShadyCSS === 'object' && !ShadyCSS.nativeShadow) { + // No ShadowDOM => IE, Edge + + /** @type {Object.} */ + const newCssVariablesObj = {}; + + Object.keys(newStyles).forEach(key => { + if (key.indexOf('--') === -1) { + newStylesString += `${key}:${newStyles[key]};`; + } else { + newCssVariablesObj[key] = newStyles[key]; + } + }); + this.setAttribute('style', newStylesString); + // @ts-ignore not sure how to type ShadyCSS.. + ShadyCSS.styleSubtree(this, newCssVariablesObj); + } else { + // has shadowdom => Chrome, Firefox, Safari + Object.keys(newStyles).forEach(key => { + newStylesString += `${key}: ${newStyles[key]};`; + }); + this.setAttribute('style', newStylesString); } - }, -); + } + }; + +export const UpdateStylesMixin = dedupeMixin(UpdateStylesMixinImplementation); diff --git a/packages/core/src/differentKeyEventNamesShimIE.js b/packages/core/src/differentKeyEventNamesShimIE.js index f67a28ae9..c62b5e8c5 100644 --- a/packages/core/src/differentKeyEventNamesShimIE.js +++ b/packages/core/src/differentKeyEventNamesShimIE.js @@ -3,6 +3,7 @@ if (typeof window.KeyboardEvent !== 'function') { const event = KeyboardEvent.prototype; const descriptor = Object.getOwnPropertyDescriptor(event, 'key'); if (descriptor) { + /** @type {Object.} */ const keys = { Win: 'Meta', Scroll: 'ScrollLock', @@ -26,10 +27,13 @@ if (typeof window.KeyboardEvent !== 'function') { Object.defineProperty(event, 'key', { // eslint-disable-next-line object-shorthand, func-names get: function () { - const key = descriptor.get.call(this); + if (descriptor.get) { + const key = descriptor.get.call(this); - // eslint-disable-next-line no-prototype-builtins - return keys.hasOwnProperty(key) ? keys[key] : key; + // eslint-disable-next-line no-prototype-builtins + return keys.hasOwnProperty(key) ? keys[key] : key; + } + return undefined; }, }); } diff --git a/packages/core/test/DelegateMixin.test.js b/packages/core/test/DelegateMixin.test.js index 09191b15e..a9265bf7e 100644 --- a/packages/core/test/DelegateMixin.test.js +++ b/packages/core/test/DelegateMixin.test.js @@ -14,7 +14,7 @@ describe('DelegateMixin', () => { get delegations() { return { ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), + target: () => this.shadowRoot?.getElementById('button1'), events: ['click'], }; } @@ -27,7 +27,7 @@ describe('DelegateMixin', () => { const element = await fixture(`<${tag}>`); const cb = sinon.spy(); element.addEventListener('click', cb); - element.shadowRoot.getElementById('button1').click(); + element.shadowRoot?.getElementById('button1')?.click(); expect(cb.callCount).to.equal(1); }); @@ -37,7 +37,7 @@ describe('DelegateMixin', () => { get delegations() { return { ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), + target: () => this.shadowRoot?.getElementById('button1'), events: ['click'], }; } @@ -47,12 +47,12 @@ describe('DelegateMixin', () => { } }, ); - const element = document.createElement(tag); + const element = /** @type {LitElement} */ (document.createElement(tag)); const cb = sinon.spy(); element.addEventListener('click', cb); document.body.appendChild(element); await element.updateComplete; - element.shadowRoot.getElementById('button1').click(); + element.shadowRoot?.getElementById('button1')?.click(); expect(cb.callCount).to.equal(1); // cleanup @@ -80,37 +80,37 @@ describe('DelegateMixin', () => { }, ); - const element = await fixture(` - <${tag}>`); + const element = await fixture(`<${tag}>`); const cb = sinon.spy(); element.addEventListener('click', cb); - Array.from(element.children) - .find(child => child.slot === 'button') - .click(); + const childEl = /** @type {HTMLElement} */ (Array.from(element.children)?.find( + child => child.slot === 'button', + )); + childEl?.click(); expect(cb.callCount).to.equal(1); }); it('will still support other events', async () => { - const tag = defineCE( - class extends DelegateMixin(LitElement) { - get delegations() { - return { - ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), - events: ['click'], - }; - } + class FooDelegate extends DelegateMixin(LitElement) { + get delegations() { + return { + ...super.delegations, + target: () => this.shadowRoot?.getElementById('button1'), + events: ['click'], + }; + } - render() { - return html``; - } + render() { + return html``; + } - foo() { - this.dispatchEvent(new CustomEvent('foo-event', { bubbles: true, composed: true })); - } - }, - ); - const element = await fixture(`<${tag}>`); + foo() { + this.dispatchEvent(new CustomEvent('foo-event', { bubbles: true, composed: true })); + } + } + + const tag = defineCE(FooDelegate); + const element = /** @type {FooDelegate} */ (await fixture(`<${tag}>`)); const cb = sinon.spy(); element.addEventListener('foo-event', cb); element.foo(); @@ -123,7 +123,7 @@ describe('DelegateMixin', () => { get delegations() { return { ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), + target: () => this.shadowRoot?.getElementById('button1'), methods: ['click'], }; } @@ -133,9 +133,9 @@ describe('DelegateMixin', () => { } }, ); - const element = await fixture(`<${tag}>`); + const element = /** @type {HTMLElement} */ (await fixture(`<${tag}>`)); const cb = sinon.spy(); - element.shadowRoot.getElementById('button1').addEventListener('click', cb); + element.shadowRoot?.getElementById('button1')?.addEventListener('click', cb); element.click(); expect(cb.callCount).to.equal(1); }); @@ -147,56 +147,69 @@ describe('DelegateMixin', () => { this.foo = { a: 'a', b: 'b' }; } + /** + * @param {?} a + * @param {?} b + */ setFooAandB(a, b) { this.foo.a = a; this.foo.b = b; } } customElements.define('delegate-argument-sub', DelegateArgumentSub); - const tag = defineCE( - class extends DelegateMixin(LitElement) { - get delegations() { - return { - ...super.delegations, - target: () => this.shadowRoot.getElementById('sub'), - methods: ['setFooAandB'], - }; - } - render() { - return html``; - } - }, - ); + class DelegateArgumentParent extends DelegateMixin(LitElement) { + get delegations() { + return { + ...super.delegations, + target: () => this.shadowRoot?.getElementById('sub'), + methods: ['setFooAandB'], + }; + } - const element = await fixture(`<${tag}>`); - element.disabled = true; + render() { + return html``; + } + } + const tag = defineCE(DelegateArgumentParent); + + const element = /** @type {DelegateArgumentParent} */ (await fixture(`<${tag}>`)); + + // @ts-ignore because this method, even though it doesn't exist on the parent, gets delegated through delegations to the child, where it does exist! element.setFooAandB('newA', 'newB'); - expect(element.shadowRoot.getElementById('sub').foo.a).to.equal('newA'); - expect(element.shadowRoot.getElementById('sub').foo.b).to.equal('newB'); + + const sub = /** @type {DelegateArgumentSub} */ (element.shadowRoot?.getElementById('sub')); + expect(sub.foo.a).to.equal('newA'); + expect(sub.foo.b).to.equal('newB'); }); it('will set delegated properties', async () => { - const tag = defineCE( - class extends DelegateMixin(LitElement) { - get delegations() { - return { - ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), - properties: ['disabled'], - }; - } + class PropDelegate extends DelegateMixin(LitElement) { + get delegations() { + return { + ...super.delegations, + target: () => this.shadowRoot?.getElementById('button1'), + properties: ['disabled'], + }; + } - render() { - return html``; - } - }, - ); - const element = await fixture(`<${tag}>`); + render() { + return html``; + } + } + const tag = defineCE(PropDelegate); + const element = /** @type {PropDelegate} */ (await fixture(`<${tag}>`)); + + // @ts-ignore ignoring this one, because disabled is delegated through target so it indeed does not inherently exist on the div element element.disabled = true; + await element.updateComplete; - expect(element.shadowRoot.getElementById('button1').disabled).to.equal(true); - expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(true); + + /** @typedef {Object.} Btn */ + /** @typedef {Btn & HTMLElement} DelegatedBtn */ + const btn = /** @type {DelegatedBtn} */ (element.shadowRoot?.getElementById('button1')); + expect(btn?.disabled).to.equal(true); + expect(btn?.hasAttribute('disabled')).to.equal(true); }); it('delegates properties before delegation target is attached to DOM', async () => { @@ -205,7 +218,7 @@ describe('DelegateMixin', () => { get delegations() { return { ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), + target: () => this.shadowRoot?.getElementById('button1'), properties: ['disabled'], }; } @@ -215,12 +228,18 @@ describe('DelegateMixin', () => { } }, ); - const element = document.createElement(tag); + /** @typedef {Object.} Btn */ + /** @typedef {Btn & LitElement} DelegatedEl */ + const element = /** @type {DelegatedEl} */ (document.createElement(tag)); + element.disabled = true; document.body.appendChild(element); await element.updateComplete; - expect(element.shadowRoot.getElementById('button1').disabled).to.equal(true); + /** @typedef {Btn & HTMLElement} DelegatedBtn */ + const btn = /** @type {DelegatedBtn} */ (element.shadowRoot?.getElementById('button1')); + + expect(btn?.disabled).to.equal(true); // cleanup document.body.removeChild(element); }); @@ -231,7 +250,7 @@ describe('DelegateMixin', () => { get delegations() { return { ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), + target: () => this.shadowRoot?.getElementById('button1'), attributes: ['disabled'], }; } @@ -241,11 +260,11 @@ describe('DelegateMixin', () => { } }, ); - const element = await fixture(`<${tag}>`); + const element = /** @type {LitElement} */ (await fixture(`<${tag}>`)); element.setAttribute('disabled', ''); await element.updateComplete; expect(element.hasAttribute('disabled')).to.equal(false); - expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(true); + expect(element.shadowRoot?.getElementById('button1')?.hasAttribute('disabled')).to.equal(true); }); it('will read inital attributes', async () => { @@ -254,7 +273,7 @@ describe('DelegateMixin', () => { get delegations() { return { ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), + target: () => this.shadowRoot?.getElementById('button1'), attributes: ['disabled'], }; } @@ -266,7 +285,7 @@ describe('DelegateMixin', () => { ); const element = await fixture(`<${tag} disabled>`); expect(element.hasAttribute('disabled')).to.equal(false); - expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(true); + expect(element.shadowRoot?.getElementById('button1')?.hasAttribute('disabled')).to.equal(true); }); it('will delegate removeAttribute', async () => { @@ -275,7 +294,7 @@ describe('DelegateMixin', () => { get delegations() { return { ...super.delegations, - target: () => this.shadowRoot.getElementById('button1'), + target: () => this.shadowRoot?.getElementById('button1'), attributes: ['disabled'], }; } @@ -285,135 +304,146 @@ describe('DelegateMixin', () => { } }, ); - const element = await fixture(`<${tag} disabled>`); - element.removeAttribute('disabled', ''); + const element = /** @type {LitElement} */ (await fixture(`<${tag} disabled>`)); + element.removeAttribute('disabled'); await element.updateComplete; expect(element.hasAttribute('disabled')).to.equal(false); - expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(false); + expect(element.shadowRoot?.getElementById('button1')?.hasAttribute('disabled')).to.equal(false); }); it('respects user defined values for delegated attributes and properties', async () => { - const tag = defineCE( - class extends DelegateMixin(LitElement) { - get delegations() { - return { - ...super.delegations, - // this just means itś config is set to the queue when called before connectedCallback - target: () => this.scheduledElement, - attributes: ['type'], - properties: ['type'], - }; - } + class ScheduledElement extends DelegateMixin(LitElement) { + get delegations() { + return { + ...super.delegations, + // this just means itś config is set to the queue when called before connectedCallback + target: () => this.scheduledElement, + attributes: ['type'], + properties: ['type'], + }; + } - get scheduledElement() { - return this.querySelector('input'); - } + get scheduledElement() { + return this.querySelector('input'); + } - constructor() { - super(); - this.type = 'email'; // 1. here we set the delegated prop and it should be scheduled - } + constructor() { + super(); + this.type = 'email'; // 1. here we set the delegated prop and it should be scheduled + } - connectedCallback() { - // 2. this is where we add teh delegation target (so after 1) - this.appendChild(document.createElement('input')); - super.connectedCallback(); // let the DelegateMixin do its work - } - }, - ); + connectedCallback() { + // 2. this is where we add teh delegation target (so after 1) + this.appendChild(document.createElement('input')); + super.connectedCallback(); // let the DelegateMixin do its work + } + } + + const tag = defineCE(ScheduledElement); const tagName = unsafeStatic(tag); + // Here, the Application Developerd tries to set the type via attribute - const elementAttr = await fixture(`<${tag} type="radio">`); - expect(elementAttr.scheduledElement.type).to.equal('radio'); + const elementAttr = /** @type {ScheduledElement} */ (await fixture( + `<${tag} type="radio">`, + )); + expect(elementAttr.scheduledElement?.type).to.equal('radio'); // Here, the Application Developer tries to set the type via property - const elementProp = await fixture(html`<${tagName} .type=${'radio'}>`); - expect(elementProp.scheduledElement.type).to.equal('radio'); + const elementProp = /** @type {ScheduledElement} */ (await fixture( + html`<${tagName} .type=${'radio'}>`, + )); + expect(elementProp.scheduledElement?.type).to.equal('radio'); }); it(`uses attribute value as a fallback for delegated property getter when property not set by user and delegationTarget not ready`, async () => { - const tag = defineCE( - class extends DelegateMixin(LitElement) { - get delegations() { - return { - ...super.delegations, - target: () => this.delegatedEl, - properties: ['type'], - attributes: ['type'], - }; - } + class FallbackEl extends DelegateMixin(LitElement) { + get delegations() { + return { + ...super.delegations, + target: () => this.delegatedEl, + properties: ['type'], + attributes: ['type'], + }; + } - get delegatedEl() { - // returns null, so we can test that "cached" attr is used as fallback - return null; - } - }, - ); - const element = await fixture(`<${tag} type="radio">`); + get delegatedEl() { + // returns null, so we can test that "cached" attr is used as fallback + return null; + } + } + const tag = defineCE(FallbackEl); + const element = /** @type {FallbackEl} */ (await fixture(`<${tag} type="radio">`)); expect(element.delegatedEl).to.equal(null); + // @ts-ignore ignoring this one, because type is delegated through target so it indeed does not inherently exist on the div element expect(element.type).to.equal('radio'); // value retrieved from host instead of delegatedTarget }); it('works with connectedCallback', async () => { - const tag = await defineCE( - class extends DelegateMixin(HTMLElement) { - get delegations() { - return { - ...super.delegations, - target: () => this.querySelector('div'), - properties: ['foo'], - }; - } - }, - ); - const element = await fixture(`<${tag}>
`); + class ConnectedElement extends DelegateMixin(HTMLElement) { + get delegations() { + return { + ...super.delegations, + target: () => this.querySelector('div'), + properties: ['foo'], + }; + } + } + const tag = await defineCE(ConnectedElement); + const element = /** @type {ConnectedElement} */ (await fixture(`<${tag}>
`)); + + // @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element element.foo = 'new'; - expect(element.querySelector('div').foo).to.equal('new'); + // @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element + expect(element.querySelector('div')?.foo).to.equal('new'); }); it('works with shadow dom', async () => { - const tag = await defineCE( - class extends DelegateMixin(LitElement) { - get delegations() { - return { - ...super.delegations, - target: () => this.shadowRoot.querySelector('div'), - properties: ['foo'], - }; - } + class A extends DelegateMixin(LitElement) { + get delegations() { + return { + ...super.delegations, + target: () => this.shadowRoot?.querySelector('div'), + properties: ['foo'], + }; + } - render() { - return html`
`; - } - }, - ); + render() { + return html`
`; + } + } + const tag = await defineCE(A); const element = await fixture(`<${tag}>`); + + // @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element element.foo = 'new'; - expect(element.shadowRoot.querySelector('div').foo).to.equal('new'); + // @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element + expect(element.shadowRoot?.querySelector('div')?.foo).to.equal('new'); }); it('works with light dom', async () => { - const tag = await defineCE( - class extends DelegateMixin(LitElement) { - get delegations() { - return { - ...super.delegations, - target: () => this.querySelector('div'), - properties: ['foo'], - }; - } + class A extends DelegateMixin(LitElement) { + get delegations() { + return { + ...super.delegations, + target: () => this.querySelector('div'), + properties: ['foo'], + }; + } - createRenderRoot() { - return this; - } + createRenderRoot() { + return this; + } - render() { - return html`
`; - } - }, - ); + render() { + return html`
`; + } + } + + const tag = await defineCE(A); const element = await fixture(`<${tag}>`); + // @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element element.foo = 'new'; - expect(element.querySelector('div').foo).to.equal('new'); + // @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element + expect(element.querySelector('div')?.foo).to.equal('new'); }); }); diff --git a/packages/core/test/DisabledMixin.test.js b/packages/core/test/DisabledMixin.test.js index c9bcf11a7..52f1bc9cc 100644 --- a/packages/core/test/DisabledMixin.test.js +++ b/packages/core/test/DisabledMixin.test.js @@ -3,21 +3,26 @@ import { LitElement } from '../index.js'; import { DisabledMixin } from '../src/DisabledMixin.js'; describe('DisabledMixin', () => { + class CanBeDisabled extends DisabledMixin(LitElement) {} before(() => { - class CanBeDisabled extends DisabledMixin(LitElement) {} customElements.define('can-be-disabled', CanBeDisabled); }); it('reflects disabled to attribute', async () => { - const el = await fixture(html``); + const el = /** @type {CanBeDisabled} */ (await fixture( + html``, + )); expect(el.hasAttribute('disabled')).to.be.false; + el.makeRequestToBeDisabled(); el.disabled = true; await el.updateComplete; expect(el.hasAttribute('disabled')).to.be.true; }); it('can be requested to be disabled', async () => { - const el = await fixture(html``); + const el = /** @type {CanBeDisabled} */ (await fixture( + html``, + )); el.makeRequestToBeDisabled(); expect(el.disabled).to.be.true; await el.updateComplete; @@ -25,7 +30,9 @@ describe('DisabledMixin', () => { }); it('will not allow to become enabled after makeRequestToBeDisabled()', async () => { - const el = await fixture(html``); + const el = /** @type {CanBeDisabled} */ (await fixture( + html``, + )); el.makeRequestToBeDisabled(); expect(el.disabled).to.be.true; @@ -34,14 +41,18 @@ describe('DisabledMixin', () => { }); it('will stay disabled after retractRequestToBeDisabled() if it was disabled before', async () => { - const el = await fixture(html``); + const el = /** @type {CanBeDisabled} */ (await fixture( + html``, + )); el.makeRequestToBeDisabled(); el.retractRequestToBeDisabled(); expect(el.disabled).to.be.true; }); it('will become enabled after retractRequestToBeDisabled() if it was enabled before', async () => { - const el = await fixture(html``); + const el = /** @type {CanBeDisabled} */ (await fixture( + html``, + )); el.makeRequestToBeDisabled(); expect(el.disabled).to.be.true; el.retractRequestToBeDisabled(); @@ -49,7 +60,9 @@ describe('DisabledMixin', () => { }); it('may allow multiple calls to makeRequestToBeDisabled()', async () => { - const el = await fixture(html``); + const el = /** @type {CanBeDisabled} */ (await fixture( + html``, + )); el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled(); el.retractRequestToBeDisabled(); @@ -57,7 +70,9 @@ describe('DisabledMixin', () => { }); it('will restore last state after retractRequestToBeDisabled()', async () => { - const el = await fixture(html``); + const el = /** @type {CanBeDisabled} */ (await fixture( + html``, + )); el.makeRequestToBeDisabled(); el.disabled = true; el.retractRequestToBeDisabled(); diff --git a/packages/core/test/DisabledWithTabIndexMixin.test.js b/packages/core/test/DisabledWithTabIndexMixin.test.js index 3a037f8bc..4afd740da 100644 --- a/packages/core/test/DisabledWithTabIndexMixin.test.js +++ b/packages/core/test/DisabledWithTabIndexMixin.test.js @@ -4,23 +4,23 @@ import { LitElement } from '../index.js'; import { DisabledWithTabIndexMixin } from '../src/DisabledWithTabIndexMixin.js'; describe('DisabledWithTabIndexMixin', () => { + class WithTabIndex extends DisabledWithTabIndexMixin(LitElement) {} before(() => { - class WithTabIndex extends DisabledWithTabIndexMixin(LitElement) {} customElements.define('can-be-disabled-with-tab-index', WithTabIndex); }); it('has an initial tabIndex of 0', async () => { - const el = await fixture(html` + const el = /** @type {WithTabIndex} */ (await fixture(html` - `); + `)); expect(el.tabIndex).to.equal(0); expect(el.getAttribute('tabindex')).to.equal('0'); }); it('sets tabIndex to -1 if disabled', async () => { - const el = await fixture(html` + const el = /** @type {WithTabIndex} */ (await fixture(html` - `); + `)); el.disabled = true; expect(el.tabIndex).to.equal(-1); await el.updateComplete; @@ -28,9 +28,9 @@ describe('DisabledWithTabIndexMixin', () => { }); it('disabled does not override user provided tabindex', async () => { - const el = await fixture(html` + const el = /** @type {WithTabIndex} */ (await fixture(html` - `); + `)); expect(el.getAttribute('tabindex')).to.equal('-1'); el.disabled = false; await el.updateComplete; @@ -38,9 +38,9 @@ describe('DisabledWithTabIndexMixin', () => { }); it('can be disabled imperatively', async () => { - const el = await fixture(html` + const el = /** @type {WithTabIndex} */ (await fixture(html` - `); + `)); expect(el.getAttribute('tabindex')).to.equal('-1'); el.disabled = false; @@ -55,9 +55,9 @@ describe('DisabledWithTabIndexMixin', () => { }); it('will not allow to change tabIndex after makeRequestToBeDisabled()', async () => { - const el = await fixture(html` + const el = /** @type {WithTabIndex} */ (await fixture(html` - `); + `)); el.makeRequestToBeDisabled(); el.tabIndex = 5; @@ -67,9 +67,9 @@ describe('DisabledWithTabIndexMixin', () => { }); it('will restore last tabIndex after retractRequestToBeDisabled()', async () => { - const el = await fixture(html` + const el = /** @type {WithTabIndex} */ (await fixture(html` - `); + `)); el.makeRequestToBeDisabled(); expect(el.tabIndex).to.equal(-1); await el.updateComplete; @@ -96,9 +96,9 @@ describe('DisabledWithTabIndexMixin', () => { }); it('may allow multiple calls to retractRequestToBeDisabled', async () => { - const el = await fixture(html` + const el = /** @type {WithTabIndex} */ (await fixture(html` - `); + `)); el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled(); expect(el.disabled).to.be.true; diff --git a/packages/core/test/LionSingleton.test.js b/packages/core/test/LionSingleton.test.js index b6208fb3b..27d808294 100644 --- a/packages/core/test/LionSingleton.test.js +++ b/packages/core/test/LionSingleton.test.js @@ -11,6 +11,11 @@ describe('LionSingleton', () => { it('supports parameters for .getInstance(foo, bar)', async () => { class MySingleton extends LionSingleton { + /** + * + * @param {string} foo + * @param {string} bar + */ constructor(foo, bar) { super(); this.foo = foo; @@ -59,6 +64,7 @@ describe('LionSingleton', () => { }); it('can at any time add mixins via .addInstanceMixin()', () => { + // @ts-ignore because we're getting rid of LionSingleton altogether const MyMixin = superclass => class extends superclass { constructor() { @@ -72,6 +78,7 @@ describe('LionSingleton', () => { const mySingleton = MySingleton.getInstance(); expect(mySingleton.myMixin).to.be.true; + // @ts-ignore because we're getting rid of LionSingleton altogether const OtherMixin = superclass => class extends superclass { constructor() { @@ -89,6 +96,7 @@ describe('LionSingleton', () => { }); it('can provide new instances (with applied Mixins) via .getNewInstance()', async () => { + // @ts-ignore because we're getting rid of LionSingleton altogether const MyMixin = superclass => class extends superclass { constructor() { @@ -100,13 +108,19 @@ describe('LionSingleton', () => { MySingleton.addInstanceMixin(MyMixin); const singletonOne = MySingleton.getNewInstance(); + // @ts-ignore because we're getting rid of LionSingleton altogether singletonOne.one = true; + // @ts-ignore because we're getting rid of LionSingleton altogether expect(singletonOne.myMixin).to.be.true; + // @ts-ignore because we're getting rid of LionSingleton altogether expect(singletonOne.one).to.be.true; const singletonTwo = MySingleton.getNewInstance(); + // @ts-ignore because we're getting rid of LionSingleton altogether expect(singletonTwo.myMixin).to.be.true; + // @ts-ignore because we're getting rid of LionSingleton altogether expect(singletonTwo.one).to.be.undefined; + // @ts-ignore because we're getting rid of LionSingleton altogether expect(singletonOne.one).to.be.true; // to be sure }); }); diff --git a/packages/core/test/SlotMixin.test.js b/packages/core/test/SlotMixin.test.js index 0ff42feba..675c875ca 100644 --- a/packages/core/test/SlotMixin.test.js +++ b/packages/core/test/SlotMixin.test.js @@ -14,8 +14,8 @@ describe('SlotMixin', () => { } }, ); - const element = await fixture(`<${tag}>`); - expect(element.children[0].slot).to.equal('feedback'); + const el = await fixture(`<${tag}>`); + expect(el.children[0].slot).to.equal('feedback'); }); it('does not override user provided slots', async () => { @@ -31,7 +31,7 @@ describe('SlotMixin', () => { ); const el = await fixture(`<${tag}>

user-content

`); expect(el.children[0].tagName).to.equal('P'); - expect(el.children[0].innerText).to.equal('user-content'); + expect(/** @type HTMLParagraphElement */ (el.children[0]).innerText).to.equal('user-content'); }); it('supports complex dom trees as element', async () => { @@ -57,10 +57,12 @@ describe('SlotMixin', () => { } }, ); - const element = await fixture(`<${tag}>`); - expect(element.children[0].slot).to.equal('feedback'); - expect(element.children[0].getAttribute('foo')).to.equal('bar'); - expect(element.children[0].children[0].innerText).to.equal('cat'); + const el = await fixture(`<${tag}>`); + expect(el.children[0].slot).to.equal('feedback'); + expect(el.children[0].getAttribute('foo')).to.equal('bar'); + expect(/** @type HTMLParagraphElement */ (el.children[0].children[0]).innerText).to.equal( + 'cat', + ); }); it('supports conditional slots', async () => { @@ -82,35 +84,37 @@ describe('SlotMixin', () => { } }, ); - const elementSlot = await fixture(`<${tag}><${tag}>`); - expect(elementSlot.querySelector('#someSlot')).to.exist; + const elSlot = await fixture(`<${tag}><${tag}>`); + expect(elSlot.querySelector('#someSlot')).to.exist; renderSlot = false; - const elementNoSlot = await fixture(`<${tag}><${tag}>`); - expect(elementNoSlot.querySelector('#someSlot')).to.not.exist; + const elNoSlot = await fixture(`<${tag}><${tag}>`); + expect(elNoSlot.querySelector('#someSlot')).to.not.exist; }); it("allows to check which slots have been created via this._isPrivateSlot('slotname')", async () => { let renderSlot = true; - const tag = defineCE( - class extends SlotMixin(HTMLElement) { - get slots() { - return { - ...super.slots, - conditional: () => (renderSlot ? document.createElement('div') : undefined), - }; - } + class SlotPrivateText extends SlotMixin(HTMLElement) { + get slots() { + return { + ...super.slots, + conditional: () => (renderSlot ? document.createElement('div') : undefined), + }; + } - didCreateConditionalSlot() { - return this._isPrivateSlot('conditional'); - } - }, - ); - const el = await fixture(`<${tag}><${tag}>`); + didCreateConditionalSlot() { + return this._isPrivateSlot('conditional'); + } + } + + const tag = defineCE(SlotPrivateText); + const el = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`)); expect(el.didCreateConditionalSlot()).to.be.true; - const elUserSlot = await fixture(`<${tag}>

foo

<${tag}>`); + const elUserSlot = /** @type {SlotPrivateText} */ (await fixture( + `<${tag}>

foo

<${tag}>`, + )); expect(elUserSlot.didCreateConditionalSlot()).to.be.false; renderSlot = false; - const elNoSlot = await fixture(`<${tag}><${tag}>`); + const elNoSlot = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`)); expect(elNoSlot.didCreateConditionalSlot()).to.be.false; }); }); diff --git a/packages/core/test/UpdateStylesMixin.test.js b/packages/core/test/UpdateStylesMixin.test.js index c8c99f307..8c2dc3843 100644 --- a/packages/core/test/UpdateStylesMixin.test.js +++ b/packages/core/test/UpdateStylesMixin.test.js @@ -4,82 +4,75 @@ import { UpdateStylesMixin } from '../src/UpdateStylesMixin.js'; describe('UpdateStylesMixin', () => { it('handles css variables && direct e.g. host css properties correctly', async () => { - const tag = defineCE( - class extends UpdateStylesMixin(LitElement) { - static get styles() { - return [ - css` - :host { - text-align: right; + class UpdateStylesElement extends UpdateStylesMixin(LitElement) { + static get styles() { + return [ + css` + :host { + text-align: right; - --color: rgb(128, 128, 128); - } + --color: rgb(128, 128, 128); + } - h1 { - color: var(--color); - } - `, - ]; - } + h1 { + color: var(--color); + } + `, + ]; + } - render() { - return html`

hey

`; - } - }, - ); - const el = await fixture(`<${tag}>`); - expect(window.getComputedStyle(el.shadowRoot.getElementById('header')).color).to.equal( - 'rgb(128, 128, 128)', - ); + render() { + return html`

hey

`; + } + } + + const tag = defineCE(UpdateStylesElement); + const el = /** @type {UpdateStylesElement} */ (await fixture(`<${tag}>`)); + const header = /** @type {Element} */ (el.shadowRoot?.getElementById('header')); + + expect(window.getComputedStyle(header).color).to.equal('rgb(128, 128, 128)'); expect(window.getComputedStyle(el).textAlign).to.equal('right'); el.updateStyles({ '--color': 'rgb(255, 0, 0)', 'text-align': 'center', }); - await tag.updateComplete; - expect(window.getComputedStyle(el.shadowRoot.getElementById('header')).color).to.equal( - 'rgb(255, 0, 0)', - ); + await el.updateComplete; + expect(window.getComputedStyle(header).color).to.equal('rgb(255, 0, 0)'); expect(window.getComputedStyle(el).textAlign).to.equal('center'); }); it('preserves existing styles', async () => { - const tag = defineCE( - class extends UpdateStylesMixin(LitElement) { - static get styles() { - return [ - css` - :host { - --color: rgb(128, 128, 128); - } + class UpdateStylesElement extends UpdateStylesMixin(LitElement) { + static get styles() { + return [ + css` + :host { + --color: rgb(128, 128, 128); + } - h1 { - color: var(--color); - } - `, - ]; - } + h1 { + color: var(--color); + } + `, + ]; + } - render() { - return html`

hey

`; - } - }, - ); - const el = await fixture(`<${tag}>`); - expect(window.getComputedStyle(el.shadowRoot.getElementById('header')).color).to.equal( - 'rgb(128, 128, 128)', - ); + render() { + return html`

hey

`; + } + } + const tag = defineCE(UpdateStylesElement); + const el = /** @type {UpdateStylesElement} */ (await fixture(`<${tag}>`)); + const header = /** @type {Element} */ (el.shadowRoot?.getElementById('header')); + + expect(window.getComputedStyle(header).color).to.equal('rgb(128, 128, 128)'); el.updateStyles({ '--color': 'rgb(255, 0, 0)' }); - await tag.updateComplete; - expect(window.getComputedStyle(el.shadowRoot.getElementById('header')).color).to.equal( - 'rgb(255, 0, 0)', - ); + expect(window.getComputedStyle(header).color).to.equal('rgb(255, 0, 0)'); el.updateStyles({ 'text-align': 'left' }); - await tag.updateComplete; - const styles = window.getComputedStyle(el.shadowRoot.getElementById('header')); + const styles = window.getComputedStyle(header); expect(styles.color).to.equal('rgb(255, 0, 0)'); expect(styles.textAlign).to.equal('left'); }); diff --git a/packages/core/types/DelegateMixinTypes.d.ts b/packages/core/types/DelegateMixinTypes.d.ts new file mode 100644 index 000000000..0840c3a64 --- /dev/null +++ b/packages/core/types/DelegateMixinTypes.d.ts @@ -0,0 +1,51 @@ +import { Constructor } from '@open-wc/dedupe-mixin'; + +export type Delegations = { + target: Function; + events: string[]; + methods: string[]; + properties: string[]; + attributes: string[]; +}; + +export declare class DelegateMixinHost { + delegations: Delegations; + + protected _connectDelegateMixin(): void; + + private __setupPropertyDelegation(): void; + + private __initialAttributeDelegation(): void; + + private __emptyEventListenerQueue(): void; + + private __emptyPropertiesQueue(): void; +} + +/** + * # DelegateMixin + * Forwards defined events, methods, properties and attributes to the defined target. + * + * @example + * get delegations() { + * return { + * ...super.delegations, + * target: () => this.shadowRoot.getElementById('button1'), + * events: ['click'], + * methods: ['click'], + * properties: ['disabled'], + * attributes: ['disabled'], + * }; + * } + * + * render() { + * return html` + * + * `; + * } + */ +declare function DelegateMixinImplementation>( + superclass: T, +): T & Constructor; + +export type DelegateMixin = typeof DelegateMixinImplementation; diff --git a/packages/core/types/DisabledMixinTypes.d.ts b/packages/core/types/DisabledMixinTypes.d.ts new file mode 100644 index 000000000..ade1a1178 --- /dev/null +++ b/packages/core/types/DisabledMixinTypes.d.ts @@ -0,0 +1,29 @@ +import { Constructor } from '@open-wc/dedupe-mixin'; + +export declare class DisabledMixinHost { + static get properties(): { + disabled: { + type: BooleanConstructor; + reflect: boolean; + }; + }; + disabled: boolean; + + /** + * Makes request to make the element disabled + */ + public makeRequestToBeDisabled(): void; + + /** + * Retract request to make the element disabled and restore disabled to previous + */ + public retractRequestToBeDisabled(): void; + + private __internalSetDisabled(value: boolean): void; +} + +export declare function DisabledMixinImplementation>( + superclass: T, +): T & Constructor; + +export type DisabledMixin = typeof DisabledMixinImplementation; diff --git a/packages/core/types/DisabledWithTabIndexMixinTypes.d.ts b/packages/core/types/DisabledWithTabIndexMixinTypes.d.ts new file mode 100644 index 000000000..358005a3b --- /dev/null +++ b/packages/core/types/DisabledWithTabIndexMixinTypes.d.ts @@ -0,0 +1,29 @@ +import { Constructor } from '@open-wc/dedupe-mixin'; +import { DisabledMixinHost } from './DisabledMixinTypes'; +export declare class DisabledWithTabIndexMixinHost { + static get properties(): { + tabIndex: { + type: NumberConstructor; + reflect: boolean; + attribute: string; + }; + }; + tabIndex: number; + /** + * Makes request to make the element disabled and set the tabindex + */ + public makeRequestToBeDisabled(): void; + + /** + * Retract request to make the element disabled and restore disabled and tabindex to previous + */ + public retractRequestToBeDisabled(): void; + + private __internalSetTabIndex(value: boolean): void; +} + +export declare function DisabledWithTabIndexMixinImplementation>( + superclass: T, +): T & Constructor & Constructor; + +export type DisabledWithTabIndexMixin = typeof DisabledWithTabIndexMixinImplementation; diff --git a/packages/core/types/SlotMixinTypes.d.ts b/packages/core/types/SlotMixinTypes.d.ts new file mode 100644 index 000000000..d911b267f --- /dev/null +++ b/packages/core/types/SlotMixinTypes.d.ts @@ -0,0 +1,53 @@ +import { Constructor } from '@open-wc/dedupe-mixin'; + +declare function slotFunction(): HTMLElement | undefined; + +export type SlotsMap = { + [key: string]: typeof slotFunction; +}; + +export declare class SlotMixinHost { + /** + * Obtains all the slots to create + */ + slots: SlotsMap; + + /** + * Starts the creation of slots + */ + protected _connectSlotMixin(): void; + + /** + * Useful to decide if a given slot should be manipulated depending on if it was auto generated + * or not. + * + * @param {string} slotName Name of the slot + * @return {boolean} true if given slot name been created by SlotMixin + */ + protected _isPrivateSlot(slotName: string): boolean; +} + +/** + * # SlotMixin + * + * `SlotMixin`, when attached to the DOM it creates content for defined slots in the Light DOM. + * The content element is created using a factory function and is assigned a slot name from the key. + * Existing slot content is not overridden. + * + * The purpose is to have the default content in the Light DOM rather than hidden in Shadow DOM + * like default slot content works natively. + * + * @example + * get slots() { + * return { + * ...super.slots, + * // appends
to the Light DOM of this element + * foo: () => document.createElement('div'), + * }; + * } + */ +export declare function SlotMixinImplementation>( + superclass: T, +): T & Constructor; + +export type SlotMixin = typeof SlotMixinImplementation; diff --git a/packages/core/types/UpdateStylesMixinTypes.d.ts b/packages/core/types/UpdateStylesMixinTypes.d.ts new file mode 100644 index 000000000..a0d931c3c --- /dev/null +++ b/packages/core/types/UpdateStylesMixinTypes.d.ts @@ -0,0 +1,34 @@ +import { Constructor } from '@open-wc/dedupe-mixin'; + +export type StylesMap = { + [key: string]: string; +}; +export declare class UpdateStylesMixinHost { + /** + * @example + * + * + * + * + * $0.updateStyles({'background': 'orange', '--foo': '#fff'}) + * Chrome, Firefox: + * IE: + * => to head: + * + * @param {StylesMap} updateStyles + */ + public updateStyles(updateStyles: StylesMap): void; +} + +/** + * # UpdateStylesMixin + */ +declare function UpdateStylesMixinImplementation>( + superclass: T, +): T & Constructor; + +export type UpdateStylesMixin = typeof UpdateStylesMixinImplementation;