feat(steps): add types

This commit is contained in:
Joren Broekema 2020-09-30 17:44:35 +02:00 committed by Thomas Allmer
parent 66c5f2cd36
commit 7d4b6d32db
6 changed files with 176 additions and 94 deletions

View file

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

View file

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

View file

@ -1,5 +1,9 @@
import { css, html, LitElement } from '@lion/core'; import { css, html, LitElement } from '@lion/core';
/**
* @typedef {import('./LionSteps').LionSteps} LionSteps
*/
/** /**
* `LionStep` is one of many in a LionSteps Controller * `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 { export class LionStep extends LitElement {
static get properties() { 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 { return {
/** /**
* Step status, one of: "untouched", "entered", "left", "skipped". * 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)`. * Takes lion-steps data as a first argument `myConditionFunc(data)`.
*/ */
condition: { condition: {
type: Function, attribute: false,
}, },
/** /**
* Allows to invert condition function result. * Allows to invert condition function result.
*/ */
invertCondition: { invertCondition: {
type: Boolean, type: Boolean,
reflect: true,
attribute: 'invert-condition', attribute: 'invert-condition',
}, },
/** /**
@ -55,6 +42,7 @@ export class LionStep extends LitElement {
*/ */
forwardOnly: { forwardOnly: {
type: Boolean, type: Boolean,
reflect: true,
attribute: 'forward-only', attribute: 'forward-only',
}, },
/** /**
@ -63,6 +51,7 @@ export class LionStep extends LitElement {
*/ */
initialStep: { initialStep: {
type: Boolean, type: Boolean,
reflect: true,
attribute: 'initial-step', attribute: 'initial-step',
}, },
}; };
@ -71,7 +60,8 @@ export class LionStep extends LitElement {
constructor() { constructor() {
super(); super();
this.status = 'untouched'; this.status = 'untouched';
this.condition = () => true; // eslint-disable-next-line no-unused-vars
this.condition = /** @param {Object} [data] */ data => true;
this.invertCondition = false; this.invertCondition = false;
this.forwardOnly = false; this.forwardOnly = false;
this.initialStep = false; this.initialStep = false;
@ -99,9 +89,10 @@ export class LionStep extends LitElement {
return html`<slot></slot>`; return html`<slot></slot>`;
} }
firstUpdated() { /** @param {import('lit-element').PropertyValues } changedProperties */
super.firstUpdated(); firstUpdated(changedProperties) {
this.controller = this.parentNode; super.firstUpdated(changedProperties);
this.controller = /** @type {LionSteps} */ (this.parentNode);
} }
enter() { enter() {
@ -119,6 +110,7 @@ export class LionStep extends LitElement {
this.dispatchEvent(new CustomEvent('skip', { bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent('skip', { bubbles: true, composed: true }));
} }
/** @param {Object} [data] */
passesCondition(data) { passesCondition(data) {
const result = this.condition(data); const result = this.condition(data);
return this.invertCondition ? !result : result; return this.invertCondition ? !result : result;

View file

@ -1,5 +1,9 @@
import { css, html, LitElement } from '@lion/core'; import { css, html, LitElement } from '@lion/core';
/**
* @typedef {import('./LionStep.js').LionStep} LionStep
*/
/** /**
* `LionSteps` is a controller for a multi step system. * `LionSteps` is a controller for a multi step system.
* *
@ -7,6 +11,20 @@ import { css, html, LitElement } from '@lion/core';
* @extends {LitElement} * @extends {LitElement}
*/ */
export class LionSteps extends LitElement { export class LionSteps extends LitElement {
static get styles() {
return [
css`
:host {
display: block;
}
:host([hidden]) {
display: none;
}
`,
];
}
static get properties() { static get properties() {
/** /**
* Fired when a transition between steps happens. * 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() { constructor() {
super(); super();
/** @type {{[key: string]: ?}} */
this.data = {}; this.data = {};
this._internalCurrentSync = true; // necessary for preventing side effects on initialization this._internalCurrentSync = true; // necessary for preventing side effects on initialization
/** @type {number} */
this.current = 0; this.current = 0;
this._max = 0;
} }
static get styles() { /** @param {import('lit-element').PropertyValues } changedProperties */
return [ firstUpdated(changedProperties) {
css` super.firstUpdated(changedProperties);
:host {
display: block;
}
:host([hidden]) {
display: none;
}
`,
];
}
render() {
return html`<slot></slot>`;
}
firstUpdated() {
super.firstUpdated();
this._max = this.steps.length - 1; this._max = this.steps.length - 1;
let hasInitial = false; 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`<slot></slot>`;
}
next() { next() {
this._goTo(this.current + 1, this.current); this._goTo(this.current + 1, this.current);
} }
@ -91,10 +100,18 @@ export class LionSteps extends LitElement {
} }
get steps() { get steps() {
const defaultSlot = this.shadowRoot.querySelector('slot:not([name])'); const defaultSlot = /** @type {HTMLSlotElement} */ (this.shadowRoot?.querySelector(
return defaultSlot.assignedNodes().filter(node => node.nodeType === Node.ELEMENT_NODE); 'slot:not([name])',
));
return /** @type {LionStep[]} */ (defaultSlot.assignedNodes()).filter(
node => node.nodeType === Node.ELEMENT_NODE,
);
} }
/**
* @param {number} newCurrent
* @param {number} oldCurrent
*/
_goTo(newCurrent, oldCurrent) { _goTo(newCurrent, oldCurrent) {
if (newCurrent < 0 || newCurrent > this._max) { if (newCurrent < 0 || newCurrent > this._max) {
throw new Error(`There is no step at index ${newCurrent}.`); 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) { _changeStep(newCurrent, oldCurrent) {
const oldStepElement = this.steps[oldCurrent]; const oldStepElement = this.steps[oldCurrent];
const newStepElement = this.steps[newCurrent]; const newStepElement = this.steps[newCurrent];
@ -137,6 +158,10 @@ export class LionSteps extends LitElement {
this._dispatchTransitionEvent(fromStep, toStep); this._dispatchTransitionEvent(fromStep, toStep);
} }
/**
* @param {{number: number, element: LionStep}} fromStep
* @param {{number: number, element: LionStep}} toStep
*/
_dispatchTransitionEvent(fromStep, toStep) { _dispatchTransitionEvent(fromStep, toStep) {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('transition', { new CustomEvent('transition', {
@ -147,6 +172,10 @@ export class LionSteps extends LitElement {
); );
} }
/**
* @param {{current: number}} newValues
* @param {{current: number}} oldValues
*/
_onCurrentChanged(newValues, oldValues) { _onCurrentChanged(newValues, oldValues) {
if (this._internalCurrentSync) { if (this._internalCurrentSync) {
this._internalCurrentSync = false; this._internalCurrentSync = false;

View file

@ -2,6 +2,10 @@ import { expect, fixture, html, oneEvent } from '@open-wc/testing';
import '../lion-step.js'; import '../lion-step.js';
/**
* @typedef {import('../src/LionStep').LionStep} LionStep
*/
describe('lion-step', () => { describe('lion-step', () => {
it('has a condition which allows it to become active (condition is true by default)', async () => { it('has a condition which allows it to become active (condition is true by default)', async () => {
const el = await fixture(html` const el = await fixture(html`
@ -9,8 +13,8 @@ describe('lion-step', () => {
<lion-step>Step 1</lion-step> <lion-step>Step 1</lion-step>
</fake-lion-steps> </fake-lion-steps>
`); `);
expect(el.children[0].condition()).to.equal(true); expect(/** @type {LionStep} */ (el.children[0]).condition()).to.equal(true);
expect(el.children[0].passesCondition()).to.equal(true); expect(/** @type {LionStep} */ (el.children[0]).passesCondition()).to.equal(true);
}); });
it('does not invert condition by default', async () => { it('does not invert condition by default', async () => {
@ -19,8 +23,8 @@ describe('lion-step', () => {
<lion-step>Step 1</lion-step> <lion-step>Step 1</lion-step>
</fake-lion-steps> </fake-lion-steps>
`); `);
expect(el.children[0].invertCondition).to.equal(false); expect(/** @type {LionStep} */ (el.children[0]).invertCondition).to.equal(false);
expect(el.children[0].passesCondition()).to.equal(true); expect(/** @type {LionStep} */ (el.children[0]).passesCondition()).to.equal(true);
}); });
it('can invert its condition', async () => { it('can invert its condition', async () => {
@ -29,10 +33,10 @@ describe('lion-step', () => {
<lion-step>Step 1</lion-step> <lion-step>Step 1</lion-step>
</fake-lion-steps> </fake-lion-steps>
`); `);
el.children[0].condition = () => true; /** @type {LionStep} */ (el.children[0]).condition = () => true;
el.children[0].invertCondition = true; /** @type {LionStep} */ (el.children[0]).invertCondition = true;
expect(el.children[0].condition()).to.equal(true); expect(/** @type {LionStep} */ (el.children[0]).condition()).to.equal(true);
expect(el.children[0].passesCondition()).to.equal(false); expect(/** @type {LionStep} */ (el.children[0]).passesCondition()).to.equal(false);
}); });
it('has "untouched" status by default', async () => { it('has "untouched" status by default', async () => {
@ -41,7 +45,7 @@ describe('lion-step', () => {
<lion-step>Step 1</lion-step> <lion-step>Step 1</lion-step>
</fake-lion-steps> </fake-lion-steps>
`); `);
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 () => { it('is hidden when attribute hidden is true', async () => {
@ -61,7 +65,7 @@ describe('lion-step', () => {
<lion-step>Step 1</lion-step> <lion-step>Step 1</lion-step>
</fake-lion-steps> </fake-lion-steps>
`); `);
expect(el.children[0].controller).to.equal(el); expect(/** @type {LionStep} */ (el.children[0]).controller).to.equal(el);
}); });
describe('navigation', () => { describe('navigation', () => {
@ -71,9 +75,9 @@ describe('lion-step', () => {
<lion-step>Step 1</lion-step> <lion-step>Step 1</lion-step>
</fake-lion-steps> </fake-lion-steps>
`); `);
setTimeout(() => el.children[0].enter(), 0); setTimeout(() => /** @type {LionStep} */ (el.children[0]).enter(), 0);
await oneEvent(el.children[0], 'enter'); 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 () => { it('can be left', async () => {
@ -82,9 +86,9 @@ describe('lion-step', () => {
<lion-step>Step 1</lion-step> <lion-step>Step 1</lion-step>
</fake-lion-steps> </fake-lion-steps>
`); `);
setTimeout(() => el.children[0].leave(), 0); setTimeout(() => /** @type {LionStep} */ (el.children[0]).leave(), 0);
await oneEvent(el.children[0], 'leave'); 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 () => { it('can be skipped', async () => {
@ -93,9 +97,9 @@ describe('lion-step', () => {
<lion-step>Step 1</lion-step> <lion-step>Step 1</lion-step>
</fake-lion-steps> </fake-lion-steps>
`); `);
setTimeout(() => el.children[0].skip(), 0); setTimeout(() => /** @type {LionStep} */ (el.children[0]).skip(), 0);
await oneEvent(el.children[0], 'skip'); await oneEvent(el.children[0], 'skip');
expect(el.children[0].status).to.equal('skipped'); expect(/** @type {LionStep} */ (el.children[0]).status).to.equal('skipped');
}); });
}); });
}); });

View file

@ -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 sinon from 'sinon';
import '../lion-step.js'; import '../lion-step.js';
import '../lion-steps.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<LionSteps>} */ (_fixture);
/**
*
* @param {LionSteps} steps
* @param {Object} expected
* @param {string} expected.transitions
* @param {string} expected.statuses
*/
async function checkWorkflow(steps, expected) { async function checkWorkflow(steps, expected) {
return new Promise(resolve => { return new Promise(resolve => {
/** @type {number[]} */
const transitions = []; const transitions = [];
steps.addEventListener('transition', event => { steps.addEventListener('transition', event => {
const _event = /** @type {CustomEvent} */ (event);
if (!transitions.length) { 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(() => { setTimeout(() => {
// allow time for other transitions to happen if any // allow time for other transitions to happen if any
const transitionsString = transitions.join(' => '); const transitionsString = transitions.join(' => ');
expect(transitionsString).to.equal(expected.transitions, 'transition flow is different'); 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) .map(step => step.status)
.join(' '); .join(' ');
expect(statusesString).to.equal(expected.statuses, 'steps statuses are different'); expect(statusesString).to.equal(expected.statuses, 'steps statuses are different');
@ -60,7 +78,7 @@ describe('lion-steps', () => {
</lion-steps> </lion-steps>
`); `);
expect(el.current).to.equal(0); 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` const el = await fixture(html`
<lion-steps .data=${{ age: 25 }}> <lion-steps .data=${{ age: 25 }}>
<lion-step initial-step>Step 0</lion-step> <lion-step initial-step>Step 0</lion-step>
<lion-step .condition=${data => data.age < 18}>Step 1</lion-step> <lion-step .condition=${/** @param {UnknownData} data */ data => data.age < 18}
>Step 1</lion-step
>
<lion-step>Step 2</lion-step> <lion-step>Step 2</lion-step>
<lion-step .condition=${data => data.age < 22}>Step 3</lion-step> <lion-step .condition=${/** @param {UnknownData} data */ data => data.age < 22}
>Step 3</lion-step
>
<lion-step>Step 4</lion-step> <lion-step>Step 4</lion-step>
</lion-steps> </lion-steps>
`); `);
@ -170,8 +192,8 @@ describe('lion-steps', () => {
describe('events', () => { describe('events', () => {
it('will fire lion-step @leave event before changing .current', async () => { it('will fire lion-step @leave event before changing .current', async () => {
let currentInLeaveEvent; let currentInLeaveEvent;
const onLeave = ev => { const onLeave = /** @param {Event} ev */ ev => {
currentInLeaveEvent = ev.target.controller.current; currentInLeaveEvent = /** @type {LionStep} */ (ev.target).controller?.current;
}; };
const el = await fixture(html` const el = await fixture(html`
<lion-steps> <lion-steps>
@ -186,8 +208,8 @@ describe('lion-steps', () => {
it('will fire lion-step @enter event after changing .current', async () => { it('will fire lion-step @enter event after changing .current', async () => {
let currentInEnterEvent; let currentInEnterEvent;
const onEnter = ev => { const onEnter = /** @param {Event} ev */ ev => {
currentInEnterEvent = ev.target.controller.current; currentInEnterEvent = /** @type {LionStep} */ (ev.target).controller?.current;
}; };
const el = await fixture(html` const el = await fixture(html`
<lion-steps> <lion-steps>
@ -245,9 +267,16 @@ describe('lion-steps', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-steps .data=${{ age: 25 }}> <lion-steps .data=${{ age: 25 }}>
<lion-step initial-step>Step 0</lion-step> <lion-step initial-step>Step 0</lion-step>
<lion-step .condition=${data => data.age < 18}>Step 1</lion-step> <lion-step .condition=${/** @param {UnknownData} data */ data => data.age < 18}
<lion-step .condition=${data => data.age >= 18 && data.age < 21}>Step 2</lion-step> >Step 1</lion-step
<lion-step .condition=${data => data.age >= 21}>Step 3</lion-step> >
<lion-step
.condition=${/** @param {UnknownData} data */ data => data.age >= 18 && data.age < 21}
>Step 2</lion-step
>
<lion-step .condition=${/** @param {UnknownData} data */ data => data.age >= 21}
>Step 3</lion-step
>
<lion-step>Step 4</lion-step> <lion-step>Step 4</lion-step>
</lion-steps> </lion-steps>
`); `);
@ -264,9 +293,16 @@ describe('lion-steps', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-steps .data=${{ age: 19 }}> <lion-steps .data=${{ age: 19 }}>
<lion-step initial-step>Step 0</lion-step> <lion-step initial-step>Step 0</lion-step>
<lion-step .condition=${data => data.age < 18}>Step 1</lion-step> <lion-step .condition=${/** @param {UnknownData} data */ data => data.age < 18}
<lion-step .condition=${data => data.age >= 18 && data.age < 21}>Step 2</lion-step> >Step 1</lion-step
<lion-step .condition=${data => data.age >= 21}>Step 3</lion-step> >
<lion-step
.condition=${/** @param {UnknownData} data */ data => data.age >= 18 && data.age < 21}
>Step 2</lion-step
>
<lion-step .condition=${/** @param {UnknownData} data */ data => data.age >= 21}
>Step 3</lion-step
>
<lion-step>Step 4</lion-step> <lion-step>Step 4</lion-step>
</lion-steps> </lion-steps>
`); `);
@ -285,9 +321,16 @@ describe('lion-steps', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-steps .data=${{ age: 15 }}> <lion-steps .data=${{ age: 15 }}>
<lion-step initial-step>Step 0</lion-step> <lion-step initial-step>Step 0</lion-step>
<lion-step .condition=${data => data.age < 18}>Step 1</lion-step> <lion-step .condition=${/** @param {UnknownData} data */ data => data.age < 18}
<lion-step .condition=${data => data.age >= 18 && data.age < 21}>Step 2</lion-step> >Step 1</lion-step
<lion-step .condition=${data => data.age >= 21}>Step 3</lion-step> >
<lion-step
.condition=${/** @param {UnknownData} data */ data => data.age >= 18 && data.age < 21}
>Step 2</lion-step
>
<lion-step .condition=${/** @param {UnknownData} data */ data => data.age >= 21}
>Step 3</lion-step
>
<lion-step>Step 4</lion-step> <lion-step>Step 4</lion-step>
</lion-steps> </lion-steps>
`); `);
@ -309,7 +352,11 @@ describe('lion-steps', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-steps .data=${{}}> <lion-steps .data=${{}}>
<lion-step initial-step>Step 0</lion-step> <lion-step initial-step>Step 0</lion-step>
<lion-step .condition=${data => data.age < 18} invert-condition>Step 1</lion-step> <lion-step
.condition=${/** @param {UnknownData} data */ data => data.age < 18}
invert-condition
>Step 1</lion-step
>
<lion-step>Step 2</lion-step> <lion-step>Step 2</lion-step>
</lion-steps> </lion-steps>
`); `);
@ -332,7 +379,7 @@ describe('lion-steps', () => {
}); });
it('behaves like "if/else" in case both condition and inverted condition are present', async () => { 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` const el = await fixture(html`
<lion-steps .data=${{}}> <lion-steps .data=${{}}>
<lion-step initial-step>Step 0</lion-step> <lion-step initial-step>Step 0</lion-step>
@ -411,7 +458,7 @@ describe('lion-steps', () => {
el.next(); el.next();
el.previous(); el.previous();
expect(el.children[1].status).to.equal('left'); expect(/** @type {LionStep} */ (el.children[1]).status).to.equal('left');
}); });
}); });
}); });