mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 03:00:58 +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)
|
- [Input](#version-7x-input)
|
||||||
- [Overlays](#version-7x-overlays)
|
- [Overlays](#version-7x-overlays)
|
||||||
- [Range](#version-7x-range)
|
- [Range](#version-7x-range)
|
||||||
|
- [Segment](#version-7x-segment)
|
||||||
- [Slides](#version-7x-slides)
|
- [Slides](#version-7x-slides)
|
||||||
- [Virtual Scroll](#version-7x-virtual-scroll)
|
- [Virtual Scroll](#version-7x-virtual-scroll)
|
||||||
- [Utilities](#version-7x-utilities)
|
- [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-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`|
|
|`$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>
|
<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,scrollable,boolean,false,false,false
|
||||||
ion-segment,prop,selectOnFocus,boolean,false,false,false
|
ion-segment,prop,selectOnFocus,boolean,false,false,false
|
||||||
ion-segment,prop,swipeGesture,boolean,true,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,event,ionChange,SegmentChangeEventDetail,true
|
||||||
ion-segment,css-prop,--background
|
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.
|
* the value of the segment.
|
||||||
*/
|
*/
|
||||||
"value"?: string | null;
|
"value"?: string;
|
||||||
}
|
}
|
||||||
interface IonSegmentButton {
|
interface IonSegmentButton {
|
||||||
/**
|
/**
|
||||||
@ -6212,7 +6212,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"onIonChange"?: (event: IonSegmentCustomEvent<SegmentChangeEventDetail>) => void;
|
"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;
|
"onIonSelect"?: (event: IonSegmentCustomEvent<SegmentChangeEventDetail>) => void;
|
||||||
/**
|
/**
|
||||||
@ -6234,7 +6234,7 @@ declare namespace LocalJSX {
|
|||||||
/**
|
/**
|
||||||
* the value of the segment.
|
* the value of the segment.
|
||||||
*/
|
*/
|
||||||
"value"?: string | null;
|
"value"?: string;
|
||||||
}
|
}
|
||||||
interface IonSegmentButton {
|
interface IonSegmentButton {
|
||||||
/**
|
/**
|
||||||
|
@ -22,11 +22,10 @@ import { createColorClasses, hostContext } from '../../utils/theme';
|
|||||||
})
|
})
|
||||||
export class Segment implements ComponentInterface {
|
export class Segment implements ComponentInterface {
|
||||||
private gesture?: Gesture;
|
private gesture?: Gesture;
|
||||||
private didInit = false;
|
|
||||||
private checked?: HTMLIonSegmentButtonElement;
|
private checked?: HTMLIonSegmentButtonElement;
|
||||||
|
|
||||||
// Value to be emitted when gesture ends
|
// Value before the segment is dragged
|
||||||
private valueAfterGesture?: any;
|
private valueBeforeGesture?: string;
|
||||||
|
|
||||||
@Element() el!: HTMLIonSegmentElement;
|
@Element() el!: HTMLIonSegmentElement;
|
||||||
|
|
||||||
@ -76,18 +75,15 @@ export class Segment implements ComponentInterface {
|
|||||||
/**
|
/**
|
||||||
* the value of the segment.
|
* the value of the segment.
|
||||||
*/
|
*/
|
||||||
@Prop({ mutable: true }) value?: string | null;
|
@Prop({ mutable: true }) value?: string;
|
||||||
|
|
||||||
@Watch('value')
|
@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 });
|
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>;
|
@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
|
* @internal
|
||||||
*/
|
*/
|
||||||
@Event() ionSelect!: EventEmitter<SegmentChangeEventDetail>;
|
@Event() ionSelect!: EventEmitter<SegmentChangeEventDetail>;
|
||||||
@ -157,10 +155,10 @@ export class Segment implements ComponentInterface {
|
|||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
this.disabledChanged();
|
this.disabledChanged();
|
||||||
}
|
}
|
||||||
this.didInit = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStart(detail: GestureDetail) {
|
onStart(detail: GestureDetail) {
|
||||||
|
this.valueBeforeGesture = this.value;
|
||||||
this.activate(detail);
|
this.activate(detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,11 +177,24 @@ export class Segment implements ComponentInterface {
|
|||||||
this.addRipple(detail);
|
this.addRipple(detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = this.valueAfterGesture;
|
const value = this.value;
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
this.ionChange.emit({ value });
|
if (this.valueBeforeGesture !== value) {
|
||||||
this.valueAfterGesture = undefined;
|
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() {
|
private getButtons() {
|
||||||
@ -491,8 +502,11 @@ export class Segment implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (keyDownSelectsButton) {
|
if (keyDownSelectsButton) {
|
||||||
const previous = this.checked || current;
|
const previous = this.checked;
|
||||||
this.checkButton(previous, current);
|
this.checkButton(this.checked || current, current);
|
||||||
|
if (current !== previous) {
|
||||||
|
this.emitValueChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
current.focus();
|
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