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).
*
* @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<void>} 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) {

View file

@ -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');
}
}

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';
/**
* @typedef {import('../src/LionCollapsible').LionCollapsible} LionCollapsible
* @typedef {import('lit-html').TemplateResult} TemplateResult
*/
const fixture = /** @type {(arg: TemplateResult) => Promise<LionCollapsible>} */ (_fixture);
const collapsibleTemplate = html`
<button slot="invoker">More about cars</button>
<div slot="content">
@ -10,12 +17,16 @@ const collapsibleTemplate = html`
</div>
`;
let isCollapsibleOpen = false;
/** @param {boolean} state */
const collapsibleToggle = state => {
isCollapsibleOpen = state;
};
const defaultCollapsible = html` <lion-collapsible>${collapsibleTemplate}</lion-collapsible> `;
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}
</lion-collapsible>
`;
@ -47,7 +58,7 @@ describe('<lion-collapsible>', () => {
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('<lion-collapsible>', () => {
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('<lion-collapsible>', () => {
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);
});
});
});

View file

@ -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",