Skip to content

Commit 8eaeb22

Browse files
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>
1 parent cd5c27a commit 8eaeb22

File tree

27 files changed

+239
-12
lines changed

27 files changed

+239
-12
lines changed

‎core/src/components/select/select.tsx

+35-11
Original file line numberDiff line numberDiff line change
@@ -310,19 +310,10 @@ export class Select implements ComponentInterface {
310310
}
311311
this.isExpanded = true;
312312
const overlay = (this.overlay = await this.createOverlay(event));
313-
overlay.onDidDismiss().then(() => {
314-
this.overlay = undefined;
315-
this.isExpanded = false;
316-
this.ionDismiss.emit();
317-
this.setFocus();
318-
});
319313

320-
await overlay.present();
321-
322-
// focus selected option for popovers and modals
323-
if (this.interface === 'popover' || this.interface === 'modal') {
314+
// Add logic to scroll selected item into view before presenting
315+
const scrollSelectedIntoView = () => {
324316
const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value);
325-
326317
if (indexOfSelected > -1) {
327318
const selectedItem = overlay.querySelector<HTMLElement>(
328319
`.select-interface-option:nth-child(${indexOfSelected + 1})`
@@ -345,6 +336,7 @@ export class Select implements ComponentInterface {
345336
| HTMLIonCheckboxElement
346337
| null;
347338
if (interactiveEl) {
339+
selectedItem.scrollIntoView({ block: 'nearest' });
348340
// Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
349341
// and removing `ion-focused` style
350342
interactiveEl.setFocus();
@@ -372,8 +364,40 @@ export class Select implements ComponentInterface {
372364
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
373365
}
374366
}
367+
};
368+
369+
// For modals and popovers, we can scroll before they're visible
370+
if (this.interface === 'modal') {
371+
overlay.addEventListener('ionModalWillPresent', scrollSelectedIntoView, { once: true });
372+
} else if (this.interface === 'popover') {
373+
overlay.addEventListener('ionPopoverWillPresent', scrollSelectedIntoView, { once: true });
374+
} else {
375+
/**
376+
* For alerts and action sheets, we need to wait a frame after willPresent
377+
* because these overlays don't have their content in the DOM immediately
378+
* when willPresent fires. By waiting a frame, we ensure the content is
379+
* rendered and can be properly scrolled into view.
380+
*/
381+
const scrollAfterRender = () => {
382+
requestAnimationFrame(() => {
383+
scrollSelectedIntoView();
384+
});
385+
};
386+
if (this.interface === 'alert') {
387+
overlay.addEventListener('ionAlertWillPresent', scrollAfterRender, { once: true });
388+
} else if (this.interface === 'action-sheet') {
389+
overlay.addEventListener('ionActionSheetWillPresent', scrollAfterRender, { once: true });
390+
}
375391
}
376392

393+
overlay.onDidDismiss().then(() => {
394+
this.overlay = undefined;
395+
this.isExpanded = false;
396+
this.ionDismiss.emit();
397+
this.setFocus();
398+
});
399+
400+
await overlay.present();
377401
return overlay;
378402
}
379403

‎core/src/components/select/test/basic/index.html

+163
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,169 @@
6161
</ion-item>
6262
</ion-list>
6363

64+
<ion-list>
65+
<ion-list-header>
66+
<ion-label>Single Value - Overflowing Options</ion-label>
67+
</ion-list-header>
68+
69+
<ion-item>
70+
<ion-select id="alert-select-scroll-to-selected" label="Alert" interface="alert" value="watermelon">
71+
<ion-select-option value="apple">Apple</ion-select-option>
72+
<ion-select-option value="apricot">Apricot</ion-select-option>
73+
<ion-select-option value="avocado">Avocado</ion-select-option>
74+
<ion-select-option value="banana">Banana</ion-select-option>
75+
<ion-select-option value="blackberry">Blackberry</ion-select-option>
76+
<ion-select-option value="blueberry">Blueberry</ion-select-option>
77+
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
78+
<ion-select-option value="cherry">Cherry</ion-select-option>
79+
<ion-select-option value="coconut">Coconut</ion-select-option>
80+
<ion-select-option value="cranberry">Cranberry</ion-select-option>
81+
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
82+
<ion-select-option value="fig">Fig</ion-select-option>
83+
<ion-select-option value="grape">Grape</ion-select-option>
84+
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
85+
<ion-select-option value="guava">Guava</ion-select-option>
86+
<ion-select-option value="kiwi">Kiwi</ion-select-option>
87+
<ion-select-option value="lemon">Lemon</ion-select-option>
88+
<ion-select-option value="lime">Lime</ion-select-option>
89+
<ion-select-option value="lychee">Lychee</ion-select-option>
90+
<ion-select-option value="mango">Mango</ion-select-option>
91+
<ion-select-option value="nectarine">Nectarine</ion-select-option>
92+
<ion-select-option value="orange">Orange</ion-select-option>
93+
<ion-select-option value="papaya">Papaya</ion-select-option>
94+
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
95+
<ion-select-option value="peach">Peach</ion-select-option>
96+
<ion-select-option value="pear">Pear</ion-select-option>
97+
<ion-select-option value="pineapple">Pineapple</ion-select-option>
98+
<ion-select-option value="plum">Plum</ion-select-option>
99+
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
100+
<ion-select-option value="raspberry">Raspberry</ion-select-option>
101+
<ion-select-option value="strawberry">Strawberry</ion-select-option>
102+
<ion-select-option value="tangerine">Tangerine</ion-select-option>
103+
<ion-select-option value="watermelon">Watermelon</ion-select-option>
104+
</ion-select>
105+
</ion-item>
106+
107+
<ion-item>
108+
<ion-select
109+
id="action-sheet-select-scroll-to-selected"
110+
label="Action Sheet"
111+
interface="action-sheet"
112+
value="watermelon"
113+
>
114+
<ion-select-option value="apple">Apple</ion-select-option>
115+
<ion-select-option value="apricot">Apricot</ion-select-option>
116+
<ion-select-option value="avocado">Avocado</ion-select-option>
117+
<ion-select-option value="banana">Banana</ion-select-option>
118+
<ion-select-option value="blackberry">Blackberry</ion-select-option>
119+
<ion-select-option value="blueberry">Blueberry</ion-select-option>
120+
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
121+
<ion-select-option value="cherry">Cherry</ion-select-option>
122+
<ion-select-option value="coconut">Coconut</ion-select-option>
123+
<ion-select-option value="cranberry">Cranberry</ion-select-option>
124+
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
125+
<ion-select-option value="fig">Fig</ion-select-option>
126+
<ion-select-option value="grape">Grape</ion-select-option>
127+
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
128+
<ion-select-option value="guava">Guava</ion-select-option>
129+
<ion-select-option value="kiwi">Kiwi</ion-select-option>
130+
<ion-select-option value="lemon">Lemon</ion-select-option>
131+
<ion-select-option value="lime">Lime</ion-select-option>
132+
<ion-select-option value="lychee">Lychee</ion-select-option>
133+
<ion-select-option value="mango">Mango</ion-select-option>
134+
<ion-select-option value="nectarine">Nectarine</ion-select-option>
135+
<ion-select-option value="orange">Orange</ion-select-option>
136+
<ion-select-option value="papaya">Papaya</ion-select-option>
137+
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
138+
<ion-select-option value="peach">Peach</ion-select-option>
139+
<ion-select-option value="pear">Pear</ion-select-option>
140+
<ion-select-option value="pineapple">Pineapple</ion-select-option>
141+
<ion-select-option value="plum">Plum</ion-select-option>
142+
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
143+
<ion-select-option value="raspberry">Raspberry</ion-select-option>
144+
<ion-select-option value="strawberry">Strawberry</ion-select-option>
145+
<ion-select-option value="tangerine">Tangerine</ion-select-option>
146+
<ion-select-option value="watermelon">Watermelon</ion-select-option>
147+
</ion-select>
148+
</ion-item>
149+
150+
<ion-item>
151+
<ion-select id="popover-select-scroll-to-selected" label="Popover" interface="popover" value="watermelon">
152+
<ion-select-option value="apple">Apple</ion-select-option>
153+
<ion-select-option value="apricot">Apricot</ion-select-option>
154+
<ion-select-option value="avocado">Avocado</ion-select-option>
155+
<ion-select-option value="banana">Banana</ion-select-option>
156+
<ion-select-option value="blackberry">Blackberry</ion-select-option>
157+
<ion-select-option value="blueberry">Blueberry</ion-select-option>
158+
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
159+
<ion-select-option value="cherry">Cherry</ion-select-option>
160+
<ion-select-option value="coconut">Coconut</ion-select-option>
161+
<ion-select-option value="cranberry">Cranberry</ion-select-option>
162+
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
163+
<ion-select-option value="fig">Fig</ion-select-option>
164+
<ion-select-option value="grape">Grape</ion-select-option>
165+
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
166+
<ion-select-option value="guava">Guava</ion-select-option>
167+
<ion-select-option value="kiwi">Kiwi</ion-select-option>
168+
<ion-select-option value="lemon">Lemon</ion-select-option>
169+
<ion-select-option value="lime">Lime</ion-select-option>
170+
<ion-select-option value="lychee">Lychee</ion-select-option>
171+
<ion-select-option value="mango">Mango</ion-select-option>
172+
<ion-select-option value="nectarine">Nectarine</ion-select-option>
173+
<ion-select-option value="orange">Orange</ion-select-option>
174+
<ion-select-option value="papaya">Papaya</ion-select-option>
175+
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
176+
<ion-select-option value="peach">Peach</ion-select-option>
177+
<ion-select-option value="pear">Pear</ion-select-option>
178+
<ion-select-option value="pineapple">Pineapple</ion-select-option>
179+
<ion-select-option value="plum">Plum</ion-select-option>
180+
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
181+
<ion-select-option value="raspberry">Raspberry</ion-select-option>
182+
<ion-select-option value="strawberry">Strawberry</ion-select-option>
183+
<ion-select-option value="tangerine">Tangerine</ion-select-option>
184+
<ion-select-option value="watermelon">Watermelon</ion-select-option>
185+
</ion-select>
186+
</ion-item>
187+
188+
<ion-item>
189+
<ion-select id="modal-select-scroll-to-selected" label="Modal" interface="modal" value="watermelon">
190+
<ion-select-option value="apple">Apple</ion-select-option>
191+
<ion-select-option value="apricot">Apricot</ion-select-option>
192+
<ion-select-option value="avocado">Avocado</ion-select-option>
193+
<ion-select-option value="banana">Banana</ion-select-option>
194+
<ion-select-option value="blackberry">Blackberry</ion-select-option>
195+
<ion-select-option value="blueberry">Blueberry</ion-select-option>
196+
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
197+
<ion-select-option value="cherry">Cherry</ion-select-option>
198+
<ion-select-option value="coconut">Coconut</ion-select-option>
199+
<ion-select-option value="cranberry">Cranberry</ion-select-option>
200+
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
201+
<ion-select-option value="fig">Fig</ion-select-option>
202+
<ion-select-option value="grape">Grape</ion-select-option>
203+
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
204+
<ion-select-option value="guava">Guava</ion-select-option>
205+
<ion-select-option value="kiwi">Kiwi</ion-select-option>
206+
<ion-select-option value="lemon">Lemon</ion-select-option>
207+
<ion-select-option value="lime">Lime</ion-select-option>
208+
<ion-select-option value="lychee">Lychee</ion-select-option>
209+
<ion-select-option value="mango">Mango</ion-select-option>
210+
<ion-select-option value="nectarine">Nectarine</ion-select-option>
211+
<ion-select-option value="orange">Orange</ion-select-option>
212+
<ion-select-option value="papaya">Papaya</ion-select-option>
213+
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
214+
<ion-select-option value="peach">Peach</ion-select-option>
215+
<ion-select-option value="pear">Pear</ion-select-option>
216+
<ion-select-option value="pineapple">Pineapple</ion-select-option>
217+
<ion-select-option value="plum">Plum</ion-select-option>
218+
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
219+
<ion-select-option value="raspberry">Raspberry</ion-select-option>
220+
<ion-select-option value="strawberry">Strawberry</ion-select-option>
221+
<ion-select-option value="tangerine">Tangerine</ion-select-option>
222+
<ion-select-option value="watermelon">Watermelon</ion-select-option>
223+
</ion-select>
224+
</ion-item>
225+
</ion-list>
226+
64227
<ion-list>
65228
<ion-list-header>
66229
<ion-label>Multiple Value Select</ion-label>

‎core/src/components/select/test/basic/select.e2e.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { E2ELocator } from '@utils/test/playwright';
88
* does not. The overlay rendering is already tested in the respective
99
* test files.
1010
*/
11-
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
11+
configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
1212
test.describe(title('select: basic'), () => {
1313
test.beforeEach(async ({ page }) => {
1414
await page.goto('/src/components/select/test/basic', config);
@@ -24,6 +24,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
2424

2525
await expect(page.locator('ion-alert')).toBeVisible();
2626
});
27+
28+
test('it should scroll to selected option when opened', async ({ page }) => {
29+
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
30+
31+
await page.click('#alert-select-scroll-to-selected');
32+
await ionAlertDidPresent.next();
33+
34+
const alert = page.locator('ion-alert');
35+
await expect(alert).toHaveScreenshot(screenshot(`select-basic-alert-scroll-to-selected`));
36+
});
2737
});
2838

2939
test.describe('select: action sheet', () => {
@@ -36,6 +46,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
3646

3747
await expect(page.locator('ion-action-sheet')).toBeVisible();
3848
});
49+
50+
test('it should scroll to selected option when opened', async ({ page }) => {
51+
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
52+
53+
await page.click('#action-sheet-select-scroll-to-selected');
54+
await ionActionSheetDidPresent.next();
55+
56+
const actionSheet = page.locator('ion-action-sheet');
57+
await expect(actionSheet).toHaveScreenshot(screenshot(`select-basic-action-sheet-scroll-to-selected`));
58+
});
3959
});
4060

4161
test.describe('select: popover', () => {
@@ -57,6 +77,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
5777

5878
await expect(popover).toBeVisible();
5979
});
80+
81+
test('it should scroll to selected option when opened', async ({ page }) => {
82+
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
83+
84+
await page.click('#popover-select-scroll-to-selected');
85+
await ionPopoverDidPresent.next();
86+
87+
const popover = page.locator('ion-popover');
88+
await expect(popover).toHaveScreenshot(screenshot(`select-basic-popover-scroll-to-selected`));
89+
});
6090
});
6191

6292
test.describe('select: modal', () => {
@@ -75,6 +105,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
75105

76106
await expect(modal).toBeVisible();
77107
});
108+
109+
test('it should scroll to selected option when opened', async ({ page }) => {
110+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
111+
112+
await page.click('#modal-select-scroll-to-selected');
113+
await ionModalDidPresent.next();
114+
115+
const modal = page.locator('ion-modal');
116+
await expect(modal).toHaveScreenshot(screenshot(`select-basic-modal-scroll-to-selected`));
117+
});
78118
});
79119
});
80120
});

0 commit comments

Comments
 (0)