diff --git a/.changeset/great-suits-poke.md b/.changeset/great-suits-poke.md new file mode 100644 index 000000000..b7c11604b --- /dev/null +++ b/.changeset/great-suits-poke.md @@ -0,0 +1,5 @@ +--- +'@lion/steps': minor +--- + +Add types for steps package. diff --git a/.changeset/seven-lizards-explain.md b/.changeset/seven-lizards-explain.md new file mode 100644 index 000000000..b7c11604b --- /dev/null +++ b/.changeset/seven-lizards-explain.md @@ -0,0 +1,5 @@ +--- +'@lion/steps': minor +--- + +Add types for steps package. diff --git a/packages/steps/src/LionStep.js b/packages/steps/src/LionStep.js index c961cdd2d..85fa96c15 100644 --- a/packages/steps/src/LionStep.js +++ b/packages/steps/src/LionStep.js @@ -1,5 +1,9 @@ import { css, html, LitElement } from '@lion/core'; +/** + * @typedef {import('./LionSteps').LionSteps} LionSteps + */ + /** * `LionStep` is one of many in a LionSteps Controller * @@ -8,24 +12,6 @@ import { css, html, LitElement } from '@lion/core'; */ export class LionStep extends LitElement { static get properties() { - /** - * Fired when the step is entered. - * - * @event enter - */ - - /** - * Fired when the step is left. - * - * @event left - */ - - /** - * Fired when the step is skipped. - * - * @event skipped - */ - return { /** * Step status, one of: "untouched", "entered", "left", "skipped". @@ -39,13 +25,14 @@ export class LionStep extends LitElement { * Takes lion-steps data as a first argument `myConditionFunc(data)`. */ condition: { - type: Function, + attribute: false, }, /** * Allows to invert condition function result. */ invertCondition: { type: Boolean, + reflect: true, attribute: 'invert-condition', }, /** @@ -55,6 +42,7 @@ export class LionStep extends LitElement { */ forwardOnly: { type: Boolean, + reflect: true, attribute: 'forward-only', }, /** @@ -63,6 +51,7 @@ export class LionStep extends LitElement { */ initialStep: { type: Boolean, + reflect: true, attribute: 'initial-step', }, }; @@ -71,7 +60,8 @@ export class LionStep extends LitElement { constructor() { super(); this.status = 'untouched'; - this.condition = () => true; + // eslint-disable-next-line no-unused-vars + this.condition = /** @param {Object} [data] */ data => true; this.invertCondition = false; this.forwardOnly = false; this.initialStep = false; @@ -99,9 +89,10 @@ export class LionStep extends LitElement { return html``; } - firstUpdated() { - super.firstUpdated(); - this.controller = this.parentNode; + /** @param {import('lit-element').PropertyValues } changedProperties */ + firstUpdated(changedProperties) { + super.firstUpdated(changedProperties); + this.controller = /** @type {LionSteps} */ (this.parentNode); } enter() { @@ -119,6 +110,7 @@ export class LionStep extends LitElement { this.dispatchEvent(new CustomEvent('skip', { bubbles: true, composed: true })); } + /** @param {Object} [data] */ passesCondition(data) { const result = this.condition(data); return this.invertCondition ? !result : result; diff --git a/packages/steps/src/LionSteps.js b/packages/steps/src/LionSteps.js index 0ba25ff69..a56988e4e 100644 --- a/packages/steps/src/LionSteps.js +++ b/packages/steps/src/LionSteps.js @@ -1,5 +1,9 @@ import { css, html, LitElement } from '@lion/core'; +/** + * @typedef {import('./LionStep.js').LionStep} LionStep + */ + /** * `LionSteps` is a controller for a multi step system. * @@ -7,6 +11,20 @@ import { css, html, LitElement } from '@lion/core'; * @extends {LitElement} */ export class LionSteps extends LitElement { + static get styles() { + return [ + css` + :host { + display: block; + } + + :host([hidden]) { + display: none; + } + `, + ]; + } + static get properties() { /** * Fired when a transition between steps happens. @@ -31,43 +49,19 @@ export class LionSteps extends LitElement { }; } - updated(changedProperties) { - super.updated(changedProperties); - if (changedProperties.has('current')) { - this._onCurrentChanged( - { current: this.current }, - { current: changedProperties.get('current') }, - ); - } - } - constructor() { super(); + /** @type {{[key: string]: ?}} */ this.data = {}; this._internalCurrentSync = true; // necessary for preventing side effects on initialization + /** @type {number} */ this.current = 0; + this._max = 0; } - static get styles() { - return [ - css` - :host { - display: block; - } - - :host([hidden]) { - display: none; - } - `, - ]; - } - - render() { - return html``; - } - - firstUpdated() { - super.firstUpdated(); + /** @param {import('lit-element').PropertyValues } changedProperties */ + firstUpdated(changedProperties) { + super.firstUpdated(changedProperties); this._max = this.steps.length - 1; let hasInitial = false; @@ -82,6 +76,21 @@ export class LionSteps extends LitElement { } } + /** @param {import('lit-element').PropertyValues } changedProperties */ + updated(changedProperties) { + super.updated(changedProperties); + if (changedProperties.has('current')) { + this._onCurrentChanged( + { current: this.current }, + { current: /** @type {number} */ (changedProperties.get('current')) }, + ); + } + } + + render() { + return html``; + } + next() { this._goTo(this.current + 1, this.current); } @@ -91,10 +100,18 @@ export class LionSteps extends LitElement { } get steps() { - const defaultSlot = this.shadowRoot.querySelector('slot:not([name])'); - return defaultSlot.assignedNodes().filter(node => node.nodeType === Node.ELEMENT_NODE); + const defaultSlot = /** @type {HTMLSlotElement} */ (this.shadowRoot?.querySelector( + 'slot:not([name])', + )); + return /** @type {LionStep[]} */ (defaultSlot.assignedNodes()).filter( + node => node.nodeType === Node.ELEMENT_NODE, + ); } + /** + * @param {number} newCurrent + * @param {number} oldCurrent + */ _goTo(newCurrent, oldCurrent) { if (newCurrent < 0 || newCurrent > this._max) { throw new Error(`There is no step at index ${newCurrent}.`); @@ -119,6 +136,10 @@ export class LionSteps extends LitElement { } } + /** + * @param {number} newCurrent + * @param {number} oldCurrent + */ _changeStep(newCurrent, oldCurrent) { const oldStepElement = this.steps[oldCurrent]; const newStepElement = this.steps[newCurrent]; @@ -137,6 +158,10 @@ export class LionSteps extends LitElement { this._dispatchTransitionEvent(fromStep, toStep); } + /** + * @param {{number: number, element: LionStep}} fromStep + * @param {{number: number, element: LionStep}} toStep + */ _dispatchTransitionEvent(fromStep, toStep) { this.dispatchEvent( new CustomEvent('transition', { @@ -147,6 +172,10 @@ export class LionSteps extends LitElement { ); } + /** + * @param {{current: number}} newValues + * @param {{current: number}} oldValues + */ _onCurrentChanged(newValues, oldValues) { if (this._internalCurrentSync) { this._internalCurrentSync = false; diff --git a/packages/steps/test/lion-step.test.js b/packages/steps/test/lion-step.test.js index ab3dfc82c..876cb18b2 100644 --- a/packages/steps/test/lion-step.test.js +++ b/packages/steps/test/lion-step.test.js @@ -2,6 +2,10 @@ import { expect, fixture, html, oneEvent } from '@open-wc/testing'; import '../lion-step.js'; +/** + * @typedef {import('../src/LionStep').LionStep} LionStep + */ + describe('lion-step', () => { it('has a condition which allows it to become active (condition is true by default)', async () => { const el = await fixture(html` @@ -9,8 +13,8 @@ describe('lion-step', () => { Step 1 `); - expect(el.children[0].condition()).to.equal(true); - expect(el.children[0].passesCondition()).to.equal(true); + expect(/** @type {LionStep} */ (el.children[0]).condition()).to.equal(true); + expect(/** @type {LionStep} */ (el.children[0]).passesCondition()).to.equal(true); }); it('does not invert condition by default', async () => { @@ -19,8 +23,8 @@ describe('lion-step', () => { Step 1 `); - expect(el.children[0].invertCondition).to.equal(false); - expect(el.children[0].passesCondition()).to.equal(true); + expect(/** @type {LionStep} */ (el.children[0]).invertCondition).to.equal(false); + expect(/** @type {LionStep} */ (el.children[0]).passesCondition()).to.equal(true); }); it('can invert its condition', async () => { @@ -29,10 +33,10 @@ describe('lion-step', () => { Step 1 `); - el.children[0].condition = () => true; - el.children[0].invertCondition = true; - expect(el.children[0].condition()).to.equal(true); - expect(el.children[0].passesCondition()).to.equal(false); + /** @type {LionStep} */ (el.children[0]).condition = () => true; + /** @type {LionStep} */ (el.children[0]).invertCondition = true; + expect(/** @type {LionStep} */ (el.children[0]).condition()).to.equal(true); + expect(/** @type {LionStep} */ (el.children[0]).passesCondition()).to.equal(false); }); it('has "untouched" status by default', async () => { @@ -41,7 +45,7 @@ describe('lion-step', () => { Step 1 `); - expect(el.children[0].status).to.equal('untouched'); + expect(/** @type {LionStep} */ (el.children[0]).status).to.equal('untouched'); }); it('is hidden when attribute hidden is true', async () => { @@ -61,7 +65,7 @@ describe('lion-step', () => { Step 1 `); - expect(el.children[0].controller).to.equal(el); + expect(/** @type {LionStep} */ (el.children[0]).controller).to.equal(el); }); describe('navigation', () => { @@ -71,9 +75,9 @@ describe('lion-step', () => { Step 1 `); - setTimeout(() => el.children[0].enter(), 0); + setTimeout(() => /** @type {LionStep} */ (el.children[0]).enter(), 0); await oneEvent(el.children[0], 'enter'); - expect(el.children[0].status).to.equal('entered'); + expect(/** @type {LionStep} */ (el.children[0]).status).to.equal('entered'); }); it('can be left', async () => { @@ -82,9 +86,9 @@ describe('lion-step', () => { Step 1 `); - setTimeout(() => el.children[0].leave(), 0); + setTimeout(() => /** @type {LionStep} */ (el.children[0]).leave(), 0); await oneEvent(el.children[0], 'leave'); - expect(el.children[0].status).to.equal('left'); + expect(/** @type {LionStep} */ (el.children[0]).status).to.equal('left'); }); it('can be skipped', async () => { @@ -93,9 +97,9 @@ describe('lion-step', () => { Step 1 `); - setTimeout(() => el.children[0].skip(), 0); + setTimeout(() => /** @type {LionStep} */ (el.children[0]).skip(), 0); await oneEvent(el.children[0], 'skip'); - expect(el.children[0].status).to.equal('skipped'); + expect(/** @type {LionStep} */ (el.children[0]).status).to.equal('skipped'); }); }); }); diff --git a/packages/steps/test/lion-steps.test.js b/packages/steps/test/lion-steps.test.js index 2f2a4d2e6..4d36064c3 100644 --- a/packages/steps/test/lion-steps.test.js +++ b/packages/steps/test/lion-steps.test.js @@ -1,22 +1,40 @@ -import { expect, fixture, html, oneEvent } from '@open-wc/testing'; +import { expect, fixture as _fixture, html, oneEvent } from '@open-wc/testing'; import sinon from 'sinon'; import '../lion-step.js'; import '../lion-steps.js'; +/** + * @typedef {import('../src/LionSteps').LionSteps} LionSteps + * @typedef {import('../src/LionStep').LionStep} LionStep + * @typedef {import('lit-html').TemplateResult} TemplateResult + * @typedef {{[key: string]: ?}} UnknownData + */ + +const fixture = /** @type {(arg: TemplateResult) => Promise} */ (_fixture); + +/** + * + * @param {LionSteps} steps + * @param {Object} expected + * @param {string} expected.transitions + * @param {string} expected.statuses + */ async function checkWorkflow(steps, expected) { return new Promise(resolve => { + /** @type {number[]} */ const transitions = []; steps.addEventListener('transition', event => { + const _event = /** @type {CustomEvent} */ (event); if (!transitions.length) { - transitions.push(event.detail.fromStep.number); + transitions.push(_event.detail.fromStep.number); } - transitions.push(event.detail.toStep.number); + transitions.push(_event.detail.toStep.number); }); setTimeout(() => { // allow time for other transitions to happen if any const transitionsString = transitions.join(' => '); expect(transitionsString).to.equal(expected.transitions, 'transition flow is different'); - const statusesString = Array.from(steps.children) + const statusesString = /** @type {LionStep[]} */ (Array.from(steps.children)) .map(step => step.status) .join(' '); expect(statusesString).to.equal(expected.statuses, 'steps statuses are different'); @@ -60,7 +78,7 @@ describe('lion-steps', () => { `); expect(el.current).to.equal(0); - expect(el.children[0].status).to.equal('entered'); + expect(/** @type {LionStep} */ (el.children[0]).status).to.equal('entered'); }); }); @@ -133,9 +151,13 @@ describe('lion-steps', () => { const el = await fixture(html` Step 0 - data.age < 18}>Step 1 + data.age < 18} + >Step 1 Step 2 - data.age < 22}>Step 3 + data.age < 22} + >Step 3 Step 4 `); @@ -170,8 +192,8 @@ describe('lion-steps', () => { describe('events', () => { it('will fire lion-step @leave event before changing .current', async () => { let currentInLeaveEvent; - const onLeave = ev => { - currentInLeaveEvent = ev.target.controller.current; + const onLeave = /** @param {Event} ev */ ev => { + currentInLeaveEvent = /** @type {LionStep} */ (ev.target).controller?.current; }; const el = await fixture(html` @@ -186,8 +208,8 @@ describe('lion-steps', () => { it('will fire lion-step @enter event after changing .current', async () => { let currentInEnterEvent; - const onEnter = ev => { - currentInEnterEvent = ev.target.controller.current; + const onEnter = /** @param {Event} ev */ ev => { + currentInEnterEvent = /** @type {LionStep} */ (ev.target).controller?.current; }; const el = await fixture(html` @@ -245,9 +267,16 @@ describe('lion-steps', () => { const el = await fixture(html` Step 0 - data.age < 18}>Step 1 - data.age >= 18 && data.age < 21}>Step 2 - data.age >= 21}>Step 3 + data.age < 18} + >Step 1 + data.age >= 18 && data.age < 21} + >Step 2 + data.age >= 21} + >Step 3 Step 4 `); @@ -264,9 +293,16 @@ describe('lion-steps', () => { const el = await fixture(html` Step 0 - data.age < 18}>Step 1 - data.age >= 18 && data.age < 21}>Step 2 - data.age >= 21}>Step 3 + data.age < 18} + >Step 1 + data.age >= 18 && data.age < 21} + >Step 2 + data.age >= 21} + >Step 3 Step 4 `); @@ -285,9 +321,16 @@ describe('lion-steps', () => { const el = await fixture(html` Step 0 - data.age < 18}>Step 1 - data.age >= 18 && data.age < 21}>Step 2 - data.age >= 21}>Step 3 + data.age < 18} + >Step 1 + data.age >= 18 && data.age < 21} + >Step 2 + data.age >= 21} + >Step 3 Step 4 `); @@ -309,7 +352,11 @@ describe('lion-steps', () => { const el = await fixture(html` Step 0 - data.age < 18} invert-condition>Step 1 + data.age < 18} + invert-condition + >Step 1 Step 2 `); @@ -332,7 +379,7 @@ describe('lion-steps', () => { }); it('behaves like "if/else" in case both condition and inverted condition are present', async () => { - const condition = data => data.age < 18; + const condition = /** @param {UnknownData} data */ data => data.age < 18; const el = await fixture(html` Step 0 @@ -411,7 +458,7 @@ describe('lion-steps', () => { el.next(); el.previous(); - expect(el.children[1].status).to.equal('left'); + expect(/** @type {LionStep} */ (el.children[1]).status).to.equal('left'); }); }); });