Skip to content

Commit 35b1a45

Browse files
authored
fix(range): disable scroll when range is being dragged (#29241)
Issue number: internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> There are a few tests that were disabled due to being flaky from gestures. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> While fixing the tests, I found a bug that the scroll was never being disabled on scroll. Additionally, we were not taking into account that a custom scroll target could be used so it was never disabled either. - Fixed the flaky tests. - Content doesn't scroll when range is being dragged. - Content can be either `ion-content` or a custom scroll target. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> [Preview for `ion-content`](https://ionic-framework-git-fw-2873-ionic1.vercel.app/src/components/range/test/scroll) [Preview for custom scroll target](https://ionic-framework-git-fw-2873-ionic1.vercel.app/src/components/range/test/scroll-target) How to test: 1. Open either of the previews 2. Render the screen with the device simulator from the browser 3. Verify that you can scroll the page 4. Drag the range but don't let go 5. Verify that you cannot scroll the page 6. Repeat steps 2-5 with the other preview
1 parent f4ee2bb commit 35b1a45

File tree

5 files changed

+186
-64
lines changed

5 files changed

+186
-64
lines changed

core/src/components/range/range.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,8 @@ export class Range implements ComponentInterface {
325325
this.setupGesture();
326326
}
327327

328-
this.contentEl = findClosestIonContent(this.el);
328+
const ionContent = findClosestIonContent(this.el);
329+
this.contentEl = ionContent?.querySelector('.ion-content-scroll-host') ?? ionContent;
329330
}
330331

331332
disconnectedCallback() {
@@ -418,7 +419,7 @@ export class Range implements ComponentInterface {
418419
*
419420
* This only needs to be done once.
420421
*/
421-
if (contentEl && this.initialContentScrollY === undefined) {
422+
if (contentEl && this.pressedKnob === undefined) {
422423
this.initialContentScrollY = disableContentScrollY(contentEl);
423424
}
424425

@@ -469,7 +470,7 @@ export class Range implements ComponentInterface {
469470
*
470471
* The user can now scroll on the view in the next gesture event.
471472
*/
472-
if (contentEl && initialContentScrollY !== undefined) {
473+
if (contentEl && this.pressedKnob !== undefined) {
473474
resetContentScrollY(contentEl, initialContentScrollY);
474475
}
475476

core/src/components/range/test/range-events.e2e.ts

Lines changed: 55 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import { configs, dragElementBy, test } from '@utils/test/playwright';
77
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
88
test.describe(title('range: events:'), () => {
99
test.describe('range: knob events', () => {
10-
/**
11-
* The mouse events are flaky on CI
12-
*/
13-
test.fixme('should emit start/end events', async ({ page }) => {
10+
test('should emit start/end events', async ({ page }) => {
1411
/**
1512
* Requires padding to prevent the knob from being clipped.
1613
* If it's clipped, then the value might be one off.
@@ -31,23 +28,34 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
3128

3229
const rangeEl = page.locator('ion-range');
3330

34-
await dragElementBy(rangeEl, page, 300, 0);
35-
await page.waitForChanges();
36-
3731
/**
38-
* dragElementBy defaults to starting the drag from the middle of the el,
39-
* so the start value should jump to 50 despite the range defaulting to 20.
32+
* Verify both events fire if range is dragged.
4033
*/
41-
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
34+
await dragElementBy(rangeEl, page, 180);
35+
36+
await rangeStart.next();
37+
await rangeEnd.next();
38+
39+
// Once the knob is dragged, the start event should fire with
40+
// the initial value.
41+
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
42+
// Once the knob is released, the end event should fire with
43+
// the final value.
4244
expect(rangeEnd).toHaveReceivedEventDetail({ value: 100 });
4345

4446
/**
45-
* Verify both events fire if range is clicked without dragging.
47+
* Verify both events fire if range is tapped without dragging.
4648
*/
4749
await dragElementBy(rangeEl, page, 0, 0);
48-
await page.waitForChanges();
4950

50-
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
51+
await rangeStart.next();
52+
await rangeEnd.next();
53+
54+
// Once the tap is released, the start event should fire with
55+
// the initial value.
56+
expect(rangeStart).toHaveReceivedEventDetail({ value: 100 });
57+
// Once the tap is released, the end event should fire with
58+
// the final value.
5159
expect(rangeEnd).toHaveReceivedEventDetail({ value: 50 });
5260
});
5361

@@ -99,31 +107,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
99107

100108
expect(rangeEndSpy.length).toBe(1);
101109
});
102-
103-
// TODO FW-2873
104-
test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
105-
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
106-
107-
await page.goto(`/src/components/range/test/basic`, config);
108-
109-
const knobEl = page.locator('ion-range#stacked-range .range-knob-handle');
110-
const scrollEl = page.locator('ion-content .inner-scroll');
111-
112-
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
113-
114-
await dragElementBy(knobEl, page, 30, 0, undefined, undefined, false);
115-
116-
/**
117-
* Do not use scrollToBottom() or other scrolling methods
118-
* on ion-content as those will update the scroll position.
119-
* Setting scrollTop still works even with overflow-y: hidden.
120-
* However, simulating a user gesture should not scroll the content.
121-
*/
122-
await page.mouse.wheel(0, 100);
123-
await page.waitForChanges();
124-
125-
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
126-
});
127110
});
128111

129112
test.describe('ionChange', () => {
@@ -151,14 +134,26 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
151134
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
152135
});
153136

154-
// TODO FW-2873
155-
test.skip('should emit when the knob is released', async ({ page }) => {
156-
await page.setContent(`<ion-range aria-label="range"></ion-range>`, config);
137+
test('should emit when the knob is released', async ({ page }) => {
138+
/**
139+
* Requires padding to prevent the knob from being clipped.
140+
* If it's clipped, then the value might be one off.
141+
* For example, if the knob is clipped on the right, then the value
142+
* will be 99 instead of 100.
143+
*/
144+
await page.setContent(
145+
`
146+
<div style="padding: 0 20px">
147+
<ion-range aria-label="Range"></ion-range>
148+
</div>
149+
`,
150+
config
151+
);
157152

158-
const rangeHandle = page.locator('ion-range .range-knob-handle');
153+
const rangeEl = page.locator('ion-range');
159154
const ionChangeSpy = await page.spyOnEvent('ionChange');
160155

161-
await dragElementBy(rangeHandle, page, 100, 0);
156+
await dragElementBy(rangeEl, page, 100);
162157

163158
await ionChangeSpy.next();
164159

@@ -196,16 +191,26 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
196191
});
197192

198193
test.describe('ionInput', () => {
199-
// TODO(FW-2873) Enable this test when touch events/gestures are better supported in Playwright
200-
test.skip('should emit when the knob is dragged', async ({ page }) => {
201-
await page.setContent(`<ion-range aria-label="range"></ion-range>`, config);
194+
test('should emit when the knob is dragged', async ({ page }) => {
195+
/**
196+
* Requires padding to prevent the knob from being clipped.
197+
* If it's clipped, then the value might be one off.
198+
* For example, if the knob is clipped on the right, then the value
199+
* will be 99 instead of 100.
200+
*/
201+
await page.setContent(
202+
`
203+
<div style="padding: 0 20px">
204+
<ion-range aria-label="range"></ion-range>
205+
</div>
206+
`,
207+
config
208+
);
202209

203-
const rangeHandle = page.locator('ion-range .range-knob-handle');
210+
const rangeEl = page.locator('ion-range');
204211
const ionInputSpy = await page.spyOnEvent('ionInput');
205212

206-
await rangeHandle.hover();
207-
208-
await dragElementBy(rangeHandle, page, 100, 0, undefined, undefined, false);
213+
await dragElementBy(rangeEl, page, 100);
209214

210215
await ionInputSpy.next();
211216

core/src/components/range/test/scroll-target/range.e2e.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,26 @@
11
import { expect } from '@playwright/test';
2-
import { configs, test } from '@utils/test/playwright';
2+
import { configs, test, dragElementBy } from '@utils/test/playwright';
33

4-
// TODO FW-2873
54
/**
65
* This behavior does not vary across modes/directions.
76
*/
87
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
9-
test.describe.skip(title('range: scroll-target'), () => {
8+
test.describe(title('range: scroll-target'), () => {
109
test('should not scroll when the knob is swiped in custom scroll target', async ({ page, skip }) => {
10+
/**
11+
* The Playwright team has stated that they will not implement this feature:
12+
* https://github.com/microsoft/playwright/issues/28755
13+
*/
1114
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
1215

1316
await page.goto(`/src/components/range/test/scroll-target`, config);
1417

15-
const knobEl = page.locator('ion-range .range-knob-handle');
18+
const rangeEl = page.locator('ion-range');
1619
const scrollEl = page.locator('.ion-content-scroll-host');
1720

1821
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
1922

20-
const box = (await knobEl.boundingBox())!;
21-
const centerX = box.x + box.width / 2;
22-
const centerY = box.y + box.height / 2;
23-
24-
await page.mouse.move(centerX, centerY);
25-
await page.mouse.down();
26-
await page.mouse.move(centerX + 30, centerY);
23+
await dragElementBy(rangeEl, page, 100, 0, undefined, undefined, false);
2724

2825
/**
2926
* Do not use scrollToBottom() or other scrolling methods
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Range - Scroll</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
</head>
16+
<body>
17+
<ion-app>
18+
<ion-header>
19+
<ion-toolbar>
20+
<ion-title>Range - Scroll</ion-title>
21+
</ion-toolbar>
22+
</ion-header>
23+
24+
<ion-content class="ion-padding">
25+
<p>
26+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
27+
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
28+
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
29+
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
30+
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
31+
</p>
32+
33+
<ion-range value="40">
34+
<div slot="label">Range Label</div>
35+
<ion-label slot="start">Start</ion-label>
36+
<ion-label slot="end">End</ion-label>
37+
</ion-range>
38+
39+
<p>
40+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
41+
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
42+
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
43+
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
44+
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
45+
</p>
46+
47+
<p>
48+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
49+
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
50+
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
51+
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
52+
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
53+
</p>
54+
55+
<p>
56+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
57+
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
58+
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
59+
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
60+
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
61+
</p>
62+
63+
<p>
64+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
65+
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
66+
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
67+
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
68+
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.!
69+
</p>
70+
</ion-content>
71+
</ion-app>
72+
</body>
73+
</html>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, dragElementBy, test } from '@utils/test/playwright';
3+
4+
/**
5+
* This behavior does not vary across modes/directions.
6+
*/
7+
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
8+
test.describe(title('range: scroll'), () => {
9+
test('should not scroll when the knob is being dragged', async ({ page, skip }) => {
10+
/**
11+
* The Playwright team has stated that they will not implement this feature:
12+
* https://github.com/microsoft/playwright/issues/28755
13+
*/
14+
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
15+
16+
/**
17+
* Requires padding to prevent the knob from being clipped.
18+
* If it's clipped, then the value might be one off.
19+
* For example, if the knob is clipped on the right, then the value
20+
* will be 99 instead of 100.
21+
*
22+
* The ion-content is also required to be taller than the viewport
23+
* to allow for scrolling.
24+
*/
25+
await page.goto(`/src/components/range/test/scroll`, config);
26+
27+
const rangeEl = page.locator('ion-range');
28+
const scrollEl = page.locator('ion-content .inner-scroll');
29+
30+
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
31+
32+
await dragElementBy(rangeEl, page, 100, 0, undefined, undefined, false);
33+
34+
/**
35+
* Do not use scrollToBottom() or other scrolling methods
36+
* on ion-content as those will update the scroll position.
37+
* Setting scrollTop still works even with overflow-y: hidden.
38+
* However, simulating a user gesture should not scroll the content.
39+
*/
40+
await page.mouse.wheel(0, 100);
41+
await page.waitForChanges();
42+
43+
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
44+
});
45+
});
46+
});

0 commit comments

Comments
 (0)