mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-09 08:09:32 +08:00
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:
@ -98,28 +98,5 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
await checkbox.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true));
|
await checkbox.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true));
|
||||||
expect(ionChange).not.toHaveReceivedEvent();
|
expect(ionChange).not.toHaveReceivedEvent();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clicking padded space within item should click the checkbox', async ({ page }) => {
|
|
||||||
await page.setContent(
|
|
||||||
`
|
|
||||||
<ion-item>
|
|
||||||
<ion-checkbox>Size</ion-checkbox>
|
|
||||||
</ion-item>
|
|
||||||
`,
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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(
|
||||||
|
`
|
||||||
|
<ion-item>
|
||||||
|
<ion-checkbox>Size</ion-checkbox>
|
||||||
|
</ion-item>
|
||||||
|
`,
|
||||||
|
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(
|
||||||
|
`
|
||||||
|
<ion-item>
|
||||||
|
<ion-checkbox>
|
||||||
|
Checkbox
|
||||||
|
</ion-checkbox>
|
||||||
|
</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 checkbox and not the item
|
||||||
|
const event = onClick.events[0];
|
||||||
|
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-checkbox');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -107,6 +107,10 @@
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-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%;
|
max-height: 100%;
|
||||||
|
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
@ -1,5 +1,18 @@
|
|||||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
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 type { NotchController } from '@utils/forms';
|
||||||
import { createNotchController } from '@utils/forms';
|
import { createNotchController } from '@utils/forms';
|
||||||
import type { Attributes } from '@utils/helpers';
|
import type { Attributes } from '@utils/helpers';
|
||||||
@ -363,6 +376,19 @@ export class Input implements ComponentInterface {
|
|||||||
forceUpdate(this);
|
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() {
|
componentWillLoad() {
|
||||||
this.inheritedAttributes = {
|
this.inheritedAttributes = {
|
||||||
...inheritAriaAttributes(this.el),
|
...inheritAriaAttributes(this.el),
|
||||||
|
|||||||
@ -49,6 +49,11 @@ configs().forEach(({ title, screenshot, config }) => {
|
|||||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||||
test.describe(title('input: item functionality'), () => {
|
test.describe(title('input: item functionality'), () => {
|
||||||
test('clicking padded space within item should focus the input', async ({ page }) => {
|
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(
|
await page.setContent(
|
||||||
`
|
`
|
||||||
<ion-item>
|
<ion-item>
|
||||||
@ -57,11 +62,12 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
`,
|
`,
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
const itemNative = page.locator('.item-native');
|
|
||||||
|
const item = page.locator('ion-item');
|
||||||
const input = page.locator('ion-input input');
|
const input = page.locator('ion-input input');
|
||||||
|
|
||||||
// Clicks the padded space within the item
|
// Clicks the padded space within the item
|
||||||
await itemNative.click({
|
await item.click({
|
||||||
position: {
|
position: {
|
||||||
x: 5,
|
x: 5,
|
||||||
y: 5,
|
y: 5,
|
||||||
@ -70,5 +76,86 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
|
|
||||||
await expect(input).toBeFocused();
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -286,6 +286,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
|||||||
if (firstInteractive !== undefined && !multipleInputs) {
|
if (firstInteractive !== undefined && !multipleInputs) {
|
||||||
const path = ev.composedPath();
|
const path = ev.composedPath();
|
||||||
const target = path[0] as HTMLElement;
|
const target = path[0] as HTMLElement;
|
||||||
|
|
||||||
if (ev.isTrusted) {
|
if (ev.isTrusted) {
|
||||||
/**
|
/**
|
||||||
* Dispatches a click event to the first interactive element,
|
* 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') {
|
if (firstInteractive.tagName === 'ION-INPUT' || firstInteractive.tagName === 'ION-TEXTAREA') {
|
||||||
(firstInteractive as HTMLIonInputElement | HTMLIonTextareaElement).setFocus();
|
(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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,9 +78,16 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
|
|||||||
await expect(list).toHaveScreenshot(screenshot(`radio-stacked-label-in-item`));
|
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('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(
|
await page.setContent(
|
||||||
`
|
`
|
||||||
<ion-radio-group>
|
<ion-radio-group>
|
||||||
@ -93,11 +100,11 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
|
|||||||
`,
|
`,
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
const itemNative = page.locator('.item-native');
|
const item = page.locator('ion-item');
|
||||||
const ionChange = await page.spyOnEvent('ionChange');
|
const ionChange = await page.spyOnEvent('ionChange');
|
||||||
|
|
||||||
// Clicks the padded space within the item
|
// Clicks the padded space within the item
|
||||||
await itemNative.click({
|
await item.click({
|
||||||
position: {
|
position: {
|
||||||
x: 5,
|
x: 5,
|
||||||
y: 5,
|
y: 5,
|
||||||
@ -106,5 +113,40 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
|
|||||||
|
|
||||||
expect(ionChange).toHaveReceivedEvent();
|
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(
|
||||||
|
`
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio>
|
||||||
|
Radio
|
||||||
|
</ion-radio>
|
||||||
|
</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 radio and not the item
|
||||||
|
const event = onClick.events[0];
|
||||||
|
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-radio');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -294,34 +294,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
|||||||
await select.evaluate((el: HTMLIonSelectElement) => (el.value = 'banana'));
|
await select.evaluate((el: HTMLIonSelectElement) => (el.value = 'banana'));
|
||||||
await expect(ionChange).not.toHaveReceivedEvent();
|
await expect(ionChange).not.toHaveReceivedEvent();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clicking padded space within item should click the select', async ({ page }) => {
|
|
||||||
await page.setContent(
|
|
||||||
`
|
|
||||||
<ion-item>
|
|
||||||
<ion-select label="Fruit" interface="action-sheet">
|
|
||||||
<ion-select-option value="apple">Apple</ion-select-option>
|
|
||||||
<ion-select-option value="banana">Banana</ion-select-option>
|
|
||||||
</ion-select>
|
|
||||||
</ion-item>
|
|
||||||
`,
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
`
|
||||||
|
<ion-item>
|
||||||
|
<ion-select label="Fruit" interface="action-sheet">
|
||||||
|
<ion-select-option value="apple">Apple</ion-select-option>
|
||||||
|
<ion-select-option value="banana">Banana</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
`,
|
||||||
|
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(
|
||||||
|
`
|
||||||
|
<ion-item>
|
||||||
|
<ion-select
|
||||||
|
label="Fruit"
|
||||||
|
value="apple"
|
||||||
|
>
|
||||||
|
<ion-select-option value="apple">Apple</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</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 select and not the item
|
||||||
|
const event = onClick.events[0];
|
||||||
|
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-select');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -49,6 +49,11 @@ configs().forEach(({ title, screenshot, config }) => {
|
|||||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||||
test.describe(title('textarea: item functionality'), () => {
|
test.describe(title('textarea: item functionality'), () => {
|
||||||
test('clicking padded space within item should focus the textarea', async ({ page }) => {
|
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(
|
await page.setContent(
|
||||||
`
|
`
|
||||||
<ion-item>
|
<ion-item>
|
||||||
@ -57,11 +62,11 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
`,
|
`,
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
const itemNative = page.locator('.item-native');
|
const item = page.locator('ion-item');
|
||||||
const textarea = page.locator('ion-textarea textarea');
|
const textarea = page.locator('ion-textarea textarea');
|
||||||
|
|
||||||
// Clicks the padded space within the item
|
// Clicks the padded space within the item
|
||||||
await itemNative.click({
|
await item.click({
|
||||||
position: {
|
position: {
|
||||||
x: 5,
|
x: 5,
|
||||||
y: 5,
|
y: 5,
|
||||||
@ -70,5 +75,61 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
|
|
||||||
await expect(textarea).toBeFocused();
|
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(
|
||||||
|
`
|
||||||
|
<ion-item>
|
||||||
|
<ion-textarea label="Textarea"></ion-textarea>
|
||||||
|
</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-textarea');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking native textarea within item should fire click event with target as ion-textarea', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-item>
|
||||||
|
<ion-textarea label="Textarea"></ion-textarea>
|
||||||
|
</ion-item>
|
||||||
|
`,
|
||||||
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
Element,
|
Element,
|
||||||
Event,
|
Event,
|
||||||
Host,
|
Host,
|
||||||
|
Listen,
|
||||||
Method,
|
Method,
|
||||||
Prop,
|
Prop,
|
||||||
State,
|
State,
|
||||||
@ -314,6 +315,19 @@ export class Textarea implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Event() ionFocus!: EventEmitter<FocusEvent>;
|
@Event() ionFocus!: EventEmitter<FocusEvent>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
connectedCallback() {
|
||||||
const { el } = this;
|
const { el } = this;
|
||||||
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
|
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
|
||||||
|
|||||||
@ -108,9 +108,16 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
|
|||||||
await expect(list).toHaveScreenshot(screenshot(`toggle-stacked-label-in-item`));
|
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('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(
|
await page.setContent(
|
||||||
`
|
`
|
||||||
<ion-item>
|
<ion-item>
|
||||||
@ -119,7 +126,7 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
|
|||||||
`,
|
`,
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
const itemNative = page.locator('.item-native');
|
const item = page.locator('ion-item');
|
||||||
const ionChange = await page.spyOnEvent('ionChange');
|
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.
|
* 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.
|
* 3. MD is consistent in their implementation and activates controls by clicking the label.
|
||||||
*/
|
*/
|
||||||
await itemNative.click({
|
await item.click({
|
||||||
position: {
|
position: {
|
||||||
x: 5,
|
x: 5,
|
||||||
y: 5,
|
y: 5,
|
||||||
@ -141,5 +148,40 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
|
|||||||
|
|
||||||
expect(ionChange).toHaveReceivedEvent();
|
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(
|
||||||
|
`
|
||||||
|
<ion-item>
|
||||||
|
<ion-toggle>
|
||||||
|
Toggle
|
||||||
|
</ion-toggle>
|
||||||
|
</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 toggle and not the item
|
||||||
|
const event = onClick.events[0];
|
||||||
|
expect((event.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user