diff --git a/packages/core/src/components/app/app.tsx b/packages/core/src/components/app/app.tsx
index 7533a3183c..2b759c76b8 100644
--- a/packages/core/src/components/app/app.tsx
+++ b/packages/core/src/components/app/app.tsx
@@ -1,10 +1,5 @@
-<<<<<<< HEAD
-import { Element, Component, Listen, Prop } from '@stencil/core';
-import { Nav, NavContainer } from '../../navigation/nav-interfaces';
-=======
import { Component, Element, Listen, Prop } from '@stencil/core';
-import { Nav, NavContainer, OverlayPortal } from '../../navigation/nav-interfaces';
->>>>>>> fffa9b54606f47336abfbb232405186787180b12
+import { Nav, NavContainer } from '../../navigation/nav-interfaces';
import { Config } from '../..';
import { App } from './app-interfaces';
import { isReady } from '../../utils/helpers';
diff --git a/packages/core/src/components/range/range-knob.tsx b/packages/core/src/components/range/range-knob.tsx
new file mode 100644
index 0000000000..52849f5ccf
--- /dev/null
+++ b/packages/core/src/components/range/range-knob.tsx
@@ -0,0 +1,74 @@
+import { Component, Event, EventEmitter, Listen, Prop } from '@stencil/core';
+
+@Component({
+ tag: `ion-range-knob`
+})
+export class RangeKnob {
+ @Prop() pressed: boolean;
+ @Prop() pin: boolean;
+ @Prop() min: number;
+ @Prop() max: number;
+ @Prop() val: number;
+ @Prop() disabled: boolean;
+ @Prop() labelId: string;
+ @Prop() knob: string;
+ @Prop() ratio: number;
+
+ @Event() ionIncrease: EventEmitter;
+ @Event() ionDecrease: EventEmitter;
+
+ @Listen('keydown')
+ handleKeyBoard(ev: KeyboardEvent) {
+ const keyCode = ev.keyCode;
+ if (keyCode === KEY_LEFT || keyCode === KEY_DOWN) {
+ this.ionDecrease.emit({isIncrease: false, knob: this.knob});
+ ev.preventDefault();
+ ev.stopPropagation();
+ } else if (keyCode === KEY_RIGHT || keyCode === KEY_UP) {
+ this.ionIncrease.emit({isIncrease: true, knob: this.knob});
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+ }
+
+ leftPos(val: number) {
+ return `${val * 100}%`;
+ }
+
+ hostData() {
+ return {
+ class: {
+ 'range-knob-pressed': this.pressed,
+ 'range-knob-min': this.val === this.min || this.val === undefined,
+ 'range-knob-max': this.val === this.max
+ },
+ style: {
+ 'left': this.leftPos(this.ratio)
+ },
+ attrs: {
+ 'role': 'slider',
+ 'tabindex': this.disabled ? -1 : 0,
+ 'aria-valuemin': this.min,
+ 'aria-valuemax': this.max,
+ 'aria-disabled': this.disabled,
+ 'aria-labelledby': this.labelId,
+ 'aria-valuenow': this.val
+ }
+ };
+ }
+
+ render() {
+ if (this.pin) {
+ return [
+
{this.val}
,
+
+ ];
+ }
+ return ;
+ }
+}
+
+export const KEY_LEFT = 37;
+export const KEY_UP = 38;
+export const KEY_RIGHT = 39;
+export const KEY_DOWN = 40;
diff --git a/packages/core/src/components/range/range.ios.scss b/packages/core/src/components/range/range.ios.scss
new file mode 100644
index 0000000000..eded03f7aa
--- /dev/null
+++ b/packages/core/src/components/range/range.ios.scss
@@ -0,0 +1,227 @@
+@import "../../themes/ionic.globals.ios";
+
+// iOS Range
+// --------------------------------------------------
+
+/// @prop - Padding top/bottom of the range
+$range-ios-padding-vertical: 8px !default;
+
+/// @prop - Padding start/end of the range
+$range-ios-padding-horizontal: 16px !default;
+
+/// @prop - Height of the range slider
+$range-ios-slider-height: 42px !default;
+
+/// @prop - Width of the area that will select the range knob
+$range-ios-hit-width: 42px !default;
+
+/// @prop - Height of the area that will select the range knob
+$range-ios-hit-height: $range-ios-slider-height !default;
+
+/// @prop - Height of the range bar
+$range-ios-bar-height: 1px !default;
+
+/// @prop - Background of the range bar
+$range-ios-bar-background-color: #bdbdbd !default;
+
+/// @prop - Background of the active range bar
+$range-ios-bar-active-background-color: color($colors-ios, primary) !default;
+
+/// @prop - Width of the range knob
+$range-ios-knob-width: 28px !default;
+
+/// @prop - Height of the range knob
+$range-ios-knob-height: $range-ios-knob-width !default;
+
+/// @prop - Box shadow of the range knob
+$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) !default;
+
+/// @prop - Border radius of the range knob
+$range-ios-knob-border-radius: 50% !default;
+
+/// @prop - Background of the range knob
+$range-ios-knob-background-color: #fff !default;
+
+/// @prop - Width of the range tick
+$range-ios-tick-width: $range-ios-bar-height !default;
+
+/// @prop - Height of the range tick
+$range-ios-tick-height: 8px !default;
+
+/// @prop - Border radius of the range tick
+$range-ios-tick-border-radius: 0 !default;
+
+/// @prop - Background of the range tick
+$range-ios-tick-background-color: $range-ios-bar-background-color !default;
+
+/// @prop - Background of the active range tick
+$range-ios-tick-active-background-color: $range-ios-bar-active-background-color !default;
+
+/// @prop - Background of the range pin
+$range-ios-pin-background-color: transparent !default;
+
+/// @prop - Color of the range pin
+$range-ios-pin-color: $text-ios-color !default;
+
+/// @prop - Font size of the range pin
+$range-ios-pin-font-size: 12px !default;
+
+// deprecated
+$range-ios-pin-padding: null !default;
+
+/// @prop - Padding top of the range pin
+$range-ios-pin-padding-top: 8px !default;
+
+/// @prop - Padding end of the range pin
+$range-ios-pin-padding-end: $range-ios-pin-padding-top !default;
+
+/// @prop - Padding bottom of the range pin
+$range-ios-pin-padding-bottom: $range-ios-pin-padding-top !default;
+
+/// @prop - Padding start of the range pin
+$range-ios-pin-padding-start: $range-ios-pin-padding-end !default;
+
+
+.range-ios {
+ @include padding($range-ios-padding-vertical, $range-ios-padding-horizontal);
+}
+
+.range-ios [range-left] {
+ @include margin(0, 20px, 0, 0);
+}
+
+.range-ios [range-right] {
+ @include margin(0, 0, 0, 20px);
+}
+
+.range-ios.range-has-pin {
+ @include padding($range-ios-padding-vertical + $range-ios-pin-font-size, null, null, null);
+}
+
+.range-ios .range-slider {
+ height: $range-ios-slider-height;
+}
+
+.range-ios .range-bar {
+ @include position(($range-ios-slider-height / 2), null, null, 0);
+ @include border-radius(1px);
+
+ position: absolute;
+
+ width: 100%;
+ height: $range-ios-bar-height;
+
+ background: $range-ios-bar-background-color;
+
+ pointer-events: none;
+}
+
+.range-ios.range-pressed .range-bar-active {
+ will-change: left, right;
+}
+
+.range-ios.range-pressed .range-knob-handle {
+ will-change: left;
+}
+
+.range-ios .range-bar-active {
+ bottom: 0;
+
+ width: auto;
+
+ background: $range-ios-bar-active-background-color;
+}
+
+.range-ios .range-knob-handle {
+ @include position(($range-ios-slider-height / 2), null, null, 0);
+ @include margin(-($range-ios-hit-height / 2), null, null, -($range-ios-hit-width / 2));
+ @include text-align(center);
+
+ position: absolute;
+
+ width: $range-ios-hit-width;
+ height: $range-ios-hit-height;
+}
+
+.range-ios .range-knob {
+ @include position(($range-ios-hit-height / 2) - ($range-ios-knob-height / 2) + ($range-ios-bar-height / 2) - .5px,
+ null, null, ($range-ios-hit-width / 2) - ($range-ios-knob-width / 2));
+ @include border-radius($range-ios-knob-border-radius);
+
+ position: absolute;
+
+ width: $range-ios-knob-width;
+ height: $range-ios-knob-height;
+
+ background: $range-ios-knob-background-color;
+
+ box-shadow: $range-ios-knob-box-shadow;
+
+ pointer-events: none;
+}
+
+.range-ios .range-tick {
+ @include margin-horizontal(-($range-ios-tick-width / 2), null);
+ @include border-radius($range-ios-tick-border-radius);
+
+ position: absolute;
+ top: ($range-ios-hit-height / 2) - ($range-ios-tick-height / 2) + ($range-ios-bar-height / 2);
+
+ width: $range-ios-tick-width;
+ height: $range-ios-tick-height;
+
+ background: $range-ios-tick-background-color;
+
+ pointer-events: none;
+}
+
+.range-ios .range-tick-active {
+ background: $range-ios-tick-active-background-color;
+}
+
+.range-ios .range-pin {
+ @include text-align(center);
+ @include border-radius(50px);
+ @include transform(translate3d(0, 28px, 0), scale(.01));
+
+ position: relative;
+ top: -20px;
+ display: inline-block;
+
+ min-width: 28px;
+
+ font-size: $range-ios-pin-font-size;
+
+ color: $range-ios-pin-color;
+
+ background: $range-ios-pin-background-color;
+
+ transition: transform 120ms ease;
+
+ @include deprecated-variable(padding, $range-ios-pin-padding) {
+ @include padding($range-ios-pin-padding-top, $range-ios-pin-padding-end, $range-ios-pin-padding-bottom, $range-ios-pin-padding-start);
+ }
+}
+
+.range-ios .range-knob-pressed .range-pin {
+ @include transform(translate3d(0, 0, 0), scale(1));
+}
+
+.range-ios.range-disabled {
+ opacity: .5;
+}
+
+
+// Generate iOS Range Colors
+// --------------------------------------------------
+
+@each $color-name, $color-base, $color-contrast in get-colors($colors-ios) {
+
+ .range-ios-#{$color-name} {
+ .range-bar-active,
+ .range-tick-active {
+ background: $color-base;
+ }
+ }
+
+}
diff --git a/packages/core/src/components/range/range.md.scss b/packages/core/src/components/range/range.md.scss
new file mode 100644
index 0000000000..87c4f9d81a
--- /dev/null
+++ b/packages/core/src/components/range/range.md.scss
@@ -0,0 +1,279 @@
+@import "../../themes/ionic.globals.md";
+@import "./range";
+// Material Design Range
+// --------------------------------------------------
+
+/// @prop - Padding top/bottom of the range
+$range-md-padding-vertical: 8px !default;
+
+/// @prop - Padding start/end of the range
+$range-md-padding-horizontal: 8px !default;
+
+/// @prop - Height of the range slider
+$range-md-slider-height: 42px !default;
+
+/// @prop - Width of the area that will select the range knob
+$range-md-hit-width: 42px !default;
+
+/// @prop - Height of the area that will select the range knob
+$range-md-hit-height: $range-md-slider-height !default;
+
+/// @prop - Height of the range bar
+$range-md-bar-height: 2px !default;
+
+/// @prop - Background of the range bar
+$range-md-bar-background-color: #bdbdbd !default;
+
+/// @prop - Background of the active range bar
+$range-md-bar-active-background-color: color($colors-md, primary) !default;
+
+/// @prop - Width of the range knob
+$range-md-knob-width: 18px !default;
+
+/// @prop - Height of the range knob
+$range-md-knob-height: $range-md-knob-width !default;
+
+/// @prop - Background of the range knob
+$range-md-knob-background-color: $range-md-bar-active-background-color !default;
+
+/// @prop - Background of the range knob when the value is the minimum
+$range-md-knob-min-background-color: $background-md-color !default;
+
+/// @prop - Border of the range knob when the value is the minimum
+$range-md-knob-min-border: 2px solid $range-md-bar-background-color !default;
+
+/// @prop - Width of the range tick
+$range-md-tick-width: 2px !default;
+
+/// @prop - Height of the range tick
+$range-md-tick-height: $range-md-tick-width !default;
+
+/// @prop - Border radius of the range tick
+$range-md-tick-border-radius: 50% !default;
+
+/// @prop - Background of the range tick
+$range-md-tick-background-color: #000 !default;
+
+/// @prop - Background of the active range tick
+$range-md-tick-active-background-color: $range-md-tick-background-color !default;
+
+/// @prop - Background of the range pin
+$range-md-pin-background-color: $range-md-bar-active-background-color !default;
+
+/// @prop - Color of the range pin
+$range-md-pin-color: color-contrast($colors-md, $range-md-bar-active-background-color) !default;
+
+/// @prop - Font size of the range pin
+$range-md-pin-font-size: 12px !default;
+
+/// @prop - Padding top/bottom of the range pin
+$range-md-pin-padding-vertical: 8px !default;
+
+/// @prop - Padding start/end of the range pin
+$range-md-pin-padding-horizontal: 0 !default;
+
+/// @prop - Background of the range pin when the value is the minimum
+$range-md-pin-min-background-color: $range-md-bar-background-color !default;
+
+
+.range-md {
+ @include padding($range-md-padding-vertical, $range-md-padding-horizontal);
+}
+
+.range-md [slot="range-start"] {
+ @include margin(0, 12px, 0, 0);
+}
+
+.range-md [slot="range-end"] {
+ @include margin(0, 0, 0, 12px);
+}
+
+.range-md.range-has-pin {
+ @include padding($range-md-padding-vertical + $range-md-pin-font-size + $range-md-pin-padding-vertical, null, null, null);
+}
+
+.range-md .range-slider {
+ height: $range-md-slider-height;
+}
+
+.range-md .range-bar {
+ @include position(($range-md-slider-height / 2), null, null, 0);
+
+ position: absolute;
+
+ width: 100%;
+ height: $range-md-bar-height;
+
+ background: $range-md-bar-background-color;
+
+ pointer-events: none;
+}
+
+.range-md.range-pressed .range-bar-active {
+ will-change: left, right;
+}
+
+.range-md.range-pressed .range-knob-handle {
+ will-change: left;
+}
+
+.range-md .range-bar-active {
+ bottom: 0;
+
+ width: auto;
+
+ background: $range-md-bar-active-background-color;
+}
+
+.range-md .range-knob-handle {
+ @include position(($range-md-slider-height / 2), null, null, 0);
+ @include margin(-($range-md-hit-height / 2), null, null, -($range-md-hit-width / 2));
+ @include text-align(center);
+
+ position: absolute;
+
+ width: $range-md-hit-width;
+ height: $range-md-hit-height;
+}
+
+.range-md .range-knob {
+ @include position(($range-md-hit-height / 2) - ($range-md-knob-height / 2) + ($range-md-bar-height / 2),
+ null, null, ($range-md-hit-width / 2) - ($range-md-knob-width / 2));
+ @include border-radius(50%);
+
+ position: absolute;
+
+ z-index: 2;
+
+ width: $range-md-knob-width;
+ height: $range-md-knob-height;
+
+ background: $range-md-knob-background-color;
+
+ transform: scale(.67);
+ transition-duration: 120ms;
+ transition-property: transform, background-color, border;
+ transition-timing-function: ease;
+
+ pointer-events: none;
+}
+
+.range-md .range-tick {
+ @include margin-horizontal(-($range-md-tick-width / 2), null);
+ @include border-radius($range-md-tick-border-radius);
+
+ position: absolute;
+
+ top: ($range-md-hit-height / 2) - ($range-md-tick-height / 2) + ($range-md-bar-height / 2);
+
+ z-index: 1;
+
+ width: $range-md-tick-width;
+ height: $range-md-tick-height;
+
+ background: $range-md-tick-background-color;
+
+ pointer-events: none;
+}
+
+.range-md .range-tick-active {
+ background: $range-md-tick-active-background-color;
+}
+
+.range-md .range-pin {
+ @include padding($range-md-pin-padding-vertical, $range-md-pin-padding-horizontal);
+ @include text-align(center);
+ @include border-radius(50%);
+ @include transform(translate3d(0, 28px, 0), scale(.01));
+
+ position: relative;
+ top: -20px;
+ display: inline-block;
+
+ min-width: 28px;
+ height: 28px;
+
+ font-size: $range-md-pin-font-size;
+
+ color: $range-md-pin-color;
+
+ background: $range-md-pin-background-color;
+
+ transition: transform 120ms ease, background-color 120ms ease;
+
+ &::before {
+ @include position(3px, null, null, 50%);
+ @include border-radius(50%, 50%, 50%, 0);
+ @include margin-horizontal(-13px, null);
+
+ position: absolute;
+
+ z-index: -1;
+
+ width: 26px;
+ height: 26px;
+
+ background: $range-md-pin-background-color;
+
+ content: "";
+ transform: rotate(-45deg);
+ transition: background-color 120ms ease;
+ }
+}
+
+.range-md .range-knob-pressed .range-pin {
+ @include transform(translate3d(0, 0, 0), scale(1));
+}
+
+.range-md:not(.range-has-pin) .range-knob-pressed .range-knob {
+ transform: scale(1);
+}
+
+@mixin md-range-min() {
+ .range-md .range-knob-min.range-knob-min {
+ .range-knob {
+ border: $range-md-knob-min-border;
+ background: $range-md-knob-min-background-color;
+ }
+
+ .range-pin,
+ .range-pin::before {
+ color: color-contrast($colors-md, $range-md-pin-min-background-color);
+ background: $range-md-pin-min-background-color;
+ }
+ }
+}
+
+@include md-range-min();
+
+.range-md.range-disabled {
+ .range-bar-active {
+ background-color: $range-md-bar-background-color;
+ }
+
+ .range-knob {
+ outline: 5px solid #fff;
+ background-color: $range-md-bar-background-color;
+ transform: scale(.55);
+ }
+
+}
+
+
+// Generate Material Design Range Colors
+// --------------------------------------------------
+
+@each $color-name, $color-base, $color-contrast in get-colors($colors-md) {
+
+ .range-md-#{$color-name} {
+ @include md-range-min();
+
+ .range-bar-active,
+ .range-knob,
+ .range-pin,
+ .range-pin::before {
+ background: $color-base;
+ }
+ }
+
+}
diff --git a/packages/core/src/components/range/range.scss b/packages/core/src/components/range/range.scss
new file mode 100644
index 0000000000..cd13f04164
--- /dev/null
+++ b/packages/core/src/components/range/range.scss
@@ -0,0 +1,56 @@
+@import "../../themes/ionic.globals";
+
+// Range
+// --------------------------------------------------
+
+.item .item-inner {
+ overflow: visible;
+
+ width: 100%;
+}
+
+.item .input-wrapper {
+ overflow: visible;
+
+ flex-direction: column;
+
+ width: 100%;
+}
+
+.item ion-range {
+ width: 100%;
+}
+
+.item ion-range ion-label {
+ align-self: center;
+}
+
+ion-range {
+ position: relative;
+ display: flex;
+
+ align-items: center;
+
+
+ ion-label {
+ flex: initial;
+ }
+
+ ion-icon {
+ min-height: 2.4rem;
+
+ font-size: 2.4rem;
+ line-height: 1;
+
+ }
+
+ ion-gesture,
+ .range-slider {
+ position: relative;
+
+ flex: 1;
+
+ cursor: pointer;
+ }
+}
+
diff --git a/packages/core/src/components/range/range.tsx b/packages/core/src/components/range/range.tsx
new file mode 100644
index 0000000000..d04b38db63
--- /dev/null
+++ b/packages/core/src/components/range/range.tsx
@@ -0,0 +1,401 @@
+import { Component, Element, Event, EventEmitter, Listen, Method, Prop, PropDidChange, State } from '@stencil/core';
+import { BaseInputComponent, GestureDetail } from '../../index';
+import { clamp } from '../../utils/helpers';
+
+@Component({
+ tag: 'ion-range',
+ styleUrls: {
+ ios: 'range.ios.scss',
+ md: 'range.md.scss',
+ wp: 'range.wp.scss'
+ },
+ host: {
+ theme: 'range'
+ }
+})
+export class Range implements BaseInputComponent {
+ activated: boolean = false;
+ hasFocus: boolean = false;
+ id: string;
+ labelId: string;
+ startX: number;
+ styleTmr: any;
+
+ @Element() rangeEl: HTMLElement;
+
+ @State() _barL: string;
+ @State() _barR: string;
+ @State() _valA: number = 0;
+ @State() _valB: number = 0;
+ @State() _ratioA: number = 0;
+ @State() _ratioB: number = 0;
+ @State() _ticks: any[] = [];
+ @State() _activeB: boolean;
+ @State() _rect: ClientRect;
+
+ @State() _pressed: boolean;
+ @State() _pressedA: boolean;
+ @State() _pressedB: boolean;
+
+ @Event() ionChange: EventEmitter;
+ @Event() ionStyle: EventEmitter;
+ @Event() ionFocus: EventEmitter;
+ @Event() ionBlur: EventEmitter;
+
+ @Prop() color: string;
+ @Prop() mode: string;
+
+ @Prop({ mutable: true }) value: any;
+
+ @Prop() disabled: boolean = false;
+ @Prop() min: number = 0;
+ @Prop() max: number = 100;
+ @Prop() steps: number = 1;
+ @Prop() dualKnobs: boolean = false;
+ @Prop() pin: boolean = false;
+ @Prop() snaps: boolean = false;
+ @Prop() debounce: number = 0;
+
+ fireBlur() {
+ if (this.hasFocus) {
+ this.hasFocus = false;
+ this.ionBlur.emit();
+ this.emitStyle();
+ }
+ }
+
+ @PropDidChange('disabled')
+ disabledChanged() {
+ this.emitStyle();
+ }
+
+ @PropDidChange('value')
+ valueChanged(val: boolean) {
+ this.ionChange.emit({ value: val });
+ this.emitStyle();
+ }
+
+ ionViewWillLoad() {
+ this.inputUpdated();
+ this.createTicks();
+ this.emitStyle();
+ }
+
+ private emitStyle() {
+ clearTimeout(this.styleTmr);
+
+ this.styleTmr = setTimeout(() => {
+ this.ionStyle.emit({
+ 'range-disabled': this.disabled
+ });
+ });
+ }
+
+ fireFocus() {
+ if (!this.hasFocus) {
+ this.hasFocus = true;
+ this.ionFocus.emit();
+ this.emitStyle();
+ }
+ }
+
+ inputUpdated() {
+ const val = this.value;
+ if (this.dualKnobs) {
+ this._valA = val.lower;
+ this._valB = val.upper;
+ this._ratioA = this.valueToRatio(val.lower);
+ this._ratioB = this.valueToRatio(val.upper);
+ } else {
+ this._valA = val;
+ this._ratioA = this.valueToRatio(val);
+ }
+ this.updateBar();
+ }
+
+ updateBar() {
+ const ratioA = this._ratioA;
+ const ratioB = this._ratioB;
+
+ if (this.dualKnobs) {
+ this._barL = `${Math.min(ratioA, ratioB) * 100}%`;
+ this._barR = `${100 - Math.max(ratioA, ratioB) * 100}%`;
+ } else {
+ this._barL = '';
+ this._barR = `${100 - ratioA * 100}%`;
+ }
+
+ this.updateTicks();
+ }
+
+ createTicks() {
+ if (this.snaps) {
+ for (let value = this.min; value <= this.max; value += this.steps) {
+ let ratio = this.valueToRatio(value);
+ this._ticks.push({
+ ratio,
+ left: `${ratio * 100}%`
+ });
+ }
+ this.updateTicks();
+ }
+ }
+
+ updateTicks() {
+ const ticks = this._ticks;
+ const ratio = this.ratio;
+ if (this.snaps && ticks) {
+ if (this.dualKnobs) {
+ let upperRatio = this.ratioUpper();
+
+ ticks.forEach(t => {
+ t.active = t.ratio >= ratio && t.ratio <= upperRatio;
+ });
+ } else {
+ ticks.forEach(t => {
+ t.active = t.ratio <= ratio;
+ });
+ }
+ }
+ }
+
+ valueToRatio(value: number) {
+ value = Math.round((value - this.min) / this.steps) * this.steps;
+ value = value / (this.max - this.min);
+ return clamp(0, value, 1);
+ }
+
+ ratioToValue(ratio: number) {
+ ratio = Math.round((this.max - this.min) * ratio);
+ ratio = Math.round(ratio / this.steps) * this.steps + this.min;
+ return clamp(this.min, ratio, this.max);
+ }
+
+ inputNormalize(val: any): any {
+ if (this.dualKnobs) {
+ return val;
+ } else {
+ val = parseFloat(val);
+ return isNaN(val) ? undefined : val;
+ }
+ }
+
+ update(current: { x?: number; y?: number }, rect: ClientRect, isPressed: boolean) {
+ // figure out where the pointer is currently at
+ // update the knob being interacted with
+ let ratio = clamp(0, (current.x - rect.left) / rect.width, 1);
+ let val = this.ratioToValue(ratio);
+
+ if (this.snaps) {
+ // snaps the ratio to the current value
+ ratio = this.valueToRatio(val);
+ }
+ // update which knob is pressed
+ this._pressed = isPressed;
+ let valChanged = false;
+ if (this._activeB) {
+ // when the pointer down started it was determined
+ // that knob B was the one they were interacting with
+ this._pressedB = isPressed;
+ this._pressedA = false;
+ this._ratioB = ratio;
+ valChanged = val === this._valB;
+ this._valB = val;
+ } else {
+ // interacting with knob A
+ this._pressedA = isPressed;
+ this._pressedB = false;
+ this._ratioA = ratio;
+ valChanged = val === this._valA;
+ this._valA = val;
+ }
+
+ this.updateBar();
+ if (valChanged) {
+ return false;
+ }
+
+ // value has been updated
+ let value;
+ if (this.dualKnobs) {
+ // dual knobs have an lower and upper value
+ value = {
+ lower: Math.min(this._valA, this._valB),
+ upper: Math.max(this._valA, this._valB)
+ };
+
+ } else {
+ // single knob only has one value
+ value = this._valA;
+ }
+
+ // Update input value
+ this.value = value;
+
+ return true;
+ }
+
+ @Method()
+ ratio(): number {
+ if (this.dualKnobs) {
+ return Math.min(this._ratioA, this._ratioB);
+ }
+ return this._ratioA;
+ }
+
+ @Method()
+ ratioUpper() {
+ if (this.dualKnobs) {
+ return Math.max(this._ratioA, this._ratioB);
+ }
+ return null;
+ }
+
+ @Listen('ionIncrease, ionDecrease')
+ keyChng(ev: RangeEvent) {
+ const step = this.steps;
+ if (ev.detail.knob === 'knobB') {
+ if (!!ev.detail.isIncrease) {
+ this._valB += step;
+ } else {
+ this._valB -= step;
+ }
+ this._valB = clamp(this.min, this._valB, this.max);
+ this._ratioB = this.valueToRatio(this._valB);
+ } else {
+ if (!!ev.detail.isIncrease) {
+ this._valA += step;
+ } else {
+ this._valA -= step;
+ }
+ this._valA = clamp(this.min, this._valA, this.max);
+ this._ratioA = this.valueToRatio(this._valA);
+ }
+ this.updateBar();
+ }
+
+ onDragStart(detail: GestureDetail) {
+ if (this.disabled) return false;
+ this.fireFocus();
+
+ const current = { x: detail.currentX, y: detail.currentY };
+ const el = this.rangeEl.querySelector('.range-slider');
+ this._rect = el.getBoundingClientRect();
+ const rect = this._rect;
+
+ // figure out which knob they started closer to
+ const ratio = clamp(0, (current.x - rect.left) / rect.width, 1);
+ this._activeB =
+ this.dualKnobs &&
+ Math.abs(ratio - this._ratioA) > Math.abs(ratio - this._ratioB);
+
+ // update the active knob's position
+ this.update(current, rect, true);
+
+ // return true so the pointer events
+ // know everything's still valid
+ return true;
+ }
+
+ onDragEnd(detail: GestureDetail) {
+ if (this.disabled) {
+ return;
+ }
+ // update the active knob's position
+ this.update({ x: detail.currentX, y: detail.currentY }, this._rect, false);
+ // trigger ionBlur event
+ this.fireBlur();
+ }
+
+ onDragMove(detail: GestureDetail) {
+ if (this.disabled) {
+ return;
+ }
+ const current = { x: detail.currentX, y: detail.currentY };
+ // update the active knob's position
+ this.update(current, this._rect, true);
+ }
+
+
+ hostData() {
+ return {
+ class: {
+ 'range-disabled': this.disabled,
+ 'range-pressed': this._pressed,
+ 'range-has-pin': this.pin
+ }
+ };
+ }
+
+ render() {
+ return [
+ ,
+
+
+
+ {this._ticks.map(t =>
+
+ )}
+
+
+
+
+
+ {this.dualKnobs
+ ?
+ : null}
+
+ ,
+
+ ];
+ }
+}
+
+export interface RangeEvent {
+ detail: {
+ isIncrease: boolean,
+ knob: string
+ };
+}
diff --git a/packages/core/src/components/range/range.wp.scss b/packages/core/src/components/range/range.wp.scss
new file mode 100644
index 0000000000..93fa1826ce
--- /dev/null
+++ b/packages/core/src/components/range/range.wp.scss
@@ -0,0 +1,223 @@
+@import "../../themes/ionic.globals.wp";
+
+// Windows Range
+// --------------------------------------------------
+
+/// @prop - Padding top/bottom of the range
+$range-wp-padding-vertical: 8px !default;
+
+/// @prop - Padding start/end of the range
+$range-wp-padding-horizontal: 8px !default;
+
+/// @prop - Height of the range slider
+$range-wp-slider-height: 42px !default;
+
+/// @prop - Width of the area that will select the range knob
+$range-wp-hit-width: 42px !default;
+
+/// @prop - Height of the area that will select the range knob
+$range-wp-hit-height: $range-wp-slider-height !default;
+
+/// @prop - Height of the range bar
+$range-wp-bar-height: 2px !default;
+
+/// @prop - Background of the range bar
+$range-wp-bar-background-color: #bdbdbd !default;
+
+/// @prop - Background of the active range bar
+$range-wp-bar-active-background-color: color($colors-wp, primary) !default;
+
+/// @prop - Width of the range knob
+$range-wp-knob-width: 8px !default;
+
+/// @prop - Height of the range knob
+$range-wp-knob-height: $range-wp-knob-width * 3 !default;
+
+/// @prop - Background of the range knob
+$range-wp-knob-background-color: $range-wp-bar-active-background-color !default;
+
+/// @prop - Border radius of the range knob
+$range-wp-knob-border-radius: $range-wp-knob-width / 2 !default;
+
+/// @prop - Width of the range tick
+$range-wp-tick-width: $range-wp-bar-height !default;
+
+/// @prop - Height of the range tick
+$range-wp-tick-height: $range-wp-tick-width * 3 !default;
+
+/// @prop - Border radius of the range tick
+$range-wp-tick-border-radius: $range-wp-knob-width / 2 !default;
+
+/// @prop - Background of the range tick
+$range-wp-tick-background-color: $range-wp-bar-background-color !default;
+
+/// @prop - Background of the active range tick
+$range-wp-tick-active-background-color: $range-wp-bar-active-background-color !default;
+
+/// @prop - Background of the range pin
+$range-wp-pin-background-color: $range-wp-bar-active-background-color !default;
+
+/// @prop - Color of the range pin
+$range-wp-pin-color: color-contrast($colors-wp, $range-wp-bar-active-background-color) !default;
+
+/// @prop - Font size of the range pin
+$range-wp-pin-font-size: 12px !default;
+
+// deprecated
+$range-wp-pin-padding: null !default;
+
+/// @prop - Padding top of the range pin
+$range-wp-pin-padding-top: 8px !default;
+
+/// @prop - Padding end of the range pin
+$range-wp-pin-padding-end: $range-wp-pin-padding-top !default;
+
+/// @prop - Padding bottom of the range pin
+$range-wp-pin-padding-bottom: $range-wp-pin-padding-top !default;
+
+/// @prop - Padding start of the range pin
+$range-wp-pin-padding-start: $range-wp-pin-padding-end !default;
+
+
+.range-wp {
+ @include padding($range-wp-padding-vertical, $range-wp-padding-horizontal);
+}
+
+.range-wp [range-left] {
+ @include margin(0, 12px, 0, 0);
+}
+
+.range-wp [range-right] {
+ @include margin(0, 0, 0, 12px);
+}
+
+.range-wp.range-has-pin {
+ @include padding($range-wp-padding-vertical + $range-wp-pin-font-size + $range-wp-pin-padding-top, null, null, null);
+}
+
+.range-wp .range-slider {
+ height: $range-wp-slider-height;
+}
+
+.range-wp .range-bar {
+ @include position(($range-wp-slider-height / 2), null, null, 0);
+
+ position: absolute;
+
+ width: 100%;
+ height: $range-wp-bar-height;
+
+ background: $range-wp-bar-background-color;
+
+ pointer-events: none;
+}
+
+.range-wp.range-pressed .range-bar-active {
+ will-change: left, right;
+}
+
+.range-wp.range-pressed .range-knob-handle {
+ will-change: left;
+}
+
+.range-wp .range-bar-active {
+ bottom: 0;
+
+ width: auto;
+
+ background: $range-wp-bar-active-background-color;
+}
+
+.range-wp .range-knob-handle {
+ @include position(($range-wp-slider-height / 2), null, null, 0);
+ @include margin(-($range-wp-hit-height / 2), null, null, -($range-wp-hit-width / 2));
+ @include text-align(center);
+
+ position: absolute;
+
+ width: $range-wp-hit-width;
+ height: $range-wp-hit-height;
+}
+
+.range-wp .range-knob {
+ @include position(($range-wp-hit-height / 2) - ($range-wp-knob-height / 2) + ($range-wp-bar-height / 2),
+ null, null, ($range-wp-hit-width / 2) - ($range-wp-knob-width / 2));
+ @include border-radius($range-wp-knob-border-radius);
+
+ position: absolute;
+
+ width: $range-wp-knob-width;
+ height: $range-wp-knob-height;
+
+ background: $range-wp-knob-background-color;
+
+ pointer-events: none;
+}
+
+.range-wp .range-tick {
+ @include margin-horizontal(-($range-wp-tick-width / 2), null);
+ @include border-radius($range-wp-tick-border-radius);
+
+ position: absolute;
+ top: ($range-wp-hit-height / 2) - ($range-wp-tick-height / 2) + ($range-wp-bar-height / 2);
+
+ width: $range-wp-tick-width;
+ height: $range-wp-tick-height;
+
+ background: $range-wp-tick-background-color;
+
+ pointer-events: none;
+}
+
+.range-wp .range-tick-active {
+ background: $range-wp-tick-active-background-color;
+}
+
+.range-wp .range-pin {
+ @include text-align(center);
+ @include border-radius(50px);
+ @include transform(translate3d(0, 28px, 0), scale(.01));
+
+ position: relative;
+ top: -24px;
+ display: inline-block;
+
+ min-width: 28px;
+
+ font-size: $range-wp-pin-font-size;
+
+ color: $range-wp-pin-color;
+
+ background: $range-wp-pin-background-color;
+
+ transition: transform 120ms ease;
+
+ @include deprecated-variable(padding, $range-wp-pin-padding) {
+ @include padding($range-wp-pin-padding-top, $range-wp-pin-padding-end, $range-wp-pin-padding-bottom, $range-wp-pin-padding-start);
+ }
+}
+
+.range-wp .range-knob-pressed .range-pin {
+ @include transform(translate3d(0, 0, 0), scale(1));
+}
+
+.range-wp.range-disabled {
+ opacity: .5;
+}
+
+// Generate Windows Range Colors
+// --------------------------------------------------
+
+@each $color-name, $color-base, $color-contrast in get-colors($colors-wp) {
+
+ .range-wp-#{$color-name} {
+ .range-bar-active,
+ .range-tick-active,
+ .range-knob,
+ .range-pin {
+ background: $color-base;
+ }
+ }
+
+}
+
diff --git a/packages/core/src/components/range/test/basic.html b/packages/core/src/components/range/test/basic.html
new file mode 100644
index 0000000000..25918edc1d
--- /dev/null
+++ b/packages/core/src/components/range/test/basic.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+ Ionic Range
+
+
+
+
+
+
+
+
+ Range
+
+
+
+
+
+ Range color
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Options
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Test
+
+
+
+
+
+
+
diff --git a/packages/core/src/utils/helpers.ts b/packages/core/src/utils/helpers.ts
index ee28bdcd8e..b205e1ba77 100644
--- a/packages/core/src/utils/helpers.ts
+++ b/packages/core/src/utils/helpers.ts
@@ -1,5 +1,9 @@
import { StencilElement } from '..';
+export function clamp(min: number, n: number, max: number) {
+ return Math.max(min, Math.min(n, max));
+}
+
export function isDef(v: any): boolean { return v !== undefined && v !== null; }
export function isUndef(v: any): boolean { return v === undefined || v === null; }
@@ -161,6 +165,7 @@ export function getToolbarHeight(toolbarTagName: string, pageChildren: HTMLEleme
return '';
}
+/** @hidden */
export type Side = 'left' | 'right' | 'start' | 'end';
export function checkEdgeSide(posX: number, isRightSide: boolean, maxEdgeStart: number): boolean {
diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js
index 365c08c958..22e7c9bb5a 100644
--- a/packages/core/stencil.config.js
+++ b/packages/core/stencil.config.js
@@ -29,10 +29,10 @@ exports.config = {
{ components: ['ion-slides', 'ion-slide'] },
{ components: ['ion-spinner'] },
{ components: ['ion-split-pane'] },
+ { components: ['ion-range', 'ion-range-knob']},
{ components: ['ion-tabs', 'ion-tab', 'ion-tab-bar', 'ion-tab-button', 'ion-tab-highlight'] },
{ components: ['ion-toggle'] },
{ components: ['ion-nav', 'ion-nav-controller', 'stencil-ion-nav-delegate','page-one', 'page-two', 'page-three'] },
-
{ components: ['ion-toast', 'ion-toast-controller'] },
],
preamble: '(C) Ionic http://ionicframework.com - MIT License',