Skip to content

Commit a2a584f

Browse files
committed
fix(utils): fix focus behavior for textareas and buttons in popovers
1 parent ec80920 commit a2a584f

File tree

3 files changed

+40
-6
lines changed

3 files changed

+40
-6
lines changed

core/src/components/textarea/textarea.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,13 @@ export class Textarea implements ComponentInterface {
461461
this.originalIonInput = this.ionInput;
462462
this.updateElementInternals();
463463
this.runAutoGrow();
464+
465+
// Override focus() to delegate to the native textarea.
466+
// This is needed for Safari which doesn't properly delegate
467+
// focus when calling focus() directly on the host.
468+
this.el.focus = () => {
469+
this.setFocus();
470+
};
464471
}
465472

466473
componentDidRender() {

core/src/utils/focus-trap.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { focusVisibleElement } from '@utils/helpers';
1313
* valid usage for the disabled property on ion-button.
1414
*/
1515
export const focusableQueryString =
16-
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-checkbox:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-radio:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
16+
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-checkbox:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-radio:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
1717

1818
/**
1919
* Focuses the first descendant in a context
@@ -74,7 +74,18 @@ const focusElementInContext = <T extends HTMLElement>(
7474
const shadowRoot = hostToFocus?.shadowRoot;
7575
if (shadowRoot) {
7676
// If there are no inner focusable elements, just focus the host element.
77-
elementToFocus = shadowRoot.querySelector<HTMLElement>(focusableQueryString) || hostToFocus;
77+
const innerFocusable = shadowRoot.querySelector<HTMLElement>(focusableQueryString);
78+
79+
// If the host has a setFocus() method, use it to delegate focus properly.
80+
// This is needed for shadow DOM components like ion-textarea that override
81+
// focus() to delegate to their inner native elements.
82+
const hasSetFocus = typeof (hostToFocus as any).setFocus === 'function';
83+
if (innerFocusable && hasSetFocus) {
84+
// Keep the host element so we can call setFocus() on it
85+
elementToFocus = hostToFocus;
86+
} else {
87+
elementToFocus = innerFocusable || hostToFocus;
88+
}
7889
}
7990

8091
if (elementToFocus) {

core/src/utils/helpers.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,26 @@ export const focusVisibleElement = (el: HTMLElement) => {
252252
* however, there are times when we need to manually control
253253
* this behavior so we call the `setFocus` method on ion-app
254254
* which will let us explicitly set the elements to focus.
255+
*
256+
* Note: The element passed to this function might be an inner
257+
* focusable element (e.g., a native <button> inside ion-button's
258+
* shadow root). If so, we need to find the host element that has
259+
* the ion-focusable class to pass to setFocus.
255260
*/
256-
if (el.classList.contains('ion-focusable')) {
261+
let elToFocus = el;
262+
263+
// If the element doesn't have ion-focusable, check if it's inside
264+
// a shadow root and use the host element instead
265+
if (!el.classList.contains('ion-focusable')) {
266+
const rootNode = el.getRootNode();
267+
if (rootNode instanceof ShadowRoot && rootNode.host instanceof HTMLElement) {
268+
elToFocus = rootNode.host;
269+
}
270+
}
271+
272+
if (elToFocus.classList.contains('ion-focusable')) {
257273
const appRootSelector: string = config.get('appRootSelector', 'ion-app');
258-
const app = el.closest(appRootSelector) as HTMLIonAppElement | null;
274+
const app = elToFocus.closest(appRootSelector) as HTMLIonAppElement | null;
259275
if (app) {
260276
if (appRootSelector === 'ion-app') {
261277
/**
@@ -264,7 +280,7 @@ export const focusVisibleElement = (el: HTMLElement) => {
264280
* focus-visible utility is attached to the app root
265281
* and will handle setting focus on the correct element.
266282
*/
267-
app.setFocus([el]);
283+
app.setFocus([elToFocus]);
268284
} else {
269285
/**
270286
* When using a custom app root selector, the focus-visible
@@ -280,7 +296,7 @@ export const focusVisibleElement = (el: HTMLElement) => {
280296
* The focus-visible utility is used to set focus on an
281297
* element that uses `ion-focusable`.
282298
*/
283-
focusElements([el]);
299+
focusElements([elToFocus]);
284300
});
285301
}
286302
}

0 commit comments

Comments
 (0)