ionic-framework icon indicating copy to clipboard operation
ionic-framework copied to clipboard

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

Open JulienLecoq opened this issue 1 year ago • 1 comments

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

JulienLecoq avatar Aug 09 '24 11:08 JulienLecoq

This looks to be the same as https://github.com/ionic-team/ionic-framework/issues/28803#issuecomment-1884945641.

liamdebeasi avatar Aug 09 '24 14:08 liamdebeasi

@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);

matheo avatar Jan 07 '25 00:01 matheo

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.

liamdebeasi avatar Jan 07 '25 03:01 liamdebeasi

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

matheo avatar Jan 07 '25 04:01 matheo

@matheo I like the idea too

JulienLecoq avatar Jan 08 '25 17:01 JulienLecoq

@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;
    }
  }
}

matheo avatar Jan 08 '25 18:01 matheo

@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)
	}
}

JulienLecoq avatar Mar 25 '25 16:03 JulienLecoq

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! 🙂

brandyscarney avatar Apr 25 '25 21:04 brandyscarney

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.

JulienLecoq avatar Apr 30 '25 17:04 JulienLecoq

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

brandyscarney avatar Apr 30 '25 18:04 brandyscarney

@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?

matheo avatar May 02 '25 17:05 matheo

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.

ionitron-bot[bot] avatar Jun 02 '25 11:06 ionitron-bot[bot]

@matheo Could you please create a new issue for that so we can investigate it separately?

brandyscarney avatar Jun 02 '25 15:06 brandyscarney