Skip to content

Commit 5d3547a

Browse files
fix(select): scroll to the selected option before the overlay is visible (#30214)
1 parent b8f428f commit 5d3547a

File tree

27 files changed

+246
-165
lines changed

27 files changed

+246
-165
lines changed

core/src/components/select/select.tsx

Lines changed: 76 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -310,66 +310,94 @@ 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-
});
319-
320-
await overlay.present();
321313

322-
const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value);
323-
if (indexOfSelected > -1) {
324-
const selectedItem = overlay.querySelector<HTMLElement>(
325-
`.select-interface-option:nth-child(${indexOfSelected + 1})`
326-
);
314+
// Add logic to scroll selected item into view before presenting
315+
const scrollSelectedIntoView = () => {
316+
const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value);
317+
if (indexOfSelected > -1) {
318+
const selectedItem = overlay.querySelector<HTMLElement>(
319+
`.select-interface-option:nth-child(${indexOfSelected + 1})`
320+
);
321+
322+
if (selectedItem) {
323+
/**
324+
* Browsers such as Firefox do not
325+
* correctly delegate focus when manually
326+
* focusing an element with delegatesFocus.
327+
* We work around this by manually focusing
328+
* the interactive element.
329+
* ion-radio and ion-checkbox are the only
330+
* elements that ion-select-popover uses, so
331+
* we only need to worry about those two components
332+
* when focusing.
333+
*/
334+
const interactiveEl = selectedItem.querySelector<HTMLElement>('ion-radio, ion-checkbox') as
335+
| HTMLIonRadioElement
336+
| HTMLIonCheckboxElement
337+
| null;
338+
if (interactiveEl) {
339+
selectedItem.scrollIntoView({ block: 'nearest' });
340+
// Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
341+
// and removing `ion-focused` style
342+
interactiveEl.setFocus();
343+
}
327344

328-
if (selectedItem) {
345+
focusVisibleElement(selectedItem);
346+
}
347+
} else {
329348
/**
330-
* Browsers such as Firefox do not
331-
* correctly delegate focus when manually
332-
* focusing an element with delegatesFocus.
333-
* We work around this by manually focusing
334-
* the interactive element.
335-
* ion-radio and ion-checkbox are the only
336-
* elements that ion-select-popover uses, so
337-
* we only need to worry about those two components
338-
* when focusing.
349+
* If no value is set then focus the first enabled option.
339350
*/
340-
const interactiveEl = selectedItem.querySelector<HTMLElement>('ion-radio, ion-checkbox') as
341-
| HTMLIonRadioElement
342-
| HTMLIonCheckboxElement
343-
| null;
344-
if (interactiveEl) {
345-
// Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
346-
// and removing `ion-focused` style
347-
interactiveEl.setFocus();
351+
const firstEnabledOption = overlay.querySelector<HTMLElement>(
352+
'ion-radio:not(.radio-disabled), ion-checkbox:not(.checkbox-disabled)'
353+
) as HTMLIonRadioElement | HTMLIonCheckboxElement | null;
354+
355+
if (firstEnabledOption) {
356+
/**
357+
* Focus the option for the same reason as we do above.
358+
*
359+
* Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
360+
* and removing `ion-focused` style
361+
*/
362+
firstEnabledOption.setFocus();
363+
364+
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
348365
}
349-
350-
focusVisibleElement(selectedItem);
351366
}
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 });
352374
} else {
353375
/**
354-
* If no value is set then focus the first enabled option.
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.
355380
*/
356-
const firstEnabledOption = overlay.querySelector<HTMLElement>(
357-
'ion-radio:not(.radio-disabled), ion-checkbox:not(.checkbox-disabled)'
358-
) as HTMLIonRadioElement | HTMLIonCheckboxElement | null;
359-
360-
if (firstEnabledOption) {
361-
/**
362-
* Focus the option for the same reason as we do above.
363-
*
364-
* Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
365-
* and removing `ion-focused` style
366-
*/
367-
firstEnabledOption.setFocus();
368-
369-
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
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 });
370390
}
371391
}
372392

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();
373401
return overlay;
374402
}
375403

0 commit comments

Comments
 (0)