Skip to content

Commit a016670

Browse files
authored
fix(overlays): do not return focus if application has already moved focus manually (#28850)
Issue number: resolves #28849 --------- ## What is the current behavior? If the developer tries to set focus to a custom element on overlay dismissal, Ionic will always override that focus. ## What is the new behavior? - If focus is already set by developer during dismissal, then Ionic will not restore focus to previous element ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information In the before video, you can see the text box is focused by developer code when "Mention User" is tapped, which opens the keyboard. Shortly after that, when the bottom sheet fully dismisses, Ionic focuses the button, removing focus from the text box and hiding the keyboard. In the after, Ionic detects that the developer has already focused the text box and does not change that focus. |Before|After| |---|---| |<video src="https://github.com/ionic-team/ionic-framework/assets/2166114/47d55eff-29af-4019-ac3c-00f9fe722ca7"></video>| <video src="https://github.com/ionic-team/ionic-framework/assets/2166114/508ae466-d037-41eb-b518-92338a122b22"></video>|
1 parent f07eabe commit a016670

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-3
lines changed

core/src/utils/overlays.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ export const present = async <OverlayPresentOptions>(
526526
* from returning focus as a result.
527527
*/
528528
if (overlay.el.tagName !== 'ION-TOAST') {
529-
focusPreviousElementOnDismiss(overlay.el);
529+
restoreElementFocus(overlay.el);
530530
}
531531

532532
/**
@@ -559,7 +559,7 @@ export const present = async <OverlayPresentOptions>(
559559
* to where they were before they
560560
* opened the overlay.
561561
*/
562-
const focusPreviousElementOnDismiss = async (overlayEl: any) => {
562+
const restoreElementFocus = async (overlayEl: any) => {
563563
let previousElement = document.activeElement as HTMLElement | null;
564564
if (!previousElement) {
565565
return;
@@ -572,7 +572,34 @@ const focusPreviousElementOnDismiss = async (overlayEl: any) => {
572572
}
573573

574574
await overlayEl.onDidDismiss();
575-
previousElement.focus();
575+
576+
/**
577+
* After onDidDismiss, the overlay loses focus
578+
* because it is removed from the document
579+
*
580+
* > An element will also lose focus [...]
581+
* > if the element is removed from the document)
582+
*
583+
* https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event
584+
*
585+
* Additionally, `document.activeElement` returns:
586+
*
587+
* > The Element which currently has focus,
588+
* > `<body>` or null if there is
589+
* > no focused element.
590+
*
591+
* https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement#value
592+
*
593+
* However, if the user has already focused
594+
* an element sometime between onWillDismiss
595+
* and onDidDismiss (for example, focusing a
596+
* text box after tapping a button in an
597+
* action sheet) then don't restore focus to
598+
* previous element
599+
*/
600+
if (document.activeElement === null || document.activeElement === document.body) {
601+
previousElement.focus();
602+
}
576603
};
577604

578605
export const dismiss = async <OverlayDismissOptions>(

core/src/utils/test/overlays/overlays.e2e.ts

+47
Original file line numberDiff line numberDiff line change
@@ -254,5 +254,52 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
254254

255255
await expect(modalInputOne).toBeFocused();
256256
});
257+
258+
test('should not return focus to another element if focus already manually returned', async ({
259+
page,
260+
skip,
261+
}, testInfo) => {
262+
skip.browser(
263+
'webkit',
264+
'WebKit does not consider buttons to be focusable, so this test always passes since the input is the only focusable element.'
265+
);
266+
testInfo.annotations.push({
267+
type: 'issue',
268+
description: 'https://github.com/ionic-team/ionic-framework/issues/28849',
269+
});
270+
await page.setContent(
271+
`
272+
<button id="open-action-sheet">open</button>
273+
<ion-action-sheet trigger="open-action-sheet"></ion-action-sheet>
274+
<input id="test-input" />
275+
276+
<script>
277+
const actionSheet = document.querySelector('ion-action-sheet');
278+
279+
actionSheet.addEventListener('ionActionSheetWillDismiss', () => {
280+
requestAnimationFrame(() => {
281+
document.querySelector('#test-input').focus();
282+
});
283+
});
284+
</script>
285+
`,
286+
config
287+
);
288+
289+
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
290+
const actionSheet = page.locator('ion-action-sheet');
291+
const input = page.locator('#test-input');
292+
const trigger = page.locator('#open-action-sheet');
293+
294+
// present action sheet
295+
await trigger.click();
296+
await ionActionSheetDidPresent.next();
297+
298+
// dismiss action sheet
299+
await actionSheet.evaluate((el: HTMLIonActionSheetElement) => el.dismiss());
300+
301+
// verify focus is in correct location
302+
await expect(input).toBeFocused();
303+
});
257304
});
258305
});

0 commit comments

Comments
 (0)