mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
fix(range): dragging knob no longer scrolls page (#25343)
resolves #19004
This commit is contained in:
@ -13,6 +13,7 @@ import type {
|
|||||||
RangeValue,
|
RangeValue,
|
||||||
StyleEventDetail,
|
StyleEventDetail,
|
||||||
} from '../../interface';
|
} from '../../interface';
|
||||||
|
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '../../utils/content';
|
||||||
import type { Attributes } from '../../utils/helpers';
|
import type { Attributes } from '../../utils/helpers';
|
||||||
import { inheritAriaAttributes, clamp, debounceEvent, getAriaLabel, renderHiddenInput } from '../../utils/helpers';
|
import { inheritAriaAttributes, clamp, debounceEvent, getAriaLabel, renderHiddenInput } from '../../utils/helpers';
|
||||||
import { isRTL } from '../../utils/rtl';
|
import { isRTL } from '../../utils/rtl';
|
||||||
@ -50,6 +51,8 @@ export class Range implements ComponentInterface {
|
|||||||
private rangeSlider?: HTMLElement;
|
private rangeSlider?: HTMLElement;
|
||||||
private gesture?: Gesture;
|
private gesture?: Gesture;
|
||||||
private inheritedAttributes: Attributes = {};
|
private inheritedAttributes: Attributes = {};
|
||||||
|
private contentEl: HTMLElement | null = null;
|
||||||
|
private initialContentScrollY = true;
|
||||||
|
|
||||||
@Element() el!: HTMLIonRangeElement;
|
@Element() el!: HTMLIonRangeElement;
|
||||||
|
|
||||||
@ -259,6 +262,8 @@ export class Range implements ComponentInterface {
|
|||||||
if (this.didLoad) {
|
if (this.didLoad) {
|
||||||
this.setupGesture();
|
this.setupGesture();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.contentEl = findClosestIonContent(this.el);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
@ -313,6 +318,11 @@ export class Range implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onStart(detail: GestureDetail) {
|
private onStart(detail: GestureDetail) {
|
||||||
|
const { contentEl } = this;
|
||||||
|
if (contentEl) {
|
||||||
|
this.initialContentScrollY = disableContentScrollY(contentEl);
|
||||||
|
}
|
||||||
|
|
||||||
const rect = (this.rect = this.rangeSlider!.getBoundingClientRect() as any);
|
const rect = (this.rect = this.rangeSlider!.getBoundingClientRect() as any);
|
||||||
const currentX = detail.currentX;
|
const currentX = detail.currentX;
|
||||||
|
|
||||||
@ -337,6 +347,11 @@ export class Range implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onEnd(detail: GestureDetail) {
|
private onEnd(detail: GestureDetail) {
|
||||||
|
const { contentEl, initialContentScrollY } = this;
|
||||||
|
if (contentEl) {
|
||||||
|
resetContentScrollY(contentEl, initialContentScrollY);
|
||||||
|
}
|
||||||
|
|
||||||
this.update(detail.currentX);
|
this.update(detail.currentX);
|
||||||
this.pressedKnob = undefined;
|
this.pressedKnob = undefined;
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label position="stacked">Stacked Label</ion-label>
|
<ion-label position="stacked">Stacked Label</ion-label>
|
||||||
<ion-range value="40">
|
<ion-range value="40" id="stacked-range">
|
||||||
<ion-label slot="start">Start</ion-label>
|
<ion-label slot="start">Start</ion-label>
|
||||||
<ion-label slot="end">End</ion-label>
|
<ion-label slot="end">End</ion-label>
|
||||||
</ion-range>
|
</ion-range>
|
||||||
|
@ -53,4 +53,35 @@ test.describe('range: basic', () => {
|
|||||||
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
|
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
|
||||||
expect(rangeEnd).toHaveReceivedEventDetail({ value: 21 });
|
expect(rangeEnd).toHaveReceivedEventDetail({ value: 21 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not scroll when the knob is swiped', async ({ page, browserName }, testInfo) => {
|
||||||
|
test.skip(browserName === 'webkit', 'mouse.wheel is not available in WebKit');
|
||||||
|
test.skip(testInfo.project.metadata.rtl === true, 'This feature does not have RTL-specific behaviors');
|
||||||
|
|
||||||
|
await page.goto(`/src/components/range/test/basic`);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
85
core/src/components/range/test/scroll-target/index.html
Normal file
85
core/src/components/range/test/scroll-target/index.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Range - Scroll Target</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>
|
||||||
|
<style>
|
||||||
|
.ion-content-scroll-host {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Range - Scroll Target</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content class="ion-padding" scroll-y="false">
|
||||||
|
<div class="ion-content-scroll-host">
|
||||||
|
<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-item>
|
||||||
|
<ion-label position="stacked">Stacked Label</ion-label>
|
||||||
|
<ion-range value="40">
|
||||||
|
<ion-label slot="start">Start</ion-label>
|
||||||
|
<ion-label slot="end">End</ion-label>
|
||||||
|
</ion-range>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
|
</ion-app>
|
||||||
|
</body>
|
||||||
|
</html>
|
35
core/src/components/range/test/scroll-target/range.e2e.ts
Normal file
35
core/src/components/range/test/scroll-target/range.e2e.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import { test } from '@utils/test/playwright';
|
||||||
|
|
||||||
|
test.describe('range: scroll-target', () => {
|
||||||
|
test('should not scroll when the knob is swiped in custom scroll target', async ({ page, browserName }, testInfo) => {
|
||||||
|
test.skip(browserName === 'webkit', 'mouse.wheel is not available in WebKit');
|
||||||
|
test.skip(testInfo.project.metadata.rtl === true, 'This feature does not have RTL-specific behaviors');
|
||||||
|
|
||||||
|
await page.goto(`/src/components/range/test/scroll-target`);
|
||||||
|
|
||||||
|
const knobEl = page.locator('ion-range .range-knob-handle');
|
||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
});
|
@ -101,3 +101,37 @@ export const scrollByPoint = (el: HTMLElement, x: number, y: number, durationMs:
|
|||||||
export const printIonContentErrorMsg = (el: HTMLElement) => {
|
export const printIonContentErrorMsg = (el: HTMLElement) => {
|
||||||
return printRequiredElementError(el, ION_CONTENT_ELEMENT_SELECTOR);
|
return printRequiredElementError(el, ION_CONTENT_ELEMENT_SELECTOR);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Several components in Ionic need to prevent scrolling
|
||||||
|
* during a gesture (card modal, range, item sliding, etc).
|
||||||
|
* Use this utility to account for ion-content and custom content hosts.
|
||||||
|
*/
|
||||||
|
export const disableContentScrollY = (contentEl: HTMLElement): boolean => {
|
||||||
|
if (isIonContent(contentEl)) {
|
||||||
|
const ionContent = contentEl as HTMLIonContentElement;
|
||||||
|
const initialScrollY = ionContent.scrollY;
|
||||||
|
ionContent.scrollY = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should be passed into resetContentScrollY
|
||||||
|
* so that we can revert ion-content's scrollY to the
|
||||||
|
* correct state. For example, if scrollY = false
|
||||||
|
* initially, we do not want to enable scrolling
|
||||||
|
* when we call resetContentScrollY.
|
||||||
|
*/
|
||||||
|
return initialScrollY;
|
||||||
|
} else {
|
||||||
|
contentEl.style.setProperty('overflow', 'hidden');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetContentScrollY = (contentEl: HTMLElement, initialScrollY: boolean) => {
|
||||||
|
if (isIonContent(contentEl)) {
|
||||||
|
(contentEl as HTMLIonContentElement).scrollY = initialScrollY;
|
||||||
|
} else {
|
||||||
|
contentEl.style.removeProperty('overflow');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user