diff --git a/core/src/components/accordion-group/accordion-group.tsx b/core/src/components/accordion-group/accordion-group.tsx
index fd00f64c9a..8d5c9596b0 100644
--- a/core/src/components/accordion-group/accordion-group.tsx
+++ b/core/src/components/accordion-group/accordion-group.tsx
@@ -101,6 +101,17 @@ export class AccordionGroup implements ComponentInterface {
return;
}
+ /**
+ * Make sure focus is in the header, not the body, of the accordion. This ensures
+ * that if there are any interactable elements in the body, their keyboard
+ * interaction doesn't get stolen by the accordion. Example: using up/down keys
+ * in ion-textarea.
+ */
+ const activeAccordionHeader = activeElement.closest('ion-accordion [slot="header"]');
+ if (!activeAccordionHeader) {
+ return;
+ }
+
const accordionEl =
activeElement.tagName === 'ION-ACCORDION' ? activeElement : activeElement.closest('ion-accordion');
if (!accordionEl) {
diff --git a/core/src/components/accordion/test/a11y/e2e.ts b/core/src/components/accordion/test/a11y/e2e.ts
index 1913f017a6..f25141b93a 100644
--- a/core/src/components/accordion/test/a11y/e2e.ts
+++ b/core/src/components/accordion/test/a11y/e2e.ts
@@ -5,6 +5,11 @@ const getActiveElementText = async (page) => {
return page.evaluate((el) => el?.innerText, activeElement);
};
+const getActiveInputID = async (page) => {
+ const activeElement = await page.evaluateHandle(() => document.activeElement);
+ return page.evaluate((el) => el?.closest('ion-input')?.id, activeElement);
+};
+
test('accordion: a11y', async () => {
const page = await newE2EPage({
url: '/src/components/accordion/test/a11y?ionic:_testing=true',
@@ -42,4 +47,17 @@ test('accordion: keyboard navigation', async () => {
await page.keyboard.press('ArrowUp');
expect(await getActiveElementText(page)).toEqual('Shipping Address');
+
+ // open Shipping Address accordion and move focus to the input inside it
+ await page.keyboard.press('Enter');
+ await page.waitForChanges();
+ await page.keyboard.press('Tab');
+
+ const activeID = await getActiveInputID(page);
+ expect(activeID).toEqual('address1');
+
+ // ensure keyboard interaction doesn't move focus from body
+ await page.keyboard.press('ArrowDown');
+ const activeIDAgain = await getActiveInputID(page);
+ expect(activeIDAgain).toEqual('address1');
});
diff --git a/core/src/components/accordion/test/a11y/index.html b/core/src/components/accordion/test/a11y/index.html
index 486660cfe6..0c660ef8dd 100644
--- a/core/src/components/accordion/test/a11y/index.html
+++ b/core/src/components/accordion/test/a11y/index.html
@@ -86,7 +86,7 @@
Address 1
-
+
Address 2
diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx
index e0d4091082..c30108dcf5 100644
--- a/core/src/components/menu/menu.tsx
+++ b/core/src/components/menu/menu.tsx
@@ -398,6 +398,7 @@ export class Menu implements ComponentInterface, MenuI {
}
this.beforeAnimation(shouldOpen);
+
await this.loadAnimation();
await this.startAnimation(shouldOpen, animated);
this.afterAnimation(shouldOpen);
@@ -619,12 +620,17 @@ export class Menu implements ComponentInterface, MenuI {
// emit open event
this.ionDidOpen.emit();
- // focus menu content for screen readers
- if (this.menuInnerEl) {
- this.focusFirstDescendant();
+ /**
+ * Move focus to the menu to prepare focus trapping, as long as
+ * it isn't already focused. Use the host element instead of the
+ * first descendant to avoid the scroll position jumping around.
+ */
+ const focusedMenu = document.activeElement?.closest('ion-menu');
+ if (focusedMenu !== this.el) {
+ this.el.focus();
}
- // setup focus trapping
+ // start focus trapping
document.addEventListener('focus', this.handleFocus, true);
} else {
// remove css classes and unhide content from screen readers
diff --git a/core/src/components/menu/test/basic/e2e.ts b/core/src/components/menu/test/basic/e2e.ts
index 171d4cb590..dd83dfeacf 100644
--- a/core/src/components/menu/test/basic/e2e.ts
+++ b/core/src/components/menu/test/basic/e2e.ts
@@ -28,11 +28,40 @@ test('menu: focus trap', async () => {
await menu.waitForVisible();
let activeElID = await getActiveElementID(page);
- expect(activeElID).toEqual('start-menu-button');
+ expect(activeElID).toEqual('start-menu');
await page.keyboard.press('Tab');
activeElID = await getActiveElementID(page);
expect(activeElID).toEqual('start-menu-button');
+
+ // do it again to make sure focus stays inside menu
+ await page.keyboard.press('Tab');
+ activeElID = await getActiveElementID(page);
+ expect(activeElID).toEqual('start-menu-button');
+});
+
+test('menu: preserve scroll position', async () => {
+ const page = await newE2EPage({ url: '/src/components/menu/test/basic?ionic:_testing=true' });
+
+ await page.click('#open-first');
+ const menu = await page.find('#start-menu');
+ await menu.waitForVisible();
+
+ await page.$eval('#start-menu ion-content', (menuContentEl: any) => {
+ return menuContentEl.scrollToPoint(0, 200);
+ });
+
+ await menu.callMethod('close');
+
+ await page.click('#open-first');
+ await menu.waitForVisible();
+
+ const scrollTop = await page.$eval('#start-menu ion-content', async (menuContentEl: any) => {
+ const contentScrollEl = await menuContentEl.getScrollElement();
+ return contentScrollEl.scrollTop;
+ });
+
+ expect(scrollTop).toEqual(200);
});
/**
diff --git a/core/src/components/menu/test/basic/index.html b/core/src/components/menu/test/basic/index.html
index 6012e3a119..722be74614 100644
--- a/core/src/components/menu/test/basic/index.html
+++ b/core/src/components/menu/test/basic/index.html
@@ -49,6 +49,21 @@
Menu Item
Menu Item
Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
+ Menu Item
diff --git a/core/src/components/menu/test/focus-trap/e2e.ts b/core/src/components/menu/test/focus-trap/e2e.ts
index 0c06348292..af0a01211e 100644
--- a/core/src/components/menu/test/focus-trap/e2e.ts
+++ b/core/src/components/menu/test/focus-trap/e2e.ts
@@ -18,7 +18,7 @@ test('menu: focus trap with overlays', async () => {
await menu.callMethod('open');
await ionDidOpen.next();
- expect(await getActiveElementID(page)).toEqual('open-modal-button');
+ expect(await getActiveElementID(page)).toEqual('menu');
const openModal = await page.find('#open-modal-button');
await openModal.click();
@@ -45,7 +45,7 @@ test('menu: focus trap with content inside overlays', async () => {
await menu.callMethod('open');
await ionDidOpen.next();
- expect(await getActiveElementID(page)).toEqual('open-modal-button');
+ expect(await getActiveElementID(page)).toEqual('menu');
const openModal = await page.find('#open-modal-button');
await openModal.click();
diff --git a/core/src/components/menu/test/focus-trap/index.html b/core/src/components/menu/test/focus-trap/index.html
index e07ba0c0ff..0611466001 100644
--- a/core/src/components/menu/test/focus-trap/index.html
+++ b/core/src/components/menu/test/focus-trap/index.html
@@ -14,7 +14,7 @@
-
+