Skip to content
Open
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
5 changes: 5 additions & 0 deletions core/src/components/item-sliding/item-sliding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,11 @@ export class ItemSliding implements ComponentInterface {
if (this.tmr !== undefined) {
clearTimeout(this.tmr);
this.tmr = undefined;
// Reopening cancels the close animation, including its interaction cleanup.
this.el.classList.remove('item-sliding-closing');
if (this.gesture) {
this.gesture.enable(!this.disabled);
}
}
if (!this.item) {
return;
Expand Down
40 changes: 40 additions & 0 deletions core/src/components/item-sliding/test/open/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Item Sliding - Switching Sides</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-content>
<p>
Open one side, then the other. The new option should remain clickable and the item should still swipe closed.
</p>
<button onclick="document.querySelector('ion-item-sliding').open('start')">Open start</button>
<button onclick="document.querySelector('ion-item-sliding').open('end')">Open end</button>
<ion-list>
<ion-item-sliding>
<ion-item><ion-label>Sliding item</ion-label></ion-item>
<ion-item-options side="start">
<ion-item-option onclick="document.querySelector('output').textContent = 'Start clicked'"
>Start</ion-item-option
>
</ion-item-options>
<ion-item-options side="end">
<ion-item-option onclick="document.querySelector('output').textContent = 'End clicked'"
>End</ion-item-option
>
</ion-item-options>
</ion-item-sliding>
</ion-list>
<output aria-live="polite"></output>
</ion-content>
</ion-app>
</body>
</html>
129 changes: 129 additions & 0 deletions core/src/components/item-sliding/test/open/item-sliding.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { expect } from '@playwright/test';
import type { Locator } from '@playwright/test';
import { configs, dragElementBy, test } from '@utils/test/playwright';

const openSide = async (item: Locator, side: 'start' | 'end', direction: 'ltr' | 'rtl') => {
await item.evaluate((el: HTMLIonItemSlidingElement, side) => el.open(side), side);
const ratio = (side === 'end' ? 1 : -1) * (direction === 'rtl' ? -1 : 1);
await expect.poll(() => item.evaluate((el: HTMLIonItemSlidingElement) => el.getSlidingRatio())).toBe(ratio);
const itemElement = await item.locator('ion-item').elementHandle();
await itemElement!.waitForElementState('stable');
};

const slidingItem = `
<ion-item-sliding>
<ion-item><ion-label>Sliding item</ion-label></ion-item>
<ion-item-options side="start">
<ion-item-option onclick="document.querySelector('output').textContent = 'start'">Start</ion-item-option>
</ion-item-options>
<ion-item-options side="end">
<ion-item-option onclick="document.querySelector('output').textContent = 'end'">End</ion-item-option>
</ion-item-options>
</ion-item-sliding>
`;

// Programmatic open() in RTL is tracked separately in FW-3711.
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('item-sliding: switching open sides'), () => {
test.describe('single item', () => {
test.beforeEach(async ({ page }) => {
await page.setContent(
`
<ion-list>
${slidingItem}
</ion-list>
<output></output>
`,
config
);
});

for (const side of ['start', 'end'] as const) {
const previousSide = side === 'start' ? 'end' : 'start';

test(`should allow option clicks after opening ${side} from ${previousSide}`, async ({ page }) => {
const item = page.locator('ion-item-sliding');
await openSide(item, previousSide, config.direction);
await openSide(item, side, config.direction);

await page.locator(`ion-item-options[side="${side}"] ion-item-option`).click();
await expect(page.locator('output')).toHaveText(side);
});

test(`should allow swiping closed after opening ${side} from ${previousSide}`, async ({ page }) => {
const item = page.locator('ion-item-sliding');
await openSide(item, previousSide, config.direction);
await openSide(item, side, config.direction);

await dragElementBy(
item,
page,
(await item.evaluate((el: HTMLIonItemSlidingElement) => el.getOpenAmount())) * 0.75
);
await expect.poll(() => item.evaluate((el: HTMLIonItemSlidingElement) => el.getOpenAmount())).toBe(0);
});

test(`should preserve disabled gestures when switching to ${side}`, async ({ page }) => {
const item = page.locator('ion-item-sliding');
await openSide(item, previousSide, config.direction);
await item.evaluate((el: HTMLIonItemSlidingElement) => (el.disabled = true));
await page.waitForChanges();
await openSide(item, side, config.direction);

await dragElementBy(
item,
page,
(await item.evaluate((el: HTMLIonItemSlidingElement) => el.getOpenAmount())) * 0.75
);
expect(
await item.evaluate(async (el: HTMLIonItemSlidingElement) => Math.abs(await el.getSlidingRatio()))
).toBe(1);

await item.evaluate((el: HTMLIonItemSlidingElement) => (el.disabled = false));
await page.waitForChanges();
await dragElementBy(
item,
page,
(await item.evaluate((el: HTMLIonItemSlidingElement) => el.getOpenAmount())) * 0.75
);
await expect.poll(() => item.evaluate((el: HTMLIonItemSlidingElement) => el.getOpenAmount())).toBe(0);
});
}

test('should keep options clickable after repeated side changes', async ({ page }) => {
const item = page.locator('ion-item-sliding');
for (const side of ['start', 'end', 'start', 'end'] as const) {
await openSide(item, side, config.direction);
}

await page.locator('ion-item-options[side="end"] ion-item-option').click();
await expect(page.locator('output')).toHaveText('end');
});

test('should still finish closing before allowing a new swipe', async ({ page }) => {
const item = page.locator('ion-item-sliding');
await openSide(item, 'end', config.direction);
await openSide(item, 'start', config.direction);
await item.evaluate((el: HTMLIonItemSlidingElement) => el.close());
await expect(item).not.toHaveClass(/item-sliding-active-slide/);

await dragElementBy(item, page, config.direction === 'rtl' ? 150 : -150);
await expect
.poll(() => item.evaluate((el: HTMLIonItemSlidingElement) => el.getSlidingRatio()))
.toBe(config.direction === 'rtl' ? -1 : 1);
});
});

test('should still close the first item when another item opens', async ({ page }) => {
await page.setContent(`<ion-list>${slidingItem}${slidingItem}</ion-list><output></output>`, config);
const first = page.locator('ion-item-sliding').first();
const second = page.locator('ion-item-sliding').last();
await openSide(first, 'end', config.direction);
await openSide(second, 'start', config.direction);

await expect(first).not.toHaveClass(/item-sliding-active-slide/);
await second.locator('ion-item-options[side="start"] ion-item-option').click();
await expect(page.locator('output')).toHaveText('start');
});
});
});
Loading