Skip to content

Commit bd8568f

Browse files
Fix disabled option bug + add tests
1 parent 63274c0 commit bd8568f

2 files changed

Lines changed: 128 additions & 2 deletions

File tree

core/src/components/item-sliding/item-sliding.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,14 +259,14 @@ export class ItemSliding implements ComponentInterface {
259259
}
260260

261261
/**
262-
* Check if the given item options element contains at least one expandable option.
262+
* Check if the given item options element contains at least one expandable, non-disabled option.
263263
*/
264264
private hasExpandableOptions(options: HTMLIonItemOptionsElement | undefined): boolean {
265265
if (!options) return false;
266266

267267
const optionElements = options.querySelectorAll('ion-item-option');
268268
return Array.from(optionElements).some((option: any) => {
269-
return option.expandable === true;
269+
return option.expandable === true && !option.disabled;
270270
});
271271
}
272272

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, dragElementBy, test } from '@utils/test/playwright';
3+
4+
/**
5+
* Full swipe animation behavior is mode-independent but
6+
* child components (ion-item-options, ion-item-option) have
7+
* mode-specific styling, so we test across all modes.
8+
*
9+
* When an item has at least one expandable option and the user swipes
10+
* beyond the threshold (or with sufficient velocity), the item slides
11+
* off-screen, fires ionSwipe, and returns to its closed position.
12+
*/
13+
14+
// Full animation cycle duration (100ms expand + 250ms off-screen + 300ms delay + 250ms return)
15+
const FULL_ANIMATION_MS = 1100;
16+
17+
configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, config }) => {
18+
test.describe(title('item-sliding: full swipe'), () => {
19+
test.beforeEach(async ({ page }) => {
20+
await page.goto(`/src/components/item-sliding/test/full-swipe`, config);
21+
});
22+
23+
test('should fire ionSwipe when expandable option is swiped fully (end side)', async ({
24+
page,
25+
}) => {
26+
const ionSwipe = await page.spyOnEvent('ionSwipe');
27+
const item = page.locator('#expandable-end');
28+
29+
await dragElementBy(item, page, -190);
30+
await page.waitForTimeout(FULL_ANIMATION_MS);
31+
32+
expect(ionSwipe.length).toBeGreaterThan(0);
33+
});
34+
35+
test('should fire ionSwipe when expandable option is swiped fully (start side)', async ({
36+
page,
37+
}) => {
38+
const ionSwipe = await page.spyOnEvent('ionSwipe');
39+
const item = page.locator('#expandable-start');
40+
41+
await dragElementBy(item, page, 190);
42+
await page.waitForTimeout(FULL_ANIMATION_MS);
43+
44+
expect(ionSwipe.length).toBeGreaterThan(0);
45+
});
46+
47+
test('should return to closed state after full swipe animation completes', async ({ page }) => {
48+
const item = page.locator('#expandable-end');
49+
50+
await dragElementBy(item, page, -190);
51+
await page.waitForTimeout(FULL_ANIMATION_MS);
52+
await page.waitForChanges();
53+
54+
const openAmount = await item.evaluate((el: HTMLIonItemSlidingElement) =>
55+
el.getOpenAmount()
56+
);
57+
expect(openAmount).toBe(0);
58+
});
59+
60+
test('should NOT trigger full swipe animation for non-expandable options', async ({ page }) => {
61+
const ionSwipe = await page.spyOnEvent('ionSwipe');
62+
const item = page.locator('#non-expandable');
63+
64+
await dragElementBy(item, page, -180);
65+
await page.waitForTimeout(600);
66+
67+
// Non-expandable item should never fire ionSwipe
68+
expect(ionSwipe.length).toBe(0);
69+
});
70+
});
71+
});
72+
73+
/**
74+
* Velocity-based trigger: a fast short swipe should trigger the full animation
75+
* even if the raw distance alone wouldn't exceed the threshold.
76+
* This behavior does not vary across modes.
77+
*/
78+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
79+
test.describe(title('item-sliding: full swipe velocity'), () => {
80+
test('should trigger full swipe animation with fast velocity', async ({ page }) => {
81+
await page.goto(`/src/components/item-sliding/test/full-swipe`, config);
82+
83+
const ionSwipe = await page.spyOnEvent('ionSwipe');
84+
const item = page.locator('#expandable-end');
85+
const box = (await item.boundingBox())!;
86+
87+
// Few steps = high velocity gesture
88+
const startX = box.x + box.width - 10;
89+
const startY = box.y + box.height / 2;
90+
const endX = box.x + 30;
91+
92+
await page.mouse.move(startX, startY);
93+
await page.mouse.down();
94+
await page.mouse.move(endX, startY, { steps: 3 });
95+
await page.mouse.up();
96+
await page.waitForTimeout(FULL_ANIMATION_MS);
97+
98+
expect(ionSwipe.length).toBeGreaterThan(0);
99+
});
100+
});
101+
});
102+
103+
/**
104+
* RTL support: swipe direction is mirrored. In RTL, swiping right
105+
* reveals the "end" side options and should trigger the full animation.
106+
*/
107+
configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEach(
108+
({ title, config }) => {
109+
test.describe(title('item-sliding: full swipe'), () => {
110+
test('should fire ionSwipe in the correct swipe direction', async ({ page }) => {
111+
await page.goto(`/src/components/item-sliding/test/full-swipe`, config);
112+
113+
const ionSwipe = await page.spyOnEvent('ionSwipe');
114+
const item = page.locator('#expandable-end');
115+
116+
// In RTL the "end" side is on the left, revealed by dragging right
117+
const dragByX = config.direction === 'rtl' ? 190 : -190;
118+
119+
await dragElementBy(item, page, dragByX);
120+
await page.waitForTimeout(FULL_ANIMATION_MS);
121+
122+
expect(ionSwipe.length).toBeGreaterThan(0);
123+
});
124+
});
125+
}
126+
);

0 commit comments

Comments
 (0)