feat(range): add knobMoveStart and knobMoveEnd events (#25011)

This commit is contained in:
Amanda Smith
2022-04-01 11:16:01 -05:00
committed by GitHub
parent 3145c76934
commit f5cb1f8444
9 changed files with 126 additions and 24 deletions

View File

@ -1272,6 +1272,8 @@ export class IonRadioGroup {
}
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 {
/**
* Emitted when the value property has changed.
@ -1285,6 +1287,16 @@ export declare interface IonRange extends Components.IonRange {
* Emitted when the range loses focus.
*/
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) {
c.detach();
this.el = r.nativeElement;
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
}
}

View File

@ -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,ionChange,RangeChangeEventDetail,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-active
ion-range,css-prop,--bar-border-radius

View File

@ -5,7 +5,7 @@
* It contains typing information for all components that exist in this project.
*/
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 { AlertAttributes } from "./components/alert/alert-interface";
import { CounterFormatter } from "./components/item/item-interface";
@ -5771,6 +5771,14 @@ declare namespace LocalJSX {
* Emitted when the range has focus.
*/
"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.
*/

View File

@ -8,7 +8,15 @@ export interface RangeChangeEventDetail {
value: RangeValue;
}
export interface RangeKnobMoveStartEventDetail {
value: RangeValue;
}
export interface RangeKnobMoveEndEventDetail {
value: RangeValue;
}
export interface RangeCustomEvent extends CustomEvent {
detail: RangeChangeEventDetail;
detail: RangeChangeEventDetail | RangeKnobMoveStartEventDetail | RangeKnobMoveEndEventDetail;
target: HTMLIonRangeElement;
}

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, State, Watch, h } from '@stencil/core';
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 { isRTL } from '../../utils/rtl';
import { createColorClasses, hostContext } from '../../utils/theme';
@ -191,6 +191,18 @@ export class Range implements ComponentInterface {
*/
@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 () => {
const rangeSlider = this.rangeSlider;
if (rangeSlider) {
@ -246,6 +258,8 @@ export class Range implements ComponentInterface {
}
private handleKeyboard = (knob: KnobName, isIncrease: boolean) => {
const { ensureValueInBounds } = this;
let step = this.step;
step = step > 0 ? step : 1;
step = step / (this.max - this.min);
@ -257,7 +271,10 @@ export class Range implements ComponentInterface {
} else {
this.ratioB = clamp(0, this.ratioB + step, 1);
}
this.ionKnobMoveStart.emit({ value: ensureValueInBounds(this.value) });
this.updateValue();
this.ionKnobMoveEnd.emit({ value: ensureValueInBounds(this.value) });
}
private getValue(): RangeValue {
@ -305,6 +322,8 @@ export class Range implements ComponentInterface {
// update the active knob's position
this.update(currentX);
this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
}
private onMove(detail: GestureDetail) {
@ -314,6 +333,8 @@ export class Range implements ComponentInterface {
private onEnd(detail: GestureDetail) {
this.update(detail.currentX);
this.pressedKnob = undefined;
this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
}
private update(currentX: number) {

View File

@ -25,6 +25,22 @@ interface RangeChangeEventDetail {
}
```
### RangeKnobMoveStartEventDetail
```typescript
interface RangeKnobMoveStartEventDetail {
value: RangeValue;
}
```
### RangeKnobMoveEndEventDetail
```typescript
interface RangeKnobMoveEndEventDetail {
value: RangeValue;
}
```
### 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.
@ -368,11 +384,13 @@ export default defineComponent({
## Events
| Event | Description | Type |
| ----------- | -------------------------------------------- | ------------------------------------- |
| `ionBlur` | Emitted when the range loses focus. | `CustomEvent<void>` |
| `ionChange` | Emitted when the value property has changed. | `CustomEvent<RangeChangeEventDetail>` |
| `ionFocus` | Emitted when the range has focus. | `CustomEvent<void>` |
| Event | Description | Type |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `ionBlur` | Emitted when the range loses focus. | `CustomEvent<void>` |
| `ionChange` | Emitted when the value property has changed. | `CustomEvent<RangeChangeEventDetail>` |
| `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

View File

@ -1,4 +1,5 @@
import { newE2EPage } from '@stencil/core/testing';
import { dragElementBy } from '@utils/test';
test('range: basic', async () => {
const page = await newE2EPage({
@ -17,3 +18,45 @@ test('range:rtl: basic', async () => {
const compare = await page.compareScreenshot();
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 });
});

View File

@ -66,7 +66,7 @@
</ion-label>
</ion-list-header>
<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-range value="60" color="light" step="10" pin="true" aria-label="Light Pin Range"></ion-range>
@ -240,19 +240,7 @@
knob.value = {
lower: 33,
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() {
var range = document.getElementById('range');

View File

@ -591,7 +591,9 @@ export const IonRange = /*@__PURE__*/ defineContainer<JSX.IonRange>('ion-range',
'ionChange',
'ionStyle',
'ionFocus',
'ionBlur'
'ionBlur',
'ionKnobMoveStart',
'ionKnobMoveEnd'
],
'value', 'v-ion-change', 'ionChange');