Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ 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.)
* Sets focus on elements that use `ion-focusable`.
* @param elements - The elements to set focus on.
*/
"setFocus": (elements: HTMLElement[]) => Promise<void>;
/**
Expand Down
19 changes: 9 additions & 10 deletions core/src/components/app/app.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,18 +17,17 @@ export class App implements ComponentInterface {
@Element() el!: HTMLElement;

/**
* 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.)
* Sets focus on elements that use `ion-focusable`.
*
* @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() {
Expand Down
16 changes: 16 additions & 0 deletions core/src/utils/focus-visible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
41 changes: 39 additions & 2 deletions core/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();

Expand All @@ -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]);
});
}
}
}
};
Expand Down
Loading