fix(item): emit click event once when clicking padded space on item and emit correct element (#30373)

Issue number: resolves #29758 resolves #29761

---------

## What is the current behavior?

When an `ion-item` has a click event listener, the following issues
occur:

1. **Double Click Events**:
- Clicking the padding around interactive elements (`ion-checkbox`,
`ion-toggle`, `ion-radio`, `ion-textarea`, `ion-input`) triggers the
click event twice.
2. **Incorrect Event Targets**:
- For `ion-input` and `ion-textarea`, clicking their native inputs
reports the wrong element as the event target.
- Clicking the padding within the `native-wrapper` of `ion-input` emits
a separate click event with an incorrect target element.

## What is the new behavior?
- Fires `firstInteractive.click()` in Item for all interactives (no
longer excludes input/textarea).
- Stops immediate propagation in item when the click event is in the
padding of an item, preventing two click events from firing.
- Updates input and textarea to always emit from their host elements
`ion-input`/`ion-textarea` instead of the native input elements.
- Updates input to make the native input take up 100% height. This is
necessary to avoid the `native-wrapper` triggering its own click event
when clicking on its padding.
- Adds e2e tests to check for the above behavior to avoid future
regressions.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

**Dev build**: `8.5.6-dev.11745613928.16440384`

**Previews**:
- [Checkbox
Preview](https://ionic-framework-git-fw-6503-ionic1.vercel.app/src/components/checkbox/test/item)
- [Input
Preview](https://ionic-framework-git-fw-6503-ionic1.vercel.app/src/components/input/test/item)
- [Radio
Preview](https://ionic-framework-git-fw-6503-ionic1.vercel.app/src/components/radio/test/item)
- [Select
Preview](https://ionic-framework-git-fw-6503-ionic1.vercel.app/src/components/select/test/item)
- [Textarea
Preview](https://ionic-framework-git-fw-6503-ionic1.vercel.app/src/components/textarea/test/item)
- [Toggle
Preview](https://ionic-framework-git-fw-6503-ionic1.vercel.app/src/components/toggle/test/item)

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
Brandy Smith
2025-04-30 13:12:54 -04:00
committed by GitHub
parent 8ef79cf4fb
commit 7a9d138e3d
12 changed files with 437 additions and 64 deletions

View File

@ -107,6 +107,10 @@
width: 100%;
max-width: 100%;
// Ensure the input fills the full height of the native wrapper.
// This prevents the wrapper from being the click event target.
height: 100%;
max-height: 100%;
border: 0;

View File

@ -1,5 +1,18 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, forceUpdate, h } from '@stencil/core';
import {
Build,
Component,
Element,
Event,
Host,
Listen,
Method,
Prop,
State,
Watch,
forceUpdate,
h,
} from '@stencil/core';
import type { NotchController } from '@utils/forms';
import { createNotchController } from '@utils/forms';
import type { Attributes } from '@utils/helpers';
@ -363,6 +376,19 @@ export class Input implements ComponentInterface {
forceUpdate(this);
}
/**
* This prevents the native input from emitting the click event.
* Instead, the click event from the ion-input is emitted.
*/
@Listen('click', { capture: true })
onClickCapture(ev: Event) {
const nativeInput = this.nativeInput;
if (nativeInput && ev.target === nativeInput) {
ev.stopPropagation();
this.el.click();
}
}
componentWillLoad() {
this.inheritedAttributes = {
...inheritAriaAttributes(this.el),

View File

@ -49,6 +49,11 @@ configs().forEach(({ title, screenshot, config }) => {
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('input: item functionality'), () => {
test('clicking padded space within item should focus the input', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/21982',
});
await page.setContent(
`
<ion-item>
@ -57,11 +62,12 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
`,
config
);
const itemNative = page.locator('.item-native');
const item = page.locator('ion-item');
const input = page.locator('ion-input input');
// Clicks the padded space within the item
await itemNative.click({
await item.click({
position: {
x: 5,
y: 5,
@ -70,5 +76,86 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await expect(input).toBeFocused();
});
test('clicking padded space within item should fire one click event', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/29761',
});
await page.setContent(
`
<ion-item>
<ion-input label="Input"></ion-input>
</ion-item>
`,
config
);
const item = page.locator('ion-item');
const onClick = await page.spyOnEvent('click');
// Click the padding area (5px from left edge)
await item.click({
position: {
x: 5,
y: 5,
},
});
expect(onClick).toHaveReceivedEventTimes(1);
// Verify that the event target is the input and not the item
const event = onClick.events[0];
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-input');
});
test('clicking native wrapper should fire one click event', async ({ page }) => {
await page.setContent(
`
<ion-item>
<ion-input label="Input"></ion-input>
</ion-item>
`,
config
);
const nativeWrapper = page.locator('.native-wrapper');
const onClick = await page.spyOnEvent('click');
await nativeWrapper.click({
position: {
x: 5,
y: 5,
},
});
expect(onClick).toHaveReceivedEventTimes(1);
// Verify that the event target is the input and not the native wrapper
const event = onClick.events[0];
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-input');
});
test('clicking native input within item should fire click event with target as ion-input', async ({ page }) => {
await page.setContent(
`
<ion-item>
<ion-input label="Input"></ion-input>
</ion-item>
`,
config
);
const nativeInput = page.locator('.native-input');
const onClick = await page.spyOnEvent('click');
await nativeInput.click();
expect(onClick).toHaveReceivedEventTimes(1);
// Verify that the event target is the ion-input and not the native input
const event = onClick.events[0];
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-input');
});
});
});