Skip to content

Commit 5eab76c

Browse files
authored
fix(focusVisibleElement): set focus on custom appRootSelector (#30218)
1 parent fc552ad commit 5eab76c

File tree

4 files changed

+64
-5
lines changed

4 files changed

+64
-5
lines changed

core/src/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ export namespace Components {
334334
"mode"?: "ios" | "md";
335335
/**
336336
* 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.)
337+
* @param elements - The elements to set focus on.
337338
*/
338339
"setFocus": (elements: HTMLElement[]) => Promise<void>;
339340
/**

core/src/components/app/app.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ComponentInterface } from '@stencil/core';
22
import { Component, Element, Host, Method, h } from '@stencil/core';
3-
import { getOrInitFocusVisibleUtility } from '@utils/focus-visible';
3+
import { focusElements } from '@utils/focus-visible';
44

55
import { config } from '../../global/config';
66
import { getIonTheme } from '../../global/ionic-global';
@@ -24,11 +24,16 @@ export class App implements ComponentInterface {
2424
* a result of another user action. (Ex: We focus the first element
2525
* inside of a popover when the user presents it, but the popover is not always
2626
* presented as a result of keyboard action.)
27+
*
28+
* @param elements - The elements to set focus on.
2729
*/
2830
@Method()
2931
async setFocus(elements: HTMLElement[]) {
30-
const focusVisible = getOrInitFocusVisibleUtility();
31-
focusVisible.setFocus(elements);
32+
/**
33+
* The focus-visible utility is used to set focus on an
34+
* element that uses `ion-focusable`.
35+
*/
36+
focusElements(elements);
3237
}
3338

3439
render() {

core/src/utils/focus-visible.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ export const getOrInitFocusVisibleUtility = () => {
3030
return focusVisibleUtility;
3131
};
3232

33+
/**
34+
* Used to set focus on an element that uses `ion-focusable`.
35+
* Do not use this if focusing the element as a result of a keyboard
36+
* event as the focus utility should handle this for us. This method
37+
* should be used when we want to programmatically focus an element as
38+
* a result of another user action. (Ex: We focus the first element
39+
* inside of a popover when the user presents it, but the popover is not always
40+
* presented as a result of keyboard action.)
41+
*
42+
* @param elements - The elements to set focus on.
43+
*/
44+
export const focusElements = (elements: Element[]) => {
45+
const focusVisible = getOrInitFocusVisibleUtility();
46+
focusVisible.setFocus(elements);
47+
};
48+
3349
export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => {
3450
let currentFocus: Element[] = [];
3551
let keyboardMode = true;

core/src/utils/helpers.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { EventEmitter } from '@stencil/core';
2+
import { focusElements } from '@utils/focus-visible';
23

34
import type { Side } from '../components/menu/menu-interface';
45
import { config } from '../global/config';
@@ -255,6 +256,17 @@ export const hasShadowDom = (el: HTMLElement) => {
255256
return !!el.shadowRoot && !!(el as any).attachShadow;
256257
};
257258

259+
/**
260+
* Focuses a given element while ensuring proper focus management
261+
* within the Ionic framework. If the element is marked as `ion-focusable`,
262+
* this function will delegate focus handling to `ion-app` or manually
263+
* apply focus when a custom app root is used.
264+
*
265+
* This function helps maintain accessibility and expected focus behavior
266+
* in both standard and custom root environments.
267+
*
268+
* @param el - The element to focus.
269+
*/
258270
export const focusVisibleElement = (el: HTMLElement) => {
259271
el.focus();
260272

@@ -267,10 +279,35 @@ export const focusVisibleElement = (el: HTMLElement) => {
267279
* which will let us explicitly set the elements to focus.
268280
*/
269281
if (el.classList.contains('ion-focusable')) {
270-
const appRootSelector = config.get('appRootSelector', 'ion-app');
282+
const appRootSelector: string = config.get('appRootSelector', 'ion-app');
271283
const app = el.closest(appRootSelector) as HTMLIonAppElement | null;
272284
if (app) {
273-
app.setFocus([el]);
285+
if (appRootSelector === 'ion-app') {
286+
/**
287+
* If the app root is the default, then it will be
288+
* in charge of setting focus. This is because the
289+
* focus-visible utility is attached to the app root
290+
* and will handle setting focus on the correct element.
291+
*/
292+
app.setFocus([el]);
293+
} else {
294+
/**
295+
* When using a custom app root selector, the focus-visible
296+
* utility is not available to manage focus automatically.
297+
* If we set focus immediately, the element may not be fully
298+
* rendered or interactive, especially if it was just added
299+
* to the DOM. Using requestAnimationFrame ensures that focus
300+
* is applied on the next frame, allowing the DOM to settle
301+
* before changing focus.
302+
*/
303+
requestAnimationFrame(() => {
304+
/**
305+
* The focus-visible utility is used to set focus on an
306+
* element that uses `ion-focusable`.
307+
*/
308+
focusElements([el]);
309+
});
310+
}
274311
}
275312
}
276313
};

0 commit comments

Comments
 (0)