Skip to content

Commit

Permalink
Merge branch 'next' into ROU-11604
Browse files Browse the repository at this point in the history
  • Loading branch information
BenOsodrac authored Feb 27, 2025
2 parents c3e330c + 5eab76c commit 5f39cbd
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 5 deletions.
1 change: 1 addition & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
/**
Expand Down
11 changes: 8 additions & 3 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 @@ -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() {
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

0 comments on commit 5f39cbd

Please sign in to comment.