feat(collapsible): add types for collapsible

This commit is contained in:
Joren Broekema 2020-09-28 11:38:41 +02:00 committed by Thomas Allmer
parent a31b7217ba
commit 6be72935cd
5 changed files with 78 additions and 45 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/collapsible': minor
---
Added types for collapsible package.

View file

@ -6,10 +6,9 @@ const EVENT = {
}; };
/** /**
* `CustomCollapsible` is a class for custom collapsible element (`<custom-collapsible>` web component). * `CustomCollapsible` is a class for custom collapsible element (`<custom-collapsible>` web component).
*
* @customElement custom-collapsible * @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 { export class CustomCollapsible extends LionCollapsible {
static get properties() { static get properties() {
return { return {
@ -26,15 +25,13 @@ export class CustomCollapsible extends LionCollapsible {
} }
connectedCallback() { connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback(); super.connectedCallback();
} this._contentNode?.style.setProperty(
this._contentNode.style.setProperty(
'transition', 'transition',
'max-height 0.35s, padding 0.35s, opacity 0.35s', 'max-height 0.35s, padding 0.35s, opacity 0.35s',
); );
if (this.opened) { 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. * Trigger show animation and wait for transition to be finished.
* @param {Object} options - element node and its options * @param {Object} options - element node and its options
* @param {HTMLElement} options.contentNode
* @override * @override
*/ */
async _showAnimation({ contentNode }) { async _showAnimation({ contentNode }) {
@ -66,20 +64,22 @@ export class CustomCollapsible extends LionCollapsible {
/** /**
* Trigger hide animation and wait for transition to be finished. * Trigger hide animation and wait for transition to be finished.
* @param {Object} options - element node and its options * @param {Object} options - element node and its options
* @param {HTMLElement} options.contentNode
* @override * @override
*/ */
async _hideAnimation({ contentNode }) { async _hideAnimation({ contentNode }) {
if (this._contentHeight === '0px') { if (this._contentHeight === '0px') {
return; 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 }); await this._waitForTransition({ contentNode });
} }
/** /**
* Wait until the transition event is finished. * Wait until the transition event is finished.
* @param {Object} options - element node and its options * @param {Object} options - element node and its options
* @returns {Promise} transition event * @param {HTMLElement} options.contentNode
* @returns {Promise<void>} transition event
*/ */
_waitForTransition({ contentNode }) { _waitForTransition({ contentNode }) {
return new Promise(resolve => { return new Promise(resolve => {
@ -100,7 +100,7 @@ export class CustomCollapsible extends LionCollapsible {
/** /**
* Calculate total content height after collapsible opens * Calculate total content height after collapsible opens
* @param {Object} contentNode content node * @param {HTMLElement} contentNode content node
* @private * @private
*/ */
async __calculateHeight(contentNode) { async __calculateHeight(contentNode) {

View file

@ -44,35 +44,41 @@ export class LionCollapsible extends LitElement {
} }
connectedCallback() { connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback(); super.connectedCallback();
}
const uid = uuid(); const uid = uuid();
if (this._invokerNode) {
this._invokerNode.addEventListener('click', this.toggle.bind(this)); this._invokerNode.addEventListener('click', this.toggle.bind(this));
this.__setDefaultState(); this._invokerNode.setAttribute('aria-expanded', `${this.opened}`);
this._invokerNode.setAttribute('aria-expanded', this.opened);
this._invokerNode.setAttribute('id', `collapsible-invoker-${uid}`); this._invokerNode.setAttribute('id', `collapsible-invoker-${uid}`);
this._invokerNode.setAttribute('aria-controls', `collapsible-content-${uid}`); this._invokerNode.setAttribute('aria-controls', `collapsible-content-${uid}`);
}
if (this._contentNode) {
this._contentNode.setAttribute('aria-labelledby', `collapsible-invoker-${uid}`); this._contentNode.setAttribute('aria-labelledby', `collapsible-invoker-${uid}`);
this._contentNode.setAttribute('id', `collapsible-content-${uid}`); this._contentNode.setAttribute('id', `collapsible-content-${uid}`);
} }
this.__setDefaultState();
}
/** /**
* Update aria labels on state change. * Update aria labels on state change.
* @param {Object} changedProps - changed props * @param {import('lit-element').PropertyValues } changedProperties
*/ */
updated(changedProps) { updated(changedProperties) {
if (changedProps.has('opened')) { if (changedProperties.has('opened')) {
this.__openedChanged(); this.__openedChanged();
} }
} }
disconnectedCallback() { disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback(); super.disconnectedCallback();
} if (this._invokerNode) {
this._invokerNode.removeEventListener('click', this.toggle); this._invokerNode.removeEventListener('click', this.toggle);
} }
}
/** /**
* Show extra content. * Show extra content.
@ -105,28 +111,34 @@ export class LionCollapsible extends LitElement {
/** /**
* Show animation implementation in sub-classer. * Show animation implementation in sub-classer.
* @param {Object} opts
* @protected * @protected
*/ */
// eslint-disable-next-line class-methods-use-this, no-empty-function // eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
async _showAnimation() {} async _showAnimation(opts) {}
/** /**
* Hide animation implementation in sub-classer. * Hide animation implementation in sub-classer.
* @param {Object} opts
* @protected * @protected
*/ */
// eslint-disable-next-line class-methods-use-this, no-empty-function // eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
async _hideAnimation() {} async _hideAnimation(opts) {}
get _invokerNode() { 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() { 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() { get _contentHeight() {
const size = this._contentNode.getBoundingClientRect().height; const size = this._contentNode?.getBoundingClientRect().height || 0;
return `${size}px`; return `${size}px`;
} }
@ -136,7 +148,9 @@ export class LionCollapsible extends LitElement {
*/ */
__openedChanged() { __openedChanged() {
this.__updateContentSize(); 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')); this.dispatchEvent(new CustomEvent('opened-changed'));
} }
@ -145,6 +159,7 @@ export class LionCollapsible extends LitElement {
* @private * @private
*/ */
async __updateContentSize() { async __updateContentSize() {
if (this._contentNode) {
if (this.opened) { if (this.opened) {
this._contentNode.style.setProperty('display', ''); this._contentNode.style.setProperty('display', '');
await this._showAnimation({ contentNode: this._contentNode }); await this._showAnimation({ contentNode: this._contentNode });
@ -153,13 +168,14 @@ export class LionCollapsible extends LitElement {
this._contentNode.style.setProperty('display', 'none'); this._contentNode.style.setProperty('display', 'none');
} }
} }
}
/** /**
* Set default state for content based on `opened` attr * Set default state for content based on `opened` attr
* @private * @private
*/ */
__setDefaultState() { __setDefaultState() {
if (!this.opened) { if (!this.opened && this._contentNode) {
this._contentNode.style.setProperty('display', 'none'); this._contentNode.style.setProperty('display', 'none');
} }
} }

View file

@ -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'; import '../lion-collapsible.js';
/**
* @typedef {import('../src/LionCollapsible').LionCollapsible} LionCollapsible
* @typedef {import('lit-html').TemplateResult} TemplateResult
*/
const fixture = /** @type {(arg: TemplateResult) => Promise<LionCollapsible>} */ (_fixture);
const collapsibleTemplate = html` const collapsibleTemplate = html`
<button slot="invoker">More about cars</button> <button slot="invoker">More about cars</button>
<div slot="content"> <div slot="content">
@ -10,12 +17,16 @@ const collapsibleTemplate = html`
</div> </div>
`; `;
let isCollapsibleOpen = false; let isCollapsibleOpen = false;
/** @param {boolean} state */
const collapsibleToggle = state => { const collapsibleToggle = state => {
isCollapsibleOpen = state; isCollapsibleOpen = state;
}; };
const defaultCollapsible = html` <lion-collapsible>${collapsibleTemplate}</lion-collapsible> `; const defaultCollapsible = html` <lion-collapsible>${collapsibleTemplate}</lion-collapsible> `;
const collapsibleWithEvents = html` const collapsibleWithEvents = html`
<lion-collapsible @opened-changed=${e => collapsibleToggle(e.target.opened)}> <lion-collapsible
@opened-changed=${/** @param {Event} e */ e =>
collapsibleToggle(/** @type {LionCollapsible} */ (e.target)?.opened)}
>
${collapsibleTemplate} ${collapsibleTemplate}
</lion-collapsible> </lion-collapsible>
`; `;
@ -47,7 +58,7 @@ describe('<lion-collapsible>', () => {
it('opens a invoker on click', async () => { it('opens a invoker on click', async () => {
const collapsible = await fixture(defaultCollapsible); const collapsible = await fixture(defaultCollapsible);
const invoker = collapsible.querySelector('[slot=invoker]'); const invoker = collapsible.querySelector('[slot=invoker]');
invoker.dispatchEvent(new Event('click')); invoker?.dispatchEvent(new Event('click'));
expect(collapsible.opened).to.equal(true); expect(collapsible.opened).to.equal(true);
}); });
@ -93,7 +104,7 @@ describe('<lion-collapsible>', () => {
const collapsibleElement = await fixture(defaultCollapsible); const collapsibleElement = await fixture(defaultCollapsible);
const invoker = collapsibleElement.querySelector('[slot=invoker]'); const invoker = collapsibleElement.querySelector('[slot=invoker]');
const content = collapsibleElement.querySelector('[slot=content]'); 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 () => { it('adds aria-expanded="false" to invoker when its content is not expanded', async () => {
@ -116,7 +127,7 @@ describe('<lion-collapsible>', () => {
const collapsibleElement = await fixture(defaultCollapsible); const collapsibleElement = await fixture(defaultCollapsible);
const invoker = collapsibleElement.querySelector('[slot=invoker]'); const invoker = collapsibleElement.querySelector('[slot=invoker]');
const content = collapsibleElement.querySelector('[slot=content]'); const content = collapsibleElement.querySelector('[slot=content]');
expect(content).to.have.attribute('aria-labelledby', invoker.id); expect(content).to.have.attribute('aria-labelledby', invoker?.id);
}); });
}); });
}); });

View file

@ -18,6 +18,7 @@
"packages/accordion/**/*.js", "packages/accordion/**/*.js",
"packages/button/src/**/*.js", "packages/button/src/**/*.js",
"packages/checkbox-group/**/*.js", "packages/checkbox-group/**/*.js",
"packages/collapsible/**/*.js",
"packages/core/**/*.js", "packages/core/**/*.js",
"packages/fieldset/**/*.js", "packages/fieldset/**/*.js",
"packages/form/**/*.js", "packages/form/**/*.js",