mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
chore: sync with main (#29143)
This commit is contained in:
23
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
23
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -20,12 +20,10 @@ body:
|
||||
id: affected-versions
|
||||
attributes:
|
||||
label: Ionic Framework Version
|
||||
description: Which version(s) of Ionic Framework does this issue impact? For Ionic Framework 1.x issues, please use https://github.com/ionic-team/ionic-v1. For Ionic Framework 2.x and 3.x issues, please use https://github.com/ionic-team/ionic-v3.
|
||||
description: Which version(s) of Ionic Framework does this issue impact? [Ionic Framework 1.x to 6.x are no longer supported](https://ionicframework.com/docs/reference/support#framework-maintenance-and-support-status). For extended support, considering visiting [Ionic's Enterprise offering](https://ionic.io/enterprise).
|
||||
options:
|
||||
- v4.x
|
||||
- v5.x
|
||||
- v6.x
|
||||
- v7.x
|
||||
- v8.x (Beta)
|
||||
- Nightly
|
||||
multiple: true
|
||||
validations:
|
||||
@ -51,11 +49,11 @@ body:
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Please explain the steps required to duplicate this issue.
|
||||
description: Explain the steps required to reproduce this issue.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Observe: '...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -63,8 +61,15 @@ body:
|
||||
id: reproduction-url
|
||||
attributes:
|
||||
label: Code Reproduction URL
|
||||
description: Please reproduce this issue in a blank Ionic Framework starter application and provide a link to the repo. Try out our [Getting Started Wizard](https://ionicframework.com/start#basics) to quickly spin up an Ionic Framework starter app. This is the best way to ensure this issue is triaged quickly. Issues without a code reproduction may be closed if the Ionic Team cannot reproduce the issue you are reporting.
|
||||
description: |
|
||||
Reproduce this issue in a blank [Ionic Framework starter application](https://ionicframework.com/start#basics) or a Stackblitz example.
|
||||
|
||||
You can use the Stackblitz button available on any of the [component playgrounds](https://ionicframework.com/docs/components) to open an editable example. Remember to save your changes to obtain a link to copy.
|
||||
|
||||
Reproductions cases must be minimal and focused around the specific problem you are experiencing. This is the best way to ensure this issue is triaged quickly. Issues without a code reproduction may be closed if the Ionic Team cannot reproduce the issue you are reporting.
|
||||
placeholder: https://github.com/...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: ionic-info
|
||||
|
24
core/package-lock.json
generated
24
core/package-lock.json
generated
@ -633,9 +633,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@capacitor/core": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.1.tgz",
|
||||
"integrity": "sha512-bwmka6FdvyXOpc5U6bOyx58S/Yl6r5lO2TK561f//KnjyXjxav25HWwhV4hthq3ZxJBMiAEucl9RK5vzgkP4Lw==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.2.tgz",
|
||||
"integrity": "sha512-/OUtfINmk7ke32VtKIHRAy8NlunbeK+aCqCHOS+fvtr7nUsOJXPkYgbgqZp/CWXET/gSK1xxMecaVBzpE98UKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
@ -1759,9 +1759,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.4.tgz",
|
||||
"integrity": "sha512-KrwoXu9J1loWSvQQReilGPkt6/dCH/x5eTBDecCBPclz7vxUM13Iw9almBIffEpurk/kaMAglH0G7sAF/A2y1A==",
|
||||
"version": "4.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.5.tgz",
|
||||
"integrity": "sha512-vSyFjY7XSEx0ufa9SebOd437CvnneaTXlCpuGDhjUDxAjGBlu6ie5qHyubobVGBth//aErc6wZPHc6W75Vp3iQ==",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
@ -10416,9 +10416,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@capacitor/core": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.1.tgz",
|
||||
"integrity": "sha512-bwmka6FdvyXOpc5U6bOyx58S/Yl6r5lO2TK561f//KnjyXjxav25HWwhV4hthq3ZxJBMiAEucl9RK5vzgkP4Lw==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.2.tgz",
|
||||
"integrity": "sha512-/OUtfINmk7ke32VtKIHRAy8NlunbeK+aCqCHOS+fvtr7nUsOJXPkYgbgqZp/CWXET/gSK1xxMecaVBzpE98UKA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
@ -11229,9 +11229,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@stencil/core": {
|
||||
"version": "4.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.4.tgz",
|
||||
"integrity": "sha512-KrwoXu9J1loWSvQQReilGPkt6/dCH/x5eTBDecCBPclz7vxUM13Iw9almBIffEpurk/kaMAglH0G7sAF/A2y1A=="
|
||||
"version": "4.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.5.tgz",
|
||||
"integrity": "sha512-vSyFjY7XSEx0ufa9SebOd437CvnneaTXlCpuGDhjUDxAjGBlu6ie5qHyubobVGBth//aErc6wZPHc6W75Vp3iQ=="
|
||||
},
|
||||
"@stencil/react-output-target": {
|
||||
"version": "0.5.3",
|
||||
|
@ -187,6 +187,7 @@ export class Checkbox implements ComponentInterface {
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-checked={indeterminate ? 'mixed' : `${checked}`}
|
||||
class={createColorClasses(color, {
|
||||
[mode]: true,
|
||||
'in-item': hostContext('ion-item', el),
|
||||
|
@ -39,3 +39,18 @@ describe('ion-checkbox: disabled', () => {
|
||||
expect(checkbox.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-checkbox: indeterminate', () => {
|
||||
it('should have a mixed value for aria-checked', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Checkbox],
|
||||
html: `
|
||||
<ion-checkbox indeterminate="true">Checkbox</ion-checkbox>
|
||||
`,
|
||||
});
|
||||
|
||||
const checkbox = page.body.querySelector('ion-checkbox')!;
|
||||
|
||||
expect(checkbox.getAttribute('aria-checked')).toBe('mixed');
|
||||
});
|
||||
});
|
||||
|
@ -275,8 +275,14 @@ export class Range implements ComponentInterface {
|
||||
el: rangeSlider,
|
||||
gestureName: 'range',
|
||||
gesturePriority: 100,
|
||||
threshold: 0,
|
||||
onStart: (ev) => this.onStart(ev),
|
||||
/**
|
||||
* Provide a threshold since the drag movement
|
||||
* might be a user scrolling the view.
|
||||
* If this is true, then the range
|
||||
* should not move.
|
||||
*/
|
||||
threshold: 10,
|
||||
onStart: () => this.onStart(),
|
||||
onMove: (ev) => this.onMove(ev),
|
||||
onEnd: (ev) => this.onEnd(ev),
|
||||
});
|
||||
@ -378,42 +384,101 @@ export class Range implements ComponentInterface {
|
||||
this.ionChange.emit({ value: this.value });
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// figure out which knob they started closer to
|
||||
let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
|
||||
if (isRTL(this.el)) {
|
||||
ratio = 1 - ratio;
|
||||
}
|
||||
|
||||
this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
|
||||
|
||||
this.setFocus(this.pressedKnob);
|
||||
|
||||
// update the active knob's position
|
||||
this.update(currentX);
|
||||
|
||||
/**
|
||||
* The value should be updated on touch end or
|
||||
* when the component is being dragged.
|
||||
* This follows the native behavior of mobile devices.
|
||||
*
|
||||
* For example: When the user lifts their finger from the
|
||||
* screen after tapping the bar or dragging the bar or knob.
|
||||
*/
|
||||
private onStart() {
|
||||
this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
|
||||
}
|
||||
|
||||
/**
|
||||
* The value should be updated while dragging the
|
||||
* bar or knob.
|
||||
*
|
||||
* While the user is dragging, the view
|
||||
* should not scroll. This is to prevent the user from
|
||||
* feeling disoriented while dragging.
|
||||
*
|
||||
* The user can scroll on the view if the knob or
|
||||
* bar is not being dragged.
|
||||
*
|
||||
* @param detail The details of the gesture event.
|
||||
*/
|
||||
private onMove(detail: GestureDetail) {
|
||||
this.update(detail.currentX);
|
||||
const { contentEl, pressedKnob } = this;
|
||||
const currentX = detail.currentX;
|
||||
|
||||
/**
|
||||
* Since the user is dragging on the bar or knob, the view should not scroll.
|
||||
*
|
||||
* This only needs to be done once.
|
||||
*/
|
||||
if (contentEl && this.initialContentScrollY === undefined) {
|
||||
this.initialContentScrollY = disableContentScrollY(contentEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* The `pressedKnob` can be undefined if the user just
|
||||
* started dragging the knob.
|
||||
*
|
||||
* This is necessary to determine which knob the user is dragging,
|
||||
* especially when it's a dual knob.
|
||||
* Plus, it determines when to apply certain styles.
|
||||
*
|
||||
* This only needs to be done once since the knob won't change
|
||||
* while the user is dragging.
|
||||
*/
|
||||
if (pressedKnob === undefined) {
|
||||
this.setPressedKnob(currentX);
|
||||
}
|
||||
|
||||
this.update(currentX);
|
||||
}
|
||||
|
||||
private onEnd(detail: GestureDetail) {
|
||||
/**
|
||||
* The value should be updated on touch end:
|
||||
* - When the user lifts their finger from the screen after
|
||||
* tapping the bar.
|
||||
*
|
||||
* @param detail The details of the gesture or mouse event.
|
||||
*/
|
||||
private onEnd(detail: GestureDetail | MouseEvent) {
|
||||
const { contentEl, initialContentScrollY } = this;
|
||||
if (contentEl) {
|
||||
const currentX = (detail as GestureDetail).currentX || (detail as MouseEvent).clientX;
|
||||
|
||||
/**
|
||||
* The `pressedKnob` can be undefined if the user never
|
||||
* dragged the knob. They just tapped on the bar.
|
||||
*
|
||||
* This is necessary to determine which knob the user is changing,
|
||||
* especially when it's a dual knob.
|
||||
* Plus, it determines when to apply certain styles.
|
||||
*/
|
||||
if (this.pressedKnob === undefined) {
|
||||
this.setPressedKnob(currentX);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user is no longer dragging the bar or
|
||||
* knob (if they were dragging it).
|
||||
*
|
||||
* The user can now scroll on the view in the next gesture event.
|
||||
*/
|
||||
if (contentEl && initialContentScrollY !== undefined) {
|
||||
resetContentScrollY(contentEl, initialContentScrollY);
|
||||
}
|
||||
|
||||
this.update(detail.currentX);
|
||||
// update the active knob's position
|
||||
this.update(currentX);
|
||||
/**
|
||||
* Reset the pressed knob to undefined since the user
|
||||
* may start dragging a different knob in the next gesture event.
|
||||
*/
|
||||
this.pressedKnob = undefined;
|
||||
|
||||
this.emitValueChange();
|
||||
@ -445,6 +510,19 @@ export class Range implements ComponentInterface {
|
||||
this.updateValue();
|
||||
}
|
||||
|
||||
private setPressedKnob(currentX: number) {
|
||||
const rect = (this.rect = this.rangeSlider!.getBoundingClientRect() as any);
|
||||
|
||||
// figure out which knob they started closer to
|
||||
let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
|
||||
if (isRTL(this.el)) {
|
||||
ratio = 1 - ratio;
|
||||
}
|
||||
this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
|
||||
|
||||
this.setFocus(this.pressedKnob);
|
||||
}
|
||||
|
||||
private get valA() {
|
||||
return ratioToValue(this.ratioA, this.min, this.max, this.step);
|
||||
}
|
||||
@ -643,7 +721,39 @@ export class Range implements ComponentInterface {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="range-slider" ref={(rangeEl) => (this.rangeSlider = rangeEl)}>
|
||||
<div
|
||||
class="range-slider"
|
||||
ref={(rangeEl) => (this.rangeSlider = rangeEl)}
|
||||
/**
|
||||
* Since the gesture has a threshold, the value
|
||||
* won't change until the user has dragged past
|
||||
* the threshold. This is to prevent the range
|
||||
* from moving when the user is scrolling.
|
||||
*
|
||||
* This results in the value not being updated
|
||||
* and the event emitters not being triggered
|
||||
* if the user taps on the range. This is why
|
||||
* we need to listen for the "pointerUp" event.
|
||||
*/
|
||||
onPointerUp={(ev: PointerEvent) => {
|
||||
/**
|
||||
* If the user drags the knob on the web
|
||||
* version (does not occur on mobile),
|
||||
* the "pointerUp" event will be triggered
|
||||
* along with the gesture's events.
|
||||
* This leads to duplicate events.
|
||||
*
|
||||
* By checking if the pressedKnob is undefined,
|
||||
* we can determine if the "pointerUp" event was
|
||||
* triggered by a tap or a drag. If it was
|
||||
* dragged, the pressedKnob will be defined.
|
||||
*/
|
||||
if (this.pressedKnob === undefined) {
|
||||
this.onStart();
|
||||
this.onEnd(ev);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ticks.map((tick) => (
|
||||
<div
|
||||
style={tickStyle(tick)}
|
||||
|
@ -67,6 +67,39 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
expect(rangeEnd).toHaveReceivedEventDetail({ value: 21 });
|
||||
});
|
||||
|
||||
test('should emit end event on tap', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/28487',
|
||||
});
|
||||
|
||||
await page.setContent(`<ion-range aria-label="Range" value="20"></ion-range>`, config);
|
||||
|
||||
const range = page.locator('ion-range');
|
||||
const rangeEndSpy = await page.spyOnEvent('ionKnobMoveEnd');
|
||||
const rangeBoundingBox = await range.boundingBox();
|
||||
/**
|
||||
* Coordinates for the click event.
|
||||
* These need to be near the end of the range
|
||||
* (or anything that isn't the current value).
|
||||
*
|
||||
* The number 50 is arbitrary, but it should be
|
||||
* less than the width of the range.
|
||||
*/
|
||||
const x = rangeBoundingBox!.width - 50;
|
||||
// The y coordinate is the middle of the range.
|
||||
const y = rangeBoundingBox!.height / 2;
|
||||
|
||||
// Click near the end of the range.
|
||||
await range.click({
|
||||
position: { x, y },
|
||||
});
|
||||
|
||||
await rangeEndSpy.next();
|
||||
|
||||
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');
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { ElementRef, Injector, EnvironmentInjector, NgZone, ChangeDetectorRef, Directive } from '@angular/core';
|
||||
import {
|
||||
ElementRef,
|
||||
Injector,
|
||||
EnvironmentInjector,
|
||||
NgZone,
|
||||
ChangeDetectorRef,
|
||||
Directive,
|
||||
EventEmitter,
|
||||
} from '@angular/core';
|
||||
import type { Components } from '@ionic/core';
|
||||
|
||||
import { AngularDelegate } from '../../providers/angular-delegate';
|
||||
@ -22,8 +30,16 @@ const NAV_METHODS = [
|
||||
'getPrevious',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export declare interface IonNav extends Components.IonNav {}
|
||||
export declare interface IonNav extends Components.IonNav {
|
||||
/**
|
||||
* Event fired when the nav will change components
|
||||
*/
|
||||
ionNavWillChange: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Event fired when the nav has changed components
|
||||
*/
|
||||
ionNavDidChange: EventEmitter<CustomEvent<void>>;
|
||||
}
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: NAV_INPUTS,
|
||||
|
Reference in New Issue
Block a user