diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 592664e9c0..990c2a99db 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth import { getIonMode } from '../../global/ionic-global'; import { ActionSheetButton, ActionSheetOptions, AlertInput, AlertOptions, CssClassMap, OverlaySelect, PopoverOptions, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, StyleEventDetail } from '../../interface'; -import { findItemLabel, getAriaLabel, renderHiddenInput } from '../../utils/helpers'; +import { findItemLabel, focusElement, getAriaLabel, renderHiddenInput } from '../../utils/helpers'; import { actionSheetController, alertController, popoverController } from '../../utils/overlays'; import { hostContext } from '../../utils/theme'; import { watchForOptions } from '../../utils/watch-options'; @@ -179,11 +179,18 @@ export class Select implements ComponentInterface { this.setFocus(); }); + await overlay.present(); + + // focus selected option for popovers if (this.interface === 'popover') { - await (overlay as HTMLIonPopoverElement).presentFromTrigger(event, true); - } else { - await overlay.present(); + let indexOfSelected = this.childOpts.map(o => o.value).indexOf(this.value); + indexOfSelected = indexOfSelected > -1 ? indexOfSelected : 0; // default to first option if nothing selected + const selectedEl = overlay.querySelector(`.select-interface-option:nth-child(${indexOfSelected + 1})`); + if (selectedEl) { + focusElement(selectedEl); + } } + return overlay; } diff --git a/core/src/components/select/test/basic/e2e.ts b/core/src/components/select/test/basic/e2e.ts index d8f556bd1e..5d0154fd2b 100644 --- a/core/src/components/select/test/basic/e2e.ts +++ b/core/src/components/select/test/basic/e2e.ts @@ -48,12 +48,28 @@ test('select: basic', async () => { select = await page.find('#customPopoverSelect'); await select.click(); - const popover = await page.find('ion-popover'); + let popover = await page.find('ion-popover'); await popover.waitForVisible(); await page.waitForTimeout(250); compares.push(await page.compareScreenshot('should open custom popover select')); + // select has no value, so first option should be focused by default + const popoverOption1 = await popover.find('.select-interface-option:first-child'); + expect(popoverOption1).toHaveClass('ion-focused'); + + let popoverOption2 = await popover.find('.select-interface-option:nth-child(2)'); + await popoverOption2.click(); + await page.waitForTimeout(500); + + await select.click(); + popover = await page.find('ion-popover'); + await popover.waitForVisible(); + await page.waitForTimeout(250); + + popoverOption2 = await popover.find('.select-interface-option:nth-child(2)'); + expect(popoverOption2).toHaveClass('ion-focused'); + await popover.callMethod('dismiss'); // Custom Action Sheet Select diff --git a/core/src/utils/helpers.ts b/core/src/utils/helpers.ts index f00bfb3b40..eaaf1d968d 100644 --- a/core/src/utils/helpers.ts +++ b/core/src/utils/helpers.ts @@ -174,6 +174,25 @@ export const findItemLabel = (componentEl: HTMLElement): HTMLIonLabelElement | n return null; }; +export const focusElement = (el: HTMLElement) => { + el.focus(); + + /** + * When programmatically focusing an element, + * the focus-visible utility will not run because + * it is expecting a keyboard event to have triggered this; + * however, there are times when we need to manually control + * this behavior so we call the `setFocus` method on ion-app + * which will let us explicitly set the elements to focus. + */ + if (el.classList.contains('ion-focusable')) { + const app = el.closest('ion-app'); + if (app) { + app.setFocus([el]); + } + } +}; + /** * This method is used for Ionic's input components that use Shadow DOM. In * order to properly label the inputs to work with screen readers, we need diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 1397b0c1ae..544d533f4c 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -3,7 +3,7 @@ import { getIonMode } from '../global/ionic-global'; import { ActionSheetOptions, AlertOptions, Animation, AnimationBuilder, BackButtonEvent, HTMLIonOverlayElement, IonicConfig, LoadingOptions, ModalOptions, OverlayInterface, PickerOptions, PopoverOptions, ToastOptions } from '../interface'; import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button'; -import { addEventListener, componentOnReady, getElementRoot, removeEventListener } from './helpers'; +import { addEventListener, componentOnReady, focusElement, getElementRoot, removeEventListener } from './helpers'; let lastId = 0; @@ -78,22 +78,7 @@ export const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElemen } if (firstInput) { - firstInput.focus(); - - /** - * When programmatically focusing an element, - * the focus-visible utility will not run because - * it is expecting a keyboard event to have triggered this; - * however, there are times when we need to manually control - * this behavior so we call the `setFocus` method on ion-app - * which will let us explicitly set the elements to focus. - */ - if (firstInput.classList.contains('ion-focusable')) { - const app = overlay.closest('ion-app'); - if (app) { - app.setFocus([firstInput]); - } - } + focusElement(firstInput); } else { // Focus overlay instead of letting focus escape overlay.focus();