fix(select): auto-scroll to selected item for all interfaces (#30202)
Issue number: resolves #19296 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> - when the ion-select is with the interface action-sheet or alert is not scrolling to the selected item on open ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - change test page so all select have scroll; - guarantee focusVisibleElement is called on all interfaces; ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> --------- Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
@ -310,19 +310,10 @@ export class Select implements ComponentInterface {
|
||||
}
|
||||
this.isExpanded = true;
|
||||
const overlay = (this.overlay = await this.createOverlay(event));
|
||||
overlay.onDidDismiss().then(() => {
|
||||
this.overlay = undefined;
|
||||
this.isExpanded = false;
|
||||
this.ionDismiss.emit();
|
||||
this.setFocus();
|
||||
});
|
||||
|
||||
await overlay.present();
|
||||
|
||||
// focus selected option for popovers and modals
|
||||
if (this.interface === 'popover' || this.interface === 'modal') {
|
||||
// Add logic to scroll selected item into view before presenting
|
||||
const scrollSelectedIntoView = () => {
|
||||
const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value);
|
||||
|
||||
if (indexOfSelected > -1) {
|
||||
const selectedItem = overlay.querySelector<HTMLElement>(
|
||||
`.select-interface-option:nth-child(${indexOfSelected + 1})`
|
||||
@ -345,6 +336,7 @@ export class Select implements ComponentInterface {
|
||||
| HTMLIonCheckboxElement
|
||||
| null;
|
||||
if (interactiveEl) {
|
||||
selectedItem.scrollIntoView({ block: 'nearest' });
|
||||
// Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
|
||||
// and removing `ion-focused` style
|
||||
interactiveEl.setFocus();
|
||||
@ -372,8 +364,40 @@ export class Select implements ComponentInterface {
|
||||
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// For modals and popovers, we can scroll before they're visible
|
||||
if (this.interface === 'modal') {
|
||||
overlay.addEventListener('ionModalWillPresent', scrollSelectedIntoView, { once: true });
|
||||
} else if (this.interface === 'popover') {
|
||||
overlay.addEventListener('ionPopoverWillPresent', scrollSelectedIntoView, { once: true });
|
||||
} else {
|
||||
/**
|
||||
* For alerts and action sheets, we need to wait a frame after willPresent
|
||||
* because these overlays don't have their content in the DOM immediately
|
||||
* when willPresent fires. By waiting a frame, we ensure the content is
|
||||
* rendered and can be properly scrolled into view.
|
||||
*/
|
||||
const scrollAfterRender = () => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollSelectedIntoView();
|
||||
});
|
||||
};
|
||||
if (this.interface === 'alert') {
|
||||
overlay.addEventListener('ionAlertWillPresent', scrollAfterRender, { once: true });
|
||||
} else if (this.interface === 'action-sheet') {
|
||||
overlay.addEventListener('ionActionSheetWillPresent', scrollAfterRender, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
overlay.onDidDismiss().then(() => {
|
||||
this.overlay = undefined;
|
||||
this.isExpanded = false;
|
||||
this.ionDismiss.emit();
|
||||
this.setFocus();
|
||||
});
|
||||
|
||||
await overlay.present();
|
||||
return overlay;
|
||||
}
|
||||
|
||||
|
||||
@ -61,6 +61,169 @@
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>Single Value - Overflowing Options</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="alert-select-scroll-to-selected" label="Alert" interface="alert" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select
|
||||
id="action-sheet-select-scroll-to-selected"
|
||||
label="Action Sheet"
|
||||
interface="action-sheet"
|
||||
value="watermelon"
|
||||
>
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="popover-select-scroll-to-selected" label="Popover" interface="popover" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="modal-select-scroll-to-selected" label="Modal" interface="modal" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>Multiple Value Select</ion-label>
|
||||
|
||||
@ -8,7 +8,7 @@ import type { E2ELocator } from '@utils/test/playwright';
|
||||
* does not. The overlay rendering is already tested in the respective
|
||||
* test files.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
test.describe(title('select: basic'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/select/test/basic', config);
|
||||
@ -24,6 +24,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(page.locator('ion-alert')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
|
||||
|
||||
await page.click('#alert-select-scroll-to-selected');
|
||||
await ionAlertDidPresent.next();
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
await expect(alert).toHaveScreenshot(screenshot(`select-basic-alert-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: action sheet', () => {
|
||||
@ -36,6 +46,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(page.locator('ion-action-sheet')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
|
||||
|
||||
await page.click('#action-sheet-select-scroll-to-selected');
|
||||
await ionActionSheetDidPresent.next();
|
||||
|
||||
const actionSheet = page.locator('ion-action-sheet');
|
||||
await expect(actionSheet).toHaveScreenshot(screenshot(`select-basic-action-sheet-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: popover', () => {
|
||||
@ -57,6 +77,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(popover).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
|
||||
await page.click('#popover-select-scroll-to-selected');
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
const popover = page.locator('ion-popover');
|
||||
await expect(popover).toHaveScreenshot(screenshot(`select-basic-popover-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: modal', () => {
|
||||
@ -75,6 +105,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(modal).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#modal-select-scroll-to-selected');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modal = page.locator('ion-modal');
|
||||
await expect(modal).toHaveScreenshot(screenshot(`select-basic-modal-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 33 KiB |