feat(range): add support for customizing pin format (#22972)

This commit is contained in:
Victor Berchet
2021-07-09 07:57:52 -07:00
committed by GitHub
parent 1c9435c3f5
commit 8f2c4f73db
13 changed files with 164 additions and 26 deletions

View File

@@ -609,8 +609,8 @@ export class IonRadioGroup {
}
export declare interface IonRange extends Components.IonRange {
}
@ProxyCmp({ inputs: ["color", "debounce", "disabled", "dualKnobs", "max", "min", "mode", "name", "pin", "snaps", "step", "ticks", "value"] })
@Component({ selector: "ion-range", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["color", "debounce", "disabled", "dualKnobs", "max", "min", "mode", "name", "pin", "snaps", "step", "ticks", "value"] })
@ProxyCmp({ inputs: ["color", "debounce", "disabled", "dualKnobs", "max", "min", "mode", "name", "pin", "pinFormatter", "snaps", "step", "ticks", "value"] })
@Component({ selector: "ion-range", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["color", "debounce", "disabled", "dualKnobs", "max", "min", "mode", "name", "pin", "pinFormatter", "snaps", "step", "ticks", "value"] })
export class IonRange {
ionChange!: EventEmitter<CustomEvent>;
ionFocus!: EventEmitter<CustomEvent>;

View File

@@ -953,6 +953,7 @@ ion-range,prop,min,number,0,false,false
ion-range,prop,mode,"ios" | "md",undefined,false,false
ion-range,prop,name,string,'',false,false
ion-range,prop,pin,boolean,false,false,false
ion-range,prop,pinFormatter,(value: number) => string | number,(value: number): number => Math.round(value),false,false
ion-range,prop,snaps,boolean,false,false,false
ion-range,prop,step,number,1,false,false
ion-range,prop,ticks,boolean,true,false,false

View File

@@ -7,6 +7,7 @@
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { AccordionGroupChangeEventDetail, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, MenuChangeEventDetail, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, 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 { IonicSafeString } from "./utils/sanitization";
import { PinFormatter } from "./components/range/range-interface";
import { NavigationHookCallback } from "./components/route/route-interface";
import { SelectCompareFn } from "./components/select/select-interface";
export namespace Components {
@@ -1952,6 +1953,10 @@ export namespace Components {
* If `true`, a pin with integer value is shown when the knob is pressed.
*/
"pin": boolean;
/**
* A callback used to format the pin text. By default the pin text is set to `Math.round(value)`.
*/
"pinFormatter": PinFormatter;
/**
* If `true`, the knob snaps to tick marks evenly spaced based on the step property value.
*/
@@ -5493,6 +5498,10 @@ declare namespace LocalJSX {
* If `true`, a pin with integer value is shown when the knob is pressed.
*/
"pin"?: boolean;
/**
* A callback used to format the pin text. By default the pin text is set to `Math.round(value)`.
*/
"pinFormatter"?: PinFormatter;
/**
* If `true`, the knob snaps to tick marks evenly spaced based on the step property value.
*/

View File

@@ -2,6 +2,8 @@ export type KnobName = 'A' | 'B' | undefined;
export type RangeValue = number | {lower: number, upper: number};
export type PinFormatter = (value: number) => number | string;
export interface RangeChangeEventDetail {
value: RangeValue;
}

View File

@@ -5,6 +5,8 @@ import { Color, Gesture, GestureDetail, KnobName, RangeChangeEventDetail, RangeV
import { clamp, debounceEvent, getAriaLabel, inheritAttributes, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
import { PinFormatter } from './range-interface';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
@@ -102,6 +104,12 @@ export class Range implements ComponentInterface {
*/
@Prop() pin = false;
/**
* A callback used to format the pin text.
* By default the pin text is set to `Math.round(value)`.
*/
@Prop() pinFormatter: PinFormatter = (value: number): number => Math.round(value);
/**
* If `true`, the knob snaps to tick marks evenly spaced based
* on the step property value.
@@ -409,7 +417,7 @@ export class Range implements ComponentInterface {
}
render() {
const { min, max, step, el, handleKeyboard, pressedKnob, disabled, pin, ratioLower, ratioUpper, inheritedAttributes, rangeId } = this;
const { min, max, step, el, handleKeyboard, pressedKnob, disabled, pin, ratioLower, ratioUpper, inheritedAttributes, rangeId, pinFormatter } = this;
/**
* Look for external label, ion-label, or aria-labelledby.
@@ -500,6 +508,7 @@ export class Range implements ComponentInterface {
value: this.valA,
ratio: this.ratioA,
pin,
pinFormatter,
disabled,
handleKeyboard,
min,
@@ -513,6 +522,7 @@ export class Range implements ComponentInterface {
value: this.valB,
ratio: this.ratioB,
pin,
pinFormatter,
disabled,
handleKeyboard,
min,
@@ -535,12 +545,13 @@ interface RangeKnob {
disabled: boolean;
pressed: boolean;
pin: boolean;
pinFormatter: PinFormatter;
labelText?: string | null;
handleKeyboard: (name: KnobName, isIncrease: boolean) => void;
}
const renderKnob = (isRTL: boolean, { knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard, labelText }: RangeKnob) => {
const renderKnob = (isRTL: boolean, { knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard, labelText, pinFormatter }: RangeKnob) => {
const start = isRTL ? 'right' : 'left';
const knobStyle = () => {
@@ -583,7 +594,7 @@ const renderKnob = (isRTL: boolean, { knob, value, ratio, min, max, disabled, pr
aria-disabled={disabled ? 'true' : null}
aria-valuenow={value}
>
{pin && <div class="range-pin" role="presentation" part="pin">{Math.round(value)}</div>}
{pin && <div class="range-pin" role="presentation" part="pin">{pinFormatter(value)}</div>}
<div class="range-knob" role="presentation" part="knob" />
</div>
);

View File

@@ -11,6 +11,10 @@ Labels can be placed on either side of the range by adding the
be an `ion-label`, it can be added to any element to place it to the
left or right of the range.
## Custom Pin Formatters
When using a pin, the default behavior is to round the value that gets displayed using `Math.round()`. This behavior can be customized by passing in a formatter function to the `pinFormatter` property. See the [Usage](#usage) section for an example.
<!-- Auto Generated Below -->
@@ -22,7 +26,7 @@ left or right of the range.
```html
<ion-list>
<ion-item>
<ion-range color="danger" pin="true"></ion-range>
<ion-range color="danger" [pin]="true"></ion-range>
</ion-item>
<ion-item>
@@ -50,9 +54,26 @@ left or right of the range.
<ion-item>
<ion-range dualKnobs="true" min="21" max="72" step="3" snaps="true"></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" max="100" [pinFormatter]="customFormatter" [pin]="true"></ion-range>
</ion-item>
</ion-list>
```
```typescript
import { Component } from '@angular/core';
@Component({})
export class MyComponent {
constructor() {}
public customFormatter(value: number) {
return `${value}%`
}
}
```
### Javascript
@@ -87,7 +108,16 @@ left or right of the range.
<ion-item>
<ion-range dual-knobs="true" min="21" max="72" step="3" snaps="true"></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" max="100" pin="true" id="custom-range"></ion-range>
</ion-item>
</ion-list>
<script>
const customRange = document.querySelector('#custom-range');
customRange.pinFormatter = (value) => `${value}%`;
</script>
```
@@ -106,6 +136,8 @@ export const RangeExamples: React.FC = () => {
lower: number;
upper: number;
}>({ lower: 0, upper: 0 });
const customFormatter = (value: number) => `${value}%`;
return (
<IonPage>
@@ -157,6 +189,10 @@ export const RangeExamples: React.FC = () => {
<IonItem>
<IonLabel>Value: lower: {rangeValue.lower} upper: {rangeValue.upper}</IonLabel>
</IonItem>
<IonItem>
<IonRange min={0} max={100} pinFormatter={customFormatter} pin={true}></IonRange>
</IonItem>
</IonList>
</IonContent>
</IonPage>
@@ -175,6 +211,8 @@ import { Component, h } from '@stencil/core';
styleUrl: 'range-example.css'
})
export class RangeExample {
private customFormatter = (value: number) => `${value}%`;
render() {
return [
<ion-list>
@@ -207,6 +245,10 @@ export class RangeExample {
<ion-item>
<ion-range dualKnobs={true} min={21} max={72} step={3} snaps={true}></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" max="100" pinFormatter={this.customFormatter} pin={true}></ion-range>
</ion-item>
</ion-list>
];
}
@@ -220,7 +262,7 @@ export class RangeExample {
<template>
<ion-list>
<ion-item>
<ion-range color="danger" pin="true"></ion-range>
<ion-range color="danger" :pin="true"></ion-range>
</ion-item>
<ion-item>
@@ -248,10 +290,14 @@ export class RangeExample {
<ion-item>
<ion-range ref="rangeDualKnobs" dual-knobs="true" min="21" max="72" step="3" snaps="true"></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" max="100" :pin-formatter="customFormatter" :pin="true"></ion-range>
</ion-item>
</ion-list>
</template>
<script>
<script lang="ts">
import { IonItem, IonLabel, IonList, IonRange } from '@ionic/vue';
import { defineComponent } from 'vue';
@@ -260,6 +306,11 @@ export default defineComponent({
mounted() {
// Sets initial value for dual-knob ion-range
this.$refs.rangeDualKnobs.value = { lower: 24, upper: 42 };
},
setup() {
const customFormatter = (value: number) => `${value}%`;
return { customFormatter };
}
});
</script>
@@ -269,21 +320,22 @@ export default defineComponent({
## Properties
| Property | Attribute | Description | Type | Default |
| ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ----------- |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `debounce` | `debounce` | How long, in milliseconds, to wait to trigger the `ionChange` event after each change in the range value. This also impacts form bindings such as `ngModel` or `v-model`. | `number` | `0` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the range. | `boolean` | `false` |
| `dualKnobs` | `dual-knobs` | Show two knobs. | `boolean` | `false` |
| `max` | `max` | Maximum integer value of the range. | `number` | `100` |
| `min` | `min` | Minimum integer value of the range. | `number` | `0` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `name` | `name` | The name of the control, which is submitted with the form data. | `string` | `''` |
| `pin` | `pin` | If `true`, a pin with integer value is shown when the knob is pressed. | `boolean` | `false` |
| `snaps` | `snaps` | If `true`, the knob snaps to tick marks evenly spaced based on the step property value. | `boolean` | `false` |
| `step` | `step` | Specifies the value granularity. | `number` | `1` |
| `ticks` | `ticks` | If `true`, tick marks are displayed based on the step value. Only applies when `snaps` is `true`. | `boolean` | `true` |
| `value` | `value` | the value of the range. | `number \| { lower: number; upper: number; }` | `0` |
| Property | Attribute | Description | Type | Default |
| -------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------- |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `debounce` | `debounce` | How long, in milliseconds, to wait to trigger the `ionChange` event after each change in the range value. This also impacts form bindings such as `ngModel` or `v-model`. | `number` | `0` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the range. | `boolean` | `false` |
| `dualKnobs` | `dual-knobs` | Show two knobs. | `boolean` | `false` |
| `max` | `max` | Maximum integer value of the range. | `number` | `100` |
| `min` | `min` | Minimum integer value of the range. | `number` | `0` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `name` | `name` | The name of the control, which is submitted with the form data. | `string` | `''` |
| `pin` | `pin` | If `true`, a pin with integer value is shown when the knob is pressed. | `boolean` | `false` |
| `pinFormatter` | -- | A callback used to format the pin text. By default the pin text is set to `Math.round(value)`. | `(value: number) => string \| number` | `(value: number): number => Math.round(value)` |
| `snaps` | `snaps` | If `true`, the knob snaps to tick marks evenly spaced based on the step property value. | `boolean` | `false` |
| `step` | `step` | Specifies the value granularity. | `number` | `1` |
| `ticks` | `ticks` | If `true`, tick marks are displayed based on the step value. Only applies when `snaps` is `true`. | `boolean` | `true` |
| `value` | `value` | the value of the range. | `number \| { lower: number; upper: number; }` | `0` |
## Events

View File

@@ -177,6 +177,18 @@
<ion-range min="0" value="50" max="100" id="targetRange" aria-label="Coupled Range"></ion-range>
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label>
Custom pin label
</ion-label>
</ion-list-header>
<ion-item>
<ion-range pin min="1" step="0.1" max="2" id="customLabel"></ion-range>
</ion-item>
</ion-list>
</ion-content>
</ion-app>
@@ -264,6 +276,9 @@
lower: '-100',
upper: '100'
}
const customLabel = document.getElementById('customLabel');
customLabel.pinFormatter = (value) => value.toFixed(1);
</script>
</body>

View File

@@ -1,7 +1,7 @@
```html
<ion-list>
<ion-item>
<ion-range color="danger" pin="true"></ion-range>
<ion-range color="danger" [pin]="true"></ion-range>
</ion-item>
<ion-item>
@@ -29,5 +29,22 @@
<ion-item>
<ion-range dualKnobs="true" min="21" max="72" step="3" snaps="true"></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" max="100" [pinFormatter]="customFormatter" [pin]="true"></ion-range>
</ion-item>
</ion-list>
```
```typescript
import { Component } from '@angular/core';
@Component({})
export class MyComponent {
constructor() {}
public customFormatter(value: number) {
return `${value}%`
}
}
```

View File

@@ -29,5 +29,14 @@
<ion-item>
<ion-range dual-knobs="true" min="21" max="72" step="3" snaps="true"></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" max="100" pin="true" id="custom-range"></ion-range>
</ion-item>
</ion-list>
<script>
const customRange = document.querySelector('#custom-range');
customRange.pinFormatter = (value) => `${value}%`;
</script>
```

View File

@@ -11,6 +11,8 @@ export const RangeExamples: React.FC = () => {
lower: number;
upper: number;
}>({ lower: 0, upper: 0 });
const customFormatter = (value: number) => `${value}%`;
return (
<IonPage>
@@ -62,6 +64,10 @@ export const RangeExamples: React.FC = () => {
<IonItem>
<IonLabel>Value: lower: {rangeValue.lower} upper: {rangeValue.upper}</IonLabel>
</IonItem>
<IonItem>
<IonRange min={0} max={100} pinFormatter={customFormatter} pin={true}></IonRange>
</IonItem>
</IonList>
</IonContent>
</IonPage>

View File

@@ -6,6 +6,8 @@ import { Component, h } from '@stencil/core';
styleUrl: 'range-example.css'
})
export class RangeExample {
private customFormatter = (value: number) => `${value}%`;
render() {
return [
<ion-list>
@@ -38,6 +40,10 @@ export class RangeExample {
<ion-item>
<ion-range dualKnobs={true} min={21} max={72} step={3} snaps={true}></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" max="100" pinFormatter={this.customFormatter} pin={true}></ion-range>
</ion-item>
</ion-list>
];
}

View File

@@ -2,7 +2,7 @@
<template>
<ion-list>
<ion-item>
<ion-range color="danger" pin="true"></ion-range>
<ion-range color="danger" :pin="true"></ion-range>
</ion-item>
<ion-item>
@@ -30,10 +30,14 @@
<ion-item>
<ion-range ref="rangeDualKnobs" dual-knobs="true" min="21" max="72" step="3" snaps="true"></ion-range>
</ion-item>
<ion-item>
<ion-range min="0" max="100" :pin-formatter="customFormatter" :pin="true"></ion-range>
</ion-item>
</ion-list>
</template>
<script>
<script lang="ts">
import { IonItem, IonLabel, IonList, IonRange } from '@ionic/vue';
import { defineComponent } from 'vue';
@@ -42,6 +46,11 @@ export default defineComponent({
mounted() {
// Sets initial value for dual-knob ion-range
this.$refs.rangeDualKnobs.value = { lower: 24, upper: 42 };
},
setup() {
const customFormatter = (value: number) => `${value}%`;
return { customFormatter };
}
});
</script>

View File

@@ -573,6 +573,7 @@ export const IonRange = /*@__PURE__*/ defineContainer<JSX.IonRange>('ion-range',
'min',
'max',
'pin',
'pinFormatter',
'snaps',
'step',
'ticks',