mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
fix(datetime): switching months in wheel picker now selected nearest neighbor (#25559)
This commit is contained in:
@ -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.
|
||||
|
@ -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(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '1', value: 1 },
|
||||
{ text: '2', value: 2 },
|
||||
{ text: '3', value: 3 },
|
||||
{ text: '4', value: 4 },
|
||||
{ text: '5', value: 5 },
|
||||
];
|
||||
column.value = 5;
|
||||
</script>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '1', value: 1 },
|
||||
{ text: '2', value: 2 },
|
||||
{ text: '3', value: 3 },
|
||||
{ text: '4', value: 4 },
|
||||
{ text: '5', value: 5 },
|
||||
];
|
||||
column.value = 5;
|
||||
</script>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '1', value: 1 },
|
||||
{ text: '2', value: 2 },
|
||||
{ text: '3', value: 3 },
|
||||
{ text: '4', value: 4 },
|
||||
{ text: '5', value: 5 },
|
||||
];
|
||||
column.value = 5;
|
||||
</script>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '1', value: 1 },
|
||||
{ text: '2', value: 2 },
|
||||
{ text: '3', value: 3 },
|
||||
{ text: '4', value: 4 },
|
||||
{ text: '5', value: 5 },
|
||||
];
|
||||
column.value = 5;
|
||||
</script>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '1', value: 1 },
|
||||
{ text: '2', value: 2 },
|
||||
{ text: '3', value: 3 },
|
||||
{ text: '4', value: 4 },
|
||||
{ text: '5', value: 5 },
|
||||
];
|
||||
column.value = 5;
|
||||
</script>
|
||||
`);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user