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).
|
* `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) {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue