Merge branch 'main' into chore/update-next-from-main
@ -125,7 +125,7 @@
|
||||
|
||||
<ion-toolbar color="dark">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button class="ion-hide"></ion-back-button>
|
||||
<ion-back-button class="ion-display-none"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Hidden</ion-title>
|
||||
</ion-toolbar>
|
||||
|
||||
@ -22,15 +22,15 @@ export type DatetimePresentation = 'date-time' | 'time-date' | 'date' | 'time' |
|
||||
|
||||
export type TitleSelectedDatesFormatter = (selectedDates: string[]) => string;
|
||||
|
||||
export type DatetimeHighlightStyle =
|
||||
| {
|
||||
textColor: string;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
| {
|
||||
textColor?: string;
|
||||
backgroundColor: string;
|
||||
};
|
||||
/**
|
||||
* DatetimeHighlightStyle must include textColor, backgroundColor, or border.
|
||||
* It cannot be an empty object.
|
||||
*/
|
||||
export type DatetimeHighlightStyle = {
|
||||
textColor?: string;
|
||||
backgroundColor?: string;
|
||||
border?: string;
|
||||
} & ({ textColor: string } | { backgroundColor: string } | { border: string });
|
||||
|
||||
export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;
|
||||
|
||||
|
||||
@ -2343,6 +2343,7 @@ export class Datetime implements ComponentInterface {
|
||||
`${dateStyle ? dateStyle.backgroundColor : ''}`,
|
||||
'important'
|
||||
);
|
||||
el.style.setProperty('border', `${dateStyle ? dateStyle.border : ''}`, 'important');
|
||||
}
|
||||
}}
|
||||
tabindex="-1"
|
||||
|
||||
@ -5,6 +5,8 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('datetime: custom'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(`/src/components/datetime/test/custom`, config);
|
||||
|
||||
await page.locator('.datetime-ready').last().waitFor();
|
||||
});
|
||||
|
||||
test('should allow styling wheel style datetimes', async ({ page }) => {
|
||||
@ -30,6 +32,13 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test('should allow styling calendar days in grid style datetimes', async ({ page }) => {
|
||||
const datetime = page.locator('#custom-calendar-days');
|
||||
|
||||
// Wait for calendar days to be rendered
|
||||
await page.waitForFunction(() => {
|
||||
const datetime = document.querySelector('#custom-calendar-days');
|
||||
const calendarDays = datetime?.shadowRoot?.querySelectorAll('.calendar-day');
|
||||
return calendarDays && calendarDays.length > 0;
|
||||
});
|
||||
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-calendar-days`));
|
||||
});
|
||||
});
|
||||
|
||||
@ -164,7 +164,7 @@
|
||||
const customDatetime = document.querySelector('#custom-calendar-days');
|
||||
|
||||
// Mock the current day to always have the same screenshots
|
||||
const mockToday = '2023-06-10T16:22';
|
||||
const mockToday = '2023-06-10T16:22:00.000Z';
|
||||
Date = class extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
|
||||
@ -22,11 +22,23 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(monthYearToggle).toContainText('January 2022');
|
||||
|
||||
// Click to open the picker
|
||||
await monthYearToggle.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
// February
|
||||
await monthColumnItems.nth(1).click();
|
||||
// Wait for the picker to be open
|
||||
await page.locator('.month-year-picker-open').waitFor();
|
||||
|
||||
// Wait a bit for the picker to fully load
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
|
||||
// Click on February
|
||||
await monthColumnItems.filter({ hasText: 'February' }).click();
|
||||
|
||||
// Wait for changes
|
||||
await ionChange.next();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(monthYearToggle).toContainText('February 2022');
|
||||
@ -38,13 +50,23 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
|
||||
// Click to open the picker
|
||||
await monthYearToggle.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
// February
|
||||
await monthColumnItems.nth(1).click();
|
||||
// Wait for the picker to be open
|
||||
await page.locator('.month-year-picker-open').waitFor();
|
||||
|
||||
// Wait a bit for the picker to fully load
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Click on February
|
||||
await monthColumnItems.filter({ hasText: 'February' }).click();
|
||||
|
||||
// Wait for changes
|
||||
await ionChange.next();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventTimes(1);
|
||||
await expect(datetime).toHaveJSProperty('value', '2022-02-28');
|
||||
});
|
||||
|
||||
@ -21,16 +21,19 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
date: '2023-01-01', // ensure selected date style overrides highlight
|
||||
textColor: '#800080',
|
||||
backgroundColor: '#ffc0cb',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
{
|
||||
date: '2023-01-02',
|
||||
textColor: '#b22222',
|
||||
backgroundColor: '#fa8072',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
{
|
||||
date: '2023-01-03',
|
||||
textColor: '#0000ff',
|
||||
backgroundColor: '#add8e6',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -52,6 +55,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
return {
|
||||
textColor: '#b22222',
|
||||
backgroundColor: '#fa8072',
|
||||
border: '2px solid purple',
|
||||
};
|
||||
}
|
||||
|
||||
@ -59,6 +63,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
return {
|
||||
textColor: '#800080',
|
||||
backgroundColor: '#ffc0cb',
|
||||
border: '2px solid purple',
|
||||
};
|
||||
}
|
||||
|
||||
@ -66,6 +71,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
return {
|
||||
textColor: '#0000ff',
|
||||
backgroundColor: '#add8e6',
|
||||
border: '2px solid purple',
|
||||
};
|
||||
}
|
||||
|
||||
@ -77,7 +83,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-highlightedDates-callback`));
|
||||
});
|
||||
|
||||
test('should render highlights correctly when only using one color or the other', async ({ page }) => {
|
||||
test('should render highlights correctly when only using only one color property', async ({ page }) => {
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => {
|
||||
@ -90,6 +96,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
date: '2023-01-03',
|
||||
textColor: '#0000ff',
|
||||
},
|
||||
{
|
||||
date: '2023-01-04',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
@ -78,6 +78,10 @@
|
||||
textColor: 'blue',
|
||||
backgroundColor: 'lightblue',
|
||||
},
|
||||
{
|
||||
date: '2023-01-07',
|
||||
border: '2px dotted red',
|
||||
},
|
||||
];
|
||||
|
||||
document.querySelector('#withCallback').highlightedDates = (isoString) => {
|
||||
@ -103,6 +107,7 @@
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
textColor: 'purple',
|
||||
backgroundColor: 'pink',
|
||||
border: '2px solid purple',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
@ -207,6 +207,7 @@ export const getHighlightStyles = (
|
||||
return {
|
||||
textColor: matchingHighlight.textColor,
|
||||
backgroundColor: matchingHighlight.backgroundColor,
|
||||
border: matchingHighlight.border,
|
||||
} as DatetimeHighlightStyle;
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Infinite Scroll - Basic</title>
|
||||
<title>Infinite Scroll - Top</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
@ -18,7 +18,7 @@
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Infinite Scroll - Basic</ion-title>
|
||||
<ion-title>Infinite Scroll - Top</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@ -28,9 +28,9 @@
|
||||
</ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
|
||||
<button onclick="toggleInfiniteScroll()" class="expand">Toggle InfiniteScroll</button>
|
||||
<div id="list"></div>
|
||||
|
||||
<ion-list id="list"></ion-list>
|
||||
<button onclick="toggleInfiniteScroll()" class="expand">Toggle InfiniteScroll</button>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
@ -46,17 +46,26 @@
|
||||
console.log('Loading data...');
|
||||
await wait(500);
|
||||
infiniteScroll.complete();
|
||||
appendItems();
|
||||
appendItems(true);
|
||||
// Custom event consumed in the e2e tests
|
||||
window.dispatchEvent(new CustomEvent('ionInfiniteComplete'));
|
||||
|
||||
console.log('Done');
|
||||
});
|
||||
|
||||
function appendItems() {
|
||||
function appendItems(newItems = false) {
|
||||
const randomColor =
|
||||
'#' +
|
||||
Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0');
|
||||
|
||||
for (var i = 0; i < 30; i++) {
|
||||
const el = document.createElement('ion-item');
|
||||
el.textContent = `${1 + i}`;
|
||||
el.textContent = `Item ${1 + i}`;
|
||||
if (newItems) {
|
||||
el.style.borderLeft = `4px solid ${randomColor}`;
|
||||
}
|
||||
list.prepend(el);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,6 +95,8 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
el.separators = [2, 3];
|
||||
});
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(await hasSeparatorAfter(page, 0)).toBe(false);
|
||||
await expect(await hasSeparatorAfter(page, 1)).toBe(true);
|
||||
await expect(await hasSeparatorAfter(page, 2)).toBe(true);
|
||||
|
||||
@ -84,7 +84,7 @@
|
||||
}
|
||||
function initGroup(group) {
|
||||
var groupEl = document.getElementById(group.id);
|
||||
groupEl.addEventListener('ionItemReorder', function (ev) {
|
||||
groupEl.addEventListener('ionReorderEnd', function (ev) {
|
||||
ev.detail.complete();
|
||||
});
|
||||
var groupItems = [];
|
||||
|
||||
@ -18,7 +18,13 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
const heading = page.locator('ion-menu h1');
|
||||
await expect(heading).toHaveText('Open Menu');
|
||||
|
||||
const results = await new AxeBuilder({ page }).analyze();
|
||||
/**
|
||||
* Disable the 'scrollable-region-focusable' rule because this test
|
||||
* is missing the required `ion-app` wrapper component. The `ion-app`
|
||||
* wrapper provides the necessary focus management that allows the
|
||||
* menu content to be focusable.
|
||||
*/
|
||||
const results = await new AxeBuilder({ page }).disableRules('scrollable-region-focusable').analyze();
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -135,6 +135,7 @@ const renderProgress = (value: number, buffer: number) => {
|
||||
* When finalBuffer === 1, we use display: none
|
||||
* instead of removing the element to avoid flickering.
|
||||
*/
|
||||
// TODO(FW-6697): change `ion-hide` class to `ion-display-none` or another class
|
||||
<div
|
||||
class={{ 'buffer-circles-container': true, 'ion-hide': finalBuffer === 1 }}
|
||||
style={{ transform: `translateX(${finalBuffer * 100}%)` }}
|
||||
|
||||
@ -1,10 +1,33 @@
|
||||
// TODO(FW-6590): Remove this once the deprecated event is removed
|
||||
export interface ItemReorderEventDetail {
|
||||
from: number;
|
||||
to: number;
|
||||
complete: (data?: boolean | any[]) => any;
|
||||
}
|
||||
|
||||
// TODO(FW-6590): Remove this once the deprecated event is removed
|
||||
export interface ItemReorderCustomEvent extends CustomEvent {
|
||||
detail: ItemReorderEventDetail;
|
||||
target: HTMLIonReorderGroupElement;
|
||||
}
|
||||
|
||||
export interface ReorderMoveEventDetail {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
export interface ReorderEndEventDetail {
|
||||
from: number;
|
||||
to: number;
|
||||
complete: (data?: boolean | any[]) => any;
|
||||
}
|
||||
|
||||
export interface ReorderMoveCustomEvent extends CustomEvent {
|
||||
detail: ReorderMoveEventDetail;
|
||||
target: HTMLIonReorderGroupElement;
|
||||
}
|
||||
|
||||
export interface ReorderEndCustomEvent extends CustomEvent {
|
||||
detail: ReorderEndEventDetail;
|
||||
target: HTMLIonReorderGroupElement;
|
||||
}
|
||||
|
||||
@ -6,8 +6,9 @@ import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from
|
||||
|
||||
import { getIonTheme } from '../../global/ionic-global';
|
||||
import type { Gesture, GestureDetail } from '../../interface';
|
||||
import type { HTMLStencilElement } from '../../utils/element-interface';
|
||||
|
||||
import type { ItemReorderEventDetail } from './reorder-group-interface';
|
||||
import type { ItemReorderEventDetail, ReorderMoveEventDetail, ReorderEndEventDetail } from './reorder-group-interface';
|
||||
|
||||
// TODO(FW-2832): types
|
||||
|
||||
@ -42,7 +43,7 @@ export class ReorderGroup implements ComponentInterface {
|
||||
|
||||
@State() state = ReorderGroupState.Idle;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
@Element() el!: HTMLStencilElement;
|
||||
|
||||
/**
|
||||
* If `true`, the reorder will be hidden.
|
||||
@ -55,12 +56,35 @@ export class ReorderGroup implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(FW-6590): Remove this in a major release.
|
||||
/**
|
||||
* Event that needs to be listened to in order to complete the reorder action.
|
||||
* @deprecated Use `ionReorderEnd` instead. If you are accessing
|
||||
* `event.detail.from` or `event.detail.to` and relying on them
|
||||
* being different you should now add checks as they are always emitted
|
||||
* in `ionReorderEnd`, even when they are the same.
|
||||
*/
|
||||
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>;
|
||||
|
||||
/**
|
||||
* Event that is emitted when the reorder gesture starts.
|
||||
*/
|
||||
@Event() ionReorderStart!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Event that is emitted as the reorder gesture moves.
|
||||
*/
|
||||
@Event() ionReorderMove!: EventEmitter<ReorderMoveEventDetail>;
|
||||
|
||||
/**
|
||||
* Event that is emitted when the reorder gesture ends.
|
||||
* The from and to properties are always available, regardless of
|
||||
* if the reorder gesture moved the item. If the item did not change
|
||||
* from its start position, the from and to properties will be the same.
|
||||
* Once the event has been emitted, the `complete()` method then needs
|
||||
* to be called in order to finalize the reorder action.
|
||||
*/
|
||||
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>;
|
||||
@Event() ionReorderEnd!: EventEmitter<ReorderEndEventDetail>;
|
||||
|
||||
async connectedCallback() {
|
||||
const contentEl = findClosestIonContent(this.el);
|
||||
@ -92,7 +116,8 @@ export class ReorderGroup implements ComponentInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the reorder operation. Must be called by the `ionItemReorder` event.
|
||||
* Completes the reorder operation. Must be called by the `ionReorderEnd` event.
|
||||
*
|
||||
* If a list of items is passed, the list will be reordered and returned in the
|
||||
* proper order.
|
||||
*
|
||||
@ -132,7 +157,7 @@ export class ReorderGroup implements ComponentInterface {
|
||||
const heights = this.cachedHeights;
|
||||
heights.length = 0;
|
||||
const el = this.el;
|
||||
const children: any = el.children;
|
||||
const children: any = el.__children;
|
||||
if (!children || children.length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -167,6 +192,8 @@ export class ReorderGroup implements ComponentInterface {
|
||||
item.classList.add(ITEM_REORDER_SELECTED);
|
||||
|
||||
hapticSelectionStart();
|
||||
|
||||
this.ionReorderStart.emit();
|
||||
}
|
||||
|
||||
private onMove(ev: GestureDetail) {
|
||||
@ -183,6 +210,7 @@ export class ReorderGroup implements ComponentInterface {
|
||||
const currentY = Math.max(top, Math.min(ev.currentY, bottom));
|
||||
const deltaY = scroll + currentY - ev.startY;
|
||||
const normalizedY = currentY - top;
|
||||
const fromIndex = this.lastToIndex;
|
||||
const toIndex = this.itemIndexForTop(normalizedY);
|
||||
if (toIndex !== this.lastToIndex) {
|
||||
const fromIndex = indexForItem(selectedItem);
|
||||
@ -194,6 +222,11 @@ export class ReorderGroup implements ComponentInterface {
|
||||
|
||||
// Update selected item position
|
||||
selectedItem.style.transform = `translateY(${deltaY}px)`;
|
||||
|
||||
this.ionReorderMove.emit({
|
||||
from: fromIndex,
|
||||
to: toIndex,
|
||||
});
|
||||
}
|
||||
|
||||
private onEnd() {
|
||||
@ -210,6 +243,7 @@ export class ReorderGroup implements ComponentInterface {
|
||||
if (toIndex === fromIndex) {
|
||||
this.completeReorder();
|
||||
} else {
|
||||
// TODO(FW-6590): Remove this once the deprecated event is removed
|
||||
this.ionItemReorder.emit({
|
||||
from: fromIndex,
|
||||
to: toIndex,
|
||||
@ -218,12 +252,18 @@ export class ReorderGroup implements ComponentInterface {
|
||||
}
|
||||
|
||||
hapticSelectionEnd();
|
||||
|
||||
this.ionReorderEnd.emit({
|
||||
from: fromIndex,
|
||||
to: toIndex,
|
||||
complete: this.completeReorder.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
private completeReorder(listOrReorder?: boolean | any[]): any {
|
||||
const selectedItemEl = this.selectedItemEl;
|
||||
if (selectedItemEl && this.state === ReorderGroupState.Complete) {
|
||||
const children = this.el.children as any;
|
||||
const children: any = this.el.__children;
|
||||
const len = children.length;
|
||||
const toIndex = this.lastToIndex;
|
||||
const fromIndex = indexForItem(selectedItemEl);
|
||||
@ -273,7 +313,7 @@ export class ReorderGroup implements ComponentInterface {
|
||||
/********* DOM WRITE ********* */
|
||||
private reorderMove(fromIndex: number, toIndex: number) {
|
||||
const itemHeight = this.selectedItemHeight;
|
||||
const children = this.el.children;
|
||||
const children: any = this.el.__children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const style = (children[i] as any).style;
|
||||
let value = '';
|
||||
|
||||
@ -122,8 +122,25 @@
|
||||
const reorderGroup = document.getElementById('reorder');
|
||||
reorderGroup.disabled = !reorderGroup.disabled;
|
||||
|
||||
// TODO(FW-6590): Remove this once the deprecated event is removed
|
||||
reorderGroup.addEventListener('ionItemReorder', ({ detail }) => {
|
||||
console.log('Dragged from index', detail.from, 'to', detail.to);
|
||||
console.log('ionItemReorder: Dragged from index', detail.from, 'to', detail.to);
|
||||
});
|
||||
|
||||
reorderGroup.addEventListener('ionReorderStart', () => {
|
||||
console.log('ionReorderStart');
|
||||
});
|
||||
|
||||
reorderGroup.addEventListener('ionReorderMove', ({ detail }) => {
|
||||
console.log('ionReorderMove: Dragged from index', detail.from, 'to', detail.to);
|
||||
});
|
||||
|
||||
reorderGroup.addEventListener('ionReorderEnd', ({ detail }) => {
|
||||
if (detail.from !== detail.to) {
|
||||
console.log('ionReorderEnd: Dragged from index', detail.from, 'to', detail.to);
|
||||
} else {
|
||||
console.log('ionReorderEnd: No position change occurred');
|
||||
}
|
||||
|
||||
detail.complete();
|
||||
});
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body onLoad="render()">
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
@ -24,7 +24,7 @@
|
||||
|
||||
<ion-content id="content">
|
||||
<ion-list>
|
||||
<ion-reorder-group id="reorderGroup" disabled="false">
|
||||
<ion-reorder-group disabled="false">
|
||||
<!-- items will be inserted here -->
|
||||
</ion-reorder-group>
|
||||
</ion-list>
|
||||
@ -36,27 +36,44 @@
|
||||
for (var i = 0; i < 30; i++) {
|
||||
items.push(i + 1);
|
||||
}
|
||||
const reorderGroup = document.getElementById('reorderGroup');
|
||||
|
||||
function render() {
|
||||
let html = '';
|
||||
for (let item of items) {
|
||||
html += `
|
||||
<ion-item>
|
||||
<ion-label>${item}</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>`;
|
||||
}
|
||||
reorderGroup.innerHTML = html;
|
||||
}
|
||||
|
||||
reorderGroup.addEventListener('ionItemReorder', ({ detail }) => {
|
||||
console.log('Dragged from index', detail.from, 'to', detail.to);
|
||||
const reorderGroup = document.querySelector('ion-reorder-group');
|
||||
reorderItems(items);
|
||||
|
||||
reorderGroup.addEventListener('ionReorderEnd', ({ detail }) => {
|
||||
// Before complete is called with the items they will remain in the
|
||||
// order before the drag
|
||||
console.log('Before complete', items);
|
||||
|
||||
// Finish the reorder and position the item in the DOM based on
|
||||
// where the gesture ended. Update the items variable to the
|
||||
// new order of items
|
||||
items = detail.complete(items);
|
||||
|
||||
// Reorder the items in the DOM
|
||||
reorderItems(items);
|
||||
|
||||
// After complete is called the items will be in the new order
|
||||
console.log('After complete', items);
|
||||
});
|
||||
|
||||
function reorderItems(items) {
|
||||
reorderGroup.replaceChildren();
|
||||
|
||||
let reordered = '';
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
reordered += `
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
Item ${items[i]}
|
||||
</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
`;
|
||||
}
|
||||
|
||||
reorderGroup.innerHTML = reordered;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -37,9 +37,9 @@
|
||||
</ion-reorder-group>
|
||||
<script>
|
||||
const group = document.querySelector('ion-reorder-group');
|
||||
group.addEventListener('ionItemReorder', (ev) => {
|
||||
group.addEventListener('ionReorderEnd', (ev) => {
|
||||
ev.detail.complete();
|
||||
window.dispatchEvent(new CustomEvent('ionItemReorderComplete'));
|
||||
window.dispatchEvent(new CustomEvent('ionReorderComplete'));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
});
|
||||
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
|
||||
const items = page.locator('ion-item');
|
||||
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
|
||||
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
|
||||
|
||||
await dragElementBy(items.nth(1), page, 0, 300);
|
||||
await ionItemReorderComplete.next();
|
||||
await ionReorderComplete.next();
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
|
||||
});
|
||||
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
|
||||
const reorderHandle = page.locator('ion-reorder');
|
||||
const items = page.locator('ion-item');
|
||||
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
|
||||
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
|
||||
|
||||
await dragElementBy(reorderHandle.nth(0), page, 0, 300);
|
||||
await ionItemReorderComplete.next();
|
||||
await ionReorderComplete.next();
|
||||
|
||||
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
|
||||
});
|
||||
|
||||
@ -68,9 +68,9 @@
|
||||
customElements.define('app-reorder', AppReorder);
|
||||
|
||||
const group = document.querySelector('ion-reorder-group');
|
||||
group.addEventListener('ionItemReorder', (ev) => {
|
||||
group.addEventListener('ionReorderEnd', (ev) => {
|
||||
ev.detail.complete();
|
||||
window.dispatchEvent(new CustomEvent('ionItemReorderComplete'));
|
||||
window.dispatchEvent(new CustomEvent('ionReorderComplete'));
|
||||
});
|
||||
</script>
|
||||
</ion-app>
|
||||
|
||||
@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
});
|
||||
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
|
||||
const items = page.locator('app-reorder');
|
||||
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
|
||||
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
|
||||
|
||||
await dragElementBy(items.nth(1), page, 0, 300);
|
||||
await ionItemReorderComplete.next();
|
||||
await ionReorderComplete.next();
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
|
||||
});
|
||||
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
|
||||
const reorderHandle = page.locator('app-reorder ion-reorder');
|
||||
const items = page.locator('app-reorder');
|
||||
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
|
||||
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
|
||||
|
||||
await dragElementBy(reorderHandle.nth(0), page, 0, 300);
|
||||
await ionItemReorderComplete.next();
|
||||
await ionReorderComplete.next();
|
||||
|
||||
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
|
||||
});
|
||||
|
||||
@ -0,0 +1,289 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, dragElementBy, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('reorder-group: events:'), () => {
|
||||
test.describe('ionReorderStart', () => {
|
||||
test('should emit when the reorder operation starts', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-reorder-group disabled="false">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
</ion-reorder-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const reorderGroup = page.locator('ion-reorder-group');
|
||||
const ionReorderStart = await page.spyOnEvent('ionReorderStart');
|
||||
|
||||
await expect(ionReorderStart).toHaveReceivedEventTimes(0);
|
||||
|
||||
// Start the drag to verify it emits the event without having to
|
||||
// actually move the item. Do not release the drag here.
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 0, undefined, undefined, false);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionReorderStart).toHaveReceivedEventTimes(1);
|
||||
|
||||
// Drag the reorder item further to verify it does
|
||||
// not emit the event again
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionReorderStart).toHaveReceivedEventTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('ionReorderMove', () => {
|
||||
test('should emit when the reorder operation does not move the item position', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-reorder-group disabled="false">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
</ion-reorder-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const reorderGroup = page.locator('ion-reorder-group');
|
||||
const ionReorderMove = await page.spyOnEvent('ionReorderMove');
|
||||
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 0);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(ionReorderMove.events.length).toBeGreaterThan(0);
|
||||
|
||||
// Grab the last event to verify that it is emitting
|
||||
// the correct from and to positions
|
||||
const lastEvent = ionReorderMove.events[ionReorderMove.events.length - 1];
|
||||
expect(lastEvent?.detail.from).toBe(0);
|
||||
expect(lastEvent?.detail.to).toBe(0);
|
||||
});
|
||||
|
||||
test('should emit when the reorder operation moves the item by multiple positions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-reorder-group disabled="false">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
</ion-reorder-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const reorderGroup = page.locator('ion-reorder-group');
|
||||
const ionReorderMove = await page.spyOnEvent('ionReorderMove');
|
||||
|
||||
// Drag the reorder item by a lot to verify it emits the event
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(ionReorderMove.events.length).toBeGreaterThan(0);
|
||||
|
||||
// Grab the last event where the from and to are different to
|
||||
// verify that it is not using the gesture start position as the from
|
||||
const lastDifferentEvent = ionReorderMove.events
|
||||
.reverse()
|
||||
.find((event) => event.detail.from !== event.detail.to);
|
||||
expect(lastDifferentEvent?.detail.from).toBe(1);
|
||||
expect(lastDifferentEvent?.detail.to).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('ionReorderEnd', () => {
|
||||
test('should emit without details when the reorder operation ends without moving the item position', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-reorder-group disabled="false">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
</ion-reorder-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const reorderGroup = page.locator('ion-reorder-group');
|
||||
const ionReorderEnd = await page.spyOnEvent('ionReorderEnd');
|
||||
|
||||
// Drag the reorder item a little bit but not enough to
|
||||
// make it switch to a different position
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 20);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionReorderEnd).toHaveReceivedEventTimes(1);
|
||||
await expect(ionReorderEnd).toHaveReceivedEventDetail({ from: 0, to: 0, complete: undefined });
|
||||
});
|
||||
|
||||
test('should emit with details when the reorder operation ends and the item has moved', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-reorder-group disabled="false">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
</ion-reorder-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const reorderGroup = page.locator('ion-reorder-group');
|
||||
const ionReorderEnd = await page.spyOnEvent('ionReorderEnd');
|
||||
|
||||
// Start the drag to verify it does not emit the event at the start
|
||||
// of the drag or during the drag. Do not release the drag here.
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 100, undefined, undefined, false);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionReorderEnd).toHaveReceivedEventTimes(0);
|
||||
|
||||
// Drag the reorder item further and release the drag to verify it emits the event
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionReorderEnd).toHaveReceivedEventTimes(1);
|
||||
await expect(ionReorderEnd).toHaveReceivedEventDetail({ from: 0, to: 2, complete: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(FW-6590): Remove this once the deprecated event is removed
|
||||
test.describe('ionItemReorder', () => {
|
||||
test('should not emit when the reorder operation ends without moving the item position', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-reorder-group disabled="false">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
</ion-reorder-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const reorderGroup = page.locator('ion-reorder-group');
|
||||
const ionItemReorder = await page.spyOnEvent('ionItemReorder');
|
||||
|
||||
// Drag the reorder item a little bit but not enough to
|
||||
// make it switch to a different position
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 20);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionItemReorder).toHaveReceivedEventTimes(0);
|
||||
});
|
||||
|
||||
test('should emit when the reorder operation ends and the item has moved', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-reorder-group disabled="false">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-reorder slot="end"></ion-reorder>
|
||||
</ion-item>
|
||||
</ion-reorder-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const reorderGroup = page.locator('ion-reorder-group');
|
||||
const ionItemReorder = await page.spyOnEvent('ionItemReorder');
|
||||
|
||||
// Start the drag to verify it does not emit the event at the start
|
||||
// of the drag or during the drag. Do not release the drag here.
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 100, undefined, undefined, false);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionItemReorder).toHaveReceivedEventTimes(0);
|
||||
|
||||
// Drag the reorder item further and release the drag to verify it emits the event
|
||||
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionItemReorder).toHaveReceivedEventTimes(1);
|
||||
await expect(ionItemReorder).toHaveReceivedEventDetail({ from: 0, to: 2, complete: undefined });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -57,9 +57,9 @@
|
||||
|
||||
<script>
|
||||
const group = document.querySelector('ion-reorder-group');
|
||||
group.addEventListener('ionItemReorder', (ev) => {
|
||||
group.addEventListener('ionReorderEnd', (ev) => {
|
||||
ev.detail.complete();
|
||||
window.dispatchEvent(new CustomEvent('ionItemReorderComplete'));
|
||||
window.dispatchEvent(new CustomEvent('ionReorderComplete'));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
});
|
||||
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
|
||||
const items = page.locator('ion-item');
|
||||
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
|
||||
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
|
||||
|
||||
await dragElementBy(items.nth(1), page, 0, 300);
|
||||
await ionItemReorderComplete.next();
|
||||
await ionReorderComplete.next();
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
|
||||
});
|
||||
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
|
||||
const reorderHandle = page.locator('ion-reorder');
|
||||
const items = page.locator('ion-item');
|
||||
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
|
||||
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
|
||||
|
||||
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
|
||||
|
||||
await dragElementBy(reorderHandle.nth(0), page, 0, 300);
|
||||
await ionItemReorderComplete.next();
|
||||
await ionReorderComplete.next();
|
||||
|
||||
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
|
||||
});
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import type { AnimationBuilder, ComponentProps } from '../../../interface';
|
||||
import type { AnimationBuilder, ComponentProps, HTMLStencilElement } from '../../../interface';
|
||||
import type { NavigationHookCallback } from '../../route/route-interface';
|
||||
|
||||
export interface HTMLStencilElement extends HTMLElement {
|
||||
componentOnReady(): Promise<this>;
|
||||
}
|
||||
|
||||
export interface NavOutlet {
|
||||
setRouteId(
|
||||
id: string,
|
||||
|
||||
@ -71,7 +71,27 @@ export class Tabs implements NavOutlet {
|
||||
componentWillRender() {
|
||||
const tabBar = this.el.querySelector('ion-tab-bar');
|
||||
if (tabBar) {
|
||||
const tab = this.selectedTab ? this.selectedTab.tab : undefined;
|
||||
let tab = this.selectedTab ? this.selectedTab.tab : undefined;
|
||||
|
||||
// Fallback: if no selectedTab is set but we're using router mode,
|
||||
// determine the active tab from the current URL. This works around
|
||||
// timing issues in React Router integration where setRouteId may not
|
||||
// be called in time for the initial render.
|
||||
// TODO(FW-6724): Remove this with React Router upgrade
|
||||
if (!tab && this.useRouter && typeof window !== 'undefined') {
|
||||
const currentPath = window.location.pathname;
|
||||
const tabButtons = this.el.querySelectorAll('ion-tab-button');
|
||||
|
||||
// Look for a tab button that matches the current path pattern
|
||||
for (const tabButton of tabButtons) {
|
||||
const tabId = tabButton.getAttribute('tab');
|
||||
if (tabId && currentPath.includes(tabId)) {
|
||||
tab = tabId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tabBar.selectedTab = tab;
|
||||
}
|
||||
}
|
||||
|
||||