mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
chore(): sync feature-6.1 with main
This commit is contained in:
@ -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) {
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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', () => {
|
||||||
|
Reference in New Issue
Block a user