diff --git a/.changeset/tidy-ads-greet.md b/.changeset/tidy-ads-greet.md new file mode 100644 index 000000000..13c4e2b72 --- /dev/null +++ b/.changeset/tidy-ads-greet.md @@ -0,0 +1,5 @@ +--- +'@lion/collapsible': minor +--- + +Added types for collapsible package. diff --git a/packages/collapsible/demo/CustomCollapsible.js b/packages/collapsible/demo/CustomCollapsible.js index ca57ea5d1..560334606 100644 --- a/packages/collapsible/demo/CustomCollapsible.js +++ b/packages/collapsible/demo/CustomCollapsible.js @@ -6,10 +6,9 @@ const EVENT = { }; /** * `CustomCollapsible` is a class for custom collapsible element (`` web component). - * * @customElement custom-collapsible - * @extends LionCollapsible */ +// @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you. export class CustomCollapsible extends LionCollapsible { static get properties() { return { @@ -26,15 +25,13 @@ export class CustomCollapsible extends LionCollapsible { } connectedCallback() { - if (super.connectedCallback) { - super.connectedCallback(); - } - this._contentNode.style.setProperty( + super.connectedCallback(); + this._contentNode?.style.setProperty( 'transition', 'max-height 0.35s, padding 0.35s, opacity 0.35s', ); if (this.opened) { - this._contentNode.style.setProperty('padding', '12px 0'); + this._contentNode?.style.setProperty('padding', '12px 0'); } } @@ -51,6 +48,7 @@ export class CustomCollapsible extends LionCollapsible { /** * Trigger show animation and wait for transition to be finished. * @param {Object} options - element node and its options + * @param {HTMLElement} options.contentNode * @override */ async _showAnimation({ contentNode }) { @@ -66,20 +64,22 @@ export class CustomCollapsible extends LionCollapsible { /** * Trigger hide animation and wait for transition to be finished. * @param {Object} options - element node and its options + * @param {HTMLElement} options.contentNode * @override */ async _hideAnimation({ contentNode }) { if (this._contentHeight === '0px') { return; } - ['opacity', 'padding', 'max-height'].map(prop => contentNode.style.setProperty(prop, 0)); + ['opacity', 'padding', 'max-height'].map(prop => contentNode.style.setProperty(prop, `${0}`)); await this._waitForTransition({ contentNode }); } /** * Wait until the transition event is finished. * @param {Object} options - element node and its options - * @returns {Promise} transition event + * @param {HTMLElement} options.contentNode + * @returns {Promise} transition event */ _waitForTransition({ contentNode }) { return new Promise(resolve => { @@ -100,7 +100,7 @@ export class CustomCollapsible extends LionCollapsible { /** * Calculate total content height after collapsible opens - * @param {Object} contentNode content node + * @param {HTMLElement} contentNode content node * @private */ async __calculateHeight(contentNode) { diff --git a/packages/collapsible/src/LionCollapsible.js b/packages/collapsible/src/LionCollapsible.js index 748e34a2b..237b3b2c5 100644 --- a/packages/collapsible/src/LionCollapsible.js +++ b/packages/collapsible/src/LionCollapsible.js @@ -44,34 +44,40 @@ export class LionCollapsible extends LitElement { } connectedCallback() { - if (super.connectedCallback) { - super.connectedCallback(); - } + super.connectedCallback(); + const uid = uuid(); - this._invokerNode.addEventListener('click', this.toggle.bind(this)); + + if (this._invokerNode) { + this._invokerNode.addEventListener('click', this.toggle.bind(this)); + this._invokerNode.setAttribute('aria-expanded', `${this.opened}`); + this._invokerNode.setAttribute('id', `collapsible-invoker-${uid}`); + this._invokerNode.setAttribute('aria-controls', `collapsible-content-${uid}`); + } + + if (this._contentNode) { + this._contentNode.setAttribute('aria-labelledby', `collapsible-invoker-${uid}`); + this._contentNode.setAttribute('id', `collapsible-content-${uid}`); + } + this.__setDefaultState(); - this._invokerNode.setAttribute('aria-expanded', this.opened); - this._invokerNode.setAttribute('id', `collapsible-invoker-${uid}`); - this._invokerNode.setAttribute('aria-controls', `collapsible-content-${uid}`); - this._contentNode.setAttribute('aria-labelledby', `collapsible-invoker-${uid}`); - this._contentNode.setAttribute('id', `collapsible-content-${uid}`); } /** * Update aria labels on state change. - * @param {Object} changedProps - changed props + * @param {import('lit-element').PropertyValues } changedProperties */ - updated(changedProps) { - if (changedProps.has('opened')) { + updated(changedProperties) { + if (changedProperties.has('opened')) { this.__openedChanged(); } } disconnectedCallback() { - if (super.disconnectedCallback) { - super.disconnectedCallback(); + super.disconnectedCallback(); + if (this._invokerNode) { + this._invokerNode.removeEventListener('click', this.toggle); } - this._invokerNode.removeEventListener('click', this.toggle); } /** @@ -105,28 +111,34 @@ export class LionCollapsible extends LitElement { /** * Show animation implementation in sub-classer. + * @param {Object} opts * @protected */ - // eslint-disable-next-line class-methods-use-this, no-empty-function - async _showAnimation() {} + // eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars + async _showAnimation(opts) {} /** * Hide animation implementation in sub-classer. + * @param {Object} opts * @protected */ - // eslint-disable-next-line class-methods-use-this, no-empty-function - async _hideAnimation() {} + // eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars + async _hideAnimation(opts) {} get _invokerNode() { - return Array.from(this.children).find(child => child.slot === 'invoker'); + return /** @type {HTMLElement[]} */ (Array.from(this.children)).find( + child => child.slot === 'invoker', + ); } get _contentNode() { - return Array.from(this.children).find(child => child.slot === 'content'); + return /** @type {HTMLElement[]} */ (Array.from(this.children)).find( + child => child.slot === 'content', + ); } get _contentHeight() { - const size = this._contentNode.getBoundingClientRect().height; + const size = this._contentNode?.getBoundingClientRect().height || 0; return `${size}px`; } @@ -136,7 +148,9 @@ export class LionCollapsible extends LitElement { */ __openedChanged() { this.__updateContentSize(); - this._invokerNode.setAttribute('aria-expanded', this.opened); + if (this._invokerNode) { + this._invokerNode.setAttribute('aria-expanded', `${this.opened}`); + } this.dispatchEvent(new CustomEvent('opened-changed')); } @@ -145,12 +159,14 @@ export class LionCollapsible extends LitElement { * @private */ async __updateContentSize() { - if (this.opened) { - this._contentNode.style.setProperty('display', ''); - await this._showAnimation({ contentNode: this._contentNode }); - } else { - await this._hideAnimation({ contentNode: this._contentNode }); - this._contentNode.style.setProperty('display', 'none'); + if (this._contentNode) { + if (this.opened) { + this._contentNode.style.setProperty('display', ''); + await this._showAnimation({ contentNode: this._contentNode }); + } else { + await this._hideAnimation({ contentNode: this._contentNode }); + this._contentNode.style.setProperty('display', 'none'); + } } } @@ -159,7 +175,7 @@ export class LionCollapsible extends LitElement { * @private */ __setDefaultState() { - if (!this.opened) { + if (!this.opened && this._contentNode) { this._contentNode.style.setProperty('display', 'none'); } } diff --git a/packages/collapsible/test/lion-collapsible.test.js b/packages/collapsible/test/lion-collapsible.test.js index 94bd93681..a3848d266 100644 --- a/packages/collapsible/test/lion-collapsible.test.js +++ b/packages/collapsible/test/lion-collapsible.test.js @@ -1,7 +1,14 @@ -import { expect, fixture, html } from '@open-wc/testing'; +import { expect, fixture as _fixture, html } from '@open-wc/testing'; import '../lion-collapsible.js'; +/** + + * @typedef {import('../src/LionCollapsible').LionCollapsible} LionCollapsible + * @typedef {import('lit-html').TemplateResult} TemplateResult + */ +const fixture = /** @type {(arg: TemplateResult) => Promise} */ (_fixture); + const collapsibleTemplate = html`
@@ -10,12 +17,16 @@ const collapsibleTemplate = html`
`; let isCollapsibleOpen = false; +/** @param {boolean} state */ const collapsibleToggle = state => { isCollapsibleOpen = state; }; const defaultCollapsible = html` ${collapsibleTemplate} `; const collapsibleWithEvents = html` - collapsibleToggle(e.target.opened)}> + + collapsibleToggle(/** @type {LionCollapsible} */ (e.target)?.opened)} + > ${collapsibleTemplate} `; @@ -47,7 +58,7 @@ describe('', () => { it('opens a invoker on click', async () => { const collapsible = await fixture(defaultCollapsible); const invoker = collapsible.querySelector('[slot=invoker]'); - invoker.dispatchEvent(new Event('click')); + invoker?.dispatchEvent(new Event('click')); expect(collapsible.opened).to.equal(true); }); @@ -93,7 +104,7 @@ describe('', () => { const collapsibleElement = await fixture(defaultCollapsible); const invoker = collapsibleElement.querySelector('[slot=invoker]'); const content = collapsibleElement.querySelector('[slot=content]'); - expect(invoker.getAttribute('aria-controls')).to.equal(content.id); + expect(invoker?.getAttribute('aria-controls')).to.equal(content?.id); }); it('adds aria-expanded="false" to invoker when its content is not expanded', async () => { @@ -116,7 +127,7 @@ describe('', () => { const collapsibleElement = await fixture(defaultCollapsible); const invoker = collapsibleElement.querySelector('[slot=invoker]'); const content = collapsibleElement.querySelector('[slot=content]'); - expect(content).to.have.attribute('aria-labelledby', invoker.id); + expect(content).to.have.attribute('aria-labelledby', invoker?.id); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index fb65d1d3e..f672325f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "packages/accordion/**/*.js", "packages/button/src/**/*.js", "packages/checkbox-group/**/*.js", + "packages/collapsible/**/*.js", "packages/core/**/*.js", "packages/fieldset/**/*.js", "packages/form/**/*.js",