diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts
index 3c76469d93..1b01f51f2d 100644
--- a/angular/src/directives/proxies.ts
+++ b/angular/src/directives/proxies.ts
@@ -586,7 +586,7 @@ export class IonRadioGroup {
proxyInputs(IonRadioGroup, ['allowEmptySelection', 'name', 'value']);
export declare interface IonRange extends StencilComponents<'IonRange'> {}
-@Component({ selector: 'ion-range', changeDetection: 0, template: '', inputs: ['color', 'mode', 'debounce', 'name', 'dualKnobs', 'min', 'max', 'pin', 'snaps', 'step', 'disabled', 'value'] })
+@Component({ selector: 'ion-range', changeDetection: 0, template: '', inputs: ['color', 'mode', 'neutralPoint', 'debounce', 'name', 'dualKnobs', 'min', 'max', 'pin', 'snaps', 'step', 'disabled', 'value'] })
export class IonRange {
ionChange!: EventEmitter;
ionFocus!: EventEmitter;
@@ -598,7 +598,7 @@ export class IonRange {
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
}
}
-proxyInputs(IonRange, ['color', 'mode', 'debounce', 'name', 'dualKnobs', 'min', 'max', 'pin', 'snaps', 'step', 'disabled', 'value']);
+proxyInputs(IonRange, ['color', 'mode', 'neutralPoint', 'debounce', 'name', 'dualKnobs', 'min', 'max', 'pin', 'snaps', 'step', 'disabled', 'value']);
export declare interface IonRefresher extends StencilComponents<'IonRefresher'> {}
@Component({ selector: 'ion-refresher', changeDetection: 0, template: '', inputs: ['pullMin', 'pullMax', 'closeDuration', 'snapbackDuration', 'disabled'] })
diff --git a/core/api.txt b/core/api.txt
index 679d727fb6..dba34552d3 100644
--- a/core/api.txt
+++ b/core/api.txt
@@ -800,10 +800,11 @@ ion-range,prop,max,number,100,false,false
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,neutralPoint,number,0,false,false
ion-range,prop,pin,boolean,false,false,false
ion-range,prop,snaps,boolean,false,false,false
ion-range,prop,step,number,1,false,false
-ion-range,prop,value,number | { lower: number; upper: number; },0,false,false
+ion-range,prop,value,null | number | { lower: number; upper: number; },null,false,false
ion-range,event,ionBlur,void,true
ion-range,event,ionChange,RangeChangeEventDetail,true
ion-range,event,ionFocus,void,true
diff --git a/core/src/components.d.ts b/core/src/components.d.ts
index 9119916d90..baca746f3b 100644
--- a/core/src/components.d.ts
+++ b/core/src/components.d.ts
@@ -3309,6 +3309,10 @@ export namespace Components {
*/
'name': string;
/**
+ * The neutral point of the range slider. Default: value is `0` or the `min` when `neutralPoint < min` or `max` when `max < neutralPoint`.
+ */
+ 'neutralPoint': number;
+ /**
* If `true`, a pin with integer value is shown when the knob is pressed.
*/
'pin': boolean;
@@ -3323,7 +3327,7 @@ export namespace Components {
/**
* the value of the range.
*/
- 'value': RangeValue;
+ 'value': RangeValue | null;
}
interface IonRangeAttributes extends StencilHTMLAttributes {
/**
@@ -3359,6 +3363,10 @@ export namespace Components {
*/
'name'?: string;
/**
+ * The neutral point of the range slider. Default: value is `0` or the `min` when `neutralPoint < min` or `max` when `max < neutralPoint`.
+ */
+ 'neutralPoint'?: number;
+ /**
* Emitted when the range loses focus.
*/
'onIonBlur'?: (event: CustomEvent) => void;
@@ -3385,7 +3393,7 @@ export namespace Components {
/**
* the value of the range.
*/
- 'value'?: RangeValue;
+ 'value'?: RangeValue | null;
}
interface IonRefresherContent {
diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx
index d84a8e095a..56c4c3841a 100644
--- a/core/src/components/range/range.tsx
+++ b/core/src/components/range/range.tsx
@@ -44,6 +44,25 @@ export class Range implements ComponentInterface {
*/
@Prop() mode!: Mode;
+ /**
+ * The neutral point of the range slider.
+ * Default: value is `0` or the `min` when `neutralPoint < min` or `max` when `max < neutralPoint`.
+ */
+ @Prop() neutralPoint = 0;
+ protected neutralPointChanged() {
+ if (this.noUpdate) {
+ return;
+ }
+ const { min, max, neutralPoint } = this;
+
+ if (max < neutralPoint) {
+ this.neutralPoint = max;
+ }
+ if (neutralPoint < min) {
+ this.neutralPoint = min;
+ }
+ }
+
/**
* How long, in milliseconds, to wait to trigger the
* `ionChange` event after each change in the range value.
@@ -119,7 +138,7 @@ export class Range implements ComponentInterface {
/**
* the value of the range.
*/
- @Prop({ mutable: true }) value: RangeValue = 0;
+ @Prop({ mutable: true }) value: RangeValue | null = null;
@Watch('value')
protected valueChanged(value: RangeValue) {
if (!this.noUpdate) {
@@ -210,14 +229,14 @@ export class Range implements ComponentInterface {
}
private getValue(): RangeValue {
- const value = this.value || 0;
+ const value = this.value || this.neutralPoint || 0;
if (this.dualKnobs) {
if (typeof value === 'object') {
return value;
}
return {
- lower: 0,
- upper: value
+ lower: this.value === null ? this.neutralPoint : 0,
+ upper: this.value === null ? this.neutralPoint : value
};
} else {
if (typeof value === 'object') {
@@ -361,25 +380,67 @@ export class Range implements ComponentInterface {
}
};
}
+ protected getActiveBarPosition() {
+ const { min, max, neutralPoint, ratioLower, ratioUpper } = this;
+ const neutralPointRatio = valueToRatio(neutralPoint, min, max);
+
+ // dual knob handling
+ let left = `${ratioLower * 100}%`;
+ let right = `${100 - ratioUpper * 100}%`;
+
+ // single knob handling
+ if (!this.dualKnobs) {
+ if (this.ratioA < neutralPointRatio) {
+ right = `${neutralPointRatio * 100}%`;
+ left = `${this.ratioA * 100}%`;
+ } else {
+ right = `${100 - this.ratioA * 100}%`;
+ left = `${neutralPointRatio * 100}%`;
+ }
+ }
+
+ return {
+ left,
+ right
+ };
+ }
+
+ protected isTickActive(stepRatio: number) {
+ const { min, max, neutralPoint, ratioLower, ratioUpper } = this;
+ const neutralPointRatio = valueToRatio(neutralPoint, min, max);
+
+ if (this.dualKnobs) {
+ return (stepRatio >= ratioLower && stepRatio <= ratioUpper);
+ }
+
+ if (this.ratioA <= neutralPointRatio && stepRatio >= this.ratioA && stepRatio <= neutralPointRatio) {
+ return true;
+ }
+
+ if (this.ratioA >= neutralPointRatio && stepRatio <= this.ratioA && stepRatio >= neutralPointRatio) {
+ return true;
+ }
+
+ return false;
+ }
render() {
- const { min, max, step, ratioLower, ratioUpper } = this;
-
- const barStart = `${ratioLower * 100}%`;
- const barEnd = `${100 - ratioUpper * 100}%`;
+ const { min, max, neutralPoint, step } = this;
+ const barPosition = this.getActiveBarPosition();
const isRTL = document.dir === 'rtl';
const start = isRTL ? 'right' : 'left';
const end = isRTL ? 'left' : 'right';
- const ticks = [];
+ const ticks: any[] = [];
+
if (this.snaps) {
for (let value = min; value <= max; value += step) {
const ratio = valueToRatio(value, min, max);
const tick: any = {
ratio,
- active: ratio >= ratioLower && ratio <= ratioUpper,
+ active: this.isTickActive(ratio),
};
tick[start] = `${ratio * 100}%`;
@@ -390,7 +451,6 @@ export class Range implements ComponentInterface {
const tickStyle = (tick: any) => {
const style: any = {};
-
style[start] = tick[start];
return style;
@@ -399,8 +459,8 @@ export class Range implements ComponentInterface {
const barStyle = () => {
const style: any = {};
- style[start] = barStart;
- style[end] = barEnd;
+ style[start] = barPosition[start];
+ style[end] = barPosition[end];
return style;
};
@@ -435,7 +495,8 @@ export class Range implements ComponentInterface {
disabled: this.disabled,
handleKeyboard: this.handleKeyboard,
min,
- max
+ max,
+ neutralPoint
})}
{ this.dualKnobs && renderKnob({
@@ -447,7 +508,8 @@ export class Range implements ComponentInterface {
disabled: this.disabled,
handleKeyboard: this.handleKeyboard,
min,
- max
+ max,
+ neutralPoint
})}
,
@@ -461,6 +523,7 @@ interface RangeKnob {
ratio: number;
min: number;
max: number;
+ neutralPoint: number;
disabled: boolean;
pressed: boolean;
pin: boolean;
@@ -468,7 +531,7 @@ interface RangeKnob {
handleKeyboard: (name: KnobName, isIncrease: boolean) => void;
}
-function renderKnob({ knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard }: RangeKnob) {
+function renderKnob({ knob, value, ratio, min, max, neutralPoint, disabled, pressed, pin, handleKeyboard }: RangeKnob) {
const isRTL = document.dir === 'rtl';
const start = isRTL ? 'right' : 'left';
@@ -501,7 +564,8 @@ function renderKnob({ knob, value, ratio, min, max, disabled, pressed, pin, hand
'range-knob-b': knob === 'B',
'range-knob-pressed': pressed,
'range-knob-min': value === min,
- 'range-knob-max': value === max
+ 'range-knob-max': value === max,
+ 'range-knob-neutral': value === neutralPoint
}}
style={knobStyle()}
role="slider"
@@ -510,6 +574,7 @@ function renderKnob({ knob, value, ratio, min, max, disabled, pressed, pin, hand
aria-valuemax={max}
aria-disabled={disabled ? 'true' : null}
aria-valuenow={value}
+ aria-valueneutral={neutralPoint}
>
{pin && {Math.round(value)}
}
diff --git a/core/src/components/range/readme.md b/core/src/components/range/readme.md
index ca9655cb49..0fcc30661f 100644
--- a/core/src/components/range/readme.md
+++ b/core/src/components/range/readme.md
@@ -43,9 +43,17 @@ left or right of the range.
+
+
+
+
+
+
+
+
```
@@ -76,9 +84,17 @@ left or right of the range.
+
+
+
+
+
+
+
+
```
@@ -86,20 +102,21 @@ left or right of the range.
## 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. | `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` |
-| `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. | `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` | `''` |
+| `neutralPoint` | `neutral-point` | The neutral point of the range slider. Default: value is `0` or the `min` when `neutralPoint < min` or `max` when `max < neutralPoint`. | `number` | `0` |
+| `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` |
+| `value` | `value` | the value of the range. | `null \| number \| { lower: number; upper: number; }` | `null` |
## Events
diff --git a/core/src/components/range/test/basic/index.html b/core/src/components/range/test/basic/index.html
index fe31c7410d..20cd38ba7c 100644
--- a/core/src/components/range/test/basic/index.html
+++ b/core/src/components/range/test/basic/index.html
@@ -101,6 +101,12 @@
+
+
+
+
+
+
diff --git a/core/src/components/range/usage/angular.md b/core/src/components/range/usage/angular.md
index 677afff2a4..aa8f87655f 100644
--- a/core/src/components/range/usage/angular.md
+++ b/core/src/components/range/usage/angular.md
@@ -22,9 +22,17 @@
+
+
+
+
+
+
+
+
```
diff --git a/core/src/components/range/usage/javascript.md b/core/src/components/range/usage/javascript.md
index 4bcbb39678..6c1d5e9527 100644
--- a/core/src/components/range/usage/javascript.md
+++ b/core/src/components/range/usage/javascript.md
@@ -22,8 +22,16 @@
+
+
+
+
+
+
+
+
```