Skip to content

bug: ion-item click event emitted two times (with ion-input) #29761

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
3 tasks done
JulienLecoq opened this issue Aug 9, 2024 · 11 comments · Fixed by #30373
Closed
3 tasks done

bug: ion-item click event emitted two times (with ion-input) #29761

JulienLecoq opened this issue Aug 9, 2024 · 11 comments · Fixed by #30373
Labels
package: core @ionic/core package type: bug a confirmed bug report

Comments

@JulienLecoq
Copy link

Prerequisites

Ionic Framework Version

v8.x

Current Behavior

When putting a ion-input inside of an ion-item, the click event on the ion-item is emitted two times when clicking just above or below the ion-input.

Expected Behavior

The click event should be emitted just once.

Steps to Reproduce

  1. Paste the following code in a page:
<ion-item (click)="onClick()">
    <ion-input value="Hello world!">
    </ion-input>
</ion-item>
onClick() {
    console.log("Item clicked")
}
  1. Click just above or below the ion-input.
  2. See the double log of "Item clicked".

Code Reproduction URL

https://github.com/JulienLecoq/bug_ion-item_click_with_ion-input

Ionic Info

Ionic:

Ionic CLI : 7.2.0 (/Users/julien_lecoq/.nvm/versions/node/v20.14.0/lib/node_modules/@ionic/cli)
Ionic Framework : @ionic/angular 8.2.6
@angular-devkit/build-angular : 18.1.4
@angular-devkit/schematics : 18.1.4
@angular/cli : 18.1.4
@ionic/angular-toolkit : 11.0.1

Capacitor:

Capacitor CLI : 6.1.2
@capacitor/android : not installed
@capacitor/core : 6.1.2
@capacitor/ios : not installed

Utility:

cordova-res : not installed globally
native-run : 2.0.1

System:

NodeJS : v20.14.0 (/Users/julien_lecoq/.nvm/versions/node/v20.14.0/bin/node)
npm : 10.7.0
OS : macOS Unknown

Additional Information

No response

@ionitron-bot ionitron-bot bot added the triage label Aug 9, 2024
@liamdebeasi
Copy link
Contributor

This looks to be the same as #28803 (comment).

@matheo
Copy link
Contributor

matheo commented Jan 7, 2025

@liamdebeasi this seems like a good case for a missing feature:

<ion-item [do-not-click-childs]="true" ...

I'm building a screen with ion-toggles inside ion-items but I need the clicks on the ion-items to not be propagated to the firstInteractive child:
https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/item/item.tsx#L308

We need an input flag to disable that click, and that would help my case and this double click stuff too.

PS/ this check discards the click inside the input to avoid double-clicks?

clickedWithinShadowRoot = this.el.shadowRoot!.contains(target);

@liamdebeasi
Copy link
Contributor

Hey there! I no longer work for Ionic, so I won't be much help in getting this resolved. However, if this issue is important for your use case I recommend adding a thumbs up reaction to the original post in addition to your comment above.

@matheo
Copy link
Contributor

matheo commented Jan 7, 2025

@liamdebeasi thank you very much!
@brandyscarney can you give us a hand here please? :)

@JulienLecoq
Copy link
Author

@matheo I like the idea too

@matheo
Copy link
Contributor

matheo commented Jan 8, 2025

@JulienLecoq I had to build this Directive to intercept the ion-item and ion-toggle click to revert their effects in my case, I don't want the toggle to be affected when clicking the ion-item, so I will only emit onToggle when the ion-toggle is clicked but the item is not:

// no-toggle.directive.ts
import { DOCUMENT } from '@angular/common';
import { DestroyRef, Directive, ElementRef, OnInit, contentChildren, inject, output } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { IonToggle } from '@ionic/angular/standalone';
import { tap } from 'rxjs';

@Directive({
  selector: 'ion-item[no-toggle]',
  standalone: true,
})
export class ItemNoToggleDirective implements OnInit {
  readonly #document = inject(DOCUMENT);
  readonly #destroy = inject(DestroyRef);
  readonly #el = inject(ElementRef);

  readonly toggle = contentChildren(IonToggle);

  readonly onToggle = output<CustomEvent>();

  private target?: HTMLInputElement;
  private timeout?: ReturnType<typeof setTimeout>;

  ngOnInit() {
    if (this.#el.nativeElement.tagName !== 'ION-ITEM' || !this.toggle().length) {
      throw new Error('"no-toggle" directive must be used on an <ion-item> with an <ion-toggle>');
    }

    // ion-item clicks the toggle when it's clicked
    this.#el.nativeElement.addEventListener('click', this.onItemClick.bind(this));

    this.toggle()[0]
      .ionChange.pipe(
        takeUntilDestroyed(this.#destroy),
        tap((event) => this.onChange(event)),
      )
      .subscribe();
  }

  onChange(event: CustomEvent) {
    // keep the reference to the target element
    this.target = event.target as HTMLInputElement;

    this.timeout = setTimeout(() => {
      // only emit if it's not a ion-item click
      this.onToggle.emit(event);

      this.target = undefined;
    }, 1);
  }

  onItemClick() {
    (this.#document.activeElement as HTMLElement)?.blur();

    clearTimeout(this.timeout);

    // reset the target element if this was a click on the ion-item
    if (this.target) {
      this.target.checked = !this.target.checked;
      this.target = undefined;
    }
  }
}

@JulienLecoq
Copy link
Author

@matheo Thanks a lot, you inspired me this that ChatGPT built for me cause I'm not a pro of those stuff haha.
It emit a click anywhere inside the ion-item, whether it’s the item itself or its children (like ion-input, ion-label, etc)…
It emit with the event’s target to be the ion-item, so popovers position correctly in the middle of the ion-item on a click and not
in the left or the right depending on what child you clicked on.
Also, it only emit once and not twice like often with Ionic click events...

import { Directive, ElementRef, HostListener, inject, output, } from '@angular/core'

@Directive({
	selector: 'ion-item[smartClick]',
})
export class IonItemSmartClickDirective {

	private readonly element = inject(ElementRef<HTMLElement>).nativeElement

	readonly smartClick = output<MouseEvent>()

	@HostListener('click', ['$event'])
	handleClick(event: MouseEvent) {
		// Ignore synthetic or non-user-initiated click.
		if (event.detail === 0) return

		// Clone the event with a modified target pointing to ion-item.
		const proxyEvent = new MouseEvent(event.type, event)

		Object.defineProperty(proxyEvent, 'target', {
			value: this.element,
			writable: false,
		})

		this.smartClick.emit(proxyEvent)
	}
}

@brandyscarney
Copy link
Member

Thank you for the issue! I have opened this PR with fixes for this issue and #29758.

Here is a dev build if you would like to try it out: 8.5.6-dev.11745613928.16440384

Please let me know if you find any issues. Thanks! 🙂

github-merge-queue bot pushed a commit that referenced this issue Apr 30, 2025
…nd 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 <[email protected]>
@JulienLecoq
Copy link
Author

Super cool, I'll wait for the stable build to update to it but I'm very excited for this! :)

I'll let you know if I find any issues once I have done the update.

@brandyscarney
Copy link
Member

This has been released in 8.5.6. If you run into any other problems please open a new issue. Thank you!

@matheo
Copy link
Contributor

matheo commented May 2, 2025

@brandyscarney is it possible to have a boolean input in the ion-item to NOT propagate the click into the inner control?
currently I have a "card of items" and clicking in the title should redirect to the details page,
and clicking specifically in the toggle will pop a de/activation prompt.

Image

it would be good to have control on that behavior right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package: core @ionic/core package type: bug a confirmed bug report
Projects
None yet
4 participants