feat(core): fix types and export type definition files for core

This commit is contained in:
Joren Broekema 2020-07-13 11:30:21 +02:00
parent 5efd243958
commit ec65da5da6
20 changed files with 943 additions and 703 deletions

View file

@ -1,8 +1,3 @@
/**
* Info for TypeScript users:
* For now please import types from lit-element and lit-html directly.
*/
// lit-element // lit-element
export { export {
css, css,

View file

@ -13,11 +13,14 @@
"main": "index.js", "main": "index.js",
"module": "index.js", "module": "index.js",
"files": [ "files": [
"*.d.ts",
"*.js", "*.js",
"docs", "docs",
"src", "src",
"test",
"test-helpers", "test-helpers",
"translations" "translations",
"types"
], ],
"scripts": { "scripts": {
"prepublishOnly": "../../scripts/npm-prepublish.js", "prepublishOnly": "../../scripts/npm-prepublish.js",
@ -26,7 +29,7 @@
}, },
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@open-wc/dedupe-mixin": "^1.2.1", "@open-wc/dedupe-mixin": "^1.2.18",
"@open-wc/scoped-elements": "^1.0.3", "@open-wc/scoped-elements": "^1.0.3",
"lit-element": "^2.2.1", "lit-element": "^2.2.1",
"lit-html": "^1.0.0" "lit-html": "^1.0.0"

View file

@ -1,203 +1,177 @@
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import { dedupeMixin } from '@open-wc/dedupe-mixin'; import { dedupeMixin } from '@open-wc/dedupe-mixin';
/** /**
* # DelegateMixin * @typedef {import('../types/DelegateMixinTypes').DelegateMixin} 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`
* <button id="button1">with delegation</button>
* `;
* }
*
* @type {function()}
* @polymerMixin
* @mixinFunction
*/ */
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}} * @typedef DelegateEvent
*/ * @property {string} type - Type of event
get delegations() { * @property {Array<?>} args - Event arguments
return { */
target: null,
events: [],
methods: [],
properties: [],
attributes: [],
};
}
connectedCallback() { /** @type {DelegateMixin} */
if (super.connectedCallback) { const DelegateMixinImplementation = superclass =>
super.connectedCallback(); // eslint-disable-next-line
} class DelegateMixin extends superclass {
this._connectDelegateMixin(); constructor() {
} super();
updated(...args) { /** @type {DelegateEvent[]} */
super.updated(...args); this.__eventsQueue = [];
this._connectDelegateMixin();
}
/** /** @type {Object.<string,?>} */
* @param {string} type this.__propertiesQueue = {};
* @param {Object} args this.__setupPropertyDelegation();
*/ }
addEventListener(type, ...args) {
const delegatedEvents = this.delegations.events; /**
if (delegatedEvents.indexOf(type) > -1) { * @returns {{target: Function, events: string[], methods: string[], properties: string[], attributes: string[]}}
if (this.delegationTarget) { */
this.delegationTarget.addEventListener(type, ...args); get delegations() {
} else { return {
this.__eventsQueue.push({ type, args }); 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 { } else {
super.addEventListener(type, ...args); this.__eventsQueue.push({ type, args });
} }
} else {
super.addEventListener(type, ...args);
} }
}
/** /**
* @param {string} name * @param {string} name
* @param {string} value * @param {string} value
*/ */
setAttribute(name, value) { setAttribute(name, value) {
const attributeNames = this.delegations.attributes; const attributeNames = this.delegations.attributes;
if (attributeNames.indexOf(name) > -1) { if (attributeNames.indexOf(name) > -1) {
if (this.delegationTarget) { if (this.delegationTarget) {
this.delegationTarget.setAttribute(name, value); 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);
}
} }
super.removeAttribute(name); super.removeAttribute(name);
} else {
super.setAttribute(name, value);
} }
}
/** /**
* @protected * @param {string} name
*/ */
_connectDelegateMixin() { removeAttribute(name) {
if (this.__connectedDelegateMixin) return; const attributeNames = this.delegations.attributes;
if (attributeNames.indexOf(name) > -1) {
if (!this.delegationTarget) {
this.delegationTarget = this.delegations.target();
}
if (this.delegationTarget) { if (this.delegationTarget) {
this.__emptyEventListenerQueue(); this.delegationTarget.removeAttribute(name);
this.__emptyPropertiesQueue();
this.__initialAttributeDelegation();
this.__connectedDelegateMixin = true;
} }
} }
super.removeAttribute(name);
}
/** _connectDelegateMixin() {
* @private if (this.__connectedDelegateMixin) return;
*/
__setupPropertyDelegation() { if (!this.delegationTarget) {
const propertyNames = this.delegations.properties.concat(this.delegations.methods); this.delegationTarget = this.delegations.target();
propertyNames.forEach(propertyName => { }
Object.defineProperty(this, propertyName, {
get() { if (this.delegationTarget) {
const target = this.delegationTarget; this.__emptyEventListenerQueue();
if (target) { this.__emptyPropertiesQueue();
if (typeof target[propertyName] === 'function') { this.__initialAttributeDelegation();
return target[propertyName].bind(target);
} this.__connectedDelegateMixin = true;
return target[propertyName]; }
}
__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 target[propertyName];
return this.__propertiesQueue[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. } else {
// and the property is not set, but the attribute is (it serves as a fallback for this.__propertiesQueue[propertyName] = newValue;
// __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;
}
},
});
}); });
} });
}
/** __initialAttributeDelegation() {
* @private const attributeNames = this.delegations.attributes;
*/ attributeNames.forEach(attributeName => {
__initialAttributeDelegation() { const attributeValue = this.getAttribute(attributeName);
const attributeNames = this.delegations.attributes; if (typeof attributeValue === 'string') {
attributeNames.forEach(attributeName => { this.delegationTarget.setAttribute(attributeName, attributeValue);
const attributeValue = this.getAttribute(attributeName); super.removeAttribute(attributeName);
if (typeof attributeValue === 'string') { }
this.delegationTarget.setAttribute(attributeName, attributeValue); });
super.removeAttribute(attributeName); }
}
});
}
/** __emptyEventListenerQueue() {
* @private this.__eventsQueue.forEach(ev => {
*/ this.delegationTarget.addEventListener(ev.type, ...ev.args);
__emptyEventListenerQueue() { });
this.__eventsQueue.forEach(ev => { }
this.delegationTarget.addEventListener(ev.type, ...ev.args);
});
}
/** __emptyPropertiesQueue() {
* @private 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);
},
);

View file

@ -1,64 +1,67 @@
import { dedupeMixin } from '@open-wc/dedupe-mixin'; import { dedupeMixin } from '@open-wc/dedupe-mixin';
/** /**
* #DisabledMixin * @typedef {import('../types/DisabledMixinTypes').DisabledMixin} DisabledMixin
*
* @polymerMixin
* @mixinFunction
*/ */
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() { /** @type {DisabledMixin} */
super(); 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.__requestedToBeDisabled = false;
this.__isUserSettingDisabled = true; this.__internalSetDisabled(this.__restoreDisabledTo);
this.__restoreDisabledTo = false;
this.disabled = false;
} }
}
makeRequestToBeDisabled() { /** @param {boolean} value */
if (this.__requestedToBeDisabled === false) { __internalSetDisabled(value) {
this.__requestedToBeDisabled = true; 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; this.__restoreDisabledTo = this.disabled;
}
if (this.disabled === false && this.__requestedToBeDisabled === true) {
this.__internalSetDisabled(true); this.__internalSetDisabled(true);
} }
} }
}
};
retractRequestToBeDisabled() { export const DisabledMixin = dedupeMixin(DisabledMixinImplementation);
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);
}
}
}
},
);

View file

@ -2,85 +2,91 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
import { DisabledMixin } from './DisabledMixin.js'; import { DisabledMixin } from './DisabledMixin.js';
/** /**
* #DisabledWithTabIndexMixin * @typedef {import('../types/DisabledWithTabIndexMixinTypes').DisabledWithTabIndexMixin} DisabledWithTabIndexMixin
*
* @polymerMixin
* @mixinFunction
*/ */
export const DisabledWithTabIndexMixin = dedupeMixin(
superclass => /** @type {DisabledWithTabIndexMixin} */
// eslint-disable-next-line no-shadow const DisabledWithTabIndexMixinImplementation = superclass =>
class DisabledWithTabIndexMixin extends DisabledMixin(superclass) { // eslint-disable-next-line no-shadow
static get properties() { class DisabledWithTabIndexMixinHost extends DisabledMixin(superclass) {
return { static get properties() {
// we use a property here as if we use the native tabIndex we can not set a default value return {
// in the constructor as it synchronously sets the attribute which is not allowed in the // we use a property here as if we use the native tabIndex we can not set a default value
// constructor phase // in the constructor as it synchronously sets the attribute which is not allowed in the
tabIndex: { // constructor phase
type: Number, tabIndex: {
reflect: true, type: Number,
attribute: 'tabindex', 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() { retractRequestToBeDisabled() {
super(); super.retractRequestToBeDisabled();
this.__isUserSettingTabIndex = true; if (this.__requestedToBeDisabled === true) {
this.__internalSetTabIndex(this.__restoreTabIndexTo);
this.__restoreTabIndexTo = 0;
this.__internalSetTabIndex(0);
} }
}
makeRequestToBeDisabled() { /**
super.makeRequestToBeDisabled(); * @param {number} value
if (this.__requestedToBeDisabled === false) { */
this.__restoreTabIndexTo = this.tabIndex; __internalSetTabIndex(value) {
} this.__isUserSettingTabIndex = false;
} this.tabIndex = value;
this.__isUserSettingTabIndex = true;
}
retractRequestToBeDisabled() { /**
super.retractRequestToBeDisabled(); * @param {PropertyKey} name
if (this.__requestedToBeDisabled === true) { * @param {?} oldValue
*/
_requestUpdate(name, oldValue) {
super._requestUpdate(name, oldValue);
if (name === 'disabled') {
if (this.disabled) {
this.__internalSetTabIndex(-1);
} else {
this.__internalSetTabIndex(this.__restoreTabIndexTo); this.__internalSetTabIndex(this.__restoreTabIndexTo);
} }
} }
__internalSetTabIndex(value) { if (name === 'tabIndex') {
this.__isUserSettingTabIndex = false; if (this.__isUserSettingTabIndex && this.tabIndex != null) {
this.tabIndex = value; this.__restoreTabIndexTo = this.tabIndex;
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.tabIndex !== -1 && this.__requestedToBeDisabled === true) {
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) {
this.__internalSetTabIndex(-1); 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);

View file

@ -6,10 +6,11 @@
*/ */
export class LionSingleton { export class LionSingleton {
/** /**
* @param {function()} mixin * @param {function} mixin
*/ */
static addInstanceMixin(mixin) { static addInstanceMixin(mixin) {
if (!this.__instanceMixins) { if (!this.__instanceMixins) {
/** @type {function[]} */
this.__instanceMixins = []; this.__instanceMixins = [];
} }
this.__instanceMixins.push(mixin); this.__instanceMixins.push(mixin);
@ -26,6 +27,8 @@ export class LionSingleton {
Klass = mixin(Klass); 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); return new Klass(...args);
} }

View file

@ -1,78 +1,59 @@
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import { dedupeMixin } from '@open-wc/dedupe-mixin'; import { dedupeMixin } from '@open-wc/dedupe-mixin';
/** /**
* # SlotMixin * @typedef {import('../types/SlotMixinTypes').SlotMixin} SlotMixin
* `SlotMixin`, when attached to the DOM it creates content for defined slots in the Light DOM. * @typedef {import('../types/SlotMixinTypes').SlotsMap} SlotsMap
* 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 <div slot="foo"></div> to the Light DOM of this element
* foo: () => document.createElement('div'),
* };
* }
*
* @type {function()}
* @polymerMixin
* @mixinFunction
*/ */
export const SlotMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line no-unused-vars, no-shadow
class SlotMixin extends superclass {
/**
* @returns {{}}
*/
get slots() {
return {};
}
constructor() { /** @type {SlotMixin} */
super(); const SlotMixinImplementation = superclass =>
this.__privateSlots = new Set(null); // eslint-disable-next-line no-unused-vars, no-shadow
} class SlotMixinHost extends superclass {
/**
* @return {SlotsMap}
*/
get slots() {
return {};
}
connectedCallback() { constructor() {
if (super.connectedCallback) { super();
super.connectedCallback(); this.__privateSlots = new Set(null);
} }
this._connectSlotMixin();
}
/** connectedCallback() {
* @protected if (super.connectedCallback) {
*/ super.connectedCallback();
_connectSlotMixin() { }
if (!this.__isConnectedSlotMixin) { this._connectSlotMixin();
Object.keys(this.slots).forEach(slotName => { }
if (!this.querySelector(`[slot=${slotName}]`)) {
const slotFactory = this.slots[slotName]; _connectSlotMixin() {
const slotContent = slotFactory(); if (!this.__isConnectedSlotMixin) {
if (slotContent instanceof Element) { Object.keys(this.slots).forEach(slotName => {
slotContent.setAttribute('slot', slotName); if (!this.querySelector(`[slot=${slotName}]`)) {
this.appendChild(slotContent); const slotFactory = this.slots[slotName];
this.__privateSlots.add(slotName); const slotContent = slotFactory();
} // ignore non-elements to enable conditional slots // 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
* @param {string} slotName Name of the slot * @return {boolean} true if given slot name been created by SlotMixin
* @return {boolean} true if given slot name been created by SlotMixin */
*/ _isPrivateSlot(slotName) {
_isPrivateSlot(slotName) { return this.__privateSlots.has(slotName);
return this.__privateSlots.has(slotName); }
} };
},
); export const SlotMixin = dedupeMixin(SlotMixinImplementation);

View file

@ -1,59 +1,78 @@
/* global ShadyCSS */ /* global ShadyCSS */
import { dedupeMixin } from '@open-wc/dedupe-mixin'; import { dedupeMixin } from '@open-wc/dedupe-mixin';
export const UpdateStylesMixin = dedupeMixin( /**
superclass => * @typedef {import('../types/UpdateStylesMixinTypes').UpdateStylesMixin} UpdateStylesMixin
// eslint-disable-next-line no-shadow * @typedef {import('../types/UpdateStylesMixinTypes').StylesMap} StylesMap
class UpdateStylesMixin extends superclass { */
/**
* @example
* <my-element>
* <style>
* :host {
* color: var(--foo);
* }
* </style>
* </my-element>
*
* $0.updateStyles({'background': 'orange', '--foo': '#fff'})
* Chrome, Firefox: <my-element style="background: orange; --foo: bar;">
* IE: <my-element>
* => to head: <style>color: #fff</style>
*
* @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;
}, {});
const newStyles = { ...currentStyles, ...updateStyles }; /** @type {UpdateStylesMixin} */
let newStylesString = ''; const UpdateStylesMixinImplementation = superclass =>
if (typeof ShadyCSS === 'object' && !ShadyCSS.nativeShadow) { // eslint-disable-next-line no-shadow
// No ShadowDOM => IE, Edge class UpdateStylesMixinHost extends superclass {
const newCssVariablesObj = {}; /**
Object.keys(newStyles).forEach(key => { * @example
if (key.indexOf('--') === -1) { * <my-element>
newStylesString += `${key}:${newStyles[key]};`; * <style>
} else { * :host {
newCssVariablesObj[key] = newStyles[key]; * color: var(--foo);
} * }
}); * </style>
this.setAttribute('style', newStylesString); * </my-element>
ShadyCSS.styleSubtree(this, newCssVariablesObj); *
} else { * $0.updateStyles({'background': 'orange', '--foo': '#fff'})
// has shadowdom => Chrome, Firefox, Safari * Chrome, Firefox: <my-element style="background: orange; --foo: bar;">
Object.keys(newStyles).forEach(key => { * IE: <my-element>
newStylesString += `${key}: ${newStyles[key]};`; * => to head: <style>color: #fff</style>
}); *
this.setAttribute('style', newStylesString); * @param {StylesMap} updateStyles
*/
updateStyles(updateStyles) {
const styleString = this.getAttribute('style') || this.getAttribute('data-style') || '';
/**
* reducer function
* @param {Object.<string, string>} acc
* @param {string} stylePair
*/
const reducer = (acc, stylePair) => {
/** @type {Array.<string>} */
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.<string, string>} */
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);

View file

@ -3,6 +3,7 @@ if (typeof window.KeyboardEvent !== 'function') {
const event = KeyboardEvent.prototype; const event = KeyboardEvent.prototype;
const descriptor = Object.getOwnPropertyDescriptor(event, 'key'); const descriptor = Object.getOwnPropertyDescriptor(event, 'key');
if (descriptor) { if (descriptor) {
/** @type {Object.<string, string>} */
const keys = { const keys = {
Win: 'Meta', Win: 'Meta',
Scroll: 'ScrollLock', Scroll: 'ScrollLock',
@ -26,10 +27,13 @@ if (typeof window.KeyboardEvent !== 'function') {
Object.defineProperty(event, 'key', { Object.defineProperty(event, 'key', {
// eslint-disable-next-line object-shorthand, func-names // eslint-disable-next-line object-shorthand, func-names
get: function () { get: function () {
const key = descriptor.get.call(this); if (descriptor.get) {
const key = descriptor.get.call(this);
// eslint-disable-next-line no-prototype-builtins // eslint-disable-next-line no-prototype-builtins
return keys.hasOwnProperty(key) ? keys[key] : key; return keys.hasOwnProperty(key) ? keys[key] : key;
}
return undefined;
}, },
}); });
} }

View file

@ -14,7 +14,7 @@ describe('DelegateMixin', () => {
get delegations() { get delegations() {
return { return {
...super.delegations, ...super.delegations,
target: () => this.shadowRoot.getElementById('button1'), target: () => this.shadowRoot?.getElementById('button1'),
events: ['click'], events: ['click'],
}; };
} }
@ -27,7 +27,7 @@ describe('DelegateMixin', () => {
const element = await fixture(`<${tag}></${tag}>`); const element = await fixture(`<${tag}></${tag}>`);
const cb = sinon.spy(); const cb = sinon.spy();
element.addEventListener('click', cb); element.addEventListener('click', cb);
element.shadowRoot.getElementById('button1').click(); element.shadowRoot?.getElementById('button1')?.click();
expect(cb.callCount).to.equal(1); expect(cb.callCount).to.equal(1);
}); });
@ -37,7 +37,7 @@ describe('DelegateMixin', () => {
get delegations() { get delegations() {
return { return {
...super.delegations, ...super.delegations,
target: () => this.shadowRoot.getElementById('button1'), target: () => this.shadowRoot?.getElementById('button1'),
events: ['click'], events: ['click'],
}; };
} }
@ -47,12 +47,12 @@ describe('DelegateMixin', () => {
} }
}, },
); );
const element = document.createElement(tag); const element = /** @type {LitElement} */ (document.createElement(tag));
const cb = sinon.spy(); const cb = sinon.spy();
element.addEventListener('click', cb); element.addEventListener('click', cb);
document.body.appendChild(element); document.body.appendChild(element);
await element.updateComplete; await element.updateComplete;
element.shadowRoot.getElementById('button1').click(); element.shadowRoot?.getElementById('button1')?.click();
expect(cb.callCount).to.equal(1); expect(cb.callCount).to.equal(1);
// cleanup // cleanup
@ -80,37 +80,37 @@ describe('DelegateMixin', () => {
}, },
); );
const element = await fixture(` const element = await fixture(`<${tag}><button slot="button">click me</button></${tag}>`);
<${tag}><button slot="button">click me</button></${tag}>`);
const cb = sinon.spy(); const cb = sinon.spy();
element.addEventListener('click', cb); element.addEventListener('click', cb);
Array.from(element.children) const childEl = /** @type {HTMLElement} */ (Array.from(element.children)?.find(
.find(child => child.slot === 'button') child => child.slot === 'button',
.click(); ));
childEl?.click();
expect(cb.callCount).to.equal(1); expect(cb.callCount).to.equal(1);
}); });
it('will still support other events', async () => { it('will still support other events', async () => {
const tag = defineCE( class FooDelegate extends DelegateMixin(LitElement) {
class extends DelegateMixin(LitElement) { get delegations() {
get delegations() { return {
return { ...super.delegations,
...super.delegations, target: () => this.shadowRoot?.getElementById('button1'),
target: () => this.shadowRoot.getElementById('button1'), events: ['click'],
events: ['click'], };
}; }
}
render() { render() {
return html`<button id="button1">with delegation</button>`; return html`<button id="button1">with delegation</button>`;
} }
foo() { foo() {
this.dispatchEvent(new CustomEvent('foo-event', { bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent('foo-event', { bubbles: true, composed: true }));
} }
}, }
);
const element = await fixture(`<${tag}></${tag}>`); const tag = defineCE(FooDelegate);
const element = /** @type {FooDelegate} */ (await fixture(`<${tag}></${tag}>`));
const cb = sinon.spy(); const cb = sinon.spy();
element.addEventListener('foo-event', cb); element.addEventListener('foo-event', cb);
element.foo(); element.foo();
@ -123,7 +123,7 @@ describe('DelegateMixin', () => {
get delegations() { get delegations() {
return { return {
...super.delegations, ...super.delegations,
target: () => this.shadowRoot.getElementById('button1'), target: () => this.shadowRoot?.getElementById('button1'),
methods: ['click'], methods: ['click'],
}; };
} }
@ -133,9 +133,9 @@ describe('DelegateMixin', () => {
} }
}, },
); );
const element = await fixture(`<${tag}></${tag}>`); const element = /** @type {HTMLElement} */ (await fixture(`<${tag}></${tag}>`));
const cb = sinon.spy(); const cb = sinon.spy();
element.shadowRoot.getElementById('button1').addEventListener('click', cb); element.shadowRoot?.getElementById('button1')?.addEventListener('click', cb);
element.click(); element.click();
expect(cb.callCount).to.equal(1); expect(cb.callCount).to.equal(1);
}); });
@ -147,56 +147,69 @@ describe('DelegateMixin', () => {
this.foo = { a: 'a', b: 'b' }; this.foo = { a: 'a', b: 'b' };
} }
/**
* @param {?} a
* @param {?} b
*/
setFooAandB(a, b) { setFooAandB(a, b) {
this.foo.a = a; this.foo.a = a;
this.foo.b = b; this.foo.b = b;
} }
} }
customElements.define('delegate-argument-sub', DelegateArgumentSub); 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() { class DelegateArgumentParent extends DelegateMixin(LitElement) {
return html`<delegate-argument-sub id="sub"></delegate-argument-sub>`; get delegations() {
} return {
}, ...super.delegations,
); target: () => this.shadowRoot?.getElementById('sub'),
methods: ['setFooAandB'],
};
}
const element = await fixture(`<${tag}></${tag}>`); render() {
element.disabled = true; return html`<delegate-argument-sub id="sub"></delegate-argument-sub>`;
}
}
const tag = defineCE(DelegateArgumentParent);
const element = /** @type {DelegateArgumentParent} */ (await fixture(`<${tag}></${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'); 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 () => { it('will set delegated properties', async () => {
const tag = defineCE( class PropDelegate extends DelegateMixin(LitElement) {
class extends DelegateMixin(LitElement) { get delegations() {
get delegations() { return {
return { ...super.delegations,
...super.delegations, target: () => this.shadowRoot?.getElementById('button1'),
target: () => this.shadowRoot.getElementById('button1'), properties: ['disabled'],
properties: ['disabled'], };
}; }
}
render() { render() {
return html`<button id="button1">with delegation</button>`; return html`<button id="button1">with delegation</button>`;
} }
}, }
); const tag = defineCE(PropDelegate);
const element = await fixture(`<${tag}></${tag}>`); const element = /** @type {PropDelegate} */ (await fixture(`<${tag}></${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; element.disabled = true;
await element.updateComplete; await element.updateComplete;
expect(element.shadowRoot.getElementById('button1').disabled).to.equal(true);
expect(element.shadowRoot.getElementById('button1').hasAttribute('disabled')).to.equal(true); /** @typedef {Object.<string,boolean>} 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 () => { it('delegates properties before delegation target is attached to DOM', async () => {
@ -205,7 +218,7 @@ describe('DelegateMixin', () => {
get delegations() { get delegations() {
return { return {
...super.delegations, ...super.delegations,
target: () => this.shadowRoot.getElementById('button1'), target: () => this.shadowRoot?.getElementById('button1'),
properties: ['disabled'], properties: ['disabled'],
}; };
} }
@ -215,12 +228,18 @@ describe('DelegateMixin', () => {
} }
}, },
); );
const element = document.createElement(tag); /** @typedef {Object.<string,boolean>} Btn */
/** @typedef {Btn & LitElement} DelegatedEl */
const element = /** @type {DelegatedEl} */ (document.createElement(tag));
element.disabled = true; element.disabled = true;
document.body.appendChild(element); document.body.appendChild(element);
await element.updateComplete; 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 // cleanup
document.body.removeChild(element); document.body.removeChild(element);
}); });
@ -231,7 +250,7 @@ describe('DelegateMixin', () => {
get delegations() { get delegations() {
return { return {
...super.delegations, ...super.delegations,
target: () => this.shadowRoot.getElementById('button1'), target: () => this.shadowRoot?.getElementById('button1'),
attributes: ['disabled'], attributes: ['disabled'],
}; };
} }
@ -241,11 +260,11 @@ describe('DelegateMixin', () => {
} }
}, },
); );
const element = await fixture(`<${tag}></${tag}>`); const element = /** @type {LitElement} */ (await fixture(`<${tag}></${tag}>`));
element.setAttribute('disabled', ''); element.setAttribute('disabled', '');
await element.updateComplete; await element.updateComplete;
expect(element.hasAttribute('disabled')).to.equal(false); 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 () => { it('will read inital attributes', async () => {
@ -254,7 +273,7 @@ describe('DelegateMixin', () => {
get delegations() { get delegations() {
return { return {
...super.delegations, ...super.delegations,
target: () => this.shadowRoot.getElementById('button1'), target: () => this.shadowRoot?.getElementById('button1'),
attributes: ['disabled'], attributes: ['disabled'],
}; };
} }
@ -266,7 +285,7 @@ describe('DelegateMixin', () => {
); );
const element = await fixture(`<${tag} disabled></${tag}>`); const element = await fixture(`<${tag} disabled></${tag}>`);
expect(element.hasAttribute('disabled')).to.equal(false); 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 () => { it('will delegate removeAttribute', async () => {
@ -275,7 +294,7 @@ describe('DelegateMixin', () => {
get delegations() { get delegations() {
return { return {
...super.delegations, ...super.delegations,
target: () => this.shadowRoot.getElementById('button1'), target: () => this.shadowRoot?.getElementById('button1'),
attributes: ['disabled'], attributes: ['disabled'],
}; };
} }
@ -285,135 +304,146 @@ describe('DelegateMixin', () => {
} }
}, },
); );
const element = await fixture(`<${tag} disabled></${tag}>`); const element = /** @type {LitElement} */ (await fixture(`<${tag} disabled></${tag}>`));
element.removeAttribute('disabled', ''); element.removeAttribute('disabled');
await element.updateComplete; await element.updateComplete;
expect(element.hasAttribute('disabled')).to.equal(false); 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 () => { it('respects user defined values for delegated attributes and properties', async () => {
const tag = defineCE( class ScheduledElement extends DelegateMixin(LitElement) {
class extends DelegateMixin(LitElement) { get delegations() {
get delegations() { return {
return { ...super.delegations,
...super.delegations, // this just means itś config is set to the queue when called before connectedCallback
// this just means itś config is set to the queue when called before connectedCallback target: () => this.scheduledElement,
target: () => this.scheduledElement, attributes: ['type'],
attributes: ['type'], properties: ['type'],
properties: ['type'], };
}; }
}
get scheduledElement() { get scheduledElement() {
return this.querySelector('input'); return this.querySelector('input');
} }
constructor() { constructor() {
super(); super();
this.type = 'email'; // 1. here we set the delegated prop and it should be scheduled this.type = 'email'; // 1. here we set the delegated prop and it should be scheduled
} }
connectedCallback() { connectedCallback() {
// 2. this is where we add teh delegation target (so after 1) // 2. this is where we add teh delegation target (so after 1)
this.appendChild(document.createElement('input')); this.appendChild(document.createElement('input'));
super.connectedCallback(); // let the DelegateMixin do its work super.connectedCallback(); // let the DelegateMixin do its work
} }
}, }
);
const tag = defineCE(ScheduledElement);
const tagName = unsafeStatic(tag); const tagName = unsafeStatic(tag);
// Here, the Application Developerd tries to set the type via attribute // Here, the Application Developerd tries to set the type via attribute
const elementAttr = await fixture(`<${tag} type="radio"></${tag}>`); const elementAttr = /** @type {ScheduledElement} */ (await fixture(
expect(elementAttr.scheduledElement.type).to.equal('radio'); `<${tag} type="radio"></${tag}>`,
));
expect(elementAttr.scheduledElement?.type).to.equal('radio');
// Here, the Application Developer tries to set the type via property // Here, the Application Developer tries to set the type via property
const elementProp = await fixture(html`<${tagName} .type=${'radio'}></${tagName}>`); const elementProp = /** @type {ScheduledElement} */ (await fixture(
expect(elementProp.scheduledElement.type).to.equal('radio'); html`<${tagName} .type=${'radio'}></${tagName}>`,
));
expect(elementProp.scheduledElement?.type).to.equal('radio');
}); });
it(`uses attribute value as a fallback for delegated property getter it(`uses attribute value as a fallback for delegated property getter
when property not set by user and delegationTarget not ready`, async () => { when property not set by user and delegationTarget not ready`, async () => {
const tag = defineCE( class FallbackEl extends DelegateMixin(LitElement) {
class extends DelegateMixin(LitElement) { get delegations() {
get delegations() { return {
return { ...super.delegations,
...super.delegations, target: () => this.delegatedEl,
target: () => this.delegatedEl, properties: ['type'],
properties: ['type'], attributes: ['type'],
attributes: ['type'], };
}; }
}
get delegatedEl() { get delegatedEl() {
// returns null, so we can test that "cached" attr is used as fallback // returns null, so we can test that "cached" attr is used as fallback
return null; return null;
} }
}, }
); const tag = defineCE(FallbackEl);
const element = await fixture(`<${tag} type="radio"></${tag}>`); const element = /** @type {FallbackEl} */ (await fixture(`<${tag} type="radio"></${tag}>`));
expect(element.delegatedEl).to.equal(null); 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 expect(element.type).to.equal('radio'); // value retrieved from host instead of delegatedTarget
}); });
it('works with connectedCallback', async () => { it('works with connectedCallback', async () => {
const tag = await defineCE( class ConnectedElement extends DelegateMixin(HTMLElement) {
class extends DelegateMixin(HTMLElement) { get delegations() {
get delegations() { return {
return { ...super.delegations,
...super.delegations, target: () => this.querySelector('div'),
target: () => this.querySelector('div'), properties: ['foo'],
properties: ['foo'], };
}; }
} }
}, const tag = await defineCE(ConnectedElement);
); const element = /** @type {ConnectedElement} */ (await fixture(`<${tag}><div></div></${tag}>`));
const element = await fixture(`<${tag}><div></div></${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'; 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 () => { it('works with shadow dom', async () => {
const tag = await defineCE( class A extends DelegateMixin(LitElement) {
class extends DelegateMixin(LitElement) { get delegations() {
get delegations() { return {
return { ...super.delegations,
...super.delegations, target: () => this.shadowRoot?.querySelector('div'),
target: () => this.shadowRoot.querySelector('div'), properties: ['foo'],
properties: ['foo'], };
}; }
}
render() { render() {
return html`<div></div>`; return html`<div></div>`;
} }
}, }
); const tag = await defineCE(A);
const element = await fixture(`<${tag}></${tag}>`); const element = await fixture(`<${tag}></${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'; 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 () => { it('works with light dom', async () => {
const tag = await defineCE( class A extends DelegateMixin(LitElement) {
class extends DelegateMixin(LitElement) { get delegations() {
get delegations() { return {
return { ...super.delegations,
...super.delegations, target: () => this.querySelector('div'),
target: () => this.querySelector('div'), properties: ['foo'],
properties: ['foo'], };
}; }
}
createRenderRoot() { createRenderRoot() {
return this; return this;
} }
render() { render() {
return html`<div></div>`; return html`<div></div>`;
} }
}, }
);
const tag = await defineCE(A);
const element = await fixture(`<${tag}></${tag}>`); const element = await fixture(`<${tag}></${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'; 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');
}); });
}); });

View file

@ -3,21 +3,26 @@ import { LitElement } from '../index.js';
import { DisabledMixin } from '../src/DisabledMixin.js'; import { DisabledMixin } from '../src/DisabledMixin.js';
describe('DisabledMixin', () => { describe('DisabledMixin', () => {
class CanBeDisabled extends DisabledMixin(LitElement) {}
before(() => { before(() => {
class CanBeDisabled extends DisabledMixin(LitElement) {}
customElements.define('can-be-disabled', CanBeDisabled); customElements.define('can-be-disabled', CanBeDisabled);
}); });
it('reflects disabled to attribute', async () => { it('reflects disabled to attribute', async () => {
const el = await fixture(html`<can-be-disabled></can-be-disabled>`); const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
expect(el.hasAttribute('disabled')).to.be.false; expect(el.hasAttribute('disabled')).to.be.false;
el.makeRequestToBeDisabled();
el.disabled = true; el.disabled = true;
await el.updateComplete; await el.updateComplete;
expect(el.hasAttribute('disabled')).to.be.true; expect(el.hasAttribute('disabled')).to.be.true;
}); });
it('can be requested to be disabled', async () => { it('can be requested to be disabled', async () => {
const el = await fixture(html`<can-be-disabled></can-be-disabled>`); const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;
await el.updateComplete; await el.updateComplete;
@ -25,7 +30,9 @@ describe('DisabledMixin', () => {
}); });
it('will not allow to become enabled after makeRequestToBeDisabled()', async () => { it('will not allow to become enabled after makeRequestToBeDisabled()', async () => {
const el = await fixture(html`<can-be-disabled></can-be-disabled>`); const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;
@ -34,14 +41,18 @@ describe('DisabledMixin', () => {
}); });
it('will stay disabled after retractRequestToBeDisabled() if it was disabled before', async () => { it('will stay disabled after retractRequestToBeDisabled() if it was disabled before', async () => {
const el = await fixture(html`<can-be-disabled disabled></can-be-disabled>`); const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled disabled></can-be-disabled>`,
));
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;
}); });
it('will become enabled after retractRequestToBeDisabled() if it was enabled before', async () => { it('will become enabled after retractRequestToBeDisabled() if it was enabled before', async () => {
const el = await fixture(html`<can-be-disabled></can-be-disabled>`); const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
@ -49,7 +60,9 @@ describe('DisabledMixin', () => {
}); });
it('may allow multiple calls to makeRequestToBeDisabled()', async () => { it('may allow multiple calls to makeRequestToBeDisabled()', async () => {
const el = await fixture(html`<can-be-disabled></can-be-disabled>`); const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
@ -57,7 +70,9 @@ describe('DisabledMixin', () => {
}); });
it('will restore last state after retractRequestToBeDisabled()', async () => { it('will restore last state after retractRequestToBeDisabled()', async () => {
const el = await fixture(html`<can-be-disabled></can-be-disabled>`); const el = /** @type {CanBeDisabled} */ (await fixture(
html`<can-be-disabled></can-be-disabled>`,
));
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.disabled = true; el.disabled = true;
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();

View file

@ -4,23 +4,23 @@ import { LitElement } from '../index.js';
import { DisabledWithTabIndexMixin } from '../src/DisabledWithTabIndexMixin.js'; import { DisabledWithTabIndexMixin } from '../src/DisabledWithTabIndexMixin.js';
describe('DisabledWithTabIndexMixin', () => { describe('DisabledWithTabIndexMixin', () => {
class WithTabIndex extends DisabledWithTabIndexMixin(LitElement) {}
before(() => { before(() => {
class WithTabIndex extends DisabledWithTabIndexMixin(LitElement) {}
customElements.define('can-be-disabled-with-tab-index', WithTabIndex); customElements.define('can-be-disabled-with-tab-index', WithTabIndex);
}); });
it('has an initial tabIndex of 0', async () => { it('has an initial tabIndex of 0', async () => {
const el = await fixture(html` const el = /** @type {WithTabIndex} */ (await fixture(html`
<can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> <can-be-disabled-with-tab-index></can-be-disabled-with-tab-index>
`); `));
expect(el.tabIndex).to.equal(0); expect(el.tabIndex).to.equal(0);
expect(el.getAttribute('tabindex')).to.equal('0'); expect(el.getAttribute('tabindex')).to.equal('0');
}); });
it('sets tabIndex to -1 if disabled', async () => { it('sets tabIndex to -1 if disabled', async () => {
const el = await fixture(html` const el = /** @type {WithTabIndex} */ (await fixture(html`
<can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> <can-be-disabled-with-tab-index></can-be-disabled-with-tab-index>
`); `));
el.disabled = true; el.disabled = true;
expect(el.tabIndex).to.equal(-1); expect(el.tabIndex).to.equal(-1);
await el.updateComplete; await el.updateComplete;
@ -28,9 +28,9 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('disabled does not override user provided tabindex', async () => { it('disabled does not override user provided tabindex', async () => {
const el = await fixture(html` const el = /** @type {WithTabIndex} */ (await fixture(html`
<can-be-disabled-with-tab-index tabindex="5" disabled></can-be-disabled-with-tab-index> <can-be-disabled-with-tab-index tabindex="5" disabled></can-be-disabled-with-tab-index>
`); `));
expect(el.getAttribute('tabindex')).to.equal('-1'); expect(el.getAttribute('tabindex')).to.equal('-1');
el.disabled = false; el.disabled = false;
await el.updateComplete; await el.updateComplete;
@ -38,9 +38,9 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('can be disabled imperatively', async () => { it('can be disabled imperatively', async () => {
const el = await fixture(html` const el = /** @type {WithTabIndex} */ (await fixture(html`
<can-be-disabled-with-tab-index disabled></can-be-disabled-with-tab-index> <can-be-disabled-with-tab-index disabled></can-be-disabled-with-tab-index>
`); `));
expect(el.getAttribute('tabindex')).to.equal('-1'); expect(el.getAttribute('tabindex')).to.equal('-1');
el.disabled = false; el.disabled = false;
@ -55,9 +55,9 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('will not allow to change tabIndex after makeRequestToBeDisabled()', async () => { it('will not allow to change tabIndex after makeRequestToBeDisabled()', async () => {
const el = await fixture(html` const el = /** @type {WithTabIndex} */ (await fixture(html`
<can-be-disabled-with-tab-index></can-be-disabled-with-tab-index> <can-be-disabled-with-tab-index></can-be-disabled-with-tab-index>
`); `));
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
el.tabIndex = 5; el.tabIndex = 5;
@ -67,9 +67,9 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('will restore last tabIndex after retractRequestToBeDisabled()', async () => { it('will restore last tabIndex after retractRequestToBeDisabled()', async () => {
const el = await fixture(html` const el = /** @type {WithTabIndex} */ (await fixture(html`
<can-be-disabled-with-tab-index tabindex="5"></can-be-disabled-with-tab-index> <can-be-disabled-with-tab-index tabindex="5"></can-be-disabled-with-tab-index>
`); `));
el.makeRequestToBeDisabled(); el.makeRequestToBeDisabled();
expect(el.tabIndex).to.equal(-1); expect(el.tabIndex).to.equal(-1);
await el.updateComplete; await el.updateComplete;
@ -96,9 +96,9 @@ describe('DisabledWithTabIndexMixin', () => {
}); });
it('may allow multiple calls to retractRequestToBeDisabled', async () => { it('may allow multiple calls to retractRequestToBeDisabled', async () => {
const el = await fixture(html` const el = /** @type {WithTabIndex} */ (await fixture(html`
<can-be-disabled-with-tab-index disabled></can-be-disabled-with-tab-index> <can-be-disabled-with-tab-index disabled></can-be-disabled-with-tab-index>
`); `));
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
el.retractRequestToBeDisabled(); el.retractRequestToBeDisabled();
expect(el.disabled).to.be.true; expect(el.disabled).to.be.true;

View file

@ -11,6 +11,11 @@ describe('LionSingleton', () => {
it('supports parameters for .getInstance(foo, bar)', async () => { it('supports parameters for .getInstance(foo, bar)', async () => {
class MySingleton extends LionSingleton { class MySingleton extends LionSingleton {
/**
*
* @param {string} foo
* @param {string} bar
*/
constructor(foo, bar) { constructor(foo, bar) {
super(); super();
this.foo = foo; this.foo = foo;
@ -59,6 +64,7 @@ describe('LionSingleton', () => {
}); });
it('can at any time add mixins via .addInstanceMixin()', () => { it('can at any time add mixins via .addInstanceMixin()', () => {
// @ts-ignore because we're getting rid of LionSingleton altogether
const MyMixin = superclass => const MyMixin = superclass =>
class extends superclass { class extends superclass {
constructor() { constructor() {
@ -72,6 +78,7 @@ describe('LionSingleton', () => {
const mySingleton = MySingleton.getInstance(); const mySingleton = MySingleton.getInstance();
expect(mySingleton.myMixin).to.be.true; expect(mySingleton.myMixin).to.be.true;
// @ts-ignore because we're getting rid of LionSingleton altogether
const OtherMixin = superclass => const OtherMixin = superclass =>
class extends superclass { class extends superclass {
constructor() { constructor() {
@ -89,6 +96,7 @@ describe('LionSingleton', () => {
}); });
it('can provide new instances (with applied Mixins) via .getNewInstance()', async () => { it('can provide new instances (with applied Mixins) via .getNewInstance()', async () => {
// @ts-ignore because we're getting rid of LionSingleton altogether
const MyMixin = superclass => const MyMixin = superclass =>
class extends superclass { class extends superclass {
constructor() { constructor() {
@ -100,13 +108,19 @@ describe('LionSingleton', () => {
MySingleton.addInstanceMixin(MyMixin); MySingleton.addInstanceMixin(MyMixin);
const singletonOne = MySingleton.getNewInstance(); const singletonOne = MySingleton.getNewInstance();
// @ts-ignore because we're getting rid of LionSingleton altogether
singletonOne.one = true; singletonOne.one = true;
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonOne.myMixin).to.be.true; expect(singletonOne.myMixin).to.be.true;
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonOne.one).to.be.true; expect(singletonOne.one).to.be.true;
const singletonTwo = MySingleton.getNewInstance(); const singletonTwo = MySingleton.getNewInstance();
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonTwo.myMixin).to.be.true; expect(singletonTwo.myMixin).to.be.true;
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonTwo.one).to.be.undefined; 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 expect(singletonOne.one).to.be.true; // to be sure
}); });
}); });

View file

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

View file

@ -4,82 +4,75 @@ import { UpdateStylesMixin } from '../src/UpdateStylesMixin.js';
describe('UpdateStylesMixin', () => { describe('UpdateStylesMixin', () => {
it('handles css variables && direct e.g. host css properties correctly', async () => { it('handles css variables && direct e.g. host css properties correctly', async () => {
const tag = defineCE( class UpdateStylesElement extends UpdateStylesMixin(LitElement) {
class extends UpdateStylesMixin(LitElement) { static get styles() {
static get styles() { return [
return [ css`
css` :host {
:host { text-align: right;
text-align: right;
--color: rgb(128, 128, 128); --color: rgb(128, 128, 128);
} }
h1 { h1 {
color: var(--color); color: var(--color);
} }
`, `,
]; ];
} }
render() { render() {
return html`<h1 id="header">hey</h1>`; return html`<h1 id="header">hey</h1>`;
} }
}, }
);
const el = await fixture(`<${tag}></${tag}>`); const tag = defineCE(UpdateStylesElement);
expect(window.getComputedStyle(el.shadowRoot.getElementById('header')).color).to.equal( const el = /** @type {UpdateStylesElement} */ (await fixture(`<${tag}></${tag}>`));
'rgb(128, 128, 128)', 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'); expect(window.getComputedStyle(el).textAlign).to.equal('right');
el.updateStyles({ el.updateStyles({
'--color': 'rgb(255, 0, 0)', '--color': 'rgb(255, 0, 0)',
'text-align': 'center', 'text-align': 'center',
}); });
await tag.updateComplete; await el.updateComplete;
expect(window.getComputedStyle(el.shadowRoot.getElementById('header')).color).to.equal( expect(window.getComputedStyle(header).color).to.equal('rgb(255, 0, 0)');
'rgb(255, 0, 0)',
);
expect(window.getComputedStyle(el).textAlign).to.equal('center'); expect(window.getComputedStyle(el).textAlign).to.equal('center');
}); });
it('preserves existing styles', async () => { it('preserves existing styles', async () => {
const tag = defineCE( class UpdateStylesElement extends UpdateStylesMixin(LitElement) {
class extends UpdateStylesMixin(LitElement) { static get styles() {
static get styles() { return [
return [ css`
css` :host {
:host { --color: rgb(128, 128, 128);
--color: rgb(128, 128, 128); }
}
h1 { h1 {
color: var(--color); color: var(--color);
} }
`, `,
]; ];
} }
render() { render() {
return html`<h1 id="header">hey</h1>`; return html`<h1 id="header">hey</h1>`;
} }
}, }
); const tag = defineCE(UpdateStylesElement);
const el = await fixture(`<${tag}></${tag}>`); const el = /** @type {UpdateStylesElement} */ (await fixture(`<${tag}></${tag}>`));
expect(window.getComputedStyle(el.shadowRoot.getElementById('header')).color).to.equal( const header = /** @type {Element} */ (el.shadowRoot?.getElementById('header'));
'rgb(128, 128, 128)',
); expect(window.getComputedStyle(header).color).to.equal('rgb(128, 128, 128)');
el.updateStyles({ '--color': 'rgb(255, 0, 0)' }); el.updateStyles({ '--color': 'rgb(255, 0, 0)' });
await tag.updateComplete; expect(window.getComputedStyle(header).color).to.equal('rgb(255, 0, 0)');
expect(window.getComputedStyle(el.shadowRoot.getElementById('header')).color).to.equal(
'rgb(255, 0, 0)',
);
el.updateStyles({ 'text-align': 'left' }); el.updateStyles({ 'text-align': 'left' });
await tag.updateComplete; const styles = window.getComputedStyle(header);
const styles = window.getComputedStyle(el.shadowRoot.getElementById('header'));
expect(styles.color).to.equal('rgb(255, 0, 0)'); expect(styles.color).to.equal('rgb(255, 0, 0)');
expect(styles.textAlign).to.equal('left'); expect(styles.textAlign).to.equal('left');
}); });

View file

@ -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`
* <button id="button1">with delegation</button>
* `;
* }
*/
declare function DelegateMixinImplementation<T extends Constructor<HTMLElement>>(
superclass: T,
): T & Constructor<DelegateMixinHost>;
export type DelegateMixin = typeof DelegateMixinImplementation;

View file

@ -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<T extends Constructor<HTMLElement>>(
superclass: T,
): T & Constructor<DisabledMixinHost>;
export type DisabledMixin = typeof DisabledMixinImplementation;

View file

@ -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<T extends Constructor<HTMLElement>>(
superclass: T,
): T & Constructor<DisabledWithTabIndexMixinHost> & Constructor<DisabledMixinHost>;
export type DisabledWithTabIndexMixin = typeof DisabledWithTabIndexMixinImplementation;

53
packages/core/types/SlotMixinTypes.d.ts vendored Normal file
View file

@ -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 <div slot="foo"></div> to the Light DOM of this element
* foo: () => document.createElement('div'),
* };
* }
*/
export declare function SlotMixinImplementation<T extends Constructor<HTMLElement>>(
superclass: T,
): T & Constructor<SlotMixinHost>;
export type SlotMixin = typeof SlotMixinImplementation;

View file

@ -0,0 +1,34 @@
import { Constructor } from '@open-wc/dedupe-mixin';
export type StylesMap = {
[key: string]: string;
};
export declare class UpdateStylesMixinHost {
/**
* @example
* <my-element>
* <style>
* :host {
* color: var(--foo);
* }
* </style>
* </my-element>
*
* $0.updateStyles({'background': 'orange', '--foo': '#fff'})
* Chrome, Firefox: <my-element style="background: orange; --foo: bar;">
* IE: <my-element>
* => to head: <style>color: #fff</style>
*
* @param {StylesMap} updateStyles
*/
public updateStyles(updateStyles: StylesMap): void;
}
/**
* # UpdateStylesMixin
*/
declare function UpdateStylesMixinImplementation<T extends Constructor<HTMLElement>>(
superclass: T,
): T & Constructor<UpdateStylesMixinHost>;
export type UpdateStylesMixin = typeof UpdateStylesMixinImplementation;