diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts b/core/src/components/checkbox/test/basic/checkbox.e2e.ts
index 957c6da31b8..1a41b339599 100644
--- a/core/src/components/checkbox/test/basic/checkbox.e2e.ts
+++ b/core/src/components/checkbox/test/basic/checkbox.e2e.ts
@@ -98,28 +98,5 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await checkbox.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true));
expect(ionChange).not.toHaveReceivedEvent();
});
-
- test('clicking padded space within item should click the checkbox', async ({ page }) => {
- await page.setContent(
- `
-
- Size
-
- `,
- config
- );
- const itemNative = page.locator('.item-native');
- const ionChange = await page.spyOnEvent('ionChange');
-
- // Clicks the padded space within the item
- await itemNative.click({
- position: {
- x: 5,
- y: 5,
- },
- });
-
- expect(ionChange).toHaveReceivedEvent();
- });
});
});
diff --git a/core/src/components/checkbox/test/item/checkbox.e2e.ts b/core/src/components/checkbox/test/item/checkbox.e2e.ts
index bdcf1892da2..341839739b1 100644
--- a/core/src/components/checkbox/test/item/checkbox.e2e.ts
+++ b/core/src/components/checkbox/test/item/checkbox.e2e.ts
@@ -127,3 +127,70 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
});
});
});
+
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('checkbox: item functionality'), () => {
+ test('clicking padded space within item should click the checkbox', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/27169',
+ });
+
+ await page.setContent(
+ `
+
+ Size
+
+ `,
+ config
+ );
+ const item = page.locator('ion-item');
+ const ionChange = await page.spyOnEvent('ionChange');
+
+ // Clicks the padded space within the item
+ await item.click({
+ position: {
+ x: 5,
+ y: 5,
+ },
+ });
+
+ expect(ionChange).toHaveReceivedEvent();
+ });
+
+ 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/29758',
+ });
+
+ await page.setContent(
+ `
+
+
+ Checkbox
+
+
+ `,
+ 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 checkbox and not the item
+ const event = onClick.events[0];
+ expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
+ });
+ });
+});
diff --git a/core/src/components/input/input.scss b/core/src/components/input/input.scss
index cc648e6e2f0..57d438eb2d9 100644
--- a/core/src/components/input/input.scss
+++ b/core/src/components/input/input.scss
@@ -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;
diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx
index 42bee8e005e..5f4b1257141 100644
--- a/core/src/components/input/input.tsx
+++ b/core/src/components/input/input.tsx
@@ -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),
diff --git a/core/src/components/input/test/item/input.e2e.ts b/core/src/components/input/test/item/input.e2e.ts
index 710e2f00b12..a4fd996c713 100644
--- a/core/src/components/input/test/item/input.e2e.ts
+++ b/core/src/components/input/test/item/input.e2e.ts
@@ -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(
`
@@ -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(
+ `
+
+
+
+ `,
+ 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(
+ `
+
+
+
+ `,
+ 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(
+ `
+
+
+
+ `,
+ 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');
+ });
});
});
diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx
index 6091e8b1cf8..6384927de80 100644
--- a/core/src/components/item/item.tsx
+++ b/core/src/components/item/item.tsx
@@ -286,6 +286,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
if (firstInteractive !== undefined && !multipleInputs) {
const path = ev.composedPath();
const target = path[0] as HTMLElement;
+
if (ev.isTrusted) {
/**
* Dispatches a click event to the first interactive element,
@@ -304,9 +305,14 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
*/
if (firstInteractive.tagName === 'ION-INPUT' || firstInteractive.tagName === 'ION-TEXTAREA') {
(firstInteractive as HTMLIonInputElement | HTMLIonTextareaElement).setFocus();
- } else {
- firstInteractive.click();
}
+ firstInteractive.click();
+ /**
+ * Stop the item event from being triggered
+ * as the firstInteractive click event will also
+ * trigger the item click event.
+ */
+ ev.stopImmediatePropagation();
}
}
}
diff --git a/core/src/components/radio/test/item/radio.e2e.ts b/core/src/components/radio/test/item/radio.e2e.ts
index 4fdf34bd4e1..8c9fd4be951 100644
--- a/core/src/components/radio/test/item/radio.e2e.ts
+++ b/core/src/components/radio/test/item/radio.e2e.ts
@@ -78,9 +78,16 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
await expect(list).toHaveScreenshot(screenshot(`radio-stacked-label-in-item`));
});
});
+});
- test.describe(title('radio: ionChange'), () => {
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('radio: item functionality'), () => {
test('clicking padded space within item should click the radio', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/27169',
+ });
+
await page.setContent(
`
@@ -93,11 +100,11 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
`,
config
);
- const itemNative = page.locator('.item-native');
+ const item = page.locator('ion-item');
const ionChange = await page.spyOnEvent('ionChange');
// Clicks the padded space within the item
- await itemNative.click({
+ await item.click({
position: {
x: 5,
y: 5,
@@ -106,5 +113,40 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
expect(ionChange).toHaveReceivedEvent();
});
+
+ 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/29758',
+ });
+
+ await page.setContent(
+ `
+
+
+ Radio
+
+
+ `,
+ 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 radio and not the item
+ const event = onClick.events[0];
+ expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-radio');
+ });
});
});
diff --git a/core/src/components/select/test/basic/select.e2e.ts b/core/src/components/select/test/basic/select.e2e.ts
index d5a9c3d220f..63f1c8ca10a 100644
--- a/core/src/components/select/test/basic/select.e2e.ts
+++ b/core/src/components/select/test/basic/select.e2e.ts
@@ -294,34 +294,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await select.evaluate((el: HTMLIonSelectElement) => (el.value = 'banana'));
await expect(ionChange).not.toHaveReceivedEvent();
});
-
- test('clicking padded space within item should click the select', async ({ page }) => {
- await page.setContent(
- `
-
-
- Apple
- Banana
-
-
- `,
- config
- );
- const itemNative = page.locator('.item-native');
- const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
-
- // Clicks the padded space within the item
- await itemNative.click({
- position: {
- x: 5,
- y: 5,
- },
- });
-
- await ionActionSheetDidPresent.next();
-
- expect(ionActionSheetDidPresent).toHaveReceivedEvent();
- });
});
});
diff --git a/core/src/components/select/test/item/select.e2e.ts b/core/src/components/select/test/item/select.e2e.ts
index 05ca3e4abb3..d0ad1d616d0 100644
--- a/core/src/components/select/test/item/select.e2e.ts
+++ b/core/src/components/select/test/item/select.e2e.ts
@@ -61,3 +61,78 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
});
});
});
+
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('select: item functionality'), () => {
+ test('clicking padded space within item should click the select', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/27169',
+ });
+
+ await page.setContent(
+ `
+
+
+ Apple
+ Banana
+
+
+ `,
+ config
+ );
+ const item = page.locator('ion-item');
+ const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
+
+ // Clicks the padded space within the item
+ await item.click({
+ position: {
+ x: 5,
+ y: 5,
+ },
+ });
+
+ await ionActionSheetDidPresent.next();
+
+ expect(ionActionSheetDidPresent).toHaveReceivedEvent();
+ });
+
+ 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/29758',
+ });
+
+ await page.setContent(
+ `
+
+
+ Apple
+
+
+ `,
+ 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 select and not the item
+ const event = onClick.events[0];
+ expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-select');
+ });
+ });
+});
diff --git a/core/src/components/textarea/test/item/textarea.e2e.ts b/core/src/components/textarea/test/item/textarea.e2e.ts
index ed5daeec449..9b3408733e7 100644
--- a/core/src/components/textarea/test/item/textarea.e2e.ts
+++ b/core/src/components/textarea/test/item/textarea.e2e.ts
@@ -49,6 +49,11 @@ configs().forEach(({ title, screenshot, config }) => {
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('textarea: item functionality'), () => {
test('clicking padded space within item should focus the textarea', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/21982',
+ });
+
await page.setContent(
`
@@ -57,11 +62,11 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
`,
config
);
- const itemNative = page.locator('.item-native');
+ const item = page.locator('ion-item');
const textarea = page.locator('ion-textarea textarea');
// Clicks the padded space within the item
- await itemNative.click({
+ await item.click({
position: {
x: 5,
y: 5,
@@ -70,5 +75,61 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await expect(textarea).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(
+ `
+
+
+
+ `,
+ 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-textarea');
+ });
+
+ test('clicking native textarea within item should fire click event with target as ion-textarea', async ({
+ page,
+ }) => {
+ await page.setContent(
+ `
+
+
+
+ `,
+ config
+ );
+
+ const nativeTextarea = page.locator('.native-textarea');
+ const onClick = await page.spyOnEvent('click');
+
+ await nativeTextarea.click();
+ expect(onClick).toHaveReceivedEventTimes(1);
+
+ // Verify that the event target is the ion-textarea and not the native textarea
+ const event = onClick.events[0];
+ expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-textarea');
+ });
});
});
diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx
index afb9e3bf6b6..78541e9d66b 100644
--- a/core/src/components/textarea/textarea.tsx
+++ b/core/src/components/textarea/textarea.tsx
@@ -5,6 +5,7 @@ import {
Element,
Event,
Host,
+ Listen,
Method,
Prop,
State,
@@ -314,6 +315,19 @@ export class Textarea implements ComponentInterface {
*/
@Event() ionFocus!: EventEmitter;
+ /**
+ * This prevents the native input from emitting the click event.
+ * Instead, the click event from the ion-textarea is emitted.
+ */
+ @Listen('click', { capture: true })
+ onClickCapture(ev: Event) {
+ const nativeInput = this.nativeInput;
+ if (nativeInput && ev.target === nativeInput) {
+ ev.stopPropagation();
+ this.el.click();
+ }
+ }
+
connectedCallback() {
const { el } = this;
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
diff --git a/core/src/components/toggle/test/item/toggle.e2e.ts b/core/src/components/toggle/test/item/toggle.e2e.ts
index f5db0dab8ab..866a382494f 100644
--- a/core/src/components/toggle/test/item/toggle.e2e.ts
+++ b/core/src/components/toggle/test/item/toggle.e2e.ts
@@ -108,9 +108,16 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
await expect(list).toHaveScreenshot(screenshot(`toggle-stacked-label-in-item`));
});
});
+});
- test.describe(title('toggle: ionChange'), () => {
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('toggle: item functionality'), () => {
test('clicking padded space within item should click the toggle', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/27169',
+ });
+
await page.setContent(
`
@@ -119,7 +126,7 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
`,
config
);
- const itemNative = page.locator('.item-native');
+ const item = page.locator('ion-item');
const ionChange = await page.spyOnEvent('ionChange');
/**
@@ -132,7 +139,7 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
* 2. iOS is inconsistent in their implementation and other controls can be activated by clicking the label.
* 3. MD is consistent in their implementation and activates controls by clicking the label.
*/
- await itemNative.click({
+ await item.click({
position: {
x: 5,
y: 5,
@@ -141,5 +148,40 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
expect(ionChange).toHaveReceivedEvent();
});
+
+ 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/29758',
+ });
+
+ await page.setContent(
+ `
+
+
+ Toggle
+
+
+ `,
+ 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 toggle and not the item
+ const event = onClick.events[0];
+ expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle');
+ });
});
});