fix(focusVisibleElement): set focus on custom appRootSelector (#30218)

This commit is contained in:
Maria Hutt
2025-02-27 10:47:26 -08:00
committed by GitHub
parent fc552ad89f
commit 5eab76c0f3
4 changed files with 64 additions and 5 deletions

View File

@ -334,6 +334,7 @@ export namespace Components {
"mode"?: "ios" | "md";
/**
* Used to set focus on an element that uses `ion-focusable`. Do not use this if focusing the element as a result of a keyboard event as the focus utility should handle this for us. This method should be used when we want to programmatically focus an element as a result of another user action. (Ex: We focus the first element inside of a popover when the user presents it, but the popover is not always presented as a result of keyboard action.)
* @param elements - The elements to set focus on.
*/
"setFocus": (elements: HTMLElement[]) => Promise<void>;
/**

View File

@ -1,6 +1,6 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Method, h } from '@stencil/core';
import { getOrInitFocusVisibleUtility } from '@utils/focus-visible';
import { focusElements } from '@utils/focus-visible';
import { config } from '../../global/config';
import { getIonTheme } from '../../global/ionic-global';
@ -24,11 +24,16 @@ export class App implements ComponentInterface {
* a result of another user action. (Ex: We focus the first element
* inside of a popover when the user presents it, but the popover is not always
* presented as a result of keyboard action.)
*
* @param elements - The elements to set focus on.
*/
@Method()
async setFocus(elements: HTMLElement[]) {
const focusVisible = getOrInitFocusVisibleUtility();
focusVisible.setFocus(elements);
/**
* The focus-visible utility is used to set focus on an
* element that uses `ion-focusable`.
*/
focusElements(elements);
}
render() {

View File

@ -30,6 +30,22 @@ export const getOrInitFocusVisibleUtility = () => {
return focusVisibleUtility;
};
/**
* Used to set focus on an element that uses `ion-focusable`.
* Do not use this if focusing the element as a result of a keyboard
* event as the focus utility should handle this for us. This method
* should be used when we want to programmatically focus an element as
* a result of another user action. (Ex: We focus the first element
* inside of a popover when the user presents it, but the popover is not always
* presented as a result of keyboard action.)
*
* @param elements - The elements to set focus on.
*/
export const focusElements = (elements: Element[]) => {
const focusVisible = getOrInitFocusVisibleUtility();
focusVisible.setFocus(elements);
};
export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => {
let currentFocus: Element[] = [];
let keyboardMode = true;

View File

@ -1,4 +1,5 @@
import type { EventEmitter } from '@stencil/core';
import { focusElements } from '@utils/focus-visible';
import type { Side } from '../components/menu/menu-interface';
import { config } from '../global/config';
@ -255,6 +256,17 @@ export const hasShadowDom = (el: HTMLElement) => {
return !!el.shadowRoot && !!(el as any).attachShadow;
};
/**
* Focuses a given element while ensuring proper focus management
* within the Ionic framework. If the element is marked as `ion-focusable`,
* this function will delegate focus handling to `ion-app` or manually
* apply focus when a custom app root is used.
*
* This function helps maintain accessibility and expected focus behavior
* in both standard and custom root environments.
*
* @param el - The element to focus.
*/
export const focusVisibleElement = (el: HTMLElement) => {
el.focus();
@ -267,10 +279,35 @@ export const focusVisibleElement = (el: HTMLElement) => {
* which will let us explicitly set the elements to focus.
*/
if (el.classList.contains('ion-focusable')) {
const appRootSelector = config.get('appRootSelector', 'ion-app');
const appRootSelector: string = config.get('appRootSelector', 'ion-app');
const app = el.closest(appRootSelector) as HTMLIonAppElement | null;
if (app) {
app.setFocus([el]);
if (appRootSelector === 'ion-app') {
/**
* If the app root is the default, then it will be
* in charge of setting focus. This is because the
* focus-visible utility is attached to the app root
* and will handle setting focus on the correct element.
*/
app.setFocus([el]);
} else {
/**
* When using a custom app root selector, the focus-visible
* utility is not available to manage focus automatically.
* If we set focus immediately, the element may not be fully
* rendered or interactive, especially if it was just added
* to the DOM. Using requestAnimationFrame ensures that focus
* is applied on the next frame, allowing the DOM to settle
* before changing focus.
*/
requestAnimationFrame(() => {
/**
* The focus-visible utility is used to set focus on an
* element that uses `ion-focusable`.
*/
focusElements([el]);
});
}
}
}
};