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/checkbox/test/item/index.html b/core/src/components/checkbox/test/item/index.html index b2fa0bdccd1..8692c4a022e 100644 --- a/core/src/components/checkbox/test/item/index.html +++ b/core/src/components/checkbox/test/item/index.html @@ -246,6 +246,16 @@

Multiline Label

+ + 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/index.html b/core/src/components/input/test/item/index.html index eb1b981246c..3290a1f5187 100644 --- a/core/src/components/input/test/item/index.html +++ b/core/src/components/input/test/item/index.html @@ -69,5 +69,15 @@

Inset List

+ + 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/index.html b/core/src/components/radio/test/item/index.html index 36e46779fd2..4e0bfbdc01e 100644 --- a/core/src/components/radio/test/item/index.html +++ b/core/src/components/radio/test/item/index.html @@ -207,5 +207,15 @@

Multiline Label

+ + 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/index.html b/core/src/components/select/test/item/index.html index 826ff976992..06d32380405 100644 --- a/core/src/components/select/test/item/index.html +++ b/core/src/components/select/test/item/index.html @@ -83,5 +83,15 @@

Disabled

+ + 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/index.html b/core/src/components/textarea/test/item/index.html index f22bc480247..3e6cf7827eb 100644 --- a/core/src/components/textarea/test/item/index.html +++ b/core/src/components/textarea/test/item/index.html @@ -69,5 +69,15 @@

Inset List

+ + 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/index.html b/core/src/components/toggle/test/item/index.html index caeda79d077..86357208a45 100644 --- a/core/src/components/toggle/test/item/index.html +++ b/core/src/components/toggle/test/item/index.html @@ -224,5 +224,15 @@

Multiline Label

+ + 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'); + }); }); });