ionic-framework
ionic-framework copied to clipboard
bug: ionChange not fired on iOS 16 devices for ion-picker
Prerequisites
- [X] I have read the Contributing Guidelines.
- [X] I agree to follow the Code of Conduct.
- [X] I have searched for existing issues that already report this problem, without success.
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
- create a blank angular ionic project
- 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
- 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;
}
- Try on an Android device: you see in the console the ionChange event fired when value change
- 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);
}
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.
Before this gets closed for inactivity, is there any intention from the Ionic team to prioritize this regression problem?
Any news on that subject ? Thanks
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.
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
Hello! Is this still an issue in 8.5.0?
Still happening, still not fixed.
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
Seems it does not happen in the simulator, I checked, you need a device. I'm using iPhone 8 Plus and latest update available.
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?
Seems it does not happen in web builds on the device, only in the app.
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?
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 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.
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.
This works fine for us.
@Simon54 thank you for the additional information! It would help us debug further if you can verify if
ion-input'sionChangefires 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 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);
}
});
}
}
https://github.com/ionic-team/ionic-framework/issues/29926 seems to be related to this issue.
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"
Hello,
I encountered the same issue on iPhone X with IOS 16+.
As a workaround, I have adapted my script in this way:
- Use modal with the picker inside.
- Add a valid/confirm button.
- Add a custom attribute (data-value) on the picker-column and the picker-column-option.
- 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
@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.
confirmed, works.
@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.
still works.
This issue was fixed in v8.6.2! Please update to the latest version to receive the fix.