feat(collapsible): add types for collapsible
This commit is contained in:
parent
a31b7217ba
commit
6be72935cd
5 changed files with 78 additions and 45 deletions
5
.changeset/tidy-ads-greet.md
Normal file
5
.changeset/tidy-ads-greet.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/collapsible': minor
|
||||
---
|
||||
|
||||
Added types for collapsible package.
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue