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

Open
3 tasks done
JulienLecoq opened this issue Aug 9, 2024 · 8 comments · May be fixed by #30373
Open
3 tasks done

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

JulienLecoq opened this issue Aug 9, 2024 · 8 comments · May be 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 brandyscarney added type: bug a confirmed bug report package: core @ionic/core package labels Apr 25, 2025
@ionitron-bot ionitron-bot bot removed the triage label Apr 25, 2025
@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! 🙂

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