Skip to content

Commit

Permalink
fix(range): disable scroll when range is being dragged (#29241)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
thetaPC authored Sep 11, 2024
1 parent f4ee2bb commit 35b1a45
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 64 deletions.
7 changes: 4 additions & 3 deletions core/src/components/range/range.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ export class Range implements ComponentInterface {
this.setupGesture();
}

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

disconnectedCallback() {
Expand Down Expand Up @@ -418,7 +419,7 @@ export class Range implements ComponentInterface {
*
* This only needs to be done once.
*/
if (contentEl && this.initialContentScrollY === undefined) {
if (contentEl && this.pressedKnob === undefined) {
this.initialContentScrollY = disableContentScrollY(contentEl);
}

Expand Down Expand Up @@ -469,7 +470,7 @@ export class Range implements ComponentInterface {
*
* The user can now scroll on the view in the next gesture event.
*/
if (contentEl && initialContentScrollY !== undefined) {
if (contentEl && this.pressedKnob !== undefined) {
resetContentScrollY(contentEl, initialContentScrollY);
}

Expand Down
105 changes: 55 additions & 50 deletions core/src/components/range/test/range-events.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import { configs, dragElementBy, test } from '@utils/test/playwright';
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('range: events:'), () => {
test.describe('range: knob events', () => {
/**
* The mouse events are flaky on CI
*/
test.fixme('should emit start/end events', async ({ page }) => {
test('should emit start/end events', async ({ page }) => {
/**
* Requires padding to prevent the knob from being clipped.
* If it's clipped, then the value might be one off.
Expand All @@ -31,23 +28,34 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>

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

await dragElementBy(rangeEl, page, 300, 0);
await page.waitForChanges();

/**
* dragElementBy defaults to starting the drag from the middle of the el,
* so the start value should jump to 50 despite the range defaulting to 20.
* Verify both events fire if range is dragged.
*/
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
await dragElementBy(rangeEl, page, 180);

await rangeStart.next();
await rangeEnd.next();

// Once the knob is dragged, the start event should fire with
// the initial value.
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
// Once the knob is released, the end event should fire with
// the final value.
expect(rangeEnd).toHaveReceivedEventDetail({ value: 100 });

/**
* Verify both events fire if range is clicked without dragging.
* Verify both events fire if range is tapped without dragging.
*/
await dragElementBy(rangeEl, page, 0, 0);
await page.waitForChanges();

expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
await rangeStart.next();
await rangeEnd.next();

// Once the tap is released, the start event should fire with
// the initial value.
expect(rangeStart).toHaveReceivedEventDetail({ value: 100 });
// Once the tap is released, the end event should fire with
// the final value.
expect(rangeEnd).toHaveReceivedEventDetail({ value: 50 });
});

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

expect(rangeEndSpy.length).toBe(1);
});

// TODO FW-2873
test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
skip.browser('webkit', 'mouse.wheel is not available in WebKit');

await page.goto(`/src/components/range/test/basic`, config);

const knobEl = page.locator('ion-range#stacked-range .range-knob-handle');
const scrollEl = page.locator('ion-content .inner-scroll');

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

await dragElementBy(knobEl, page, 30, 0, undefined, undefined, false);

/**
* Do not use scrollToBottom() or other scrolling methods
* on ion-content as those will update the scroll position.
* Setting scrollTop still works even with overflow-y: hidden.
* However, simulating a user gesture should not scroll the content.
*/
await page.mouse.wheel(0, 100);
await page.waitForChanges();

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

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

// TODO FW-2873
test.skip('should emit when the knob is released', async ({ page }) => {
await page.setContent(`<ion-range aria-label="range"></ion-range>`, config);
test('should emit when the knob is released', async ({ page }) => {
/**
* Requires padding to prevent the knob from being clipped.
* If it's clipped, then the value might be one off.
* For example, if the knob is clipped on the right, then the value
* will be 99 instead of 100.
*/
await page.setContent(
`
<div style="padding: 0 20px">
<ion-range aria-label="Range"></ion-range>
</div>
`,
config
);

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

await dragElementBy(rangeHandle, page, 100, 0);
await dragElementBy(rangeEl, page, 100);

await ionChangeSpy.next();

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

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

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

await rangeHandle.hover();

await dragElementBy(rangeHandle, page, 100, 0, undefined, undefined, false);
await dragElementBy(rangeEl, page, 100);

await ionInputSpy.next();

Expand Down
19 changes: 8 additions & 11 deletions core/src/components/range/test/scroll-target/range.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
import { configs, test, dragElementBy } from '@utils/test/playwright';

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

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

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

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

const box = (await knobEl.boundingBox())!;
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;

await page.mouse.move(centerX, centerY);
await page.mouse.down();
await page.mouse.move(centerX + 30, centerY);
await dragElementBy(rangeEl, page, 100, 0, undefined, undefined, false);

/**
* Do not use scrollToBottom() or other scrolling methods
Expand Down
73 changes: 73 additions & 0 deletions core/src/components/range/test/scroll/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Range - Scroll</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<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-header>
<ion-toolbar>
<ion-title>Range - Scroll</ion-title>
</ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
</p>

<ion-range value="40">
<div slot="label">Range Label</div>
<ion-label slot="start">Start</ion-label>
<ion-label slot="end">End</ion-label>
</ion-range>

<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
</p>

<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
</p>

<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
</p>

<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.!
</p>
</ion-content>
</ion-app>
</body>
</html>
46 changes: 46 additions & 0 deletions core/src/components/range/test/scroll/range.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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('range: scroll'), () => {
test('should not scroll when the knob is being dragged', async ({ page, skip }) => {
/**
* The Playwright team has stated that they will not implement this feature:
* https://github.com/microsoft/playwright/issues/28755
*/
skip.browser('webkit', 'mouse.wheel is not available in WebKit');

/**
* Requires padding to prevent the knob from being clipped.
* If it's clipped, then the value might be one off.
* For example, if the knob is clipped on the right, then the value
* will be 99 instead of 100.
*
* The ion-content is also required to be taller than the viewport
* to allow for scrolling.
*/
await page.goto(`/src/components/range/test/scroll`, config);

const rangeEl = page.locator('ion-range');
const scrollEl = page.locator('ion-content .inner-scroll');

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

await dragElementBy(rangeEl, page, 100, 0, undefined, undefined, false);

/**
* Do not use scrollToBottom() or other scrolling methods
* on ion-content as those will update the scroll position.
* Setting scrollTop still works even with overflow-y: hidden.
* However, simulating a user gesture should not scroll the content.
*/
await page.mouse.wheel(0, 100);
await page.waitForChanges();

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

0 comments on commit 35b1a45

Please sign in to comment.