Skip to content
Merged
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
33 changes: 30 additions & 3 deletions core/src/utils/overlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export const present = async <OverlayPresentOptions>(
* from returning focus as a result.
*/
if (overlay.el.tagName !== 'ION-TOAST') {
focusPreviousElementOnDismiss(overlay.el);
restoreElementFocus(overlay.el);
}

/**
Expand All @@ -520,7 +520,7 @@ export const present = async <OverlayPresentOptions>(
* to where they were before they
* opened the overlay.
*/
const focusPreviousElementOnDismiss = async (overlayEl: any) => {
const restoreElementFocus = async (overlayEl: any) => {
let previousElement = document.activeElement as HTMLElement | null;
if (!previousElement) {
return;
Expand All @@ -533,7 +533,34 @@ const focusPreviousElementOnDismiss = async (overlayEl: any) => {
}

await overlayEl.onDidDismiss();
previousElement.focus();

/**
* After onDidDismiss, the overlay loses focus
* because it is removed from the document
*
* > An element will also lose focus [...]
* > if the element is removed from the document)
*
* https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event
*
* Additionally, `document.activeElement` returns:
*
* > The Element which currently has focus,
* > `<body>` or null if there is
* > no focused element.
*
* https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement#value
*
* However, if the user has already focused
* an element sometime between onWillDismiss
* and onDidDismiss (for example, focusing a
* text box after tapping a button in an
* action sheet) then don't restore focus to
* previous element
*/
if (document.activeElement === null || document.activeElement === document.body) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need a test for this. Something like the following should suffice:

test('should not return focus to another element if focus already manually returned', async ({
  page,
  skip,
}, testInfo) => {
  skip.browser(
    'webkit',
    'WebKit does not consider buttons to be focusable, so this test always passes since the input is the only focusable element.'
  );
  testInfo.annotations.push({
    type: 'issue',
    description: 'https://github.com/ionic-team/ionic-framework/issues/28849',
  });
  await page.setContent(
    `
    <button id="open-action-sheet">open</button>
    <ion-action-sheet trigger="open-action-sheet"></ion-action-sheet>
    <input id="test-input" />

    <script>
      const actionSheet = document.querySelector('ion-action-sheet');

      actionSheet.addEventListener('ionActionSheetWillDismiss', () => {
        requestAnimationFrame(() => {
          document.querySelector('#test-input').focus();
        });
      });
    </script>
  `,
    config
  );

  const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
  const actionSheet = page.locator('ion-action-sheet');
  const input = page.locator('#test-input');
  const trigger = page.locator('#open-action-sheet');

  // present action sheet
  await trigger.click();
  await ionActionSheetDidPresent.next();

  // dismiss action sheet
  await actionSheet.evaluate((el: HTMLIonActionSheetElement) => el.dismiss());

  // verify focus is in correct location
  await expect(input).toBeFocused();
});

You can put this in src/utils/test/overlays/overlays.e2e.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add a comment that explains focus is always moved to the body when the overlay is dismissed? Maintainers may not know why we specifically check the body here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some MDN references. Apologies if its now a bit too wordy, feel free to make further suggestions!

previousElement.focus();
}
};

export const dismiss = async <OverlayDismissOptions>(
Expand Down
47 changes: 47 additions & 0 deletions core/src/utils/test/overlays/overlays.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,52 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>

await expect(modalInputOne).toBeFocused();
});

test('should not return focus to another element if focus already manually returned', async ({
page,
skip,
}, testInfo) => {
skip.browser(
'webkit',
'WebKit does not consider buttons to be focusable, so this test always passes since the input is the only focusable element.'
);
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/28849',
});
await page.setContent(
`
<button id="open-action-sheet">open</button>
<ion-action-sheet trigger="open-action-sheet"></ion-action-sheet>
<input id="test-input" />

<script>
const actionSheet = document.querySelector('ion-action-sheet');

actionSheet.addEventListener('ionActionSheetWillDismiss', () => {
requestAnimationFrame(() => {
document.querySelector('#test-input').focus();
});
});
</script>
`,
config
);

const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
const actionSheet = page.locator('ion-action-sheet');
const input = page.locator('#test-input');
const trigger = page.locator('#open-action-sheet');

// present action sheet
await trigger.click();
await ionActionSheetDidPresent.next();

// dismiss action sheet
await actionSheet.evaluate((el: HTMLIonActionSheetElement) => el.dismiss());

// verify focus is in correct location
await expect(input).toBeFocused();
});
});
});