From 7d639b0412120523f758942c855cb69f9a52e9d9 Mon Sep 17 00:00:00 2001 From: Shane Date: Fri, 2 May 2025 09:27:25 -0700 Subject: [PATCH 1/3] fix(labels): prevent clicking a label from triggering onClick twice on several components (#30384) Issue number: resolves #30165 --------- ## What is the current behavior? Currently, several components will trigger their `onClick` twice if you click on their labels. ## What is the new behavior? After this fix, the affected components will only trigger `onClick` once per click of their labels or click directly on the element. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information The affected components are: - Checkbox - Select - Textarea - Toggle - Input I also tested radio and range but could not reproduce the issue for them. Note that two of the components, checkbox and toggle, had to have special implementations for both their test and fix because of how the host component acts as the component for accessibility purposes. Current dev build: `8.5.7-dev.11746044124.147aab6c` --------- Co-authored-by: Maria Hutt Co-authored-by: Brandy Smith --- core/src/components/checkbox/checkbox.tsx | 9 +++ .../checkbox/test/basic/checkbox.e2e.ts | 34 ++++++++ core/src/components/input/input.tsx | 16 +++- .../components/input/test/basic/input.e2e.ts | 77 +++++++++++++++++++ core/src/components/select/select.tsx | 14 +++- .../select/test/basic/select.e2e.ts | 41 +++++++++- .../textarea/test/basic/textarea.e2e.ts | 35 +++++++++ core/src/components/textarea/textarea.tsx | 14 +++- .../toggle/test/basic/toggle.e2e.ts | 38 +++++++++ core/src/components/toggle/toggle.tsx | 9 +++ 10 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 core/src/components/textarea/test/basic/textarea.e2e.ts create mode 100644 core/src/components/toggle/test/basic/toggle.e2e.ts diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 3b9c3e6724a..4a9132665fd 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -199,6 +199,14 @@ export class Checkbox implements ComponentInterface { this.toggleChecked(ev); }; + /** + * Stops propagation when the display label is clicked, + * otherwise, two clicks will be triggered. + */ + private onDivLabelClick = (ev: MouseEvent) => { + ev.stopPropagation(); + }; + private getHintTextID(): string | undefined { const { el, helperText, errorText, helperTextId, errorTextId } = this; @@ -314,6 +322,7 @@ export class Checkbox implements ComponentInterface { }} part="label" id={this.inputLabelId} + onClick={this.onDivLabelClick} > {this.renderHintText()} diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts b/core/src/components/checkbox/test/basic/checkbox.e2e.ts index 1a41b339599..159e86f9a30 100644 --- a/core/src/components/checkbox/test/basic/checkbox.e2e.ts +++ b/core/src/components/checkbox/test/basic/checkbox.e2e.ts @@ -99,4 +99,38 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => expect(ionChange).not.toHaveReceivedEvent(); }); }); + + test.describe(title('checkbox: click'), () => { + test('should trigger onclick only once when clicking the label', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30165', + }); + + // Create a spy function in page context + await page.setContent(`Test Checkbox`, config); + + // Track calls to the exposed function + let clickCount = 0; + page.on('console', (msg) => { + if (msg.text().includes('click called')) { + clickCount++; + } + }); + + const input = page.locator('div.label-text-wrapper'); + + // Use position to make sure we click into the label enough to trigger + // what would be the double click + await input.click({ + position: { + x: 5, + y: 5, + }, + }); + + // Verify the click was triggered exactly once + expect(clickCount).toBe(1); + }); + }); }); diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 5f4b1257141..c2834138aa8 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -720,6 +720,18 @@ export class Input implements ComponentInterface { return this.label !== undefined || this.labelSlot !== null; } + /** + * Stops propagation when the label is clicked, + * otherwise, two clicks will be triggered. + */ + private onLabelClick = (ev: MouseEvent) => { + // Only stop propagation if the click was directly on the label + // and not on the input or other child elements + if (ev.target === ev.currentTarget) { + ev.stopPropagation(); + } + }; + /** * Renders the border container * when fill="outline". @@ -815,9 +827,9 @@ export class Input implements ComponentInterface { * interactable, clicking the label would focus that instead * since it comes before the input in the DOM. */} -