diff --git a/core/src/components/picker-column-internal/picker-column-internal.tsx b/core/src/components/picker-column-internal/picker-column-internal.tsx
index 465aff86a4..5a1186bea4 100644
--- a/core/src/components/picker-column-internal/picker-column-internal.tsx
+++ b/core/src/components/picker-column-internal/picker-column-internal.tsx
@@ -36,6 +36,70 @@ export class PickerColumnInternal implements ComponentInterface {
* A list of options to be displayed in the picker
*/
@Prop() items: PickerColumnItem[] = [];
+ @Watch('items')
+ itemsChange(currentItems: PickerColumnItem[], previousItems: PickerColumnItem[]) {
+ const { value } = this;
+
+ /**
+ * When the items change, it is possible for the item
+ * that was selected to no longer exist. In that case, we need
+ * to automatically select the nearest item. If we do not,
+ * then the scroll position will be reset to zero and it will
+ * look like the first item was automatically selected.
+ *
+ * If we cannot find a closest item then we do nothing, and
+ * the browser will reset the scroll position to 0.
+ */
+ const findCurrentItem = currentItems.find((item) => item.value === value);
+ if (!findCurrentItem) {
+ /**
+ * The default behavior is to assume
+ * that the new set of data is similar to the old
+ * set of data, just with some items filtered out.
+ * We walk backwards through the data to find the
+ * closest enabled picker item and select it.
+ *
+ * Developers can also swap the items out for an entirely
+ * new set of data. In that case, the value we select
+ * here likely will not make much sense. For this use case,
+ * developers should update the `value` prop themselves
+ * when swapping out the data.
+ */
+ const findPreviousItemIndex = previousItems.findIndex((item) => item.value === value);
+ if (findPreviousItemIndex === -1) {
+ return;
+ }
+
+ /**
+ * Step through the current items backwards
+ * until we find a neighbor we can select.
+ * We start at the last known location of the
+ * current selected item in order to
+ * account for data that has been added. This
+ * search prioritizes stability in that it
+ * tries to keep the scroll position as close
+ * to where it was before the update.
+ * Before Items: ['a', 'b', 'c'], Selected Value: 'b'
+ * After Items: ['a', 'dog', 'c']
+ * Even though 'dog' is a different item than 'b',
+ * it is the closest item we can select while
+ * preserving the scroll position.
+ */
+ let nearestItem;
+ for (let i = findPreviousItemIndex; i >= 0; i--) {
+ const item = currentItems[i];
+ if (item !== undefined && item.disabled !== true) {
+ nearestItem = item;
+ break;
+ }
+ }
+
+ if (nearestItem) {
+ this.setValue(nearestItem.value);
+ return;
+ }
+ }
+ }
/**
* The selected option in the picker.
diff --git a/core/src/components/picker-column-internal/test/update-items/picker-column-internal.e2e.ts b/core/src/components/picker-column-internal/test/update-items/picker-column-internal.e2e.ts
new file mode 100644
index 0000000000..4ca337ab38
--- /dev/null
+++ b/core/src/components/picker-column-internal/test/update-items/picker-column-internal.e2e.ts
@@ -0,0 +1,175 @@
+import { expect } from '@playwright/test';
+import { test } from '@utils/test/playwright';
+
+test.describe('picker-column-internal: updating items', () => {
+ test('should select nearest neighbor when updating items', async ({ page }) => {
+ await page.setContent(`
+
+
+
+
+
+ `);
+
+ const pickerColumn = page.locator('ion-picker-column-internal');
+ await expect(pickerColumn).toHaveJSProperty('value', 5);
+
+ await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
+ el.items = [
+ { text: '1', value: 1 },
+ { text: '2', value: 2 },
+ ];
+ });
+
+ await page.waitForChanges();
+ await expect(pickerColumn).toHaveJSProperty('value', 2);
+ });
+ test('should select same position item even if item value is different', async ({ page }) => {
+ await page.setContent(`
+
+
+
+
+
+ `);
+
+ const pickerColumn = page.locator('ion-picker-column-internal');
+ await expect(pickerColumn).toHaveJSProperty('value', 5);
+
+ await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
+ el.items = [
+ { text: '1', value: 1 },
+ { text: '2', value: 2 },
+ { text: '3', value: 3 },
+ { text: '4', value: 4 },
+ { text: '1000', value: 1000 },
+ ];
+ });
+
+ await page.waitForChanges();
+ await expect(pickerColumn).toHaveJSProperty('value', 1000);
+ });
+ test('should not select a disabled item', async ({ page }) => {
+ await page.setContent(`
+
+
+
+
+
+ `);
+
+ const pickerColumn = page.locator('ion-picker-column-internal');
+ await expect(pickerColumn).toHaveJSProperty('value', 5);
+
+ await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
+ el.items = [
+ { text: '1', value: 1 },
+ { text: '2', value: 2 },
+ { text: '3', value: 3, disabled: true },
+ ];
+ });
+
+ await page.waitForChanges();
+ await expect(pickerColumn).toHaveJSProperty('value', 2);
+ });
+ test('should reset to the first item if no good item was found', async ({ page }) => {
+ await page.setContent(`
+
+
+
+
+
+ `);
+
+ const pickerColumn = page.locator('ion-picker-column-internal');
+ await expect(pickerColumn).toHaveJSProperty('value', 5);
+
+ await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
+ el.items = [
+ { text: '1', value: 1 },
+ { text: '2', value: 2, disabled: true },
+ { text: '3', value: 3, disabled: true },
+ ];
+ });
+
+ await page.waitForChanges();
+ await expect(pickerColumn).toHaveJSProperty('value', 1);
+ });
+ test('should still select correct value if data was added', async ({ page }) => {
+ await page.setContent(`
+
+
+
+
+
+ `);
+
+ const pickerColumn = page.locator('ion-picker-column-internal');
+ await expect(pickerColumn).toHaveJSProperty('value', 5);
+
+ await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
+ el.items = [
+ { text: '1', value: 1 },
+ { text: '2', value: 2 },
+ { text: '3', value: 3 },
+ { text: '4', value: 4 },
+ { text: '6', value: 6 },
+ { text: '7', value: 7 },
+ { text: '5', value: 5 },
+ ];
+ });
+
+ await page.waitForChanges();
+ await expect(pickerColumn).toHaveJSProperty('value', 5);
+ });
+});