Skip to content

Commit dd256e1

Browse files
authored
fix(datetime): switching months in wheel picker now selected nearest neighbor (#25559)
1 parent fba4cc0 commit dd256e1

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

core/src/components/picker-column-internal/picker-column-internal.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,70 @@ export class PickerColumnInternal implements ComponentInterface {
3636
* A list of options to be displayed in the picker
3737
*/
3838
@Prop() items: PickerColumnItem[] = [];
39+
@Watch('items')
40+
itemsChange(currentItems: PickerColumnItem[], previousItems: PickerColumnItem[]) {
41+
const { value } = this;
42+
43+
/**
44+
* When the items change, it is possible for the item
45+
* that was selected to no longer exist. In that case, we need
46+
* to automatically select the nearest item. If we do not,
47+
* then the scroll position will be reset to zero and it will
48+
* look like the first item was automatically selected.
49+
*
50+
* If we cannot find a closest item then we do nothing, and
51+
* the browser will reset the scroll position to 0.
52+
*/
53+
const findCurrentItem = currentItems.find((item) => item.value === value);
54+
if (!findCurrentItem) {
55+
/**
56+
* The default behavior is to assume
57+
* that the new set of data is similar to the old
58+
* set of data, just with some items filtered out.
59+
* We walk backwards through the data to find the
60+
* closest enabled picker item and select it.
61+
*
62+
* Developers can also swap the items out for an entirely
63+
* new set of data. In that case, the value we select
64+
* here likely will not make much sense. For this use case,
65+
* developers should update the `value` prop themselves
66+
* when swapping out the data.
67+
*/
68+
const findPreviousItemIndex = previousItems.findIndex((item) => item.value === value);
69+
if (findPreviousItemIndex === -1) {
70+
return;
71+
}
72+
73+
/**
74+
* Step through the current items backwards
75+
* until we find a neighbor we can select.
76+
* We start at the last known location of the
77+
* current selected item in order to
78+
* account for data that has been added. This
79+
* search prioritizes stability in that it
80+
* tries to keep the scroll position as close
81+
* to where it was before the update.
82+
* Before Items: ['a', 'b', 'c'], Selected Value: 'b'
83+
* After Items: ['a', 'dog', 'c']
84+
* Even though 'dog' is a different item than 'b',
85+
* it is the closest item we can select while
86+
* preserving the scroll position.
87+
*/
88+
let nearestItem;
89+
for (let i = findPreviousItemIndex; i >= 0; i--) {
90+
const item = currentItems[i];
91+
if (item !== undefined && item.disabled !== true) {
92+
nearestItem = item;
93+
break;
94+
}
95+
}
96+
97+
if (nearestItem) {
98+
this.setValue(nearestItem.value);
99+
return;
100+
}
101+
}
102+
}
39103

40104
/**
41105
* The selected option in the picker.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { expect } from '@playwright/test';
2+
import { test } from '@utils/test/playwright';
3+
4+
test.describe('picker-column-internal: updating items', () => {
5+
test('should select nearest neighbor when updating items', async ({ page }) => {
6+
await page.setContent(`
7+
<ion-picker-internal>
8+
<ion-picker-column-internal></ion-picker-column-internal>
9+
</ion-picker-internal>
10+
11+
<script>
12+
const column = document.querySelector('ion-picker-column-internal');
13+
column.items = [
14+
{ text: '1', value: 1 },
15+
{ text: '2', value: 2 },
16+
{ text: '3', value: 3 },
17+
{ text: '4', value: 4 },
18+
{ text: '5', value: 5 },
19+
];
20+
column.value = 5;
21+
</script>
22+
`);
23+
24+
const pickerColumn = page.locator('ion-picker-column-internal');
25+
await expect(pickerColumn).toHaveJSProperty('value', 5);
26+
27+
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
28+
el.items = [
29+
{ text: '1', value: 1 },
30+
{ text: '2', value: 2 },
31+
];
32+
});
33+
34+
await page.waitForChanges();
35+
await expect(pickerColumn).toHaveJSProperty('value', 2);
36+
});
37+
test('should select same position item even if item value is different', async ({ page }) => {
38+
await page.setContent(`
39+
<ion-picker-internal>
40+
<ion-picker-column-internal></ion-picker-column-internal>
41+
</ion-picker-internal>
42+
43+
<script>
44+
const column = document.querySelector('ion-picker-column-internal');
45+
column.items = [
46+
{ text: '1', value: 1 },
47+
{ text: '2', value: 2 },
48+
{ text: '3', value: 3 },
49+
{ text: '4', value: 4 },
50+
{ text: '5', value: 5 },
51+
];
52+
column.value = 5;
53+
</script>
54+
`);
55+
56+
const pickerColumn = page.locator('ion-picker-column-internal');
57+
await expect(pickerColumn).toHaveJSProperty('value', 5);
58+
59+
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
60+
el.items = [
61+
{ text: '1', value: 1 },
62+
{ text: '2', value: 2 },
63+
{ text: '3', value: 3 },
64+
{ text: '4', value: 4 },
65+
{ text: '1000', value: 1000 },
66+
];
67+
});
68+
69+
await page.waitForChanges();
70+
await expect(pickerColumn).toHaveJSProperty('value', 1000);
71+
});
72+
test('should not select a disabled item', async ({ page }) => {
73+
await page.setContent(`
74+
<ion-picker-internal>
75+
<ion-picker-column-internal></ion-picker-column-internal>
76+
</ion-picker-internal>
77+
78+
<script>
79+
const column = document.querySelector('ion-picker-column-internal');
80+
column.items = [
81+
{ text: '1', value: 1 },
82+
{ text: '2', value: 2 },
83+
{ text: '3', value: 3 },
84+
{ text: '4', value: 4 },
85+
{ text: '5', value: 5 },
86+
];
87+
column.value = 5;
88+
</script>
89+
`);
90+
91+
const pickerColumn = page.locator('ion-picker-column-internal');
92+
await expect(pickerColumn).toHaveJSProperty('value', 5);
93+
94+
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
95+
el.items = [
96+
{ text: '1', value: 1 },
97+
{ text: '2', value: 2 },
98+
{ text: '3', value: 3, disabled: true },
99+
];
100+
});
101+
102+
await page.waitForChanges();
103+
await expect(pickerColumn).toHaveJSProperty('value', 2);
104+
});
105+
test('should reset to the first item if no good item was found', async ({ page }) => {
106+
await page.setContent(`
107+
<ion-picker-internal>
108+
<ion-picker-column-internal></ion-picker-column-internal>
109+
</ion-picker-internal>
110+
111+
<script>
112+
const column = document.querySelector('ion-picker-column-internal');
113+
column.items = [
114+
{ text: '1', value: 1 },
115+
{ text: '2', value: 2 },
116+
{ text: '3', value: 3 },
117+
{ text: '4', value: 4 },
118+
{ text: '5', value: 5 },
119+
];
120+
column.value = 5;
121+
</script>
122+
`);
123+
124+
const pickerColumn = page.locator('ion-picker-column-internal');
125+
await expect(pickerColumn).toHaveJSProperty('value', 5);
126+
127+
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
128+
el.items = [
129+
{ text: '1', value: 1 },
130+
{ text: '2', value: 2, disabled: true },
131+
{ text: '3', value: 3, disabled: true },
132+
];
133+
});
134+
135+
await page.waitForChanges();
136+
await expect(pickerColumn).toHaveJSProperty('value', 1);
137+
});
138+
test('should still select correct value if data was added', async ({ page }) => {
139+
await page.setContent(`
140+
<ion-picker-internal>
141+
<ion-picker-column-internal></ion-picker-column-internal>
142+
</ion-picker-internal>
143+
144+
<script>
145+
const column = document.querySelector('ion-picker-column-internal');
146+
column.items = [
147+
{ text: '1', value: 1 },
148+
{ text: '2', value: 2 },
149+
{ text: '3', value: 3 },
150+
{ text: '4', value: 4 },
151+
{ text: '5', value: 5 },
152+
];
153+
column.value = 5;
154+
</script>
155+
`);
156+
157+
const pickerColumn = page.locator('ion-picker-column-internal');
158+
await expect(pickerColumn).toHaveJSProperty('value', 5);
159+
160+
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => {
161+
el.items = [
162+
{ text: '1', value: 1 },
163+
{ text: '2', value: 2 },
164+
{ text: '3', value: 3 },
165+
{ text: '4', value: 4 },
166+
{ text: '6', value: 6 },
167+
{ text: '7', value: 7 },
168+
{ text: '5', value: 5 },
169+
];
170+
});
171+
172+
await page.waitForChanges();
173+
await expect(pickerColumn).toHaveJSProperty('value', 5);
174+
});
175+
});

0 commit comments

Comments
 (0)