mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 03:00:58 +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
|
* A list of options to be displayed in the picker
|
||||||
*/
|
*/
|
||||||
@Prop() items: PickerColumnItem[] = [];
|
@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.
|
* 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