mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +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,
|
||||
StyleEventDetail,
|
||||
} from '../../interface';
|
||||
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '../../utils/content';
|
||||
import type { Attributes } from '../../utils/helpers';
|
||||
import { inheritAriaAttributes, clamp, debounceEvent, getAriaLabel, renderHiddenInput } from '../../utils/helpers';
|
||||
import { isRTL } from '../../utils/rtl';
|
||||
@ -50,6 +51,8 @@ export class Range implements ComponentInterface {
|
||||
private rangeSlider?: HTMLElement;
|
||||
private gesture?: Gesture;
|
||||
private inheritedAttributes: Attributes = {};
|
||||
private contentEl: HTMLElement | null = null;
|
||||
private initialContentScrollY = true;
|
||||
|
||||
@Element() el!: HTMLIonRangeElement;
|
||||
|
||||
@ -259,6 +262,8 @@ export class Range implements ComponentInterface {
|
||||
if (this.didLoad) {
|
||||
this.setupGesture();
|
||||
}
|
||||
|
||||
this.contentEl = findClosestIonContent(this.el);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@ -313,6 +318,11 @@ export class Range implements ComponentInterface {
|
||||
}
|
||||
|
||||
private onStart(detail: GestureDetail) {
|
||||
const { contentEl } = this;
|
||||
if (contentEl) {
|
||||
this.initialContentScrollY = disableContentScrollY(contentEl);
|
||||
}
|
||||
|
||||
const rect = (this.rect = this.rangeSlider!.getBoundingClientRect() as any);
|
||||
const currentX = detail.currentX;
|
||||
|
||||
@ -337,6 +347,11 @@ export class Range implements ComponentInterface {
|
||||
}
|
||||
|
||||
private onEnd(detail: GestureDetail) {
|
||||
const { contentEl, initialContentScrollY } = this;
|
||||
if (contentEl) {
|
||||
resetContentScrollY(contentEl, initialContentScrollY);
|
||||
}
|
||||
|
||||
this.update(detail.currentX);
|
||||
this.pressedKnob = undefined;
|
||||
|
||||
|
@ -93,7 +93,7 @@
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<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="end">End</ion-label>
|
||||
</ion-range>
|
||||
|
@ -53,4 +53,35 @@ test.describe('range: basic', () => {
|
||||
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
|
||||
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) => {
|
||||
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