mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 02:31:34 +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
|
id: affected-versions
|
||||||
attributes:
|
attributes:
|
||||||
label: Ionic Framework Version
|
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:
|
options:
|
||||||
- v4.x
|
|
||||||
- v5.x
|
|
||||||
- v6.x
|
|
||||||
- v7.x
|
- v7.x
|
||||||
|
- v8.x (Beta)
|
||||||
- Nightly
|
- Nightly
|
||||||
multiple: true
|
multiple: true
|
||||||
validations:
|
validations:
|
||||||
@ -51,11 +49,11 @@ body:
|
|||||||
id: steps-to-reproduce
|
id: steps-to-reproduce
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to Reproduce
|
label: Steps to Reproduce
|
||||||
description: Please explain the steps required to duplicate this issue.
|
description: Explain the steps required to reproduce this issue.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1.
|
1. Go to '...'
|
||||||
2.
|
2. Click on '...'
|
||||||
3.
|
3. Observe: '...'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@ -63,8 +61,15 @@ body:
|
|||||||
id: reproduction-url
|
id: reproduction-url
|
||||||
attributes:
|
attributes:
|
||||||
label: Code Reproduction URL
|
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/...
|
placeholder: https://github.com/...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: ionic-info
|
id: ionic-info
|
||||||
|
24
core/package-lock.json
generated
24
core/package-lock.json
generated
@ -633,9 +633,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@capacitor/core": {
|
"node_modules/@capacitor/core": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.2.tgz",
|
||||||
"integrity": "sha512-bwmka6FdvyXOpc5U6bOyx58S/Yl6r5lO2TK561f//KnjyXjxav25HWwhV4hthq3ZxJBMiAEucl9RK5vzgkP4Lw==",
|
"integrity": "sha512-/OUtfINmk7ke32VtKIHRAy8NlunbeK+aCqCHOS+fvtr7nUsOJXPkYgbgqZp/CWXET/gSK1xxMecaVBzpE98UKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
@ -1759,9 +1759,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@stencil/core": {
|
"node_modules/@stencil/core": {
|
||||||
"version": "4.12.4",
|
"version": "4.12.5",
|
||||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.4.tgz",
|
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.5.tgz",
|
||||||
"integrity": "sha512-KrwoXu9J1loWSvQQReilGPkt6/dCH/x5eTBDecCBPclz7vxUM13Iw9almBIffEpurk/kaMAglH0G7sAF/A2y1A==",
|
"integrity": "sha512-vSyFjY7XSEx0ufa9SebOd437CvnneaTXlCpuGDhjUDxAjGBlu6ie5qHyubobVGBth//aErc6wZPHc6W75Vp3iQ==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"stencil": "bin/stencil"
|
"stencil": "bin/stencil"
|
||||||
},
|
},
|
||||||
@ -10416,9 +10416,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@capacitor/core": {
|
"@capacitor/core": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.2.tgz",
|
||||||
"integrity": "sha512-bwmka6FdvyXOpc5U6bOyx58S/Yl6r5lO2TK561f//KnjyXjxav25HWwhV4hthq3ZxJBMiAEucl9RK5vzgkP4Lw==",
|
"integrity": "sha512-/OUtfINmk7ke32VtKIHRAy8NlunbeK+aCqCHOS+fvtr7nUsOJXPkYgbgqZp/CWXET/gSK1xxMecaVBzpE98UKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
@ -11229,9 +11229,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@stencil/core": {
|
"@stencil/core": {
|
||||||
"version": "4.12.4",
|
"version": "4.12.5",
|
||||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.4.tgz",
|
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.5.tgz",
|
||||||
"integrity": "sha512-KrwoXu9J1loWSvQQReilGPkt6/dCH/x5eTBDecCBPclz7vxUM13Iw9almBIffEpurk/kaMAglH0G7sAF/A2y1A=="
|
"integrity": "sha512-vSyFjY7XSEx0ufa9SebOd437CvnneaTXlCpuGDhjUDxAjGBlu6ie5qHyubobVGBth//aErc6wZPHc6W75Vp3iQ=="
|
||||||
},
|
},
|
||||||
"@stencil/react-output-target": {
|
"@stencil/react-output-target": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
|
@ -187,6 +187,7 @@ export class Checkbox implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
|
aria-checked={indeterminate ? 'mixed' : `${checked}`}
|
||||||
class={createColorClasses(color, {
|
class={createColorClasses(color, {
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'in-item': hostContext('ion-item', el),
|
'in-item': hostContext('ion-item', el),
|
||||||
|
@ -39,3 +39,18 @@ describe('ion-checkbox: disabled', () => {
|
|||||||
expect(checkbox.checked).toBe(false);
|
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,
|
el: rangeSlider,
|
||||||
gestureName: 'range',
|
gestureName: 'range',
|
||||||
gesturePriority: 100,
|
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),
|
onMove: (ev) => this.onMove(ev),
|
||||||
onEnd: (ev) => this.onEnd(ev),
|
onEnd: (ev) => this.onEnd(ev),
|
||||||
});
|
});
|
||||||
@ -378,42 +384,101 @@ export class Range implements ComponentInterface {
|
|||||||
this.ionChange.emit({ value: this.value });
|
this.ionChange.emit({ value: this.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStart(detail: GestureDetail) {
|
/**
|
||||||
const { contentEl } = this;
|
* The value should be updated on touch end or
|
||||||
if (contentEl) {
|
* when the component is being dragged.
|
||||||
this.initialContentScrollY = disableContentScrollY(contentEl);
|
* This follows the native behavior of mobile devices.
|
||||||
}
|
*
|
||||||
|
* For example: When the user lifts their finger from the
|
||||||
const rect = (this.rect = this.rangeSlider!.getBoundingClientRect() as any);
|
* screen after tapping the bar or dragging the bar or knob.
|
||||||
const currentX = detail.currentX;
|
*/
|
||||||
|
private onStart() {
|
||||||
// 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);
|
|
||||||
|
|
||||||
this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
|
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) {
|
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;
|
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);
|
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.pressedKnob = undefined;
|
||||||
|
|
||||||
this.emitValueChange();
|
this.emitValueChange();
|
||||||
@ -445,6 +510,19 @@ export class Range implements ComponentInterface {
|
|||||||
this.updateValue();
|
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() {
|
private get valA() {
|
||||||
return ratioToValue(this.ratioA, this.min, this.max, this.step);
|
return ratioToValue(this.ratioA, this.min, this.max, this.step);
|
||||||
}
|
}
|
||||||
@ -643,7 +721,39 @@ export class Range implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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) => (
|
{ticks.map((tick) => (
|
||||||
<div
|
<div
|
||||||
style={tickStyle(tick)}
|
style={tickStyle(tick)}
|
||||||
|
@ -67,6 +67,39 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
expect(rangeEnd).toHaveReceivedEventDetail({ value: 21 });
|
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
|
// TODO FW-2873
|
||||||
test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
|
test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
|
||||||
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
|
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 type { Components } from '@ionic/core';
|
||||||
|
|
||||||
import { AngularDelegate } from '../../providers/angular-delegate';
|
import { AngularDelegate } from '../../providers/angular-delegate';
|
||||||
@ -22,8 +30,16 @@ const NAV_METHODS = [
|
|||||||
'getPrevious',
|
'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({
|
@ProxyCmp({
|
||||||
inputs: NAV_INPUTS,
|
inputs: NAV_INPUTS,
|
||||||
|
Reference in New Issue
Block a user