Skip to content

fix(datetime): switching months in wheel picker now selected nearest neighbor #25559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});