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

bug: ionChange not fired on iOS 16 devices for ion-picker

Open Simon54 opened this issue 1 year ago • 23 comments
trafficstars

Prerequisites

Ionic Framework Version

v8.x

Current Behavior

By scrolling the picker the value is not changed and the event is not fired on iOS devices with iOS 16 (used iPhone 8 Plus).

Expected Behavior

The event should fire and the value should change.

Steps to Reproduce

  1. create a blank angular ionic project
  2. Insert code as the documentation in the home.page.html,home.poge.ts and home.page.scss files: https://ionicframework.com/docs/api/picker#picker-in-modal
  3. To check the bug add in home.page.ts one line to the onIonChange function:
  onIonChange(event: CustomEvent) {
    console.log("onIonChange fired!", event.detail.value) // ADD THIS LINE
    this.currentValue = event.detail.value;  
  }
  1. Try on an Android device: you see in the console the ionChange event fired when value change
  2. Try on an iOS device: nothing is fired when value change

Code Reproduction URL

https://github.com/losciur/ionic-bug-ion-column-picker

Ionic Info

Ionic:

Ionic CLI : 5.4.16 (/Users/simon/.npm/_npx/864a9e3c2cd0cf50/node_modules/ionic)

Capacitor:

Capacitor CLI : 6.1.0 @capacitor/core : 6.1.0

Utility:

cordova-res : 0.15.4 native-run : 2.0.0

System:

NodeJS : v22.3.0 (/usr/local/Cellar/node/22.3.0/bin/node) npm : 10.8.1 OS : macOS Unknown

Additional Information

The code is from this closed issue: #29480

The issue was closed without it having been resolved, so I'm reopening it, because it is happening on an iPhone that has iOS 16 and should be supported, since these devices don't receive iOS 17.

I investigated the issue and it comes down to the elementsForPoint not returning the ion-picker-column-option on this devices for some reason. So I made a simple patch which would make it work, but it's probably a bit expensive:

/core/src/components/picker-column/picker-column.tsx 379:

let newActiveElement = elementsAtPoint.find((el) => el.tagName === 'ION-PICKER-COLUMN-OPTION');
if(!newActiveElement) {
    const contains = (rect,x,y)=>rect.x <= x && rect.y <= y && x <= rect.x + rect.width && y <= rect.y + rect.height
    newActiveElement = [...el.children].find(x=>contains(x.getBoundingClientRect(),centerX,centerY))
}
if (activeEl !== undefined) {
    this.setPickerItemActiveState(activeEl, false);
}

Simon54 avatar Jun 27 '24 19:06 Simon54

We are confronting the same issue as described in this Post in our React application. We need to go back to Ionic v7.x for now.

For us the problem is showing up in the IonDateTime component displayed in a Modal (which is using the IonPicker under the hood). We tried it on different devices and in the browser. We can confirm that it is only happening on iOS 16.x as described in this PR. For our case it is 100% necessarry to have the issue fixed as a lot of our users have these "older" device models.

Raphael-Schulz avatar Jul 25 '24 06:07 Raphael-Schulz

Before this gets closed for inactivity, is there any intention from the Ionic team to prioritize this regression problem?

jfcere avatar Sep 26 '24 12:09 jfcere

Any news on that subject ? Thanks

WheelyWonka avatar Feb 10 '25 15:02 WheelyWonka

We have the exact same issue on iOS 16.7.x as well. Desperately need this to be fixed since thousands of our users have an iPhone 8 which cannot install iOS 17.

vilhelmjosander avatar Feb 21 '25 12:02 vilhelmjosander

I'm using Vue and it's not being fired for any platform.

Inspecting with Vue Devtools I found out that it's not emitting the event, but rather accepting a prop ion-change, this is what I did in order to work:

<IonPicker>
      <IonPickerColumn
        :value="currentValue"
        :ion-change="{
          emit(value) {
            console.log(value) // Here you have access to the value whenever it gets changed
          },
        }"
      >
        <IonPickerColumnOption value="javascript">Javascript</IonPickerColumnOption>
        <IonPickerColumnOption value="typescript">Typescript</IonPickerColumnOption>
        <IonPickerColumnOption value="rust">Rust</IonPickerColumnOption>
        <IonPickerColumnOption value="c#">C#</IonPickerColumnOption>
      </IonPickerColumn>
    </IonPicker>

Edit: It seems it got fixed on Ionic 8.5.0, now it emits the correct @ion-change event

JoaoHCopetti avatar Mar 14 '25 16:03 JoaoHCopetti

Hello! Is this still an issue in 8.5.0?

ShaneK avatar May 07 '25 19:05 ShaneK

Still happening, still not fixed.

Simon54 avatar May 07 '25 20:05 Simon54

Very interesting, I cloned your example repo and I'm running it in an iOS 16.4 emulator and it appears to be working? I think there was a bug in these versions of iOS that don't let you use the web inspector, but I added an alert to onIonChange and it appears to work as expected. Is it exactly iOS 16 that this is happening for you in?

https://github.com/user-attachments/assets/eee82ece-cb75-42f1-9958-0587e34005e1

ShaneK avatar May 07 '25 21:05 ShaneK

Seems it does not happen in the simulator, I checked, you need a device. I'm using iPhone 8 Plus and latest update available.

Simon54 avatar May 08 '25 08:05 Simon54

Does this only happen if you build the app as a stand alone and deploy it to iOS, or does it happen for web builds too?

ShaneK avatar May 09 '25 17:05 ShaneK

Seems it does not happen in web builds on the device, only in the app.

Simon54 avatar May 09 '25 19:05 Simon54

Are these devices using iOS 16.7? I've tried as best as I can to reproduce this with just no luck. We used real device tests to try on various devices that got as close as we could but just not seeing it.

Are you seeing this on any other components like ion-input?

ShaneK avatar May 10 '25 16:05 ShaneK

I'm on the latest iOS 16, which is 16.7.2. Just to be on the same page:

  • If you short tap on the wheel, it fires the event in the log.
  • If you move the wheel slowly via dragging, it won't fire and the value is not changed.

Another symptom is that if you use a style to highlight the active option, it doesn't work after changing the value and the line color is not changed:

ion-picker-column-option.option-active {color:#00c886;}

In the simulator this is working.

Simon54 avatar May 15 '25 09:05 Simon54

@Simon54 thank you for the additional information! It would help us debug further if you can verify if ion-input's ionChange fires without issues on your iOS 16 device. Let us know of your findings as soon as you can.

thetaPC avatar May 20 '25 23:05 thetaPC

This seems to only be an issue for iOS 16.7.x which for some older iPhones is the only version you can use. We solved this by using the following patch with the patch-package npm package.

Image

This works fine for us.

vilhelmjosander avatar May 21 '25 08:05 vilhelmjosander

@Simon54 thank you for the additional information! It would help us debug further if you can verify if ion-input's ionChange fires without issues on your iOS 16 device. Let us know of your findings as soon as you can.

The ion-input ionChange fires, it doesn't depend on the elementFromPoint.

Simon54 avatar May 21 '25 19:05 Simon54

@Simon54 Please use the following code snippet and let me know what the console log says after clicking on the button (a video showing the findings would be amazing). elementsFromPoint does appear to be the likely culprit but only when it's used on a shadow component with iOS 16. With this test, we can narrow down if my theory is correct:

// shadow-test.component.ts
import { Component, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-shadow-test',
  template: `<div id="container" style="height: 300px; background: #eee;"></div>`,
})
export class ShadowTestComponent implements AfterViewInit {
  constructor(private host: ElementRef) {}

  ngAfterViewInit(): void {
    const container = this.host.nativeElement.querySelector('#container');

    // Create a shadow root manually
    const shadowHost = document.createElement('div');
    shadowHost.style.display = 'block';
    shadowHost.style.marginTop = '100px';
    this.host.nativeElement.appendChild(shadowHost);

    const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

    // Add a child to shadow DOM
    const shadowChild = document.createElement('div');
    shadowChild.textContent = 'Shadow Element';
    shadowChild.style.background = 'lightblue';
    shadowChild.style.padding = '20px';
    shadowChild.style.margin = '20px';
    shadowChild.style.display = 'inline-block';
    shadowChild.id = 'shadow-target';

    shadowRoot.appendChild(shadowChild);

    // Add a global click listener
    window.addEventListener('click', (event: MouseEvent) => {
      const x = event.clientX;
      const y = event.clientY;

      console.log('--- Click at:', x, y);

      // Try global elementsFromPoint
      const globalElements = document.elementsFromPoint(x, y);
      console.log('document.elementsFromPoint:', globalElements.map(el => el.id || el.tagName));

      // Try shadowRoot.elementsFromPoint (this may throw or return [])
      try {
        const shadowElements = (shadowRoot as any).elementsFromPoint?.(x, y);
        console.log('shadowRoot.elementsFromPoint:', shadowElements?.map((el: Element) => el.id || el.tagName));
      } catch (err) {
        console.warn('shadowRoot.elementsFromPoint failed:', err);
      }
    });
  }
}

thetaPC avatar May 22 '25 20:05 thetaPC

https://github.com/ionic-team/ionic-framework/issues/29926 seems to be related to this issue.

thetaPC avatar May 22 '25 20:05 thetaPC

I'm getting:

document.elementsFromPoint:
Array (10)
0 "DIV"
1 "APP-SHADOW-TEST"
2 "DIV"
3 "ion-overlay-1"
4 "ION-CONTENT"
5 "APP-HOME"
6 "ION-ROUTER-OUTLET"
7 "ION-APP"
8 "BODY"
9 "HTML"

shadowRoot.elementsFromPoint:
Array (11)
0 "shadow-target"
1 "DIV"
2 "APP-SHADOW-TEST"
3 "DIV"
4 "ion-overlay-1"
5 "ION-CONTENT"
6 "APP-HOME"
7 "ION-ROUTER-OUTLET"
8 "ION-APP"
9 "BODY"
10 "HTML"

Simon54 avatar May 23 '25 08:05 Simon54

Hello,

I encountered the same issue on iPhone X with IOS 16+.

As a workaround, I have adapted my script in this way:

  1. Use modal with the picker inside.
  2. Add a valid/confirm button.
  3. Add a custom attribute (data-value) on the picker-column and the picker-column-option.
  4. On click on this button, check if the picker option has the classname "option-active" and if not, get the visually active option to store the value in a variable.

This is not the best approach but this is a functional workaround until the official patch (that works in my case) 🙂

Here is a demo code:

modal-picker.page.html:

<ion-toolbar>
  <ion-buttons slot="start">
    <ion-button (click)="dismiss('cancel')">{{ 'global.actions.cancel' | translate }}</ion-button>
  </ion-buttons>
  <ion-buttons slot="end">
    <ion-button (click)="confirm()">{{ 'global.actions.validate' | translate }}</ion-button>
  </ion-buttons>
</ion-toolbar>

<ion-picker class="modal__picker" #picker>
  <ion-picker-column *ngFor="let col of columns" [attr.data-value]="col.name" [value]="selected[col.name]" (ionChange)="onChange(col.name, $event)">
    <ion-picker-column-option *ngFor="let opt of col.options" [attr.data-value]="opt.value" [value]="opt.value" [innerText]="opt.text"></ion-picker-column-option>
  </ion-picker-column>
</ion-picker>

modal-picker.page.ts:

@ViewChild('picker', { read: ElementRef }) pickerRef: ElementRef<HTMLElement>;

onChange(name: string, ev: CustomEvent) {
  this.selected[name] = ev.detail.value;
}

confirm() {
  this.devFixIos16SupportIssue();
  // Continue your code according to your needs using this.selected.
}

devFixIos16SupportIssue() {
  function getSelectedOption(columnEl: HTMLElement, pickerEl: HTMLElement) {
    const highlight = pickerEl.shadowRoot?.querySelector('.picker-highlight');
    if (!highlight) return null;

    const highlightRect = highlight.getBoundingClientRect();
    const highlightCenterY = highlightRect.top + highlightRect.height / 2;
    const options = Array.from(columnEl.querySelectorAll('ion-picker-column-option'));

    for (const option of options) {
      const optionRect = option.getBoundingClientRect();

      if (optionRect.top <= highlightCenterY && optionRect.bottom >= highlightCenterY) {
        return option;
      }
    }
    return null;
  }

  const pickerEl = this.pickerRef.nativeElement;
  const columns = pickerEl.querySelectorAll('ion-picker-column');

  columns.forEach(col => {
    if (col.querySelector('.option-active')) {
      return;
    }

    const selectedOption = getSelectedOption(col as HTMLElement, pickerEl);
    if (selectedOption) {
      const col_value = col.getAttribute('data-value');
      const option_value = selectedOption.getAttribute('data-value');
      this.selected[col_value] = option_value;
    }
  });
}

Regards, Loïc

loic-parent avatar Jun 04 '25 08:06 loic-parent

@Simon54 I really appreciate the results! It does indicate an issue with shadow DOM. Please try this dev build and let me know if it fixes the issue: npm install @ionic/[email protected]? The patch that @vilhelmjosander provided should be the solution.

thetaPC avatar Jun 05 '25 16:06 thetaPC

confirmed, works.

Simon54 avatar Jun 06 '25 21:06 Simon54

@Simon54 Thank you for checking!! I had to make some changes to the code. It would be great if you can verify that this dev build still fixes the issue: npm install @ionic/[email protected]. Please let me know of the results.

thetaPC avatar Jun 12 '25 20:06 thetaPC

still works.

Simon54 avatar Jun 16 '25 07:06 Simon54

This issue was fixed in v8.6.2! Please update to the latest version to receive the fix.

thetaPC avatar Jun 18 '25 18:06 thetaPC