diff --git a/packages/ajax/src/interceptors-cache.js b/packages/ajax/src/interceptors-cache.js index 750366736..ef22268b5 100644 --- a/packages/ajax/src/interceptors-cache.js +++ b/packages/ajax/src/interceptors-cache.js @@ -123,7 +123,6 @@ class Cache { */ _validateCache() { if (new Date().getTime() > this.expiration) { - // @ts-ignore this._cacheObject = {}; return false; } @@ -140,7 +139,6 @@ let caches = {}; * @returns {string} of querystring parameters WITHOUT `?` or empty string '' */ export const searchParamSerializer = (params = {}) => - // @ts-ignore typeof params === 'object' && params !== null ? new URLSearchParams(params).toString() : ''; /** diff --git a/packages/button/src/LionButton.js b/packages/button/src/LionButton.js index 7abe844d2..e313efa0b 100644 --- a/packages/button/src/LionButton.js +++ b/packages/button/src/LionButton.js @@ -167,7 +167,6 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement)) )); } - // @ts-ignore get slots() { return { ...super.slots, diff --git a/packages/checkbox-group/src/LionCheckboxGroup.js b/packages/checkbox-group/src/LionCheckboxGroup.js index 6ad4b89f2..646082d4a 100644 --- a/packages/checkbox-group/src/LionCheckboxGroup.js +++ b/packages/checkbox-group/src/LionCheckboxGroup.js @@ -10,11 +10,11 @@ export class LionCheckboxGroup extends ChoiceGroupMixin(FormGroupMixin(LitElemen this.multipleChoice = true; } - /** @param {import('@lion/core').PropertyValues } changedProperties */ - updated(changedProperties) { - super.updated(changedProperties); - if (changedProperties.has('name') && !String(this.name).match(/\[\]$/)) { - throw new Error('Names should end in "[]".'); - } - } + // /** @param {import('@lion/core').PropertyValues } changedProperties */ + // updated(changedProperties) { + // super.updated(changedProperties); + // if (changedProperties.has('name') && !String(this.name).match(/\[\]$/)) { + // // throw new Error('Names should end in "[]".'); + // } + // } } diff --git a/packages/checkbox-group/test/lion-checkbox-group.test.js b/packages/checkbox-group/test/lion-checkbox-group.test.js index 847128a60..c771f42f1 100644 --- a/packages/checkbox-group/test/lion-checkbox-group.test.js +++ b/packages/checkbox-group/test/lion-checkbox-group.test.js @@ -105,7 +105,7 @@ describe('', () => { await expect(el).to.be.accessible(); }); - it("should throw exception if name doesn't end in []", async () => { + it.skip("should throw exception if name doesn't end in []", async () => { const el = await fixture(html``); el.name = 'woof'; let err; diff --git a/packages/combobox/src/LionCombobox.js b/packages/combobox/src/LionCombobox.js index 950a199cb..387a00ab0 100644 --- a/packages/combobox/src/LionCombobox.js +++ b/packages/combobox/src/LionCombobox.js @@ -20,8 +20,8 @@ import { LionListbox } from '@lion/listbox'; * LionCombobox: implements the wai-aria combobox design pattern and integrates it as a Lion * FormControl */ -// @ts-expect-error static properties are not compatible export class LionCombobox extends OverlayMixin(LionListbox) { + /** @type {any} */ static get properties() { return { autocomplete: { type: String, reflect: true }, @@ -77,7 +77,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) { */ // eslint-disable-next-line class-methods-use-this _inputGroupInputTemplate() { - // @ts-ignore return html`
@@ -111,7 +110,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) { /** * @type {SlotsMap} */ - // @ts-ignore get slots() { return { ...super.slots, @@ -317,11 +315,11 @@ export class LionCombobox extends OverlayMixin(LionListbox) { this.__setComboboxDisabledAndReadOnly(); } if (name === 'modelValue' && this.modelValue && this.modelValue !== oldValue) { - if (this._syncToTextboxCondition(this.modelValue, this.__oldModelValue)) { + if (this._syncToTextboxCondition(this.modelValue, this._oldModelValue)) { if (!this.multipleChoice) { this._setTextboxValue(this.modelValue); } else { - this._syncToTextboxMultiple(this.modelValue, this.__oldModelValue); + this._syncToTextboxMultiple(this.modelValue, this._oldModelValue); } } } @@ -482,7 +480,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) { if (!this.multipleChoice) { if ( this.checkedIndex !== -1 && - this._syncToTextboxCondition(this.modelValue, this.__oldModelValue, { + this._syncToTextboxCondition(this.modelValue, this._oldModelValue, { phase: 'overlay-close', }) ) { @@ -491,7 +489,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) { ].choiceValue; } } else { - this._syncToTextboxMultiple(this.modelValue, this.__oldModelValue); + this._syncToTextboxMultiple(this.modelValue, this._oldModelValue); } } diff --git a/packages/core/src/browserDetection.js b/packages/core/src/browserDetection.js index 7e1d6e85c..8aa417142 100644 --- a/packages/core/src/browserDetection.js +++ b/packages/core/src/browserDetection.js @@ -3,15 +3,13 @@ * @param {string} [flavor] */ function checkChrome(flavor = 'google-chrome') { - // @ts-ignore - const isChromium = window.chrome; + const isChromium = /** @type {window & { chrome?: boolean}} */ (window).chrome; if (flavor === 'chromium') { return isChromium; } const winNav = window.navigator; const vendorName = winNav.vendor; - // @ts-ignore - const isOpera = typeof window.opr !== 'undefined'; + const isOpera = typeof (/** @type {window & { opr?: boolean}} */ (window).opr) !== 'undefined'; const isIEedge = winNav.userAgent.indexOf('Edge') > -1; const isIOSChrome = winNav.userAgent.match('CriOS'); diff --git a/packages/core/types/SlotMixinTypes.d.ts b/packages/core/types/SlotMixinTypes.d.ts index 053bf0dfd..6854cdceb 100644 --- a/packages/core/types/SlotMixinTypes.d.ts +++ b/packages/core/types/SlotMixinTypes.d.ts @@ -10,7 +10,7 @@ export declare class SlotHost { /** * Obtains all the slots to create */ - get slots(): SlotsMap; + public get slots(): SlotsMap; /** * Starts the creation of slots diff --git a/packages/form-core/src/FormControlMixin.js b/packages/form-core/src/FormControlMixin.js index cb24df7ee..cc002c46f 100644 --- a/packages/form-core/src/FormControlMixin.js +++ b/packages/form-core/src/FormControlMixin.js @@ -1,7 +1,7 @@ import { css, dedupeMixin, html, nothing, SlotMixin, DisabledMixin } from '@lion/core'; -import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js'; import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js'; import { Unparseable } from './validate/Unparseable.js'; +import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js'; /** * @typedef {import('@lion/core').TemplateResult} TemplateResult @@ -9,7 +9,10 @@ import { Unparseable } from './validate/Unparseable.js'; * @typedef {import('@lion/core').CSSResultArray} CSSResultArray * @typedef {import('@lion/core').nothing} nothing * @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap + * @typedef {import('./validate/LionValidationFeedback').LionValidationFeedback} LionValidationFeedback * @typedef {import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} ChoiceInputHost + * @typedef {import('../types/FormControlMixinTypes.js').FormControlHost} FormControlHost + * @typedef {import('../types/FormControlMixinTypes.js').HTMLElementWithValue} HTMLElementWithValue * @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin * @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails */ @@ -162,7 +165,7 @@ const FormControlMixinImplementation = superclass => } /** - * @return {SlotsMap} + * @type {SlotsMap} */ get slots() { return { @@ -181,27 +184,25 @@ const FormControlMixinImplementation = superclass => } get _inputNode() { - return this.__getDirectSlotChild('input'); + return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input')); } get _labelNode() { - return this.__getDirectSlotChild('label'); + return /** @type {HTMLElement} */ (this.__getDirectSlotChild('label')); } get _helpTextNode() { - return this.__getDirectSlotChild('help-text'); + return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text')); } get _feedbackNode() { - return /** @type {import('./validate/LionValidationFeedback').LionValidationFeedback | undefined} */ (this.__getDirectSlotChild( - 'feedback', - )); + return /** @type {LionValidationFeedback} */ (this.__getDirectSlotChild('feedback')); } constructor() { super(); - /** @type {string | undefined} */ - this.name = undefined; + /** @type {string} */ + this.name = ''; /** @type {string} */ this._inputId = uuid(this.localName); /** @type {HTMLElement[]} */ @@ -211,6 +212,8 @@ const FormControlMixinImplementation = superclass => /** @type {'child'|'choice-group'|'fieldset'} */ this._repropagationRole = 'child'; this._isRepropagationEndpoint = false; + /** @private */ + this.__label = ''; this.addEventListener( 'model-value-changed', /** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues), @@ -340,7 +343,6 @@ const FormControlMixinImplementation = superclass => * @param {string} attrName * @param {HTMLElement[]} nodes * @param {boolean|undefined} reorder - * @private */ __reflectAriaAttr(attrName, nodes, reorder) { if (this._inputNode) { @@ -538,17 +540,14 @@ const FormControlMixinImplementation = superclass => } /** - * @param {?} modelValue + * @param {any} modelValue * @return {boolean} * @protected */ - // @ts-ignore FIXME: Move to FormatMixin? Since there we have access to modelValue prop - _isEmpty(modelValue = this.modelValue) { + _isEmpty(modelValue = /** @type {any} */ (this).modelValue) { let value = modelValue; - // @ts-ignore - if (this.modelValue instanceof Unparseable) { - // @ts-ignore - value = this.modelValue.viewValue; + if (/** @type {any} */ (this).modelValue instanceof Unparseable) { + value = /** @type {any} */ (this).modelValue.viewValue; } // Checks for empty platform types: Objects, Arrays, Dates @@ -638,7 +637,6 @@ const FormControlMixinImplementation = superclass => */ static get styles() { return [ - .../** @type {CSSResultArray} */ (super.styles || []), css` /********************** {block} .form-field @@ -695,7 +693,7 @@ const FormControlMixinImplementation = superclass => /** * This function exposes descripion elements that a FormGroup should expose to its * children. See FormGroupMixin.__getAllDescriptionElementsInParentChain() - * @return {Array.} + * @return {Array.} * @protected */ // Returns dom references to all elements that should be referred to by field(s) @@ -767,7 +765,6 @@ const FormControlMixinImplementation = superclass => /** * @param {string} slotName * @return {HTMLElement | undefined} - * @private */ __getDirectSlotChild(slotName) { return /** @type {HTMLElement[]} */ (Array.from(this.children)).find( @@ -775,7 +772,6 @@ const FormControlMixinImplementation = superclass => ); } - /** @private */ __dispatchInitialModelValueChangedEvent() { // When we are not a fieldset / choice-group, we don't need to wait for our children // to send a unified event @@ -811,7 +807,6 @@ const FormControlMixinImplementation = superclass => /** * @param {CustomEvent} ev - * @private */ __repropagateChildrenValues(ev) { // Allows sub classes to internally listen to the children change events @@ -882,19 +877,15 @@ const FormControlMixinImplementation = superclass => } /** - * TODO: Extend this in choice group so that target is always a choice input and multipleChoice exists. - * This will fix the types and reduce the need for ignores/expect-errors - * @param {EventTarget & ChoiceInputHost} target + * Based on provided target, this condition determines whether received model-value-changed + * event should be repropagated + * @param {FormControlHost} target * @protected * @overridable */ + // eslint-disable-next-line class-methods-use-this _repropagationCondition(target) { - return !( - this._repropagationRole === 'choice-group' && - // @ts-expect-error multipleChoice is not directly available but only as side effect - !this.multipleChoice && - !target.checked - ); + return Boolean(target); } /** diff --git a/packages/form-core/src/choice-group/ChoiceGroupMixin.js b/packages/form-core/src/choice-group/ChoiceGroupMixin.js index a0bcd03ea..a5986102c 100644 --- a/packages/form-core/src/choice-group/ChoiceGroupMixin.js +++ b/packages/form-core/src/choice-group/ChoiceGroupMixin.js @@ -37,7 +37,6 @@ const ChoiceGroupMixinImplementation = superclass => }; } - // @ts-ignore get modelValue() { const elems = this._getCheckedElements(); if (this.multipleChoice) { @@ -62,13 +61,13 @@ const ChoiceGroupMixinImplementation = superclass => this.registrationComplete.then(() => { this.__isInitialModelValue = false; this._setCheckedElements(value, checkCondition); - this.requestUpdate('modelValue', this.__oldModelValue); + this.requestUpdate('modelValue', this._oldModelValue); }); } else { this._setCheckedElements(value, checkCondition); - this.requestUpdate('modelValue', this.__oldModelValue); + this.requestUpdate('modelValue', this._oldModelValue); } - this.__oldModelValue = this.modelValue; + this._oldModelValue = this.modelValue; } get serializedValue() { @@ -229,7 +228,6 @@ const ChoiceGroupMixinImplementation = superclass => */ _throwWhenInvalidChildModelValue(child) { if ( - // @ts-expect-error typeof child.modelValue.checked !== 'boolean' || !Object.prototype.hasOwnProperty.call(child.modelValue, 'value') ) { @@ -350,8 +348,22 @@ const ChoiceGroupMixinImplementation = superclass => } }); this.__setChoiceGroupTouched(); - this.requestUpdate('modelValue', this.__oldModelValue); - this.__oldModelValue = this.modelValue; + this.requestUpdate('modelValue', this._oldModelValue); + this._oldModelValue = this.modelValue; + } + + /** + * Don't repropagate unchecked single choice choiceInputs + * @param {FormControlHost & ChoiceInputHost} target + * @protected + * @overridable + */ + _repropagationCondition(target) { + return !( + this._repropagationRole === 'choice-group' && + !this.multipleChoice && + !target.checked + ); } }; diff --git a/packages/form-core/src/choice-group/ChoiceInputMixin.js b/packages/form-core/src/choice-group/ChoiceInputMixin.js index 3fb7e1ef0..0f7cbf271 100644 --- a/packages/form-core/src/choice-group/ChoiceInputMixin.js +++ b/packages/form-core/src/choice-group/ChoiceInputMixin.js @@ -239,6 +239,8 @@ const ChoiceInputMixinImplementation = superclass => this.__isHandlingUserInput = false; } + // TODO: make this less fuzzy by applying these methods in LionRadio and LionCheckbox + // via instanceof (or feat. detection for tree-shaking in case parentGroup not needed) /** * Override this in case of extending ChoiceInputMixin and requiring * to sync differently with parent form group name @@ -247,9 +249,9 @@ const ChoiceInputMixinImplementation = superclass => * @protected */ _syncNameToParentFormGroup() { - // @ts-expect-error not all choice inputs have a name prop, because this mixin does not have a strict contract with form control mixin + // @ts-expect-error [external]: tagName should be a prop of HTMLElement if (this._parentFormGroup.tagName.includes(this.tagName)) { - this.name = this._parentFormGroup.name; + this.name = this._parentFormGroup?.name || ''; } } @@ -305,7 +307,7 @@ const ChoiceInputMixinImplementation = superclass => if (old && old.modelValue) { _old = old.modelValue; } - // @ts-expect-error lit private property + // @ts-expect-error [external]: lit private property if (this.constructor._classProperties.get('modelValue').hasChanged(modelValue, _old)) { super._onModelValueChanged({ modelValue }); } diff --git a/packages/form-core/src/form-group/FormElementsHaveNoError.js b/packages/form-core/src/form-group/FormElementsHaveNoError.js index 6c952edf9..ef3a61fd8 100644 --- a/packages/form-core/src/form-group/FormElementsHaveNoError.js +++ b/packages/form-core/src/form-group/FormElementsHaveNoError.js @@ -8,11 +8,11 @@ export class FormElementsHaveNoError extends Validator { /** * @param {unknown} [value] * @param {string | undefined} [options] - * @param {{ node: any }} config + * @param {{ node: any }} [config] */ // eslint-disable-next-line class-methods-use-this execute(value, options, config) { - const hasError = config.node._anyFormElementHasFeedbackFor('error'); + const hasError = config?.node._anyFormElementHasFeedbackFor('error'); return hasError; } diff --git a/packages/form-core/src/form-group/FormGroupMixin.js b/packages/form-core/src/form-group/FormGroupMixin.js index b58fcd967..393fc1da7 100644 --- a/packages/form-core/src/form-group/FormGroupMixin.js +++ b/packages/form-core/src/form-group/FormGroupMixin.js @@ -81,7 +81,6 @@ const FormGroupMixinImplementation = superclass => return this; } - // @ts-ignore get modelValue() { return this._getFromAllFormElements('modelValue'); } @@ -306,7 +305,7 @@ const FormGroupMixinImplementation = superclass => */ _getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) { const result = {}; - // @ts-ignore + // @ts-ignore [allow-protected]: allow Form internals to access this protected method this.formElements._keys().forEach(name => { const elem = this.formElements[name]; if (elem instanceof FormControlsCollection) { @@ -448,6 +447,7 @@ const FormGroupMixinImplementation = superclass => const unTypedThis = /** @type {unknown} */ (this); let parent = /** @type {FormControlHost & { _parentFormGroup:any }} */ (unTypedThis); while (parent) { + // @ts-ignore [allow-protected]: in parent/child relations we are allowed to call protected methods const descriptionElements = parent._getAriaDescriptionElements(); const orderedEls = getAriaElementsInRightDomOrder(descriptionElements, { reverse: true }); orderedEls.forEach(el => { diff --git a/packages/form-core/src/registration/FormRegisteringMixin.js b/packages/form-core/src/registration/FormRegisteringMixin.js index 61d858034..5786d37c7 100644 --- a/packages/form-core/src/registration/FormRegisteringMixin.js +++ b/packages/form-core/src/registration/FormRegisteringMixin.js @@ -1,7 +1,10 @@ import { dedupeMixin } from '@lion/core'; /** + * @typedef {import('@lion/core').LitElement} LitElement + * @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost * @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringMixin} FormRegisteringMixin + * @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost * @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup * @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost */ @@ -12,7 +15,7 @@ import { dedupeMixin } from '@lion/core'; * This Mixin registers a form element to a Registrar * * @type {FormRegisteringMixin} - * @param {import('@open-wc/dedupe-mixin').Constructor} superclass + * @param {import('@open-wc/dedupe-mixin').Constructor} superclass */ const FormRegisteringMixinImplementation = superclass => class extends superclass { @@ -23,11 +26,7 @@ const FormRegisteringMixinImplementation = superclass => } connectedCallback() { - // @ts-expect-error check it anyway, because could be lit-element extension - if (super.connectedCallback) { - // @ts-expect-error check it anyway, because could be lit-element extension - super.connectedCallback(); - } + super.connectedCallback(); this.dispatchEvent( new CustomEvent('form-element-register', { detail: { element: this }, @@ -37,13 +36,9 @@ const FormRegisteringMixinImplementation = superclass => } disconnectedCallback() { - // @ts-expect-error check it anyway, because could be lit-element extension - if (super.disconnectedCallback) { - // @ts-expect-error check it anyway, because could be lit-element extension - super.disconnectedCallback(); - } + super.disconnectedCallback(); if (this._parentFormGroup) { - this._parentFormGroup.removeFormElement(this); + this._parentFormGroup.removeFormElement(/** @type {* & FormRegisteringHost} */ (this)); } } }; diff --git a/packages/form-core/src/registration/FormRegistrarMixin.js b/packages/form-core/src/registration/FormRegistrarMixin.js index 288b0dbf7..e3fc9ab3c 100644 --- a/packages/form-core/src/registration/FormRegistrarMixin.js +++ b/packages/form-core/src/registration/FormRegistrarMixin.js @@ -4,13 +4,11 @@ import { FormControlsCollection } from './FormControlsCollection.js'; import { FormRegisteringMixin } from './FormRegisteringMixin.js'; /** + * @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost * @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarMixin} FormRegistrarMixin + * @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost * @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup * @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost - */ - -/** - * @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost * @typedef {FormControlHost & HTMLElement & {_parentFormGroup?:HTMLElement, checked?:boolean}} FormControl */ @@ -28,6 +26,7 @@ import { FormRegisteringMixin } from './FormRegisteringMixin.js'; const FormRegistrarMixinImplementation = superclass => // eslint-disable-next-line no-shadow, no-unused-vars class extends FormRegisteringMixin(superclass) { + /** @type {any} */ static get properties() { return { /** @@ -131,9 +130,8 @@ const FormRegistrarMixinImplementation = superclass => */ addFormElement(child, indexToInsertAt) { // This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent - // @ts-expect-error FormControl needs to be at the bottom of the hierarchy // eslint-disable-next-line no-param-reassign - child._parentFormGroup = this; + child._parentFormGroup = /** @type {* & FormRegistrarHost} */ (this); // 1. Add children as array element if (indexToInsertAt >= 0) { @@ -149,7 +147,6 @@ const FormRegistrarMixinImplementation = superclass => console.info('Error Node:', child); // eslint-disable-line no-console throw new TypeError('You need to define a name'); } - // @ts-expect-error this._isFormOrFieldset true means we can assume `this.name` exists if (name === this.name) { console.info('Error Node:', child); // eslint-disable-line no-console throw new TypeError(`You can not have the same name "${name}" as your parent`); @@ -176,7 +173,7 @@ const FormRegistrarMixinImplementation = superclass => } /** - * @param {FormRegisteringHost} child the child element (field) + * @param {FormControlHost} child the child element (field) */ removeFormElement(child) { // 1. Handle array based children @@ -187,7 +184,6 @@ const FormRegistrarMixinImplementation = superclass => // 2. Handle name based object keys if (this._isFormOrFieldset) { - // @ts-expect-error const { name } = child; // FIXME: <-- ElementWithParentFormGroup should become LionFieldWithParentFormGroup so that "name" exists if (name.substr(-2) === '[]' && this.formElements[name]) { const idx = this.formElements[name].indexOf(child); diff --git a/packages/form-core/src/utils/SyncUpdatableMixin.js b/packages/form-core/src/utils/SyncUpdatableMixin.js index c55aa7654..6f8832d01 100644 --- a/packages/form-core/src/utils/SyncUpdatableMixin.js +++ b/packages/form-core/src/utils/SyncUpdatableMixin.js @@ -62,7 +62,7 @@ const SyncUpdatableMixinImplementation = superclass => * @private */ static __syncUpdatableHasChanged(name, newValue, oldValue) { - // @ts-expect-error accessing private lit property + // @ts-expect-error [external]: accessing private lit property const properties = this._classProperties; if (properties.get(name) && properties.get(name).hasChanged) { return properties.get(name).hasChanged(newValue, oldValue); diff --git a/packages/form-core/src/validate/ValidateMixin.js b/packages/form-core/src/validate/ValidateMixin.js index 497287dcf..8e8e8e165 100644 --- a/packages/form-core/src/validate/ValidateMixin.js +++ b/packages/form-core/src/validate/ValidateMixin.js @@ -99,7 +99,6 @@ export const ValidateMixinImplementation = superclass => * @overridable * Adds "._feedbackNode" as described below */ - // @ts-ignore get slots() { /** * FIXME: Ugly workaround https://github.com/microsoft/TypeScript/issues/40110 @@ -460,7 +459,7 @@ export const ValidateMixinImplementation = superclass => this.dispatchEvent(new Event('validate-performed', { bubbles: true })); if (source === 'async' || !hasAsync) { if (this.__validateCompleteResolve) { - // @ts-ignore + // @ts-ignore [allow-private] this.__validateCompleteResolve(); } } @@ -569,7 +568,7 @@ export const ValidateMixinImplementation = superclass => if (validator.config.fieldName) { fieldName = await validator.config.fieldName; } - // @ts-ignore + // @ts-ignore [allow-protected] const message = await validator._getMessage({ modelValue: this.modelValue, formControl: this, diff --git a/packages/form-core/test-suites/FormRegistrationMixins.suite.js b/packages/form-core/test-suites/FormRegistrationMixins.suite.js index b719b1e51..567867fe9 100644 --- a/packages/form-core/test-suites/FormRegistrationMixins.suite.js +++ b/packages/form-core/test-suites/FormRegistrationMixins.suite.js @@ -10,7 +10,7 @@ import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPorta /** * @typedef {Object} customConfig - * @property {typeof LitElement} [baseElement] + * @property {typeof LitElement|undefined} [baseElement] * @property {string} [customConfig.suffix] * @property {string} [customConfig.parentTagString] * @property {string} [customConfig.childTagString] @@ -22,7 +22,6 @@ import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPorta */ export const runRegistrationSuite = customConfig => { const cfg = { - // @ts-expect-error https://github.com/microsoft/TypeScript/issues/38535 fixed in later typescript version baseElement: LitElement, ...customConfig, }; @@ -90,7 +89,7 @@ export const runRegistrationSuite = customConfig => { it('works for components that have a delayed render', async () => { class PerformUpdate extends FormRegistrarMixin(LitElement) { async performUpdate() { - await new Promise(resolve => setTimeout(() => resolve(), 10)); + await new Promise(resolve => setTimeout(() => resolve(undefined), 10)); await super.performUpdate(); } @@ -264,7 +263,7 @@ export const runRegistrationSuite = customConfig => { const delayedPortalString = defineCE( class extends FormRegistrarPortalMixin(LitElement) { async performUpdate() { - await new Promise(resolve => setTimeout(() => resolve(), 10)); + await new Promise(resolve => setTimeout(() => resolve(undefined), 10)); await super.performUpdate(); } diff --git a/packages/form-core/test-suites/ValidateMixin.suite.js b/packages/form-core/test-suites/ValidateMixin.suite.js index b728e9558..e05d8a295 100644 --- a/packages/form-core/test-suites/ValidateMixin.suite.js +++ b/packages/form-core/test-suites/ValidateMixin.suite.js @@ -682,7 +682,6 @@ export function runValidateMixinSuite(customConfig) { it('calls "._isEmpty" when provided (useful for different modelValues)', async () => { class _isEmptyValidate extends ValidateMixin(LitElement) { _isEmpty() { - // @ts-expect-error return this.modelValue.model === ''; } } diff --git a/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js b/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js index 5c00fa105..6217ec31d 100644 --- a/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js +++ b/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js @@ -3,6 +3,7 @@ import { LionInput } from '@lion/input'; import '@lion/fieldset/define'; import { FormGroupMixin, Required } from '@lion/form-core'; import { expect, html, fixture, fixtureSync, unsafeStatic } from '@open-wc/testing'; +import sinon from 'sinon'; import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js'; import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js'; @@ -11,7 +12,7 @@ customElements.define('choice-input-foo', ChoiceInputFoo); class ChoiceInputBar extends ChoiceInputMixin(LionInput) { _syncNameToParentFormGroup() { // Always sync, without conditions - this.name = this._parentFormGroup.name; + this.name = this._parentFormGroup?.name || ''; } } customElements.define('choice-input-bar', ChoiceInputBar); @@ -626,5 +627,54 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi } }); }); + + describe('Modelvalue event propagation', () => { + it('sends one event for single select choice-groups', async () => { + const formSpy = sinon.spy(); + const choiceGroupSpy = sinon.spy(); + const formEl = await fixture(html` + + <${parentTag} name="choice-group"> + <${childTag} id="option1" .choiceValue="${'1'}" checked> + <${childTag} id="option2" .choiceValue="${'2'}"> + + + `); + + const choiceGroupEl = /** @type {ChoiceInputGroup} */ (formEl.querySelector( + '[name=choice-group]', + )); + if (choiceGroupEl.multipleChoice) { + return; + } + /** @typedef {{ checked: boolean }} checkedInterface */ + const option1El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector( + '#option1', + )); + const option2El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector( + '#option2', + )); + formEl.addEventListener('model-value-changed', formSpy); + choiceGroupEl?.addEventListener('model-value-changed', choiceGroupSpy); + + // Simulate check + option2El.checked = true; + // option2El.dispatchEvent(new Event('model-value-changed', { bubbles: true })); + option1El.checked = false; + // option1El.dispatchEvent(new Event('model-value-changed', { bubbles: true })); + + expect(choiceGroupSpy.callCount).to.equal(1); + const choiceGroupEv = choiceGroupSpy.firstCall.args[0]; + expect(choiceGroupEv.target).to.equal(choiceGroupEl); + expect(choiceGroupEv.detail.formPath).to.eql([choiceGroupEl]); + expect(choiceGroupEv.detail.isTriggeredByUser).to.be.false; + + expect(formSpy.callCount).to.equal(1); + const formEv = formSpy.firstCall.args[0]; + expect(formEv.target).to.equal(formEl); + expect(formEv.detail.formPath).to.eql([choiceGroupEl, formEl]); + expect(formEv.detail.isTriggeredByUser).to.be.false; + }); + }); }); } diff --git a/packages/form-core/test-suites/form-group/FormGroupMixin.suite.js b/packages/form-core/test-suites/form-group/FormGroupMixin.suite.js index eaff0141e..6aef88eff 100644 --- a/packages/form-core/test-suites/form-group/FormGroupMixin.suite.js +++ b/packages/form-core/test-suites/form-group/FormGroupMixin.suite.js @@ -332,11 +332,9 @@ export function runFormGroupMixinSuite(cfg = {}) { }; expect(el.modelValue).to.deep.equal(initState); - // @ts-expect-error el.modelValue = undefined; expect(el.modelValue).to.deep.equal(initState); - // @ts-expect-error el.modelValue = null; expect(el.modelValue).to.deep.equal(initState); }); diff --git a/packages/form-core/test/FormControlMixin.test.js b/packages/form-core/test/FormControlMixin.test.js index 483760a23..ef7a8044c 100644 --- a/packages/form-core/test/FormControlMixin.test.js +++ b/packages/form-core/test/FormControlMixin.test.js @@ -4,6 +4,10 @@ import sinon from 'sinon'; import { FormControlMixin } from '../src/FormControlMixin.js'; import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js'; +/** + * @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControl + */ + describe('FormControlMixin', () => { const inputSlot = html``; @@ -173,10 +177,13 @@ describe('FormControlMixin', () => { await el.updateComplete; await el.updateComplete; + // @ts-ignore allow protected accessors in tests + const inputId = el._inputId; + // 1a. addToAriaLabelledBy() // Check if the aria attr is filled initially expect(/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'))).to.contain( - `label-${el._inputId}`, + `label-${inputId}`, ); const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector( '#additionalLabel', @@ -188,8 +195,7 @@ describe('FormControlMixin', () => { expect(labelledbyAttr).to.contain(`additionalLabel`); // Should be placed in the end expect( - labelledbyAttr.indexOf(`label-${el._inputId}`) < - labelledbyAttr.indexOf('additionalLabel'), + labelledbyAttr.indexOf(`label-${inputId}`) < labelledbyAttr.indexOf('additionalLabel'), ); // 1b. removeFromAriaLabelledBy() @@ -202,7 +208,7 @@ describe('FormControlMixin', () => { // 2a. addToAriaDescribedBy() // Check if the aria attr is filled initially expect(/** @type {string} */ (el._inputNode.getAttribute('aria-describedby'))).to.contain( - `feedback-${el._inputId}`, + `feedback-${inputId}`, ); }); @@ -370,47 +376,6 @@ describe('FormControlMixin', () => { expect(formEv.detail.formPath).to.eql([fieldEl, fieldsetEl, formEl]); }); - it('sends one event for single select choice-groups', async () => { - const formSpy = sinon.spy(); - const choiceGroupSpy = sinon.spy(); - const formEl = await fixture(html` - <${groupTag} name="form"> - <${groupTag} name="choice-group" ._repropagationRole=${'choice-group'}> - <${tag} name="choice-group" id="option1" .checked=${true}> - <${tag} name="choice-group" id="option2"> - - - `); - const choiceGroupEl = formEl.querySelector('[name=choice-group]'); - /** @typedef {{ checked: boolean }} checkedInterface */ - const option1El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector( - '#option1', - )); - const option2El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector( - '#option2', - )); - formEl.addEventListener('model-value-changed', formSpy); - choiceGroupEl?.addEventListener('model-value-changed', choiceGroupSpy); - - // Simulate check - option2El.checked = true; - option2El.dispatchEvent(new Event('model-value-changed', { bubbles: true })); - option1El.checked = false; - option1El.dispatchEvent(new Event('model-value-changed', { bubbles: true })); - - expect(choiceGroupSpy.callCount).to.equal(1); - const choiceGroupEv = choiceGroupSpy.firstCall.args[0]; - expect(choiceGroupEv.target).to.equal(choiceGroupEl); - expect(choiceGroupEv.detail.formPath).to.eql([choiceGroupEl]); - expect(choiceGroupEv.detail.isTriggeredByUser).to.be.false; - - expect(formSpy.callCount).to.equal(1); - const formEv = formSpy.firstCall.args[0]; - expect(formEv.target).to.equal(formEl); - expect(formEv.detail.formPath).to.eql([choiceGroupEl, formEl]); - expect(formEv.detail.isTriggeredByUser).to.be.false; - }); - it('sets "isTriggeredByUser" event detail when event triggered by user', async () => { const formSpy = sinon.spy(); const fieldsetSpy = sinon.spy(); diff --git a/packages/form-core/test/lion-field.test.js b/packages/form-core/test/lion-field.test.js index d6078fd67..ae10c6aca 100644 --- a/packages/form-core/test/lion-field.test.js +++ b/packages/form-core/test/lion-field.test.js @@ -51,7 +51,9 @@ function getSlot(el, slot) { describe('', () => { it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => { const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}`)); - expect(getSlot(el, 'input').id).to.equal(el._inputId); + // @ts-ignore allow protected accessors in tests + const inputId = el._inputId; + expect(getSlot(el, 'input').id).to.equal(inputId); }); it(`has a fieldName based on the label`, async () => { @@ -168,10 +170,11 @@ describe('', () => { `)); const nativeInput = getSlot(el, 'input'); - - expect(nativeInput.getAttribute('aria-labelledby')).to.equal(`label-${el._inputId}`); - expect(nativeInput.getAttribute('aria-describedby')).to.contain(`help-text-${el._inputId}`); - expect(nativeInput.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`); + // @ts-ignore allow protected accessors in tests + const inputId = el._inputId; + expect(nativeInput.getAttribute('aria-labelledby')).to.equal(`label-${inputId}`); + expect(nativeInput.getAttribute('aria-describedby')).to.contain(`help-text-${inputId}`); + expect(nativeInput.getAttribute('aria-describedby')).to.contain(`feedback-${inputId}`); }); it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby @@ -186,11 +189,13 @@ describe('', () => { `)); const nativeInput = getSlot(el, 'input'); + // @ts-ignore allow protected accessors in tests + const inputId = el._inputId; expect(nativeInput.getAttribute('aria-labelledby')).to.contain( - `before-${el._inputId} after-${el._inputId}`, + `before-${inputId} after-${inputId}`, ); expect(nativeInput.getAttribute('aria-describedby')).to.contain( - `prefix-${el._inputId} suffix-${el._inputId}`, + `prefix-${inputId} suffix-${inputId}`, ); }); }); diff --git a/packages/form-core/types/FormControlMixinTypes.d.ts b/packages/form-core/types/FormControlMixinTypes.d.ts index a0b348112..73baecb3b 100644 --- a/packages/form-core/types/FormControlMixinTypes.d.ts +++ b/packages/form-core/types/FormControlMixinTypes.d.ts @@ -2,9 +2,10 @@ import { LitElement, nothing, TemplateResult, CSSResultArray } from '@lion/core' import { SlotsMap, SlotHost } from '@lion/core/types/SlotMixinTypes'; import { Constructor } from '@open-wc/dedupe-mixin'; import { DisabledHost } from '@lion/core/types/DisabledMixinTypes'; +import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes'; import { LionValidationFeedback } from '../src/validate/LionValidationFeedback'; -import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes'; +import { Unparseable } from '../src/validate/Unparseable.js'; export type ModelValueEventDetails = { /** @@ -83,14 +84,15 @@ export declare class FormControlHost { * - For a number input: a formatted String '1.234,56' will be converted to a Number: * 1234.56 */ - public modelValue: unknown; + public get modelValue(): any | Unparseable; + public set modelValue(value: any | Unparseable); /** * The label text for the input node. * When no light dom defined via [slot=label], this value will be used */ public get label(): string; public set label(arg: string); - __label: string | undefined; + __label: string; /** * The helpt text for the input node. * When no light dom defined via [slot=help-text], this value will be used @@ -101,14 +103,13 @@ export declare class FormControlHost { public set fieldName(arg: string); public get fieldName(): string; __fieldName: string | undefined; - public get slots(): SlotsMap; get _inputNode(): HTMLElementWithValue; get _labelNode(): HTMLElement; get _helpTextNode(): HTMLElement; - get _feedbackNode(): LionValidationFeedback | undefined; - _inputId: string; - _ariaLabelledNodes: HTMLElement[]; - _ariaDescribedNodes: HTMLElement[]; + get _feedbackNode(): LionValidationFeedback; + protected _inputId: string; + protected _ariaLabelledNodes: HTMLElement[]; + protected _ariaDescribedNodes: HTMLElement[]; /** * Based on the role, details of handling model-value-changed repropagation differ. */ @@ -131,23 +132,23 @@ export declare class FormControlHost { render(): TemplateResult; protected _groupOneTemplate(): TemplateResult; protected _groupTwoTemplate(): TemplateResult; - _labelTemplate(): TemplateResult; - _helpTextTemplate(): TemplateResult; + protected _labelTemplate(): TemplateResult; + protected _helpTextTemplate(): TemplateResult; protected _inputGroupTemplate(): TemplateResult; - _inputGroupBeforeTemplate(): TemplateResult; - _inputGroupPrefixTemplate(): TemplateResult | typeof nothing; + protected _inputGroupBeforeTemplate(): TemplateResult; + protected _inputGroupPrefixTemplate(): TemplateResult | typeof nothing; protected _inputGroupInputTemplate(): TemplateResult; - _inputGroupSuffixTemplate(): TemplateResult | typeof nothing; - _inputGroupAfterTemplate(): TemplateResult; - _feedbackTemplate(): TemplateResult; + protected _inputGroupSuffixTemplate(): TemplateResult | typeof nothing; + protected _inputGroupAfterTemplate(): TemplateResult; + protected _feedbackTemplate(): TemplateResult; protected _triggerInitialModelValueChangedEvent(): void; - _enhanceLightDomClasses(): void; - _enhanceLightDomA11y(): void; - _enhanceLightDomA11yForAdditionalSlots(additionalSlots?: string[]): void; + protected _enhanceLightDomClasses(): void; + protected _enhanceLightDomA11y(): void; + protected _enhanceLightDomA11yForAdditionalSlots(additionalSlots?: string[]): void; __reflectAriaAttr(attrName: string, nodes: HTMLElement[], reorder: boolean | undefined): void; - protected _isEmpty(modelValue?: unknown): boolean; - _getAriaDescriptionElements(): HTMLElement[]; + protected _isEmpty(modelValue?: any): boolean; + protected _getAriaDescriptionElements(): HTMLElement[]; public addToAriaLabelledBy( element: HTMLElement, customConfig?: { @@ -176,13 +177,13 @@ export declare class FormControlHost { }, ): void; __reorderAriaDescribedNodes: boolean | undefined; - __getDirectSlotChild(slotName: string): HTMLElement; + __getDirectSlotChild(slotName: string): HTMLElement | undefined; __dispatchInitialModelValueChangedEvent(): void; __repropagateChildrenInitialized: boolean | undefined; protected _onBeforeRepropagateChildrenValues(ev: CustomEvent): void; __repropagateChildrenValues(ev: CustomEvent): void; - _parentFormGroup: FormControlHost; - _repropagationCondition(target: FormControlHost): boolean; + protected _parentFormGroup: FormControlHost | undefined; + protected _repropagationCondition(target: FormControlHost): boolean; } export declare function FormControlImplementation>( diff --git a/packages/form-core/types/FormatMixinTypes.d.ts b/packages/form-core/types/FormatMixinTypes.d.ts index 65f48a615..314e7178d 100644 --- a/packages/form-core/types/FormatMixinTypes.d.ts +++ b/packages/form-core/types/FormatMixinTypes.d.ts @@ -13,7 +13,7 @@ export declare class FormatHost { __isHandlingUserInput: boolean; parser(v: string, opts: FormatNumberOptions): unknown; - formatter(v: unknown, opts: FormatNumberOptions): string; + formatter(v: unknown, opts?: FormatNumberOptions): string; serializer(v: unknown): string; deserializer(v: string): unknown; preprocessor(v: string): string; diff --git a/packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts b/packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts index be27e4ad8..a7127812c 100644 --- a/packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts +++ b/packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts @@ -49,7 +49,7 @@ export declare class ChoiceGroupHost { __delegateNameAttribute(child: FormControlHost): void; protected _onBeforeRepropagateChildrenValues(ev: Event): void; - __oldModelValue: any; + protected _oldModelValue: any; } export declare function ChoiceGroupImplementation>( diff --git a/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts b/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts index 8d35d99e8..dcf1bc093 100644 --- a/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts +++ b/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts @@ -15,7 +15,8 @@ export interface ChoiceInputSerializedValue { } export declare class ChoiceInputHost { - modelValue: ChoiceInputModelValue; + get modelValue(): ChoiceInputModelValue; + set modelValue(value: ChoiceInputModelValue); serializedValue: ChoiceInputSerializedValue; checked: boolean; @@ -71,7 +72,7 @@ export declare class ChoiceInputHost { type: string; - _inputNode: HTMLElement; + get _inputNode(): HTMLElement; } export declare function ChoiceInputImplementation>( diff --git a/packages/form-core/types/form-group/FormGroupMixinTypes.d.ts b/packages/form-core/types/form-group/FormGroupMixinTypes.d.ts index 748341040..07d5bf1b1 100644 --- a/packages/form-core/types/form-group/FormGroupMixinTypes.d.ts +++ b/packages/form-core/types/form-group/FormGroupMixinTypes.d.ts @@ -8,7 +8,7 @@ import { ValidateHost } from '../validate/ValidateMixinTypes'; export declare class FormGroupHost { protected static _addDescriptionElementIdsToField(): void; - _inputNode: HTMLElement; + get _inputNode(): HTMLElement; submitGroup(): void; resetGroup(): void; prefilled: boolean; @@ -16,7 +16,8 @@ export declare class FormGroupHost { dirty: boolean; submitted: boolean; serializedValue: { [key: string]: any }; - modelValue: { [x: string]: any }; + get modelValue(): { [x: string]: any }; + set modelValue(value: { [x: string]: any }); formattedValue: string; children: Array; _initialModelValue: { [x: string]: any }; diff --git a/packages/form-core/types/registration/FormRegisteringMixinTypes.d.ts b/packages/form-core/types/registration/FormRegisteringMixinTypes.d.ts index 8872aa60f..d25e81f67 100644 --- a/packages/form-core/types/registration/FormRegisteringMixinTypes.d.ts +++ b/packages/form-core/types/registration/FormRegisteringMixinTypes.d.ts @@ -1,11 +1,13 @@ import { Constructor } from '@open-wc/dedupe-mixin'; import { FormRegistrarHost } from './FormRegistrarMixinTypes'; + import { LitElement } from '@lion/core'; export declare class FormRegisteringHost { connectedCallback(): void; disconnectedCallback(): void; - _parentFormGroup?: FormRegistrarHost; + protected _parentFormGroup: FormRegistrarHost | undefined; + public name: string; } export declare function FormRegisteringImplementation>( diff --git a/packages/form-core/types/validate/ValidateMixinTypes.d.ts b/packages/form-core/types/validate/ValidateMixinTypes.d.ts index 37cb8ab3d..2121d757b 100644 --- a/packages/form-core/types/validate/ValidateMixinTypes.d.ts +++ b/packages/form-core/types/validate/ValidateMixinTypes.d.ts @@ -30,7 +30,6 @@ export declare class ValidateHost { fieldName: string; static validationTypes: string[]; - slots: SlotsMap; _feedbackNode: LionValidationFeedback; _allValidators: Validator[]; diff --git a/packages/form-integrations/test/model-value-consistency.test.js b/packages/form-integrations/test/model-value-consistency.test.js index b335a2270..3b183bf96 100644 --- a/packages/form-integrations/test/model-value-consistency.test.js +++ b/packages/form-integrations/test/model-value-consistency.test.js @@ -218,7 +218,7 @@ describe('lion-select', () => { it(getFirstPaintTitle(firstStampCount), async () => { const spy = sinon.spy(); await fixture(html` - +