diff --git a/.changeset/eleven-pans-shave.md b/.changeset/eleven-pans-shave.md new file mode 100644 index 000000000..3e513c9ed --- /dev/null +++ b/.changeset/eleven-pans-shave.md @@ -0,0 +1,5 @@ +--- +'@lion/form-core': patch +--- + +Stop propagation of label click event in choice inputs to deduplicate the click event on the choice input. diff --git a/packages/form-core/src/choice-group/ChoiceInputMixin.js b/packages/form-core/src/choice-group/ChoiceInputMixin.js index ea7313cbc..e737fcddb 100644 --- a/packages/form-core/src/choice-group/ChoiceInputMixin.js +++ b/packages/form-core/src/choice-group/ChoiceInputMixin.js @@ -128,6 +128,7 @@ const ChoiceInputMixinImplementation = superclass => super(); this.modelValue = { value: '', checked: false }; this.disabled = false; + this._preventDuplicateLabelClick = this._preventDuplicateLabelClick.bind(this); this.__toggleChecked = this.__toggleChecked.bind(this); } @@ -189,14 +190,37 @@ const ChoiceInputMixinImplementation = superclass => connectedCallback() { super.connectedCallback(); + if (this._labelNode) { + this._labelNode.addEventListener('click', this._preventDuplicateLabelClick); + } this.addEventListener('user-input-changed', this.__toggleChecked); } disconnectedCallback() { super.disconnectedCallback(); + if (this._labelNode) { + this._labelNode.removeEventListener('click', this._preventDuplicateLabelClick); + } this.removeEventListener('user-input-changed', this.__toggleChecked); } + /** + * The native platform fires an event for both the click on the label, and also + * the redispatched click on the native input element. + * This results in two click events arriving at the host, but we only want one. + * This method prevents the duplicate click and ensures the correct isTrusted event + * with the correct event.target arrives at the host. + * @param {Event} ev + */ + // eslint-disable-next-line no-unused-vars + _preventDuplicateLabelClick(ev) { + const __inputClickHandler = /** @param {Event} _ev */ _ev => { + _ev.stopImmediatePropagation(); + this._inputNode.removeEventListener('click', __inputClickHandler); + }; + this._inputNode.addEventListener('click', __inputClickHandler); + } + __toggleChecked() { if (this.disabled) { return; diff --git a/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js b/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js index b03fe21cb..68425ec88 100644 --- a/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js +++ b/packages/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js @@ -95,6 +95,26 @@ export function runChoiceInputMixinSuite({ tagString } = {}) { expect(counter).to.equal(1); }); + it('fires one "click" event when clicking label or input, using the right target', async () => { + const spy = sinon.spy(); + const el = /** @type {ChoiceInput} */ (await fixture(html` + <${tag} + @click="${spy}" + > + + + `)); + el.click(); + expect(spy.args[0][0].target).to.equal(el); + expect(spy.callCount).to.equal(1); + el._labelNode.click(); + expect(spy.args[1][0].target).to.equal(el._labelNode); + expect(spy.callCount).to.equal(2); + el._inputNode.click(); + expect(spy.args[2][0].target).to.equal(el._inputNode); + expect(spy.callCount).to.equal(3); + }); + it('adds "isTriggerByUser" flag on model-value-changed', async () => { let isTriggeredByUser; const el = /** @type {ChoiceInput} */ (await fixture(html` diff --git a/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts b/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts index fc0ed434e..85f1e3e54 100644 --- a/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts +++ b/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts @@ -41,6 +41,8 @@ export declare class ChoiceInputHost { connectedCallback(): void; disconnectedCallback(): void; + _preventDuplicateLabelClick(ev: Event): void; + __toggleChecked(): void; __syncModelCheckedToChecked(checked: boolean): void;