From cfa2daf6744858c2cde84a01476684ea0adb7ba9 Mon Sep 17 00:00:00 2001 From: Joren Broekema Date: Mon, 28 Sep 2020 19:00:59 +0200 Subject: [PATCH] feat: add types for input components --- .changeset/gentle-trees-beg.md | 9 ++ packages/input-date/src/LionInputDate.js | 22 ++++- .../test/lion-input-date-integrations.test.js | 1 - .../input-date/test/lion-input-date.test.js | 26 +++-- packages/input-email/src/LionInputEmail.js | 2 +- .../input-email/test/lion-input-email.test.js | 8 +- packages/input-iban/src/LionInputIban.js | 4 +- packages/input-iban/src/parsers.js | 2 +- packages/input-iban/src/validators.js | 29 +++++- .../input-iban/test/lion-input-iban.test.js | 30 +++--- packages/input-range/src/LionInputRange.js | 68 +++++++++---- .../input-range/test/lion-input-range.test.js | 42 +++++--- .../input-stepper/src/LionInputStepper.js | 95 +++++++++++-------- .../test/lion-input-stepper.test.js | 28 +++--- tsconfig.json | 5 + 15 files changed, 252 insertions(+), 119 deletions(-) create mode 100644 .changeset/gentle-trees-beg.md diff --git a/.changeset/gentle-trees-beg.md b/.changeset/gentle-trees-beg.md new file mode 100644 index 000000000..d7ed01e94 --- /dev/null +++ b/.changeset/gentle-trees-beg.md @@ -0,0 +1,9 @@ +--- +'@lion/input-date': minor +'@lion/input-email': minor +'@lion/input-iban': minor +'@lion/input-range': minor +'@lion/input-stepper': minor +--- + +Added types for all other input components except for datepicker. diff --git a/packages/input-date/src/LionInputDate.js b/packages/input-date/src/LionInputDate.js index 7c5681800..82b627a15 100644 --- a/packages/input-date/src/LionInputDate.js +++ b/packages/input-date/src/LionInputDate.js @@ -2,8 +2,12 @@ import { IsDate } from '@lion/form-core'; import { LionInput } from '@lion/input'; import { formatDate, LocalizeMixin, parseDate } from '@lion/localize'; +/** + * @param {Date|number} date + */ function isValidDate(date) { // to make sure it is a valid date we use isNaN and not Number.isNaN + // @ts-ignore dirty hack, you're not supposed to pass Date instances to isNaN // eslint-disable-next-line no-restricted-globals return date instanceof Date && !isNaN(date); } @@ -13,8 +17,8 @@ function isValidDate(date) { * on locale. * * @customElement lion-input-date - * @extends {LionInput} */ +// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110 export class LionInputDate extends LocalizeMixin(LionInput) { static get properties() { return { @@ -24,15 +28,19 @@ export class LionInputDate extends LocalizeMixin(LionInput) { constructor() { super(); - this.parser = (value, options) => (value === '' ? undefined : parseDate(value, options)); + /** + * @param {string} value + */ + this.parser = value => (value === '' ? undefined : parseDate(value)); this.formatter = formatDate; this.defaultValidators.push(new IsDate()); } + /** @param {import('lit-element').PropertyValues } changedProperties */ updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('locale')) { - this._calculateValues(); + this._calculateValues({ source: null }); } } @@ -42,6 +50,9 @@ export class LionInputDate extends LocalizeMixin(LionInput) { this.type = 'text'; } + /** + * @param {Date} modelValue + */ // eslint-disable-next-line class-methods-use-this serializer(modelValue) { if (!isValidDate(modelValue)) { @@ -50,9 +61,12 @@ export class LionInputDate extends LocalizeMixin(LionInput) { // modelValue is localized, so we take the timezone offset in milliseconds and subtract it // before converting it to ISO string. const offset = modelValue.getTimezoneOffset() * 60000; - return new Date(modelValue - offset).toISOString().slice(0, 10); + return new Date(modelValue.getTime() - offset).toISOString().slice(0, 10); } + /** + * @param {string} serializedValue + */ // eslint-disable-next-line class-methods-use-this deserializer(serializedValue) { return new Date(serializedValue); diff --git a/packages/input-date/test/lion-input-date-integrations.test.js b/packages/input-date/test/lion-input-date-integrations.test.js index c2f17dd00..b92d6c922 100644 --- a/packages/input-date/test/lion-input-date-integrations.test.js +++ b/packages/input-date/test/lion-input-date-integrations.test.js @@ -6,7 +6,6 @@ const tagString = 'lion-input-date'; describe(' integrations', () => { runInteractionStateMixinSuite({ tagString, - suffix: tagString, allowedModelValueTypes: [Date], }); diff --git a/packages/input-date/test/lion-input-date.test.js b/packages/input-date/test/lion-input-date.test.js index 298825137..681a99641 100644 --- a/packages/input-date/test/lion-input-date.test.js +++ b/packages/input-date/test/lion-input-date.test.js @@ -2,9 +2,15 @@ import { html } from '@lion/core'; import { localize } from '@lion/localize'; import { localizeTearDown } from '@lion/localize/test-helpers.js'; import { MaxDate } from '@lion/form-core'; -import { expect, fixture } from '@open-wc/testing'; +import { expect, fixture as _fixture } from '@open-wc/testing'; import '../lion-input-date.js'; +/** + * @typedef {import('../src/LionInputDate').LionInputDate} LionInputDate + * @typedef {import('lit-html').TemplateResult} TemplateResult + */ +const fixture = /** @type {(arg: TemplateResult) => Promise} */ (_fixture); + describe('', () => { beforeEach(() => { localizeTearDown(); @@ -24,16 +30,16 @@ describe('', () => { const el = await fixture(html``); el.modelValue = '2005/11/10'; expect(el.hasFeedbackFor).to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).to.have.a.property('IsDate'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).to.have.property('IsDate'); el.modelValue = new Date('2005/11/10'); expect(el.hasFeedbackFor).not.to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).not.to.have.a.property('IsDate'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).not.to.have.property('IsDate'); }); - it("does not throw on invalid dates like new Date('foo')", async () => { + it("does not throw on invalid dates like new Date('20.10.'), which could happen while the user types", async () => { const el = await fixture(html``); expect(() => { el.modelValue = new Date('foo'); @@ -48,13 +54,13 @@ describe('', () => { > `); expect(el.hasFeedbackFor).to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).to.have.a.property('MaxDate'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).to.have.property('MaxDate'); el.modelValue = new Date('2017/06/14'); expect(el.hasFeedbackFor).not.to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).not.to.have.a.property('MaxDate'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).not.to.have.property('MaxDate'); }); it('uses formatOptions.locale', async () => { diff --git a/packages/input-email/src/LionInputEmail.js b/packages/input-email/src/LionInputEmail.js index b78702875..f8479393d 100644 --- a/packages/input-email/src/LionInputEmail.js +++ b/packages/input-email/src/LionInputEmail.js @@ -6,8 +6,8 @@ import { LocalizeMixin } from '@lion/localize'; * LionInputEmail: extension of lion-input * * @customElement lion-input-email - * @extends {LionInput} */ +// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110 export class LionInputEmail extends LocalizeMixin(LionInput) { constructor() { super(); diff --git a/packages/input-email/test/lion-input-email.test.js b/packages/input-email/test/lion-input-email.test.js index 2b9bdd845..da21b7c7f 100644 --- a/packages/input-email/test/lion-input-email.test.js +++ b/packages/input-email/test/lion-input-email.test.js @@ -1,7 +1,13 @@ -import { expect, fixture } from '@open-wc/testing'; +import { expect, fixture as _fixture } from '@open-wc/testing'; import '../lion-input-email.js'; +/** + * @typedef {import('../src/LionInputEmail').LionInputEmail} LionInputEmail + * @typedef {import('lit-html').TemplateResult} TemplateResult + */ +const fixture = /** @type {(arg: TemplateResult|string) => Promise} */ (_fixture); + describe('', () => { it('has a type = text', async () => { const el = await fixture(``); diff --git a/packages/input-iban/src/LionInputIban.js b/packages/input-iban/src/LionInputIban.js index 399ef1842..d9ff6d991 100644 --- a/packages/input-iban/src/LionInputIban.js +++ b/packages/input-iban/src/LionInputIban.js @@ -6,9 +6,9 @@ import { IsIBAN } from './validators.js'; /** * `LionInputIban` is a class for an IBAN custom form element (``). - * - * @extends {LionInput} + * @customElement lion-input-iban */ +// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110 export class LionInputIban extends LocalizeMixin(LionInput) { constructor() { super(); diff --git a/packages/input-iban/src/parsers.js b/packages/input-iban/src/parsers.js index 983337a71..03854458f 100644 --- a/packages/input-iban/src/parsers.js +++ b/packages/input-iban/src/parsers.js @@ -4,7 +4,7 @@ import { isValidIBAN } from 'ibantools'; * Parses an IBAN trimming spaces and making uppercase. * * @param {string} viewValue value to be parsed - * @return {string} parsed value + * @return {string|undefined} parsed value */ export function parseIBAN(viewValue) { const trimmedValue = viewValue.replace(/\s/g, '').toUpperCase(); diff --git a/packages/input-iban/src/validators.js b/packages/input-iban/src/validators.js index c4c84fb76..28f398c9b 100644 --- a/packages/input-iban/src/validators.js +++ b/packages/input-iban/src/validators.js @@ -11,7 +11,7 @@ const loadTranslations = async () => { } await localize.loadNamespace( { - 'lion-validate+iban': locale => { + 'lion-validate+iban': /** @param {string} locale */ locale => { switch (locale) { case 'bg-BG': return import('../translations/bg-BG.js'); @@ -86,7 +86,7 @@ const loadTranslations = async () => { } }, }, - { locale: localize.localize }, + { locale: localize.locale }, ); loaded = true; }; @@ -96,11 +96,22 @@ export class IsIBAN extends Validator { return 'IsIBAN'; } + /** @param {string} value */ // eslint-disable-next-line class-methods-use-this execute(value) { return !isValidIBAN(value); } + /** + * @param {object} [data] + * @param {*} [data.modelValue] + * @param {string} [data.fieldName] + * @param {*} [data.params] + * @param {string} [data.type] + * @param {Object.} [data.config] + * @param {string} [data.name] + * @returns {Promise} + */ static async getMessage(data) { await loadTranslations(); return localize.msg('lion-validate+iban:error.IsIBAN', data); @@ -112,6 +123,10 @@ export class IsCountryIBAN extends IsIBAN { return 'IsCountryIBAN'; } + /** + * @param {?} [value] + * @returns {Boolean} + */ execute(value) { const notIBAN = super.execute(value); if (value.slice(0, 2) !== this.param) { @@ -123,6 +138,16 @@ export class IsCountryIBAN extends IsIBAN { return false; } + /** + * @param {object} [data] + * @param {*} [data.modelValue] + * @param {string} [data.fieldName] + * @param {*} [data.params] + * @param {string} [data.type] + * @param {Object.} [data.config] + * @param {string} [data.name] + * @returns {Promise} + */ static async getMessage(data) { await loadTranslations(); return localize.msg('lion-validate+iban:error.IsCountryIBAN', data); diff --git a/packages/input-iban/test/lion-input-iban.test.js b/packages/input-iban/test/lion-input-iban.test.js index 6268d08e2..18c5ea134 100644 --- a/packages/input-iban/test/lion-input-iban.test.js +++ b/packages/input-iban/test/lion-input-iban.test.js @@ -1,4 +1,4 @@ -import { expect, fixture } from '@open-wc/testing'; +import { expect, fixture as _fixture } from '@open-wc/testing'; import { html } from '@lion/core'; import { IsCountryIBAN } from '../src/validators.js'; @@ -7,6 +7,12 @@ import { parseIBAN } from '../src/parsers.js'; import '../lion-input-iban.js'; +/** + * @typedef {import('../src/LionInputIban').LionInputIban} LionInputIban + * @typedef {import('lit-html').TemplateResult} TemplateResult + */ +const fixture = /** @type {(arg: TemplateResult|string) => Promise} */ (_fixture); + describe('', () => { it('uses formatIBAN for formatting', async () => { const el = await fixture(``); @@ -27,12 +33,12 @@ describe('', () => { const el = await fixture(``); el.modelValue = 'FOO'; expect(el.hasFeedbackFor).to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).to.have.a.property('IsIBAN'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).to.have.property('IsIBAN'); el.modelValue = 'DE89370400440532013000'; expect(el.hasFeedbackFor).not.to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).not.to.have.a.property('IsIBAN'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).not.to.have.property('IsIBAN'); }); it('can apply validator "IsCountryIBAN" to restrict countries', async () => { @@ -41,17 +47,17 @@ describe('', () => { `); el.modelValue = 'DE89370400440532013000'; expect(el.hasFeedbackFor).to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).to.have.a.property('IsCountryIBAN'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).to.have.property('IsCountryIBAN'); el.modelValue = 'NL17INGB0002822608'; expect(el.hasFeedbackFor).not.to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).not.to.have.a.property('IsCountryIBAN'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).not.to.have.property('IsCountryIBAN'); el.modelValue = 'FOO'; expect(el.hasFeedbackFor).to.include('error'); - expect(el.validationStates).to.have.a.property('error'); - expect(el.validationStates.error).to.have.a.property('IsIBAN'); - expect(el.validationStates.error).to.have.a.property('IsCountryIBAN'); + expect(el.validationStates).to.have.property('error'); + expect(el.validationStates.error).to.have.property('IsIBAN'); + expect(el.validationStates.error).to.have.property('IsCountryIBAN'); }); it('is accessible', async () => { diff --git a/packages/input-range/src/LionInputRange.js b/packages/input-range/src/LionInputRange.js index 1aaf0deeb..6edffcba0 100644 --- a/packages/input-range/src/LionInputRange.js +++ b/packages/input-range/src/LionInputRange.js @@ -3,18 +3,31 @@ import { css, html, unsafeCSS } from '@lion/core'; import { LionInput } from '@lion/input'; import { formatNumber, LocalizeMixin } from '@lion/localize'; +/** + * @typedef {import('lit-element').CSSResult} CSSResult + */ + /** * LionInputRange: extension of lion-input. * * @customElement `lion-input-range` - * @extends LionInput */ +// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110 + false positive for incompatible static get properties. Lit-element merges super properties already for you. export class LionInputRange extends LocalizeMixin(LionInput) { static get properties() { return { - min: Number, - max: Number, - unit: String, + min: { + type: Number, + reflect: true, + }, + max: { + type: Number, + reflect: true, + }, + unit: { + type: String, + reflect: true, + }, step: { type: Number, reflect: true, @@ -26,6 +39,9 @@ export class LionInputRange extends LocalizeMixin(LionInput) { }; } + /** + * @param {CSSResult} scope + */ static rangeStyles(scope) { return css` /* Custom input range styling comes here, be aware that this won't work for polyfilled browsers */ @@ -37,9 +53,24 @@ export class LionInputRange extends LocalizeMixin(LionInput) { `; } - connectedCallback() { - if (super.connectedCallback) super.connectedCallback(); + constructor() { + super(); + this.min = Infinity; + this.max = Infinity; + this.step = 1; + this.unit = ''; this.type = 'range'; + this.noMinMaxLabels = false; + /** + * @param {string} modelValue + */ + this.parser = modelValue => parseFloat(modelValue); + this.scopedClass = `${this.localName}-${Math.floor(Math.random() * 10000)}`; + this.__styleTag = document.createElement('style'); + } + + connectedCallback() { + super.connectedCallback(); /* eslint-disable-next-line wc/no-self-class */ this.classList.add(this.scopedClass); @@ -47,38 +78,34 @@ export class LionInputRange extends LocalizeMixin(LionInput) { } disconnectedCallback() { - if (super.disconnectedCallback) super.disconnectedCallback(); + super.disconnectedCallback(); this.__teardownStyleTag(); } - constructor() { - super(); - this.parser = modelValue => parseFloat(modelValue); - this.scopedClass = `${this.localName}-${Math.floor(Math.random() * 10000)}`; - } - + /** @param {import('lit-element').PropertyValues } changedProperties */ updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('min')) { - this._inputNode.min = this.min; + this._inputNode.min = `${this.min}`; } if (changedProperties.has('max')) { - this._inputNode.max = this.max; + this._inputNode.max = `${this.max}`; } if (changedProperties.has('step')) { - this._inputNode.step = this.step; + this._inputNode.step = `${this.step}`; } } + /** @param {import('lit-element').PropertyValues } changedProperties */ firstUpdated(changedProperties) { super.firstUpdated(changedProperties); if (changedProperties.has('modelValue')) { // TODO: find out why this hack is needed to display the initial modelValue this.updateComplete.then(() => { - this._inputNode.value = this.modelValue; + this._inputNode.value = `${this.modelValue}`; }); } } @@ -86,7 +113,7 @@ export class LionInputRange extends LocalizeMixin(LionInput) { _inputGroupTemplate() { return html`
- ${formatNumber(this.formattedValue)} + ${formatNumber(parseFloat(this.formattedValue))} ${this.unit}
@@ -117,8 +144,9 @@ export class LionInputRange extends LocalizeMixin(LionInput) { } __setupStyleTag() { - this.__styleTag = document.createElement('style'); - this.__styleTag.textContent = this.constructor.rangeStyles(unsafeCSS(this.scopedClass)); + this.__styleTag.textContent = /** @type {typeof LionInputRange} */ (this.constructor) + .rangeStyles(unsafeCSS(this.scopedClass)) + .toString(); this.insertBefore(this.__styleTag, this.childNodes[0]); } diff --git a/packages/input-range/test/lion-input-range.test.js b/packages/input-range/test/lion-input-range.test.js index 6a858dae9..df013db0c 100644 --- a/packages/input-range/test/lion-input-range.test.js +++ b/packages/input-range/test/lion-input-range.test.js @@ -1,7 +1,13 @@ -import { expect, fixture, nextFrame, html } from '@open-wc/testing'; +import { expect, fixture as _fixture, nextFrame, html } from '@open-wc/testing'; import '../lion-input-range.js'; +/** + * @typedef {import('../src/LionInputRange').LionInputRange} LionInputRange + * @typedef {import('lit-html').TemplateResult} TemplateResult + */ +const fixture = /** @type {(arg: TemplateResult|string) => Promise} */ (_fixture); + describe('', () => { it('has a type = range', async () => { const el = await fixture(``); @@ -41,35 +47,41 @@ describe('', () => { const el = await fixture(html` `); - expect(el.shadowRoot.querySelector('.input-range__value').innerText).to.equal('75'); - expect(el.shadowRoot.querySelector('.input-range__unit').innerText).to.equal('%'); + expect( + /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('.input-range__value')).innerText, + ).to.equal('75'); + expect( + /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('.input-range__unit')).innerText, + ).to.equal('%'); }); it('displays 2 tick labels (min and max values) by default', async () => { const el = await fixture(``); - expect(el.shadowRoot.querySelectorAll('.input-range__limits span').length).to.equal(2); - expect(el.shadowRoot.querySelectorAll('.input-range__limits span')[0].innerText).to.equal( - el.min, - ); - expect(el.shadowRoot.querySelectorAll('.input-range__limits span')[1].innerText).to.equal( - el.max, - ); + expect(el.shadowRoot?.querySelectorAll('.input-range__limits span').length).to.equal(2); + expect( + /** @type {HTMLElement} */ (el.shadowRoot?.querySelectorAll('.input-range__limits span')[0]) + .innerText, + ).to.equal(el.min.toString()); + expect( + /** @type {HTMLElement} */ (el.shadowRoot?.querySelectorAll('.input-range__limits span')[1]) + .innerText, + ).to.equal(el.max.toString()); }); it('update min and max attributes when min and max property change', async () => { const el = await fixture(``); - el.min = '120'; - el.max = '220'; + el.min = 120; + el.max = 220; await nextFrame(); // sync to native element takes some time - expect(el._inputNode.min).to.equal(el.min); - expect(el._inputNode.max).to.equal(el.max); + expect(el._inputNode.min).to.equal(el.min.toString()); + expect(el._inputNode.max).to.equal(el.max.toString()); }); it('can hide the tick labels', async () => { const el = await fixture( ``, ); - expect(el.shadowRoot.querySelectorAll('.input-group__input')[0]).dom.to.equal(` + expect(el.shadowRoot?.querySelectorAll('.input-group__input')[0]).dom.to.equal(`
diff --git a/packages/input-stepper/src/LionInputStepper.js b/packages/input-stepper/src/LionInputStepper.js index b902946b1..42d09ddb0 100644 --- a/packages/input-stepper/src/LionInputStepper.js +++ b/packages/input-stepper/src/LionInputStepper.js @@ -6,8 +6,8 @@ import { IsNumber, MinNumber, MaxNumber } from '@lion/form-core'; * `LionInputStepper` is a class for custom input-stepper element (`` web component). * * @customElement lion-input-stepper - * @extends LitElement */ +// @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you. export class LionInputStepper extends LionInput { static get styles() { return [ @@ -22,30 +22,53 @@ export class LionInputStepper extends LionInput { static get properties() { return { - min: Number, - max: Number, - step: Number, - modelValue: Number, - __disableIncrementor: Boolean, - __disableDecrementor: Boolean, + min: { + type: Number, + reflect: true, + }, + max: { + type: Number, + reflect: true, + }, + step: { + type: Number, + reflect: true, + }, + __disableIncrementor: { attribute: false }, + __disableDecrementor: { attribute: false }, }; } + /** + * @returns {number} + */ get currentValue() { - return parseFloat(this.value || 0); + return parseFloat(this.value) || 0; } constructor() { super(); + /** @param {string} modelValue */ this.parser = modelValue => parseFloat(modelValue); this.__disableIncrementor = false; this.__disableDecrementor = false; + this.min = Infinity; + this.max = Infinity; + this.step = 1; + this.values = { + max: this.max, + min: this.min, + step: this.step, + }; } connectedCallback() { - if (super.connectedCallback) { - super.connectedCallback(); - } + super.connectedCallback(); + this.values = { + max: this.max, + min: this.min, + step: this.step, + }; this.role = 'spinbutton'; this.addEventListener('keydown', this.__keyDownHandler); this._inputNode.setAttribute('inputmode', 'decimal'); @@ -56,29 +79,27 @@ export class LionInputStepper extends LionInput { } disconnectedCallback() { - if (super.disconnectedCallback) { - super.disconnectedCallback(); - } + super.disconnectedCallback(); this.removeEventListener('keydown', this.__keyDownHandler); } - /** - * Update native input values - * @param {Object} changedProps - changed props - */ - updated(changedProps) { - super.updated(changedProps); + /** @param {import('lit-element').PropertyValues } changedProperties */ + updated(changedProperties) { + super.updated(changedProperties); - if (changedProps.has('min')) { - this._inputNode.min = this.min; + if (changedProperties.has('min')) { + this._inputNode.min = `${this.min}`; + this.values.min = this.min; } - if (changedProps.has('max')) { - this._inputNode.max = this.max; + if (changedProperties.has('max')) { + this._inputNode.max = `${this.max}`; + this.values.max = this.max; } - if (changedProps.has('step')) { - this._inputNode.step = this.step; + if (changedProperties.has('step')) { + this._inputNode.step = `${this.step}`; + this.values.step = this.step; } } @@ -87,27 +108,23 @@ export class LionInputStepper extends LionInput { * @private */ __setAriaLabelsAndValidator() { - this.values = { - max: parseFloat(this.max || Infinity), - min: parseFloat(this.min || Infinity), - step: parseFloat(this.step), - }; - const ariaAttributes = { 'aria-valuemax': this.values.max, 'aria-valuemin': this.values.min, }; - let validators = Object.entries(ariaAttributes) + const minMaxValidators = /** @type {(MaxNumber | MinNumber)[]} */ (Object.entries( + ariaAttributes, + ) .map(([key, val]) => { if (val !== Infinity) { - this.setAttribute(key, val); + this.setAttribute(key, `${val}`); return key === 'aria-valuemax' ? new MaxNumber(val) : new MinNumber(val); } return null; }) - .filter(validator => validator); - validators = [new IsNumber(), ...validators]; + .filter(validator => validator !== null)); + const validators = [new IsNumber(), ...minMaxValidators]; this.defaultValidators.push(...validators); } @@ -134,7 +151,7 @@ export class LionInputStepper extends LionInput { const { min, max } = this.values; this.__disableIncrementor = this.currentValue >= max && max !== Infinity; this.__disableDecrementor = this.currentValue <= min && min !== Infinity; - this.setAttribute('aria-valuenow', this.currentValue); + this.setAttribute('aria-valuenow', `${this.currentValue}`); this.dispatchEvent( new CustomEvent('user-input-changed', { bubbles: true, @@ -150,7 +167,7 @@ export class LionInputStepper extends LionInput { const { step, max } = this.values; const newValue = this.currentValue + step; if (newValue <= max || max === Infinity) { - this.value = newValue; + this.value = `${newValue}`; this.__toggleSpinnerButtonsState(); } } @@ -163,7 +180,7 @@ export class LionInputStepper extends LionInput { const { step, min } = this.values; const newValue = this.currentValue - step; if (newValue >= min || min === Infinity) { - this.value = newValue; + this.value = `${newValue}`; this.__toggleSpinnerButtonsState(); } } diff --git a/packages/input-stepper/test/lion-input-stepper.test.js b/packages/input-stepper/test/lion-input-stepper.test.js index 568c6bbc0..ec542dd9e 100644 --- a/packages/input-stepper/test/lion-input-stepper.test.js +++ b/packages/input-stepper/test/lion-input-stepper.test.js @@ -1,6 +1,12 @@ -import { expect, fixture, nextFrame, html } from '@open-wc/testing'; +import { expect, fixture as _fixture, nextFrame, html } from '@open-wc/testing'; import '../lion-input-stepper.js'; +/** + * @typedef {import('../src/LionInputStepper').LionInputStepper} LionInputStepper + * @typedef {import('lit-html').TemplateResult} TemplateResult + */ +const fixture = /** @type {(arg: TemplateResult|string) => Promise} */ (_fixture); + const defaultInputStepper = html` `; @@ -18,26 +24,26 @@ describe('', () => { it('should increment the value to 1 on + button click', async () => { const el = await fixture(defaultInputStepper); expect(el.value).to.equal(''); - const incrementButton = el.shadowRoot.querySelector('[name=increment]'); - incrementButton.dispatchEvent(new Event('click')); + const incrementButton = el.shadowRoot?.querySelector('[name=increment]'); + incrementButton?.dispatchEvent(new Event('click')); expect(el.value).to.equal('1'); }); it('should decrement the value to -1 on - button click', async () => { const el = await fixture(defaultInputStepper); expect(el.value).to.equal(''); - const incrementButton = el.shadowRoot.querySelector('[name=decrement]'); - incrementButton.dispatchEvent(new Event('click')); + const incrementButton = el.shadowRoot?.querySelector('[name=decrement]'); + incrementButton?.dispatchEvent(new Event('click')); expect(el.value).to.equal('-1'); }); it('should update min and max attributes when min and max property change', async () => { const el = await fixture(inputStepperWithAttrs); - el.min = '100'; - el.max = '200'; + el.min = 100; + el.max = 200; await nextFrame(); - expect(el._inputNode.min).to.equal(el.min); - expect(el._inputNode.max).to.equal(el.max); + expect(el._inputNode.min).to.equal(el.min.toString()); + expect(el._inputNode.max).to.equal(el.max.toString()); }); }); @@ -60,8 +66,8 @@ describe('', () => { it('updates aria-valuenow when stepper is changed', async () => { const el = await fixture(inputStepperWithAttrs); - const incrementButton = el.shadowRoot.querySelector('[name=increment]'); - incrementButton.dispatchEvent(new Event('click')); + const incrementButton = el.shadowRoot?.querySelector('[name=increment]'); + incrementButton?.dispatchEvent(new Event('click')); expect(el).to.have.attribute('aria-valuenow', '1'); }); }); diff --git a/tsconfig.json b/tsconfig.json index 8b47f4de6..fe9a45862 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,11 @@ "packages/form-core/**/*.js", "packages/input/**/*.js", "packages/input-amount/**/*.js", + "packages/input-date/**/*.js", + "packages/input-email/**/*.js", + "packages/input-iban/**/*.js", + "packages/input-range/**/*.js", + "packages/input-stepper/**/*.js", "packages/listbox/src/*.js", "packages/localize/**/*.js", "packages/overlays/**/*.js",