chore: sync with main (#29143)

This commit is contained in:
Liam DeBeasi
2024-03-12 14:33:39 -04:00
committed by GitHub
7 changed files with 233 additions and 53 deletions

View File

@ -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
View File

@ -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",

View File

@ -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),

View File

@ -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');
});
});

View File

@ -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)}

View File

@ -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');

View File

@ -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,