feat(segment): ionChange will only emit from user committed changes (#25934)

This commit is contained in:
Sean Perkins
2022-09-21 10:33:58 -04:00
committed by GitHub
parent 35041b2f3c
commit a03c8afb3d
5 changed files with 221 additions and 23 deletions

View File

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

View File

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

View File

@ -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 {
/** /**

View File

@ -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,12 +177,25 @@ 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() {
return Array.from(this.el.querySelectorAll('ion-segment-button')); return Array.from(this.el.querySelectorAll('ion-segment-button'));
@ -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();
} }

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