diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx
index d2bce0e0dfd..d3272a620ae 100644
--- a/core/src/components/item-sliding/item-sliding.tsx
+++ b/core/src/components/item-sliding/item-sliding.tsx
@@ -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;
diff --git a/core/src/components/item-sliding/test/open/index.html b/core/src/components/item-sliding/test/open/index.html
new file mode 100644
index 00000000000..32e42bc234e
--- /dev/null
+++ b/core/src/components/item-sliding/test/open/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+ Item Sliding - Switching Sides
+
+
+
+
+
+
+
+
+
+
+
+ Open one side, then the other. The new option should remain clickable and the item should still swipe closed.
+
+
+
+
+
+ Sliding item
+
+ Start
+
+
+ End
+
+
+
+
+
+
+
+
diff --git a/core/src/components/item-sliding/test/open/item-sliding.e2e.ts b/core/src/components/item-sliding/test/open/item-sliding.e2e.ts
new file mode 100644
index 00000000000..0fa02f9e5c8
--- /dev/null
+++ b/core/src/components/item-sliding/test/open/item-sliding.e2e.ts
@@ -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 = `
+
+ Sliding item
+
+ Start
+
+
+ End
+
+
+`;
+
+// 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(
+ `
+
+ ${slidingItem}
+
+
+ `,
+ 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(`${slidingItem}${slidingItem}`, 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');
+ });
+ });
+});