chore(): sync feature-6.1 with main

This commit is contained in:
Liam DeBeasi
2022-04-12 15:35:39 -04:00
11 changed files with 144 additions and 20 deletions

View File

@ -101,6 +101,17 @@ export class AccordionGroup implements ComponentInterface {
return; 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 = const accordionEl =
activeElement.tagName === 'ION-ACCORDION' ? activeElement : activeElement.closest('ion-accordion'); activeElement.tagName === 'ION-ACCORDION' ? activeElement : activeElement.closest('ion-accordion');
if (!accordionEl) { if (!accordionEl) {

View File

@ -5,6 +5,11 @@ const getActiveElementText = async (page) => {
return page.evaluate((el) => el?.innerText, activeElement); 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 () => { test('accordion: a11y', async () => {
const page = await newE2EPage({ const page = await newE2EPage({
url: '/src/components/accordion/test/a11y?ionic:_testing=true', url: '/src/components/accordion/test/a11y?ionic:_testing=true',
@ -42,4 +47,17 @@ test('accordion: keyboard navigation', async () => {
await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp');
expect(await getActiveElementText(page)).toEqual('Shipping Address'); 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');
}); });

View File

@ -86,7 +86,7 @@
<ion-list slot="content"> <ion-list slot="content">
<ion-item> <ion-item>
<ion-label>Address 1</ion-label> <ion-label>Address 1</ion-label>
<ion-input type="text"></ion-input> <ion-input id="address1" type="text"></ion-input>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label>Address 2</ion-label> <ion-label>Address 2</ion-label>

View File

@ -398,6 +398,7 @@ export class Menu implements ComponentInterface, MenuI {
} }
this.beforeAnimation(shouldOpen); this.beforeAnimation(shouldOpen);
await this.loadAnimation(); await this.loadAnimation();
await this.startAnimation(shouldOpen, animated); await this.startAnimation(shouldOpen, animated);
this.afterAnimation(shouldOpen); this.afterAnimation(shouldOpen);
@ -619,12 +620,17 @@ export class Menu implements ComponentInterface, MenuI {
// emit open event // emit open event
this.ionDidOpen.emit(); this.ionDidOpen.emit();
// focus menu content for screen readers /**
if (this.menuInnerEl) { * Move focus to the menu to prepare focus trapping, as long as
this.focusFirstDescendant(); * 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); document.addEventListener('focus', this.handleFocus, true);
} else { } else {
// remove css classes and unhide content from screen readers // remove css classes and unhide content from screen readers

View File

@ -28,11 +28,40 @@ test('menu: focus trap', async () => {
await menu.waitForVisible(); await menu.waitForVisible();
let activeElID = await getActiveElementID(page); let activeElID = await getActiveElementID(page);
expect(activeElID).toEqual('start-menu-button'); expect(activeElID).toEqual('start-menu');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab');
activeElID = await getActiveElementID(page); activeElID = await getActiveElementID(page);
expect(activeElID).toEqual('start-menu-button'); 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);
}); });
/** /**

View File

@ -49,6 +49,21 @@
<ion-item>Menu Item</ion-item> <ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item> <ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item> <ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
</ion-list> </ion-list>
</ion-content> </ion-content>
</ion-menu> </ion-menu>

View File

@ -18,7 +18,7 @@ test('menu: focus trap with overlays', async () => {
await menu.callMethod('open'); await menu.callMethod('open');
await ionDidOpen.next(); 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'); const openModal = await page.find('#open-modal-button');
await openModal.click(); await openModal.click();
@ -45,7 +45,7 @@ test('menu: focus trap with content inside overlays', async () => {
await menu.callMethod('open'); await menu.callMethod('open');
await ionDidOpen.next(); 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'); const openModal = await page.find('#open-modal-button');
await openModal.click(); await openModal.click();

View File

@ -14,7 +14,7 @@
</head> </head>
<body> <body>
<ion-app> <ion-app>
<ion-menu content-id="main"> <ion-menu content-id="main" id="menu">
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title>Menu</ion-title> <ion-title>Menu</ion-title>

View File

@ -6,6 +6,16 @@ Once the user drags an item and drops it in a new position, the `ionItemReorder`
The `detail` property of the `ionItemReorder` event includes all of the relevant information about the reorder operation, including the `from` and `to` indexes. In the context of reordering, an item moves `from` an index `to` a new index. The `detail` property of the `ionItemReorder` event includes all of the relevant information about the reorder operation, including the `from` and `to` indexes. In the context of reordering, an item moves `from` an index `to` a new index.
## Completing a Reorder
When the `ionItemReorder` event is dispatched, developers have the option to call the `complete()` method on `ion-reorder-group`. This will complete the reorder operation.
By default, the `complete()` method will re-order the DOM nodes inside of `ion-reorder-group`.
For developers who need to sort an array based on the order of the items in `ion-reorder-group`, we recommend passing the array as a parameter in `complete()`. Ionic will sort and return the array so that it can be reassigned.
In some cases, it may be necessary for an app to re-order both the array and the DOM nodes on its own. When this happens, it is recommended to pass `false` to the `complete()` method. This will prevent Ionic from re-ordering any DOM nodes inside of `ion-reorder-group`.
## Usage with Virtual Scroll ## Usage with Virtual Scroll
The reorder group requires a scroll container to function. When using a virtual scrolling solution, you will need to disable scrolling on the `ion-content` and indicate which element container is responsible for the scroll container with the `.ion-content-scroll-host` class target. The reorder group requires a scroll container to function. When using a virtual scrolling solution, you will need to disable scrolling on the `ion-content` and indicate which element container is responsible for the scroll container with the `.ion-content-scroll-host` class target.

View File

@ -65,7 +65,6 @@ export const createIonRouter = (opts: IonicVueRouterOptions, router: Router) =>
let currentRouteInfo: RouteInfo; let currentRouteInfo: RouteInfo;
let incomingRouteParams: RouteParams; let incomingRouteParams: RouteParams;
let currentTab: string | undefined;
// TODO types // TODO types
let historyChangeListeners: any[] = []; let historyChangeListeners: any[] = [];
@ -238,8 +237,7 @@ export const createIonRouter = (opts: IonicVueRouterOptions, router: Router) =>
if (action === 'replace') { if (action === 'replace') {
incomingRouteParams = { incomingRouteParams = {
routerAction: 'replace', routerAction: 'replace',
routerDirection: 'none', routerDirection: 'none'
tab: currentTab
} }
} else if (action === 'pop') { } else if (action === 'pop') {
const routeInfo = locationHistory.current(initialHistoryPosition, currentHistoryPosition - delta); const routeInfo = locationHistory.current(initialHistoryPosition, currentHistoryPosition - delta);
@ -254,16 +252,15 @@ export const createIonRouter = (opts: IonicVueRouterOptions, router: Router) =>
} else { } else {
incomingRouteParams = { incomingRouteParams = {
routerAction: 'pop', routerAction: 'pop',
routerDirection: 'none', routerDirection: 'none'
tab: currentTab
} }
} }
} }
if (!incomingRouteParams) { if (!incomingRouteParams) {
incomingRouteParams = { incomingRouteParams = {
routerAction: 'push', routerAction: 'push',
routerDirection: direction || 'forward', routerDirection: direction || 'forward'
tab: currentTab
} }
} }
} }
@ -288,7 +285,6 @@ export const createIonRouter = (opts: IonicVueRouterOptions, router: Router) =>
} }
if (isPushed) { if (isPushed) {
routeInfo.tab = leavingLocationInfo.tab;
routeInfo.pushedByRoute = (leavingLocationInfo.pathname !== '') ? leavingLocationInfo.pathname : undefined; routeInfo.pushedByRoute = (leavingLocationInfo.pathname !== '') ? leavingLocationInfo.pathname : undefined;
} else if (routeInfo.routerAction === 'pop') { } else if (routeInfo.routerAction === 'pop') {
const route = locationHistory.findLastLocation(routeInfo); const route = locationHistory.findLastLocation(routeInfo);
@ -460,9 +456,18 @@ export const createIonRouter = (opts: IonicVueRouterOptions, router: Router) =>
} }
} }
/**
* This method is invoked by the IonTabs component
* during a history change callback. It is responsible
* for ensuring that tabbed routes have the correct
* "tab" field in its routeInfo object.
*
* IonTabs will determine if the current route
* is in tabs and assign it the correct tab.
* If the current route is not in tabs,
* then IonTabs will not invoke this.
*/
const handleSetCurrentTab = (tab: string) => { const handleSetCurrentTab = (tab: string) => {
currentTab = tab;
const ri = { ...locationHistory.last() }; const ri = { ...locationHistory.last() };
if (ri.tab !== tab) { if (ri.tab !== tab) {
ri.tab = tab; ri.tab = tab;

View File

@ -435,7 +435,7 @@ describe('Tabs', () => {
}); });
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/24432 // Verifies fix for https://github.com/ionic-team/ionic-framework/issues/24432
it('should correct replace a route in a child tab route', () => { it('should correctly replace a route in a child tab route', () => {
cy.visit('http://localhost:8080/tabs'); cy.visit('http://localhost:8080/tabs');
cy.routerPush('/tabs/tab1/childone'); cy.routerPush('/tabs/tab1/childone');
@ -446,6 +446,36 @@ describe('Tabs', () => {
cy.ionPageVisible('tab1'); cy.ionPageVisible('tab1');
cy.ionPageDoesNotExist('tab1childone'); cy.ionPageDoesNotExist('tab1childone');
}) })
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/24859
it('should go back to the root page after navigating between tab and non tab outlets', () => {
cy.visit('http://localhost:8080');
cy.routerPush('/tabs/tab1');
cy.ionPageVisible('tab1');
cy.ionPageHidden('home');
cy.get('ion-tab-button#tab-button-tab2').click();
cy.ionPageHidden('tab1');
cy.ionPageVisible('tab2');
cy.routerPush('/routing');
cy.ionPageVisible('routing');
cy.ionPageHidden('tabs');
cy.routerPush('/tabs/tab1');
cy.ionPageVisible('tabs');
cy.ionPageVisible('tab1');
cy.ionPageHidden('routing');
cy.get('ion-tab-button#tab-button-tab2').click();
cy.ionPageHidden('tab1');
cy.ionPageVisible('tab2');
cy.ionBackClick('tab2');
cy.ionPageVisible('home');
cy.ionPageDoesNotExist('tabs');
});
}) })
describe('Tabs - Swipe to Go Back', () => { describe('Tabs - Swipe to Go Back', () => {