mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
feat(range): add knobMoveStart and knobMoveEnd events (#25011)
This commit is contained in:
@ -1272,6 +1272,8 @@ export class IonRadioGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import type { RangeChangeEventDetail as IRangeRangeChangeEventDetail } from '@ionic/core';
|
import type { RangeChangeEventDetail as IRangeRangeChangeEventDetail } from '@ionic/core';
|
||||||
|
import type { RangeKnobMoveStartEventDetail as IRangeRangeKnobMoveStartEventDetail } from '@ionic/core';
|
||||||
|
import type { RangeKnobMoveEndEventDetail as IRangeRangeKnobMoveEndEventDetail } from '@ionic/core';
|
||||||
export declare interface IonRange extends Components.IonRange {
|
export declare interface IonRange extends Components.IonRange {
|
||||||
/**
|
/**
|
||||||
* Emitted when the value property has changed.
|
* Emitted when the value property has changed.
|
||||||
@ -1285,6 +1287,16 @@ export declare interface IonRange extends Components.IonRange {
|
|||||||
* Emitted when the range loses focus.
|
* Emitted when the range loses focus.
|
||||||
*/
|
*/
|
||||||
ionBlur: EventEmitter<CustomEvent<void>>;
|
ionBlur: EventEmitter<CustomEvent<void>>;
|
||||||
|
/**
|
||||||
|
* Emitted when the user starts moving the range knob, whether through
|
||||||
|
mouse drag, touch gesture, or keyboard interaction.
|
||||||
|
*/
|
||||||
|
ionKnobMoveStart: EventEmitter<CustomEvent<IRangeRangeKnobMoveStartEventDetail>>;
|
||||||
|
/**
|
||||||
|
* Emitted when the user finishes moving the range knob, whether through
|
||||||
|
mouse drag, touch gesture, or keyboard interaction.
|
||||||
|
*/
|
||||||
|
ionKnobMoveEnd: EventEmitter<CustomEvent<IRangeRangeKnobMoveEndEventDetail>>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1303,7 +1315,7 @@ export class IonRange {
|
|||||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||||
c.detach();
|
c.detach();
|
||||||
this.el = r.nativeElement;
|
this.el = r.nativeElement;
|
||||||
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
|
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -984,6 +984,8 @@ ion-range,prop,value,number | { lower: number; upper: number; },0,false,false
|
|||||||
ion-range,event,ionBlur,void,true
|
ion-range,event,ionBlur,void,true
|
||||||
ion-range,event,ionChange,RangeChangeEventDetail,true
|
ion-range,event,ionChange,RangeChangeEventDetail,true
|
||||||
ion-range,event,ionFocus,void,true
|
ion-range,event,ionFocus,void,true
|
||||||
|
ion-range,event,ionKnobMoveEnd,RangeKnobMoveEndEventDetail,true
|
||||||
|
ion-range,event,ionKnobMoveStart,RangeKnobMoveStartEventDetail,true
|
||||||
ion-range,css-prop,--bar-background
|
ion-range,css-prop,--bar-background
|
||||||
ion-range,css-prop,--bar-background-active
|
ion-range,css-prop,--bar-background-active
|
||||||
ion-range,css-prop,--bar-border-radius
|
ion-range,css-prop,--bar-border-radius
|
||||||
|
10
core/src/components.d.ts
vendored
10
core/src/components.d.ts
vendored
@ -5,7 +5,7 @@
|
|||||||
* It contains typing information for all components that exist in this project.
|
* It contains typing information for all components that exist in this project.
|
||||||
*/
|
*/
|
||||||
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
||||||
import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface";
|
import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface";
|
||||||
import { IonicSafeString } from "./utils/sanitization";
|
import { IonicSafeString } from "./utils/sanitization";
|
||||||
import { AlertAttributes } from "./components/alert/alert-interface";
|
import { AlertAttributes } from "./components/alert/alert-interface";
|
||||||
import { CounterFormatter } from "./components/item/item-interface";
|
import { CounterFormatter } from "./components/item/item-interface";
|
||||||
@ -5771,6 +5771,14 @@ declare namespace LocalJSX {
|
|||||||
* Emitted when the range has focus.
|
* Emitted when the range has focus.
|
||||||
*/
|
*/
|
||||||
"onIonFocus"?: (event: CustomEvent<void>) => void;
|
"onIonFocus"?: (event: CustomEvent<void>) => void;
|
||||||
|
/**
|
||||||
|
* Emitted when the user finishes moving the range knob, whether through mouse drag, touch gesture, or keyboard interaction.
|
||||||
|
*/
|
||||||
|
"onIonKnobMoveEnd"?: (event: CustomEvent<RangeKnobMoveEndEventDetail>) => void;
|
||||||
|
/**
|
||||||
|
* Emitted when the user starts moving the range knob, whether through mouse drag, touch gesture, or keyboard interaction.
|
||||||
|
*/
|
||||||
|
"onIonKnobMoveStart"?: (event: CustomEvent<RangeKnobMoveStartEventDetail>) => void;
|
||||||
/**
|
/**
|
||||||
* Emitted when the styles change.
|
* Emitted when the styles change.
|
||||||
*/
|
*/
|
||||||
|
@ -8,7 +8,15 @@ export interface RangeChangeEventDetail {
|
|||||||
value: RangeValue;
|
value: RangeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RangeKnobMoveStartEventDetail {
|
||||||
|
value: RangeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RangeKnobMoveEndEventDetail {
|
||||||
|
value: RangeValue;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RangeCustomEvent extends CustomEvent {
|
export interface RangeCustomEvent extends CustomEvent {
|
||||||
detail: RangeChangeEventDetail;
|
detail: RangeChangeEventDetail | RangeKnobMoveStartEventDetail | RangeKnobMoveEndEventDetail;
|
||||||
target: HTMLIonRangeElement;
|
target: HTMLIonRangeElement;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, State, Watch, h } from '@stencil/core';
|
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, State, Watch, h } from '@stencil/core';
|
||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { Color, Gesture, GestureDetail, KnobName, RangeChangeEventDetail, RangeValue, StyleEventDetail } from '../../interface';
|
import { Color, Gesture, GestureDetail, KnobName, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, StyleEventDetail } from '../../interface';
|
||||||
import { Attributes, clamp, debounceEvent, getAriaLabel, inheritAttributes, renderHiddenInput } from '../../utils/helpers';
|
import { Attributes, clamp, debounceEvent, getAriaLabel, inheritAttributes, renderHiddenInput } from '../../utils/helpers';
|
||||||
import { isRTL } from '../../utils/rtl';
|
import { isRTL } from '../../utils/rtl';
|
||||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||||
@ -191,6 +191,18 @@ export class Range implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Event() ionBlur!: EventEmitter<void>;
|
@Event() ionBlur!: EventEmitter<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the user starts moving the range knob, whether through
|
||||||
|
* mouse drag, touch gesture, or keyboard interaction.
|
||||||
|
*/
|
||||||
|
@Event() ionKnobMoveStart!: EventEmitter<RangeKnobMoveStartEventDetail>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the user finishes moving the range knob, whether through
|
||||||
|
* mouse drag, touch gesture, or keyboard interaction.
|
||||||
|
*/
|
||||||
|
@Event() ionKnobMoveEnd!: EventEmitter<RangeKnobMoveEndEventDetail>;
|
||||||
|
|
||||||
private setupGesture = async () => {
|
private setupGesture = async () => {
|
||||||
const rangeSlider = this.rangeSlider;
|
const rangeSlider = this.rangeSlider;
|
||||||
if (rangeSlider) {
|
if (rangeSlider) {
|
||||||
@ -246,6 +258,8 @@ export class Range implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleKeyboard = (knob: KnobName, isIncrease: boolean) => {
|
private handleKeyboard = (knob: KnobName, isIncrease: boolean) => {
|
||||||
|
const { ensureValueInBounds } = this;
|
||||||
|
|
||||||
let step = this.step;
|
let step = this.step;
|
||||||
step = step > 0 ? step : 1;
|
step = step > 0 ? step : 1;
|
||||||
step = step / (this.max - this.min);
|
step = step / (this.max - this.min);
|
||||||
@ -257,7 +271,10 @@ export class Range implements ComponentInterface {
|
|||||||
} else {
|
} else {
|
||||||
this.ratioB = clamp(0, this.ratioB + step, 1);
|
this.ratioB = clamp(0, this.ratioB + step, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.ionKnobMoveStart.emit({ value: ensureValueInBounds(this.value) });
|
||||||
this.updateValue();
|
this.updateValue();
|
||||||
|
this.ionKnobMoveEnd.emit({ value: ensureValueInBounds(this.value) });
|
||||||
}
|
}
|
||||||
|
|
||||||
private getValue(): RangeValue {
|
private getValue(): RangeValue {
|
||||||
@ -305,6 +322,8 @@ export class Range implements ComponentInterface {
|
|||||||
|
|
||||||
// update the active knob's position
|
// update the active knob's position
|
||||||
this.update(currentX);
|
this.update(currentX);
|
||||||
|
|
||||||
|
this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMove(detail: GestureDetail) {
|
private onMove(detail: GestureDetail) {
|
||||||
@ -314,6 +333,8 @@ export class Range implements ComponentInterface {
|
|||||||
private onEnd(detail: GestureDetail) {
|
private onEnd(detail: GestureDetail) {
|
||||||
this.update(detail.currentX);
|
this.update(detail.currentX);
|
||||||
this.pressedKnob = undefined;
|
this.pressedKnob = undefined;
|
||||||
|
|
||||||
|
this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
|
||||||
}
|
}
|
||||||
|
|
||||||
private update(currentX: number) {
|
private update(currentX: number) {
|
||||||
|
@ -25,6 +25,22 @@ interface RangeChangeEventDetail {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### RangeKnobMoveStartEventDetail
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RangeKnobMoveStartEventDetail {
|
||||||
|
value: RangeValue;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### RangeKnobMoveEndEventDetail
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RangeKnobMoveEndEventDetail {
|
||||||
|
value: RangeValue;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### RangeCustomEvent
|
### RangeCustomEvent
|
||||||
|
|
||||||
While not required, this interface can be used in place of the `CustomEvent` interface for stronger typing with Ionic events emitted from this component.
|
While not required, this interface can be used in place of the `CustomEvent` interface for stronger typing with Ionic events emitted from this component.
|
||||||
@ -368,11 +384,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
| Event | Description | Type |
|
| Event | Description | Type |
|
||||||
| ----------- | -------------------------------------------- | ------------------------------------- |
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
||||||
| `ionBlur` | Emitted when the range loses focus. | `CustomEvent<void>` |
|
| `ionBlur` | Emitted when the range loses focus. | `CustomEvent<void>` |
|
||||||
| `ionChange` | Emitted when the value property has changed. | `CustomEvent<RangeChangeEventDetail>` |
|
| `ionChange` | Emitted when the value property has changed. | `CustomEvent<RangeChangeEventDetail>` |
|
||||||
| `ionFocus` | Emitted when the range has focus. | `CustomEvent<void>` |
|
| `ionFocus` | Emitted when the range has focus. | `CustomEvent<void>` |
|
||||||
|
| `ionKnobMoveEnd` | Emitted when the user finishes moving the range knob, whether through mouse drag, touch gesture, or keyboard interaction. | `CustomEvent<RangeKnobMoveEndEventDetail>` |
|
||||||
|
| `ionKnobMoveStart` | Emitted when the user starts moving the range knob, whether through mouse drag, touch gesture, or keyboard interaction. | `CustomEvent<RangeKnobMoveStartEventDetail>` |
|
||||||
|
|
||||||
|
|
||||||
## Slots
|
## Slots
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { newE2EPage } from '@stencil/core/testing';
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
import { dragElementBy } from '@utils/test';
|
||||||
|
|
||||||
test('range: basic', async () => {
|
test('range: basic', async () => {
|
||||||
const page = await newE2EPage({
|
const page = await newE2EPage({
|
||||||
@ -17,3 +18,45 @@ test('range:rtl: basic', async () => {
|
|||||||
const compare = await page.compareScreenshot();
|
const compare = await page.compareScreenshot();
|
||||||
expect(compare).toMatchScreenshot();
|
expect(compare).toMatchScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('range: start/end events', async () => {
|
||||||
|
const page = await newE2EPage({
|
||||||
|
url: '/src/components/range/test/basic?ionic:_testing=true'
|
||||||
|
});
|
||||||
|
|
||||||
|
const rangeStart = await page.spyOnEvent('ionKnobMoveStart');
|
||||||
|
const rangeEnd = await page.spyOnEvent('ionKnobMoveEnd');
|
||||||
|
const rangeEl = await page.$('#basic');
|
||||||
|
|
||||||
|
await dragElementBy(rangeEl, page, 300, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dragElementBy defaults to starting the drag from the middle of the el,
|
||||||
|
* so the start value should jump to 50 despite the range defaulting to 20.
|
||||||
|
*/
|
||||||
|
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
|
||||||
|
expect(rangeEnd).toHaveReceivedEventDetail({ value: 91 });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify both events fire if range is clicked without dragging.
|
||||||
|
*/
|
||||||
|
await dragElementBy(rangeEl, page, 0, 0);
|
||||||
|
|
||||||
|
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
|
||||||
|
expect(rangeEnd).toHaveReceivedEventDetail({ value: 50 })
|
||||||
|
});
|
||||||
|
|
||||||
|
test('range: start/end events, keyboard', async () => {
|
||||||
|
const page = await newE2EPage({
|
||||||
|
url: '/src/components/range/test/basic?ionic:_testing=true'
|
||||||
|
});
|
||||||
|
|
||||||
|
const rangeStart = await page.spyOnEvent('ionKnobMoveStart');
|
||||||
|
const rangeEnd = await page.spyOnEvent('ionKnobMoveEnd');
|
||||||
|
|
||||||
|
await page.keyboard.press('Tab'); // focus first range
|
||||||
|
await page.keyboard.press('ArrowRight');
|
||||||
|
|
||||||
|
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
|
||||||
|
expect(rangeEnd).toHaveReceivedEventDetail({ value: 21 });
|
||||||
|
});
|
@ -66,7 +66,7 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range value="20" aria-label="Default Range"></ion-range>
|
<ion-range id="basic" value="20" aria-label="Default Range"></ion-range>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-range value="60" color="light" step="10" pin="true" aria-label="Light Pin Range"></ion-range>
|
<ion-range value="60" color="light" step="10" pin="true" aria-label="Light Pin Range"></ion-range>
|
||||||
@ -240,19 +240,7 @@
|
|||||||
knob.value = {
|
knob.value = {
|
||||||
lower: 33,
|
lower: 33,
|
||||||
upper: 60
|
upper: 60
|
||||||
}
|
};
|
||||||
knob.addEventListener('ionFocus', function (ev) {
|
|
||||||
console.log('focus', ev)
|
|
||||||
})
|
|
||||||
knob.addEventListener('ionBlur', function (ev) {
|
|
||||||
console.log('blur', ev)
|
|
||||||
})
|
|
||||||
knob.addEventListener('ionChange', function (ev) {
|
|
||||||
console.log('change', ev)
|
|
||||||
})
|
|
||||||
debounceRange.addEventListener('ionChange', function (ev) {
|
|
||||||
console.log('change', ev)
|
|
||||||
})
|
|
||||||
|
|
||||||
function elTest() {
|
function elTest() {
|
||||||
var range = document.getElementById('range');
|
var range = document.getElementById('range');
|
||||||
|
@ -591,7 +591,9 @@ export const IonRange = /*@__PURE__*/ defineContainer<JSX.IonRange>('ion-range',
|
|||||||
'ionChange',
|
'ionChange',
|
||||||
'ionStyle',
|
'ionStyle',
|
||||||
'ionFocus',
|
'ionFocus',
|
||||||
'ionBlur'
|
'ionBlur',
|
||||||
|
'ionKnobMoveStart',
|
||||||
|
'ionKnobMoveEnd'
|
||||||
],
|
],
|
||||||
'value', 'v-ion-change', 'ionChange');
|
'value', 'v-ion-change', 'ionChange');
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user