mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
feat(segment): ionChange will only emit from user committed changes (#25934)
This commit is contained in:
@ -19,6 +19,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
||||
- [Input](#version-7x-input)
|
||||
- [Overlays](#version-7x-overlays)
|
||||
- [Range](#version-7x-range)
|
||||
- [Segment](#version-7x-segment)
|
||||
- [Slides](#version-7x-slides)
|
||||
- [Virtual Scroll](#version-7x-virtual-scroll)
|
||||
- [Utilities](#version-7x-utilities)
|
||||
@ -91,6 +92,12 @@ iOS:
|
||||
|`$range-ios-knob-box-shadow`|`0 3px 1px rgba(0, 0, 0, .1), 0 4px 8px rgba(0, 0, 0, .13), 0 0 0 1px rgba(0, 0, 0, .02)`|`0px 0.5px 4px rgba(0, 0, 0, 0.12), 0px 6px 13px rgba(0, 0, 0, 0.12)`|
|
||||
|`$range-ios-knob-width`|`28px`|`26px`|
|
||||
|
||||
<h4 id="version-7x-segment">Segment</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-segment` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking a segment button or dragging to activate a segment button.
|
||||
|
||||
- The type signature of `value` supports `string | undefined`. Previously the type signature was `string | null | undefined`.
|
||||
- Developers needing to clear the checked segment item should assign a value of `''` instead of `null`.
|
||||
|
||||
<h4 id="version-7x-slides">Slides</h4>
|
||||
|
||||
|
@ -1134,7 +1134,7 @@ ion-segment,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-segment,prop,scrollable,boolean,false,false,false
|
||||
ion-segment,prop,selectOnFocus,boolean,false,false,false
|
||||
ion-segment,prop,swipeGesture,boolean,true,false,false
|
||||
ion-segment,prop,value,null | string | undefined,undefined,false,false
|
||||
ion-segment,prop,value,string | undefined,undefined,false,false
|
||||
ion-segment,event,ionChange,SegmentChangeEventDetail,true
|
||||
ion-segment,css-prop,--background
|
||||
|
||||
|
6
core/src/components.d.ts
vendored
6
core/src/components.d.ts
vendored
@ -2455,7 +2455,7 @@ export namespace Components {
|
||||
/**
|
||||
* the value of the segment.
|
||||
*/
|
||||
"value"?: string | null;
|
||||
"value"?: string;
|
||||
}
|
||||
interface IonSegmentButton {
|
||||
/**
|
||||
@ -6212,7 +6212,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"onIonChange"?: (event: IonSegmentCustomEvent<SegmentChangeEventDetail>) => void;
|
||||
/**
|
||||
* Emitted when user has dragged over a new button
|
||||
* Emitted when the value of the segment changes from user committed actions or from externally assigning a value.
|
||||
*/
|
||||
"onIonSelect"?: (event: IonSegmentCustomEvent<SegmentChangeEventDetail>) => void;
|
||||
/**
|
||||
@ -6234,7 +6234,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* the value of the segment.
|
||||
*/
|
||||
"value"?: string | null;
|
||||
"value"?: string;
|
||||
}
|
||||
interface IonSegmentButton {
|
||||
/**
|
||||
|
@ -22,11 +22,10 @@ import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
})
|
||||
export class Segment implements ComponentInterface {
|
||||
private gesture?: Gesture;
|
||||
private didInit = false;
|
||||
private checked?: HTMLIonSegmentButtonElement;
|
||||
|
||||
// Value to be emitted when gesture ends
|
||||
private valueAfterGesture?: any;
|
||||
// Value before the segment is dragged
|
||||
private valueBeforeGesture?: string;
|
||||
|
||||
@Element() el!: HTMLIonSegmentElement;
|
||||
|
||||
@ -76,18 +75,15 @@ export class Segment implements ComponentInterface {
|
||||
/**
|
||||
* the value of the segment.
|
||||
*/
|
||||
@Prop({ mutable: true }) value?: string | null;
|
||||
@Prop({ mutable: true }) value?: string;
|
||||
|
||||
@Watch('value')
|
||||
protected valueChanged(value: string | undefined, oldValue: string | undefined | null) {
|
||||
protected valueChanged(value: string | undefined) {
|
||||
/**
|
||||
* `ionSelect` is emitted every time the value changes (internal or external changes).
|
||||
* Used by `ion-segment-button` to determine if the button should be checked.
|
||||
*/
|
||||
this.ionSelect.emit({ value });
|
||||
if (oldValue !== '' || this.didInit) {
|
||||
if (!this.activated) {
|
||||
this.ionChange.emit({ value });
|
||||
} else {
|
||||
this.valueAfterGesture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,7 +99,9 @@ export class Segment implements ComponentInterface {
|
||||
@Event() ionChange!: EventEmitter<SegmentChangeEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted when user has dragged over a new button
|
||||
* Emitted when the value of the segment changes from user committed actions
|
||||
* or from externally assigning a value.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@Event() ionSelect!: EventEmitter<SegmentChangeEventDetail>;
|
||||
@ -157,10 +155,10 @@ export class Segment implements ComponentInterface {
|
||||
if (this.disabled) {
|
||||
this.disabledChanged();
|
||||
}
|
||||
this.didInit = true;
|
||||
}
|
||||
|
||||
onStart(detail: GestureDetail) {
|
||||
this.valueBeforeGesture = this.value;
|
||||
this.activate(detail);
|
||||
}
|
||||
|
||||
@ -179,11 +177,24 @@ export class Segment implements ComponentInterface {
|
||||
this.addRipple(detail);
|
||||
}
|
||||
|
||||
const value = this.valueAfterGesture;
|
||||
const value = this.value;
|
||||
if (value !== undefined) {
|
||||
this.ionChange.emit({ value });
|
||||
this.valueAfterGesture = undefined;
|
||||
if (this.valueBeforeGesture !== value) {
|
||||
this.emitValueChange();
|
||||
}
|
||||
}
|
||||
this.valueBeforeGesture = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an `ionChange` event.
|
||||
*
|
||||
* This API should be called for user committed changes.
|
||||
* This API should not be used for external value changes.
|
||||
*/
|
||||
private emitValueChange() {
|
||||
const { value } = this;
|
||||
this.ionChange.emit({ value });
|
||||
}
|
||||
|
||||
private getButtons() {
|
||||
@ -491,8 +502,11 @@ export class Segment implements ComponentInterface {
|
||||
}
|
||||
|
||||
if (keyDownSelectsButton) {
|
||||
const previous = this.checked || current;
|
||||
this.checkButton(previous, current);
|
||||
const previous = this.checked;
|
||||
this.checkButton(this.checked || current, current);
|
||||
if (current !== previous) {
|
||||
this.emitValueChange();
|
||||
}
|
||||
}
|
||||
current.focus();
|
||||
}
|
||||
|
177
core/src/components/segment/test/segment-events.e2e.ts
Normal file
177
core/src/components/segment/test/segment-events.e2e.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('segment: events: ionChange', () => {
|
||||
test.beforeEach(({ skip }) => {
|
||||
skip.rtl();
|
||||
});
|
||||
|
||||
test.describe('when the segment is activated by keyboard navigation', () => {
|
||||
test('should emit when there is no initial value', async ({ page, browserName }) => {
|
||||
await page.setContent(`
|
||||
<ion-segment>
|
||||
<ion-segment-button value="1">One</ion-segment-button>
|
||||
<ion-segment-button value="2">Two</ion-segment-button>
|
||||
<ion-segment-button value="3">Three</ion-segment-button>
|
||||
</ion-segment>
|
||||
`);
|
||||
|
||||
const segment = page.locator('ion-segment');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
expect(await segment.evaluate((el: HTMLIonSegmentElement) => el.value)).toBe('2');
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: '2' });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('when the segment is clicked', () => {
|
||||
test('should emit when the value changes', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-segment value="1">
|
||||
<ion-segment-button value="1">One</ion-segment-button>
|
||||
<ion-segment-button value="2">Two</ion-segment-button>
|
||||
<ion-segment-button value="3">Three</ion-segment-button>
|
||||
</ion-segment>
|
||||
`);
|
||||
|
||||
const segment = page.locator('ion-segment');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.click('ion-segment-button[value="2"]');
|
||||
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(await segment.evaluate((el: HTMLIonSegmentElement) => el.value)).toBe('2');
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: '2' });
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
|
||||
});
|
||||
|
||||
test('when the segment does not have an initial value', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-segment>
|
||||
<ion-segment-button value="1">One</ion-segment-button>
|
||||
<ion-segment-button value="2">Two</ion-segment-button>
|
||||
<ion-segment-button value="3">Three</ion-segment-button>
|
||||
</ion-segment>
|
||||
`);
|
||||
|
||||
const segment = page.locator('ion-segment');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.click('ion-segment-button[value="2"]');
|
||||
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(await segment.evaluate((el: HTMLIonSegmentElement) => el.value)).toBe('2');
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: '2' });
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('when the pointer is released', () => {
|
||||
test('should emit if the value has changed', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/20257',
|
||||
});
|
||||
|
||||
await page.setContent(`
|
||||
<ion-app>
|
||||
<ion-toolbar>
|
||||
<ion-segment value="1">
|
||||
<ion-segment-button value="1">One</ion-segment-button>
|
||||
<ion-segment-button value="2">Two</ion-segment-button>
|
||||
<ion-segment-button value="3">Three</ion-segment-button>
|
||||
</ion-segment>
|
||||
</ion-toolbar>
|
||||
</ion-app>
|
||||
`);
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
const firstButton = page.locator('ion-segment-button[value="1"]');
|
||||
const lastButton = page.locator('ion-segment-button[value="3"]');
|
||||
|
||||
await firstButton.hover();
|
||||
await page.mouse.down();
|
||||
|
||||
await lastButton.hover();
|
||||
await page.mouse.up();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: '3' });
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
|
||||
});
|
||||
|
||||
test('should not emit if the value has not changed', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-segment value="1">
|
||||
<ion-segment-button value="1">One</ion-segment-button>
|
||||
<ion-segment-button value="2">Two</ion-segment-button>
|
||||
<ion-segment-button value="3">Three</ion-segment-button>
|
||||
</ion-segment>
|
||||
`);
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
const firstButton = page.locator('ion-segment-button[value="1"]');
|
||||
const lastButton = page.locator('ion-segment-button[value="3"]');
|
||||
|
||||
await firstButton.hover();
|
||||
await page.mouse.down();
|
||||
|
||||
await lastButton.hover();
|
||||
|
||||
await firstButton.hover();
|
||||
await page.mouse.up();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not emit if the value has not changed on click', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-segment value="1">
|
||||
<ion-segment-button value="1">One</ion-segment-button>
|
||||
<ion-segment-button value="2">Two</ion-segment-button>
|
||||
<ion-segment-button value="3">Three</ion-segment-button>
|
||||
</ion-segment>
|
||||
`);
|
||||
|
||||
const segment = page.locator('ion-segment');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.click('ion-segment-button[value="1"]');
|
||||
|
||||
expect(await segment.evaluate((el: HTMLIonSegmentElement) => el.value)).toBe('1');
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
|
||||
});
|
||||
|
||||
test('should not emit if the value is set programmatically', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-segment value="1">
|
||||
<ion-segment-button value="1">One</ion-segment-button>
|
||||
<ion-segment-button value="2">Two</ion-segment-button>
|
||||
<ion-segment-button value="3">Three</ion-segment-button>
|
||||
</ion-segment>
|
||||
`);
|
||||
|
||||
const segment = page.locator('ion-segment');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await segment.evaluate((el: HTMLIonSegmentElement) => (el.value = '2'));
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
|
||||
expect(await segment.evaluate((el: HTMLIonSegmentElement) => el.value)).toBe('2');
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user