diff --git a/src/components/form/range/_dual_range.scss b/src/components/form/range/_dual_range.scss
new file mode 100644
index 00000000000..573a374eae5
--- /dev/null
+++ b/src/components/form/range/_dual_range.scss
@@ -0,0 +1,7 @@
+.euiDualRange {
+ &__slider {
+ @include euiRangeThumbPerBrowser {
+ visibility: hidden;
+ }
+ }
+}
diff --git a/src/components/form/range/_index.scss b/src/components/form/range/_index.scss
index 7ef7e10548a..9f0f884dec0 100644
--- a/src/components/form/range/_index.scss
+++ b/src/components/form/range/_index.scss
@@ -1,3 +1,13 @@
@import 'variables';
@import 'mixins';
-@import 'range';
+@import 'range_highlight';
+@import 'range_input';
+@import 'range_label';
+@import 'range_levels';
+@import 'range_slider';
+@import 'range_thumb';
+@import 'range_ticks';
+@import 'range_tooltip';
+@import 'range_track';
+@import 'range_wrapper';
+@import 'dual_range';
diff --git a/src/components/form/range/_mixins.scss b/src/components/form/range/_mixins.scss
index dd9bdb9874d..9103408d8f0 100644
--- a/src/components/form/range/_mixins.scss
+++ b/src/components/form/range/_mixins.scss
@@ -14,6 +14,14 @@
&::-ms-fill-upper { @content; }
}
+@mixin euiRangeThumbStyle {
+ cursor: pointer;
+ border-color: $euiRangeThumbBorderColor;
+ padding: 0;
+ height: $euiRangeThumbHeight;
+ width: $euiRangeThumbWidth;
+}
+
@mixin euiRangeThumbPerBrowser {
&::-webkit-slider-thumb { @content; }
&::-moz-range-thumb { @content; }
diff --git a/src/components/form/range/_range.scss b/src/components/form/range/_range.scss
deleted file mode 100644
index 5bbe22662ad..00000000000
--- a/src/components/form/range/_range.scss
+++ /dev/null
@@ -1,379 +0,0 @@
-/*
- * 1. There's no way to target the layout of the extra input, so we must
- * use the descendant selector to allow the width to shrink.
- * 2. Align extra input slightly better with slider labels, in an IE compliant way.
- * 3. Adjust vertical alignment of input based on extras
- */
-
-.euiRange__wrapper {
- @include euiFormControlSize;
- display: flex;
- align-items: center;
-
- &--fullWidth {
- max-width: 100%;
- }
-
- &--disabled {
- .euiRange__minLabel,
- .euiRange__maxLabel,
- .euiRange__inputWrapper {
- opacity: .25;
- }
- }
-
- > .euiFormControlLayout { /* 1 */
- width: auto;
- }
-}
-
-.euiRange__inputWrapper {
- flex-grow: 1;
- position: relative; // for positioning ticks/levels
- align-self: flex-start; /* 3 */
-}
-
-.euiRange__minLabel,
-.euiRange__maxLabel {
- font-size: $euiFontSizeXS;
-}
-
-.euiRange__minLabel {
- margin-right: $euiSizeS;
-}
-
-.euiRange__maxLabel {
- margin-left: $euiSizeS;
-}
-
-.euiRange__extraInput {
- width: auto;
- margin-left: $euiSize;
- position: relative; /* 2 */
- top: -2px; /* 2 */
-}
-
-.euiRange__tick {
- overflow-x: hidden;
- text-overflow: ellipsis;
- font-size: $euiFontSizeXS;
- position: relative;
- padding-top: $euiSize;
-
- &::before {
- @include size($euiSizeXS);
-
- content: '';
- background-color: $euiColorDarkShade;
- border-radius: 100%;
- position: absolute;
- top: 0;
- left: calc(50% - #{($euiSizeXS/2)});
- }
-
- &-isCustom {
- position: absolute;
- transform: translateX(-50%);
- }
-
- &:enabled:hover,
- &:focus,
- &--selected {
- color: $euiColorPrimary;
- }
-
- &--selected {
- font-weight: $euiFontWeightMedium;
- }
-
- &:disabled {
- cursor: not-allowed;
- }
-}
-
-.euiRange__levels {
- display: flex;
- justify-content: stretch;
- z-index: $euiZLevel0;
-}
-
-[class*='euiRange__level--'] {
- display: block;
- height: 6px;
- border-radius: 6px;
- margin: 2px;
-}
-
-// Modifier naming and colors.
-$euiRangeLevelColors: (
- primary: $euiColorPrimary,
- success: $euiColorSuccess,
- warning: $euiColorWarning,
- danger: $euiColorDanger,
-);
-
-// Create level modifiers based upon the map.
-@each $name, $color in $euiRangeLevelColors {
- .euiRange__level--#{$name} {
- background-color: transparentize($color, .7);
- }
-}
-
-.euiRange__range__progress {
- height: 4px;
- border-radius: 4px;
- background-color: $euiRangeTrackColor;
-}
-
-.euiRange__value {
- // Indentation for legibility in transition
- // sass-lint:disable-block indentation
- @include euiFontSizeS;
- border: 1px solid transparentize($euiColorDarkestShade, .8);
- position: absolute;
- border-radius: $euiBorderRadius;
- padding: ($euiSizeXS / 2) $euiSizeS;
- background-color: tintOrShade($euiColorFullShade, 25%, 90%);
- color: $euiColorGhost;
- max-width: 256px;
- z-index: $euiZLevel4;
- top: ($euiFormControlHeight / 2) - 1px;
- transition:
- box-shadow $euiAnimSpeedNormal $euiAnimSlightResistance,
- transform $euiAnimSpeedNormal $euiAnimSlightResistance;
-
- // Custom sizing
- $arrowSize: $euiSizeM;
- $arrowMinusSize: (($arrowSize / 2) - 1px) * -1;
-
- &::after,
- &::before {
- content: '';
- position: absolute;
- bottom: -$arrowSize / 2;
- left: 50%;
- transform-origin: center;
- background-color: tintOrShade($euiColorFullShade, 25%, 90%);
- width: $arrowSize;
- height: $arrowSize;
- border-radius: 2px;
- }
-
- &::before {
- background-color: transparentize($euiColorDarkestShade, .8);
- }
-
- // Positions the arrow
- &.euiRange__value--right {
- transform: translateX(0) translateY(-50%);
- margin-left: $euiSizeL;
-
- &:before,
- &:after {
- bottom: 50%;
- left: $arrowMinusSize;
- transform: translateY(50%) rotateZ(45deg);
- }
-
- &::before {
- margin-left: -1px;
- }
- }
-
- &.euiRange__value--left {
- transform: translateX(-100%) translateY(-50%);
- margin-left: -$euiSizeL;
-
- &:before,
- &:after {
- bottom: 50%;
- left: auto;
- right: $arrowMinusSize;
- transform: translateY(50%) rotateZ(45deg);
- }
-
- &::before {
- margin-right: -1px;
- }
- }
-}
-
-
-/*
- * Positioning
- */
-
-.euiRange__wrapper--hasLevels {
- .euiRange__levels {
- position: absolute;
- left: 0;
- right: 0;
- top: ($euiFormControlHeight / 2) + 2px;
- }
-}
-
-.euiRange__wrapper--hasRange {
- .euiRange__range {
- position: absolute;
- left: 0;
- width: 100%;
- top: ($euiFormControlHeight / 2) - 2px;
- z-index: $euiZLevel0;
- overflow: hidden;
- }
-}
-
-.euiRange__wrapper--hasTicks {
- .euiRange {
- height: $euiFormControlHeight / 2; /* 3 */
- }
-
- .euiRange__levels {
- top: ($euiFormControlHeight / 4) + 2px;
- }
-
- .euiRange__range {
- top: ($euiFormControlHeight / 4) - 2px;
- left: 0;
- }
-
- .euiRange__value {
- top: ($euiFormControlHeight / 4) - 1px;
- }
-
- .euiRange__extraInput {
- margin-top: 0;
- }
-
- .euiRange__ticks {
- position: absolute;
- left: ($euiRangeThumbWidth / 2);
- right: ($euiRangeThumbWidth / 2);
- top: $euiSizeS;
- display: flex;
- z-index: $euiZLevel1;
- }
-}
-
-.euiRange__valueWrapper {
- // Keeps tooltip (value) aligned to percentage of actual slider
- display: block;
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: calc(100% - #{$euiRangeThumbWidth});
- margin-left: $euiRangeThumbWidth / 2;
-}
-
-/*
- * Input Range Customization by browser
- */
-
-// The following code is inspired by...
-
-// Github: https://github.com/darlanrod/input-range-sass
-// Author: Darlan Rod https://github.com/darlanrod
-// Version 1.4.1
-// MIT License
-
-// It has been modified to fit the styling patterns of Kibana and
-// to be more easily maintained / themeable going forward.
-
-.euiRange {
- // Auto means the height isn't defined
- height: $euiFormControlHeight;
- appearance: none;
- background: transparent; // Otherwise white in Chrome
- width: 100%; // ensures the slider expands to fill flex display
- position: relative;
- z-index: $euiZLevel2; // stay above tick marks
- cursor: pointer; // Keep cursor to full range bounds
-
- &:disabled {
- cursor: not-allowed;
-
- // sass-lint:disable-block mixins-before-declarations
- @include euiRangeThumbPerBrowser {
- cursor: not-allowed;
- border-color: $euiRangeThumbBorderColor;
- background-color: $euiRangeThumbBorderColor;
- box-shadow: none;
- }
- }
-
- &:focus {
- @include euiRangeThumbPerBrowser {
- @include euiCustomControlFocused;
- }
-
- @include euiRangeTrackPerBrowser {
- background-color: $euiColorPrimary;
- border-color: $euiColorPrimary;
- }
-
- ~ .euiRange__range .euiRange__range__progress {
- background-color: $euiColorPrimary;
- }
-
- ~ .euiRange__valueWrapper .euiRange__value {
- @include euiBottomShadowMedium;
-
- &.euiRange__value--right {
- transform: translateX(0) translateY(-50%) scale(1.1);
- }
-
- &.euiRange__value--left {
- transform: translateX(-100%) translateY(-50%) scale(1.1);
- }
- }
- }
-
- @include euiRangeThumbPerBrowser {
- @include euiCustomControl($type: 'round');
-
- cursor: pointer;
- border-color: $euiRangeThumbBorderColor;
- padding: 0;
- height: $euiRangeThumbHeight;
- width: $euiRangeThumbWidth;
- }
-
- @include euiRangeTrackPerBrowser {
- @include euiRangeTrackSize;
-
- background: $euiRangeTrackColor;
- border: $euiRangeTrackBorderWidth solid $euiRangeTrackBorderColor;
- border-radius: $euiRangeTrackRadius;
- }
-
- // Resets
-
- // Disable linter for these very unique vendor controls
- // sass-lint:disable-block no-vendor-prefixes
- &::-webkit-slider-thumb {
- -webkit-appearance: none;
- margin-top: ((-$euiRangeTrackBorderWidth * 2 + $euiRangeTrackHeight) / 2) - ($euiRangeThumbHeight / 2);
- }
-
- &::-ms-thumb {
- margin-top: 0;
- }
-
- &::-ms-track {
- @include euiRangeTrackSize;
-
- background: transparent;
- border-color: transparent;
- border-width: ($euiRangeThumbHeight / 2) 0;
- color: transparent;
- }
-}
-
-.euiRange__wrapper--hasRange .euiRange,
-.euiRange__wrapper--hasTicks .euiRange {
- @include euiRangeTrackPerBrowser {
- background-color: transparentize($euiRangeTrackColor, .6);
- border-color: transparentize($euiRangeTrackBorderColor, .6);
- }
-}
diff --git a/src/components/form/range/_range_highlight.scss b/src/components/form/range/_range_highlight.scss
new file mode 100644
index 00000000000..cc1da38aa55
--- /dev/null
+++ b/src/components/form/range/_range_highlight.scss
@@ -0,0 +1,23 @@
+.euiRangeHighlight {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ top: ($euiFormControlHeight / 2) - 2px;
+ z-index: $euiZLevel0;
+ overflow: hidden;
+
+ &__progress {
+ height: 4px;
+ border-radius: 4px;
+ background-color: $euiRangeTrackColor;
+
+ &--hasFocus {
+ background-color: $euiColorPrimary;
+ }
+ }
+
+ &--hasTicks {
+ top: ($euiFormControlHeight / 4) - 2px;
+ left: 0;
+ }
+}
diff --git a/src/components/form/range/_range_input.scss b/src/components/form/range/_range_input.scss
new file mode 100644
index 00000000000..eb3addf4fcd
--- /dev/null
+++ b/src/components/form/range/_range_input.scss
@@ -0,0 +1,17 @@
+/*
+ * 1. Align extra input slightly better with slider labels, in an IE compliant way.
+ */
+
+.euiRangeInput {
+ width: auto;
+ position: relative; /* 1 */
+ top: -2px; /* 1 */
+
+ &--min {
+ margin-right: $euiSize;
+ }
+
+ &--max {
+ margin-left: $euiSize;
+ }
+}
diff --git a/src/components/form/range/_range_label.scss b/src/components/form/range/_range_label.scss
new file mode 100644
index 00000000000..c5c7ca5548c
--- /dev/null
+++ b/src/components/form/range/_range_label.scss
@@ -0,0 +1,18 @@
+.euiRangeLabel {
+ &--min,
+ &--max {
+ font-size: $euiFontSizeXS;
+ }
+
+ &--min {
+ margin-right: $euiSizeS;
+ }
+
+ &--max {
+ margin-left: $euiSizeS;
+ }
+
+ &--isDisabled {
+ opacity: $euiRangeDisabledOpacity;
+ }
+}
diff --git a/src/components/form/range/_range_levels.scss b/src/components/form/range/_range_levels.scss
new file mode 100644
index 00000000000..f45d63e194e
--- /dev/null
+++ b/src/components/form/range/_range_levels.scss
@@ -0,0 +1,36 @@
+.euiRangeLevels {
+ display: flex;
+ justify-content: stretch;
+ z-index: $euiZLevel0;
+
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: ($euiFormControlHeight / 2) + 2px;
+
+ &--hasTicks {
+ top: ($euiFormControlHeight / 4) + 2px;
+ }
+}
+
+.euiRangeLevel {
+ display: block;
+ height: 6px;
+ border-radius: 6px;
+ margin: 2px;
+}
+
+// Modifier naming and colors.
+$euiRangeLevelColors: (
+ primary: $euiColorPrimary,
+ success: $euiColorSuccess,
+ warning: $euiColorWarning,
+ danger: $euiColorDanger,
+);
+
+// Create level modifiers based upon the map.
+@each $name, $color in $euiRangeLevelColors {
+ .euiRangeLevel--#{$name} {
+ background-color: transparentize($color, .7);
+ }
+}
diff --git a/src/components/form/range/_range_slider.scss b/src/components/form/range/_range_slider.scss
new file mode 100644
index 00000000000..e35714484f9
--- /dev/null
+++ b/src/components/form/range/_range_slider.scss
@@ -0,0 +1,108 @@
+/*
+ * Input Range Customization by browser
+ */
+
+// The following code is inspired by...
+
+// Github: https://github.com/darlanrod/input-range-sass
+// Author: Darlan Rod https://github.com/darlanrod
+// Version 1.4.1
+// MIT License
+
+// It has been modified to fit the styling patterns of Kibana and
+// to be more easily maintained / themeable going forward.
+
+.euiRangeSlider {
+ // Auto means the height isn't defined
+ height: $euiFormControlHeight;
+ appearance: none;
+ background: transparent; // Otherwise white in Chrome
+ width: 100%; // ensures the slider expands to fill flex display
+ position: relative;
+ z-index: $euiZLevel2; // stay above tick marks
+ cursor: pointer; // Keep cursor to full range bounds
+
+ &:disabled {
+ cursor: not-allowed;
+
+ // sass-lint:disable-block mixins-before-declarations
+ @include euiRangeThumbPerBrowser {
+ cursor: not-allowed;
+ border-color: $euiRangeThumbBorderColor;
+ background-color: $euiRangeThumbBorderColor;
+ box-shadow: none;
+ }
+ }
+
+ @include euiRangeThumbPerBrowser {
+ @include euiCustomControl($type: 'round');
+
+ @include euiRangeThumbStyle;
+ }
+
+ @include euiRangeTrackPerBrowser {
+ @include euiRangeTrackSize;
+
+ background: $euiRangeTrackColor;
+ border: $euiRangeTrackBorderWidth solid $euiRangeTrackBorderColor;
+ border-radius: $euiRangeTrackRadius;
+
+ background-color: transparentize($euiRangeTrackColor, .6);
+ border-color: transparentize($euiRangeTrackBorderColor, .6);
+ }
+
+ &:focus,
+ &--hasFocus {
+ @include euiRangeThumbPerBrowser {
+ @include euiCustomControlFocused;
+ }
+
+ @include euiRangeTrackPerBrowser {
+ background-color: $euiColorPrimary;
+ border-color: $euiColorPrimary;
+ }
+
+ ~ .euiRangeHighlight .euiRangeHighlight__progress {
+ background-color: $euiColorPrimary;
+ }
+
+ ~ .euiRangeTooltip .euiRangeTooltip__value {
+ @include euiBottomShadowMedium;
+
+ &.euiRangeTooltip__value--right {
+ transform: translateX(0) translateY(-50%) scale(1.1);
+ }
+
+ &.euiRangeTooltip__value--left {
+ transform: translateX(-100%) translateY(-50%) scale(1.1);
+ }
+ }
+ }
+
+ // Resets
+
+ // Disable linter for these very unique vendor controls
+ // sass-lint:disable-block no-vendor-prefixes
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ margin-top: ((-$euiRangeTrackBorderWidth * 2 + $euiRangeTrackHeight) / 2) - ($euiRangeThumbHeight / 2);
+ }
+
+ &::-ms-thumb {
+ margin-top: 0;
+ }
+
+ &::-ms-track {
+ @include euiRangeTrackSize;
+
+ background: transparent;
+ border-color: transparent;
+ border-width: ($euiRangeThumbHeight / 2) 0;
+ color: transparent;
+ }
+
+ // States
+ &--hasTicks {
+ height: $euiFormControlHeight / 2; // Adjust vertical alignment based on extras
+ }
+}
diff --git a/src/components/form/range/_range_thumb.scss b/src/components/form/range/_range_thumb.scss
new file mode 100644
index 00000000000..49e2e6bbbda
--- /dev/null
+++ b/src/components/form/range/_range_thumb.scss
@@ -0,0 +1,19 @@
+.euiRangeThumb {
+ @include euiCustomControl($type: 'round');
+ @include euiRangeThumbStyle;
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 50%;
+ margin-top: -($euiRangeThumbHeight / 2);
+ z-index: $euiZLevel2;
+ pointer-events: none;
+
+ &:focus {
+ @include euiCustomControlFocused;
+ }
+
+ &--hasTicks {
+ top: 25%;
+ }
+}
diff --git a/src/components/form/range/_range_ticks.scss b/src/components/form/range/_range_ticks.scss
new file mode 100644
index 00000000000..3c30bff4967
--- /dev/null
+++ b/src/components/form/range/_range_ticks.scss
@@ -0,0 +1,46 @@
+.euiRangeTicks {
+ position: absolute;
+ left: ($euiRangeThumbWidth / 2);
+ right: ($euiRangeThumbWidth / 2);
+ top: $euiSizeS;
+ display: flex;
+ z-index: $euiZLevel1;
+}
+
+.euiRangeTick {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ font-size: $euiFontSizeXS;
+ position: relative;
+ padding-top: $euiSize;
+
+ &::before {
+ @include size($euiSizeXS);
+
+ content: '';
+ background-color: $euiColorDarkShade;
+ border-radius: 100%;
+ position: absolute;
+ top: 0;
+ left: calc(50% - #{($euiSizeXS/2)});
+ }
+
+ &--isCustom {
+ position: absolute;
+ transform: translateX(-50%);
+ }
+
+ &:enabled:hover,
+ &:focus,
+ &--selected {
+ color: $euiColorPrimary;
+ }
+
+ &--selected {
+ font-weight: $euiFontWeightMedium;
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+}
diff --git a/src/components/form/range/_range_tooltip.scss b/src/components/form/range/_range_tooltip.scss
new file mode 100644
index 00000000000..ebbe92dae4e
--- /dev/null
+++ b/src/components/form/range/_range_tooltip.scss
@@ -0,0 +1,87 @@
+.euiRangeTooltip {
+ // Keeps tooltip (value) aligned to percentage of actual slider
+ display: block;
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: calc(100% - #{$euiRangeThumbWidth});
+ margin-left: $euiRangeThumbWidth / 2;
+}
+
+.euiRangeTooltip__value {
+ // Indentation for legibility in transition
+ // sass-lint:disable-block indentation
+ @include euiFontSizeS;
+ border: 1px solid transparentize($euiColorDarkestShade, .8);
+ position: absolute;
+ border-radius: $euiBorderRadius;
+ padding: ($euiSizeXS / 2) $euiSizeS;
+ background-color: tintOrShade($euiColorFullShade, 25%, 90%);
+ color: $euiColorGhost;
+ max-width: 256px;
+ z-index: $euiZLevel4;
+ top: ($euiFormControlHeight / 2) - 1px;
+ transition:
+ box-shadow $euiAnimSpeedNormal $euiAnimSlightResistance,
+ transform $euiAnimSpeedNormal $euiAnimSlightResistance;
+
+ // Custom sizing
+ $arrowSize: $euiSizeM;
+ $arrowMinusSize: (($arrowSize / 2) - 1px) * -1;
+
+ &::after,
+ &::before {
+ content: '';
+ position: absolute;
+ bottom: -$arrowSize / 2;
+ left: 50%;
+ transform-origin: center;
+ background-color: tintOrShade($euiColorFullShade, 25%, 90%);
+ width: $arrowSize;
+ height: $arrowSize;
+ border-radius: 2px;
+ }
+
+ &::before {
+ background-color: transparentize($euiColorDarkestShade, .8);
+ }
+
+ // Positions the arrow
+ &.euiRangeTooltip__value--right {
+ transform: translateX(0) translateY(-50%);
+ margin-left: $euiSizeL;
+
+ &:before,
+ &:after {
+ bottom: 50%;
+ left: $arrowMinusSize;
+ transform: translateY(50%) rotateZ(45deg);
+ }
+
+ &::before {
+ margin-left: -1px;
+ }
+ }
+
+ &.euiRangeTooltip__value--left {
+ transform: translateX(-100%) translateY(-50%);
+ margin-left: -$euiSizeL;
+
+ &:before,
+ &:after {
+ bottom: 50%;
+ left: auto;
+ right: $arrowMinusSize;
+ transform: translateY(50%) rotateZ(45deg);
+ }
+
+ &::before {
+ margin-right: -1px;
+ }
+ }
+
+ &--hasTicks {
+ top: ($euiFormControlHeight / 4) - 1px;
+ }
+}
diff --git a/src/components/form/range/_range_track.scss b/src/components/form/range/_range_track.scss
new file mode 100644
index 00000000000..0fdd33c6e31
--- /dev/null
+++ b/src/components/form/range/_range_track.scss
@@ -0,0 +1,10 @@
+.euiRangeTrack {
+ height: 100%; // Don't overflow `euiRangeWrapper`
+ flex-grow: 1;
+ position: relative; // for positioning ticks/levels
+ align-self: flex-start; // Adjust vertical alignment of input based on extras
+
+ &--disabled {
+ opacity: $euiRangeDisabledOpacity;
+ }
+}
diff --git a/src/components/form/range/_range_wrapper.scss b/src/components/form/range/_range_wrapper.scss
new file mode 100644
index 00000000000..a5427154129
--- /dev/null
+++ b/src/components/form/range/_range_wrapper.scss
@@ -0,0 +1,18 @@
+/*
+ * 1. There's no way to target the layout of the extra input, so we must
+ * use the descendant selector to allow the width to shrink.
+ */
+
+.euiRangeWrapper {
+ @include euiFormControlSize;
+ display: flex;
+ align-items: center;
+
+ &--fullWidth {
+ max-width: 100%;
+ }
+
+ > .euiFormControlLayout { /* 1 */
+ width: auto;
+ }
+}
diff --git a/src/components/form/range/_variables.scss b/src/components/form/range/_variables.scss
index 8f66e5d2b27..ce6a5cfd8e6 100644
--- a/src/components/form/range/_variables.scss
+++ b/src/components/form/range/_variables.scss
@@ -10,3 +10,5 @@ $euiRangeTrackHeight: 2px !default;
$euiRangeTrackBorderWidth: 0 !default;
$euiRangeTrackBorderColor: $euiRangeTrackColor !default;
$euiRangeTrackRadius: $euiBorderRadius !default;
+
+$euiRangeDisabledOpacity: .25;
diff --git a/src/components/form/range/dual_range.js b/src/components/form/range/dual_range.js
new file mode 100644
index 00000000000..3e6ab5026b2
--- /dev/null
+++ b/src/components/form/range/dual_range.js
@@ -0,0 +1,406 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import { keyCodes } from '../../../services';
+import { isWithinRange } from '../../../services/number';
+
+import { EuiRangeHighlight } from './range_highlight';
+import { EuiRangeInput } from './range_input';
+import { EuiRangeLabel } from './range_label';
+import { EuiRangeSlider } from './range_slider';
+import { EuiRangeThumb } from './range_thumb';
+import { EuiRangeTrack, LEVEL_COLORS } from './range_track';
+import { EuiRangeWrapper } from './range_wrapper';
+
+export class EuiDualRange extends Component {
+ state = {
+ hasFocus: false,
+ rangeSliderRefAvailable: false,
+ lastThumbInteraction: null
+ }
+
+ rangeSliderRef = null;
+ handleRangeSliderRefUpdate = (ref) => {
+ this.rangeSliderRef = ref;
+ this.setState({
+ rangeSliderRefAvailable: !!ref
+ });
+ }
+
+ get lowerValue() {
+ return this.props.value ? this.props.value[0] : this.props.min;
+ }
+ get upperValue() {
+ return this.props.value ? this.props.value[1] : this.props.max;
+ }
+ get isValid() {
+ return isWithinRange(this.props.min, this.upperValue, this.lowerValue)
+ && isWithinRange(this.lowerValue, this.props.max, this.upperValue);
+ }
+
+ _determineInvalidThumbMovement = (newVal, lower, upper, e) => {
+ const isBackwards = Number(lower) >= Number(upper);
+ const isUnbound = Number(upper) < this.props.min || Number(lower) > this.props.max;
+ const isLow = lower < this.props.min;
+ const isHigh = upper > this.props.max;
+ if (isBackwards || isUnbound) {
+ // Scenerio in which we cannot reasonably infer intention via click location due to current invalid thumb positions.
+ // Reset both values in the proximity of the click.
+ lower = newVal - (this.props.step || 1);
+ upper = newVal;
+ } else {
+ // Scenerio in which we can reasonably infer intention via click location if range extrema are respected.
+ // Reset either value to its respective terminal value.
+ lower = isLow ? this.props.min : lower;
+ upper = isHigh ? this.props.max : upper;
+ }
+ this._handleOnChange(lower, upper, e);
+ }
+
+ _determineValidThumbMovement = (newVal, lower, upper, e) => {
+ const thumbsAreEquidistant = Math.abs(lower - newVal) === Math.abs(upper - newVal);
+ // Lower thumb nearing swap with upper thumb
+ if (
+ (newVal === upper || (newVal < upper && thumbsAreEquidistant))
+ && this.state.lastThumbInteraction === 'lower'
+ ) {
+ lower = newVal;
+ }
+ // Upper thumb nearing swap with lower thumb
+ else if (
+ (newVal === lower || (newVal > lower && thumbsAreEquidistant))
+ && this.state.lastThumbInteraction === 'upper'
+ ) {
+ upper = newVal;
+ }
+ // Lower thumb targeted or right-moving swap has occured
+ else if (
+ Math.abs(lower - newVal) < Math.abs(upper - newVal)
+ || (thumbsAreEquidistant && this.state.lastThumbInteraction === 'upper')
+ ) {
+ this.setState({
+ lastThumbInteraction: 'lower'
+ });
+ lower = newVal;
+ }
+ // Upper thumb targeted or left-moving swap has occured
+ else {
+ this.setState({
+ lastThumbInteraction: 'upper'
+ });
+ upper = newVal;
+ }
+ this._handleOnChange(lower, upper, e);
+ }
+
+ _determineThumbMovement = (newVal, e) => {
+ // Determine thumb movement based on slider interaction
+ if (!this.isValid) {
+ // Non-standard positioning follows
+ this._determineInvalidThumbMovement(newVal, this.lowerValue, this.upperValue, e);
+ } else {
+ // Standard positioning based on click event proximity to thumb locations
+ this._determineValidThumbMovement(newVal, this.lowerValue, this.upperValue, e);
+ }
+ }
+
+ _handleOnChange = (lower, upper, e) => {
+ const isValid = isWithinRange(this.props.min, upper, lower) && isWithinRange(lower, this.props.max, upper);
+ this.props.onChange([lower, upper], isValid, e);
+ }
+
+ handleSliderChange = (e) => {
+ this._determineThumbMovement(e.target.value, e);
+ }
+
+ handleLowerInputChange = (e) => {
+ this._handleOnChange(e.target.value, this.upperValue, e);
+ }
+
+ handleUpperInputChange = (e) => {
+ this._handleOnChange(this.lowerValue, e.target.value, e);
+ }
+
+ _handleKeyDown = (value, e) => {
+ let newVal = Number(value);
+ let stepRemainder = 0;
+ const step = this.props.step || 1;
+ switch (e.keyCode) {
+ case keyCodes.UP:
+ case keyCodes.RIGHT:
+ e.preventDefault();
+ newVal += step;
+ stepRemainder = (newVal - this.props.min) % step;
+ if (step !== 1 && stepRemainder > 0) {
+ newVal = newVal - stepRemainder;
+ }
+ break;
+ case keyCodes.DOWN:
+ case keyCodes.LEFT:
+ e.preventDefault();
+ newVal -= step;
+ stepRemainder = (newVal - this.props.min) % step;
+ if (step !== 1 && stepRemainder > 0) {
+ newVal = newVal + (step - stepRemainder);
+ }
+ break;
+ }
+ return newVal;
+ }
+
+ handleLowerKeyDown = (e) => {
+ let lower = this.lowerValue;
+ switch (e.keyCode) {
+ case keyCodes.TAB:
+ return;
+ default:
+ lower = this._handleKeyDown(lower, e);
+ }
+ if (lower >= this.upperValue || lower < this.props.min) return;
+ this._handleOnChange(lower, this.upperValue, e);
+ }
+
+ handleUpperKeyDown = (e) => {
+ let upper = this.upperValue;
+ switch (e.keyCode) {
+ case keyCodes.TAB:
+ return;
+ default:
+ upper = this._handleKeyDown(upper, e);
+ }
+ if (upper <= this.lowerValue || upper > this.props.max) return;
+ this._handleOnChange(this.lowerValue, upper, e);
+ }
+
+ calculateThumbPositionStyle = (value) => {
+ // Calculate the left position based on value
+ const decimal = (value - this.props.min) / (this.props.max - this.props.min);
+ // Must be between 0-100%
+ let valuePosition = decimal <= 1 ? decimal : 1;
+ valuePosition = valuePosition >= 0 ? valuePosition : 0;
+
+ const EUI_THUMB_SIZE = 16;
+ const thumbToTrackRatio = (EUI_THUMB_SIZE / this.rangeSliderRef.clientWidth);
+ const trackPositionScale = (1 - thumbToTrackRatio) * 100;
+ return { left: `${valuePosition * trackPositionScale}%` };
+ }
+
+ toggleHasFocus = (shouldFocused = !this.state.hasFocus) => {
+ this.setState({
+ hasFocus: shouldFocused
+ });
+ }
+
+ render() {
+
+ const {
+ className,
+ compressed,
+ disabled,
+ fullWidth,
+ id,
+ max,
+ min,
+ name,
+ step,
+ showLabels,
+ showInput,
+ showTicks,
+ tickInterval,
+ ticks, // eslint-disable-line no-unused-vars
+ levels,
+ onChange, // eslint-disable-line no-unused-vars
+ showRange,
+ value,
+ style,
+ ...rest
+ } = this.props;
+
+ const sliderClasses = classNames('euiDualRange__slider', className);
+
+ return (
+
+ {showInput && (
+
+ )}
+ {showLabels && {min} }
+
+
+
+ {this.state.rangeSliderRefAvailable && (
+
+ this.toggleHasFocus(true)}
+ onBlur={() => this.toggleHasFocus(false)}
+ style={this.calculateThumbPositionStyle(this.lowerValue)}
+ aria-describedby={this.props['aria-describedby']}
+ aria-label={this.props['aria-label']}
+ />
+ this.toggleHasFocus(true)}
+ onBlur={() => this.toggleHasFocus(false)}
+ style={this.calculateThumbPositionStyle(this.upperValue)}
+ aria-describedby={this.props['aria-describedby']}
+ aria-label={this.props['aria-label']}
+ />
+
+ )}
+
+ {(showRange && this.isValid) && (
+
+ )}
+
+ {showLabels && {max} }
+ {showInput && (
+
+ )}
+
+ );
+ }
+}
+
+EuiDualRange.propTypes = {
+ name: PropTypes.string,
+ id: PropTypes.string,
+ min: PropTypes.number.isRequired,
+ max: PropTypes.number.isRequired,
+ step: PropTypes.number,
+ /**
+ * Array containing lower and upper values. Fallback is `min` and `max` respectively
+ */
+ value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
+ fullWidth: PropTypes.bool,
+ compressed: PropTypes.bool,
+ /**
+ * Shows static min/max labels on the sides of the range slider
+ */
+ showLabels: PropTypes.bool,
+ /**
+ * Displays a input controls for direct manipulation
+ */
+ showInput: PropTypes.bool,
+ /**
+ * Shows clickable tick marks and labels at the given interval (`step`/`tickInterval`)
+ */
+ showTicks: PropTypes.bool,
+ /**
+ * Modifies the number of tick marks and at what interval
+ */
+ tickInterval: PropTypes.number,
+ /**
+ * Specified ticks at specified values
+ */
+ ticks: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.number.isRequired,
+ label: PropTypes.node.isRequired,
+ }),
+ ),
+ /**
+ * Function signature: `([lowerValue, upperValue], isValid, event)`
+ */
+ onChange: PropTypes.func,
+ /**
+ * Create colored indicators for certain intervals
+ */
+ levels: PropTypes.arrayOf(
+ PropTypes.shape({
+ min: PropTypes.number,
+ max: PropTypes.number,
+ color: PropTypes.oneOf(LEVEL_COLORS),
+ }),
+ ),
+ /**
+ * Shows a thick line from lower value to upper value
+ */
+ showRange: PropTypes.bool,
+};
+
+EuiDualRange.defaultProps = {
+ min: 1,
+ max: 100,
+ fullWidth: false,
+ compressed: false,
+ showLabels: false,
+ showInput: false,
+ showRange: true,
+ showTicks: false,
+ levels: [],
+};
diff --git a/src/components/form/range/dual_range.test.js b/src/components/form/range/dual_range.test.js
new file mode 100644
index 00000000000..61c6e13a030
--- /dev/null
+++ b/src/components/form/range/dual_range.test.js
@@ -0,0 +1,123 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiDualRange } from './dual_range';
+
+describe('EuiDualRange', () => {
+ test('is rendered', () => {
+ const component = render(
+
{}}
+ {...requiredProps}
+ />
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ describe('props', () => {
+ test('fullWidth should render', () => {
+ const component = render(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('compressed should render', () => {
+ const component = render(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('labels should render', () => {
+ const component = render(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('ticks should render', () => {
+ const component = render(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('range should render', () => {
+ const component = render(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('inputs should render', () => {
+ const component = render(
+ {}}
+ showInput
+ {...requiredProps}
+ />
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('levels should render', () => {
+ const component = render(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+ });
+
+ test('allows value prop to accept numbers', () => {
+ const component = render(
+ {}}
+ />
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/src/components/form/range/index.d.ts b/src/components/form/range/index.d.ts
index 6b15cbf3bd8..be89c006857 100644
--- a/src/components/form/range/index.d.ts
+++ b/src/components/form/range/index.d.ts
@@ -1,14 +1,16 @@
-import { CommonProps } from '../../common';
+import { CommonProps, Omit } from '../../common';
import { SFC, ReactNode, HTMLAttributes, ChangeEventHandler, InputHTMLAttributes } from 'react';
declare module '@elastic/eui' {
+ export type EuiRangeLevelColor = 'primary' | 'success' | 'warning' | 'danger';
+
/**
+ * single range type def
+ *
* @see './range.js'
*/
- export type EuiRangeLevelColor = 'primary' | 'success' | 'warning' | 'danger';
-
export interface EuiRangeProps {
compressed?: boolean;
fullWidth?: boolean;
@@ -32,4 +34,19 @@ declare module '@elastic/eui' {
export const EuiRange: SFC<
CommonProps & InputHTMLAttributes & EuiRangeProps
>;
+
+ /**
+ * dual range type defs
+ *
+ * @see './dual_range.js'
+ */
+
+ export interface EuiDualRangeProps {
+ // Override acceptable value type
+ value: [number | string, number | string]
+ }
+
+ export const EuiDualRange: SFC<
+ CommonProps & Omit, 'value'> & EuiRangeProps & EuiDualRangeProps
+ >;
}
diff --git a/src/components/form/range/index.js b/src/components/form/range/index.js
index 3f3c671156c..b536c93abd0 100644
--- a/src/components/form/range/index.js
+++ b/src/components/form/range/index.js
@@ -1 +1,2 @@
+export { EuiDualRange } from './dual_range';
export { EuiRange } from './range';
diff --git a/src/components/form/range/range.js b/src/components/form/range/range.js
index 46dd286c67f..9dad9a7dc6b 100644
--- a/src/components/form/range/range.js
+++ b/src/components/form/range/range.js
@@ -1,19 +1,28 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import { range, find } from 'lodash';
+import { isWithinRange } from '../../../services/number';
-import { EuiFieldNumber } from '../field_number';
-
-export const LEVEL_COLORS = ['primary', 'success', 'warning', 'danger'];
+import { EuiRangeHighlight } from './range_highlight';
+import { EuiRangeInput } from './range_input';
+import { EuiRangeLabel } from './range_label';
+import { EuiRangeSlider } from './range_slider';
+import { EuiRangeTooltip } from './range_tooltip';
+import { EuiRangeTrack, LEVEL_COLORS } from './range_track';
+import { EuiRangeWrapper } from './range_wrapper';
export class EuiRange extends Component {
- constructor(props) {
- super(props);
+ handleOnChange = (e) => {
+ const isValid = isWithinRange(this.props.min, this.props.max, e.target.value);
+ this.props.onChange(e, isValid);
+ }
+
+ get isValid() {
+ return isWithinRange(this.props.min, this.props.max, this.props.value);
}
render() {
+
const {
className,
compressed,
@@ -33,304 +42,86 @@ export class EuiRange extends Component {
showRange,
showValue,
valueAppend, // eslint-disable-line no-unused-vars
- onChange,
+ onChange, // eslint-disable-line no-unused-vars
value,
style,
+ tabIndex,
...rest
} = this.props;
- const classes = classNames(
- 'euiRange',
- {
- 'euiRange--fullWidth': fullWidth,
- 'euiRange--compressed': compressed,
- },
- className
- );
-
- const wrapperClasses = classNames(
- 'euiRange__wrapper',
- {
- 'euiRange__wrapper--fullWidth': fullWidth,
- 'euiRange__wrapper--compressed': compressed,
- 'euiRange__wrapper--disabled': disabled,
- 'euiRange__wrapper--hasLabels': showLabels,
- 'euiRange__wrapper--hasLevels': levels.length,
- 'euiRange__wrapper--hasRange': showRange,
- 'euiRange__wrapper--hasTicks': showTicks,
- 'euiRange__wrapper--hasValue': showValue,
- },
- );
-
- let sliderTabIndex;
- let extraInputNode;
- if (showInput) {
- // Chrome will properly size the input based on the max value, but FF & IE does not.
- // Calculate the max-width of the input based on number of characters in min or max unit, whichever is greater.
- // Add 2 to accomodate for input stepper
- const maxWidthStyle = { maxWidth: `${Math.max(String(min).length, String(max).length) + 2}em` };
-
- // Make this input the main control by disabling screen reader access to slider control
- sliderTabIndex = '-1';
-
- extraInputNode = (
-
+ {showLabels && {min} }
+
- );
- }
-
- let tickObject;
- const inputWrapperStyle = {};
- if (showTicks) {
- tickObject = calculateTicksObject(min, max, tickInterval || step || 1);
-
- // Calculate if any extra margin should be added to the inputWrapper
- // because of longer tick labels on the ends
- const lengthOfMinLabel = String(tickObject.sequence[0]).length;
- const lenghtOfMaxLabel = String(tickObject.sequence[tickObject.sequence.length - 1]).length;
- const isLastTickTheMax = tickObject.sequence[tickObject.sequence.length - 1] === max;
- if (lengthOfMinLabel > 2) {
- inputWrapperStyle.marginLeft = `${(lengthOfMinLabel / 5)}em`;
- }
- if (isLastTickTheMax && lenghtOfMaxLabel > 2) {
- inputWrapperStyle.marginRight = `${(lenghtOfMaxLabel / 5)}em`;
- }
- }
-
- return (
-
- {this.renderLabel('min')}
-
-
-
+
- {this.renderValue()}
- {this.renderRange()}
- {this.renderLevels()}
- {this.renderTicks(tickObject)}
-
-
- {this.renderLabel('max')}
- {extraInputNode}
-
- );
- }
-
- renderLabel = (side) => {
- const {
- showLabels,
- } = this.props;
-
- if (!showLabels) { return; }
-
- return (
-
- {this.props[side]}
-
- );
-
- }
-
- renderTicks = (tickObject) => {
- const {
- disabled,
- onChange,
- showTicks,
- ticks,
- value,
- max,
- } = this.props;
-
- if (!showTicks) {
- return;
- }
-
- // Align with item labels across the range by adding
- // left and right negative margins that is half of the tick marks
- const ticksStyle = !!ticks ? undefined : { margin: `0 ${tickObject.percentageWidth / -2}%`, left: 0, right: 0 };
-
- return (
-
- {tickObject.sequence.map((tickValue) => {
- const tickStyle = {};
- let customTick;
- if (ticks) {
- customTick = find(ticks, function (o) { return o.value === tickValue; });
-
- if (customTick == null) {
- return;
- } else {
- tickStyle.left = `${(customTick.value / max) * 100}%`;
- }
- } else {
- tickStyle.width = `${tickObject.percentageWidth}%`;
- }
-
- const tickClasses = classNames(
- 'euiRange__tick',
- {
- 'euiRange__tick--selected': value === tickValue,
- 'euiRange__tick-isCustom': customTick,
- }
- );
-
- return (
-
- {customTick ? customTick.label : tickValue}
-
- );
- })}
-
- );
- }
-
- renderRange = () => {
- const {
- showRange,
- value,
- max,
- min,
- } = this.props;
-
- if (!showRange) {
- return;
- }
-
- // Calculate the width the range based on value
- const rangeWidth = (value - min) / (max - min);
- const rangeWidthStyle = { width: `${rangeWidth * 100}%` };
-
- return (
-
- );
- }
-
- renderValue = () => {
- const {
- showValue,
- value,
- valueAppend,
- max,
- min,
- name,
- } = this.props;
-
- if (!showValue) {
- return;
- }
-
- // Calculate the left position based on value
- const decimal = (value - min) / (max - min);
- // Must be between 0-100%
- let valuePosition = decimal <= 1 ? decimal : 1;
- valuePosition = valuePosition >= 0 ? valuePosition : 0;
-
- let valuePositionSide;
- if (valuePosition > .5) {
- valuePositionSide = 'left';
- } else {
- valuePositionSide = 'right';
- }
-
- const valuePositionStyle = { left: `${valuePosition * 100}%` };
-
- // Change left/right position based on value (half way point)
- const valueClasses = classNames(
- 'euiRange__value',
- `euiRange__value--${valuePositionSide}`,
- );
-
- return (
-
-
- {value}{valueAppend}
-
-
- );
- }
-
- renderLevels = () => {
- const {
- levels,
- max,
- min,
- } = this.props;
-
- if (levels.length < 1) {
- return;
- }
-
- return (
-
- {levels.map((level, index) => {
- const range = level.max - level.min;
- const width = (range / (max - min)) * 100;
-
- return (
-
- );
- })}
-
+ {(showValue && !!String(value).length) && (
+
+ )}
+
+ {(showRange && this.isValid) && (
+
+ )}
+
+ {showLabels && {max} }
+ {showInput && (
+
+ )}
+
);
}
}
-function calculateTicksObject(min, max, interval) {
- // Calculate the width of each tick mark
- const tickWidthDecimal = (interval / ((max - min) + interval));
- const tickWidthPercentage = tickWidthDecimal * 100;
-
- // Loop from min to max, creating ticks at each interval
- // (adds a very small number to the max since `range` is not inclusive of the max value)
- const toBeInclusive = .000000001;
- const sequence = range(min, max + toBeInclusive, interval);
-
- return (
- {
- decimalWidth: tickWidthDecimal,
- percentageWidth: tickWidthPercentage,
- sequence: sequence,
- }
- );
-}
-
EuiRange.propTypes = {
name: PropTypes.string,
id: PropTypes.string,
@@ -365,6 +156,9 @@ EuiRange.propTypes = {
label: PropTypes.node.isRequired,
}),
),
+ /**
+ * Function signature: `(event, isValid)`
+ */
onChange: PropTypes.func,
/**
* Create colored indicators for certain intervals
@@ -397,6 +191,7 @@ EuiRange.defaultProps = {
compressed: false,
showLabels: false,
showInput: false,
+ showRange: false,
showTicks: false,
showValue: false,
levels: [],
diff --git a/src/components/form/range/range.test.js b/src/components/form/range/range.test.js
index 3ee3786d07a..1030c9f62cc 100644
--- a/src/components/form/range/range.test.js
+++ b/src/components/form/range/range.test.js
@@ -61,7 +61,7 @@ describe('EuiRange', () => {
test('range should render', () => {
const component = render(
-
+
);
expect(component)
diff --git a/src/components/form/range/range_highlight.js b/src/components/form/range/range_highlight.js
new file mode 100644
index 00000000000..233adc8682d
--- /dev/null
+++ b/src/components/form/range/range_highlight.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export const EuiRangeHighlight = ({ hasFocus, showTicks, lowerValue, upperValue, max, min }) => {
+ // Calculate the width the range based on value
+ // const rangeWidth = (value - min) / (max - min);
+ const leftPosition = (lowerValue - min) / (max - min);
+ const rangeWidth = (upperValue - lowerValue) / (max - min);
+ const rangeWidthStyle = {
+ marginLeft: `${leftPosition * 100}%`,
+ width: `${rangeWidth * 100}%`
+ };
+
+ const classes = classNames('euiRangeHighlight', {
+ 'euiRangeHighlight--hasTicks': showTicks
+ });
+
+ const progressClasses = classNames('euiRangeHighlight__progress', {
+ 'euiRangeHighlight__progress--hasFocus': hasFocus
+ });
+
+ return (
+
+ );
+};
+
+EuiRangeHighlight.propTypes = {
+ hasFocus: PropTypes.bool,
+ showTicks: PropTypes.bool,
+ lowerValue: PropTypes.number.isRequired,
+ upperValue: PropTypes.number.isRequired,
+ max: PropTypes.number.isRequired,
+ min: PropTypes.number.isRequired
+};
diff --git a/src/components/form/range/range_input.js b/src/components/form/range/range_input.js
new file mode 100644
index 00000000000..c748b95baf1
--- /dev/null
+++ b/src/components/form/range/range_input.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { EuiFieldNumber } from '../field_number';
+
+export const EuiRangeInput = ({
+ min,
+ max,
+ step,
+ value,
+ disabled,
+ compressed,
+ onChange,
+ name,
+ side,
+ digits,
+ ...rest
+}) => {
+
+ // Chrome will properly size the input based on the max value, but FF & IE do not.
+ // Calculate the width of the input based on highest number of characters.
+ // Add 2 to accomodate for input stepper
+ const digitTolerance = !!digits ? digits : Math.max(String(min).length, String(max).length);
+ const widthStyle = { width: `${digitTolerance + 2}em` };
+
+ return (
+
+ );
+};
+
+EuiRangeInput.propTypes = {
+ min: PropTypes.number.isRequired,
+ max: PropTypes.number.isRequired,
+ step: PropTypes.number,
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ compressed: PropTypes.bool,
+ onChange: PropTypes.func,
+ name: PropTypes.string,
+ digits: PropTypes.number,
+ side: PropTypes.oneOf(['min', 'max'])
+};
+EuiRangeInput.defaultProps = {
+ side: 'max'
+};
diff --git a/src/components/form/range/range_label.js b/src/components/form/range/range_label.js
new file mode 100644
index 00000000000..132f4a51536
--- /dev/null
+++ b/src/components/form/range/range_label.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export const EuiRangeLabel = ({ children, disabled, side }) => {
+ const classes = classNames('euiRangeLabel', `euiRangeLabel--${side}`, {
+ 'euiRangeLabel--isDisabled': disabled
+ });
+ return (
+
+ {children}
+
+ );
+};
+
+EuiRangeLabel.propTypes = {
+ side: PropTypes.oneOf(['min', 'max'])
+};
+EuiRangeLabel.defaultProps = {
+ side: 'max'
+};
diff --git a/src/components/form/range/range_levels.js b/src/components/form/range/range_levels.js
new file mode 100644
index 00000000000..3cf590aa551
--- /dev/null
+++ b/src/components/form/range/range_levels.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export const LEVEL_COLORS = ['primary', 'success', 'warning', 'danger'];
+
+export const EuiRangeLevels = ({ levels, max, min, showTicks }) => {
+ const classes = classNames('euiRangeLevels', {
+ 'euiRangeLevels--hasTicks': showTicks
+ });
+ return (
+
+ {levels.map((level, index) => {
+ const range = level.max - level.min;
+ const width = (range / (max - min)) * 100;
+
+ return (
+
+ );
+ })}
+
+ );
+};
+
+EuiRangeLevels.propTypes = {
+ levels: PropTypes.arrayOf(
+ PropTypes.shape({
+ min: PropTypes.number,
+ max: PropTypes.number,
+ color: PropTypes.oneOf(LEVEL_COLORS),
+ }),
+ ),
+ max: PropTypes.number.isRequired,
+ min: PropTypes.number.isRequired,
+ showTicks: PropTypes.bool
+};
diff --git a/src/components/form/range/range_slider.js b/src/components/form/range/range_slider.js
new file mode 100644
index 00000000000..cba60615ec7
--- /dev/null
+++ b/src/components/form/range/range_slider.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export const EuiRangeSlider = React.forwardRef(({
+ className,
+ disabled,
+ id,
+ max,
+ min,
+ name,
+ step,
+ onChange,
+ tabIndex,
+ value,
+ style,
+ showTicks,
+ hasFocus,
+ ...rest
+}, ref) => {
+ const classes = classNames('euiRangeSlider', {
+ 'euiRangeSlider--hasTicks': showTicks,
+ 'euiRangeSlider--hasFocus': hasFocus
+ }, className);
+ return (
+
+ );
+});
+
+EuiRangeSlider.propTypes = {
+ id: PropTypes.string,
+ min: PropTypes.number.isRequired,
+ max: PropTypes.number.isRequired,
+ name: PropTypes.string,
+ step: PropTypes.number,
+ onChange: PropTypes.func,
+ tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ value: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string]))
+ ]),
+ hasFocus: PropTypes.bool
+};
diff --git a/src/components/form/range/range_thumb.js b/src/components/form/range/range_thumb.js
new file mode 100644
index 00000000000..0b87eadc169
--- /dev/null
+++ b/src/components/form/range/range_thumb.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export const EuiRangeThumb = ({ min, max, value, disabled, showInput, showTicks, ...rest }) => {
+ const classes = classNames(
+ 'euiRangeThumb',
+ {
+ 'euiRangeThumb--hasTicks': showTicks
+ },
+ );
+ return (
+
+ );
+};
+
+EuiRangeThumb.propTypes = {
+ min: PropTypes.number.isRequired,
+ max: PropTypes.number.isRequired,
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ showInput: PropTypes.bool,
+ showTicks: PropTypes.bool,
+};
diff --git a/src/components/form/range/range_ticks.js b/src/components/form/range/range_ticks.js
new file mode 100644
index 00000000000..16c380b809e
--- /dev/null
+++ b/src/components/form/range/range_ticks.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export const EuiRangeTicks = ({ disabled, onChange, ticks, tickObject, value, max }) => {
+ // Align with item labels across the range by adding
+ // left and right negative margins that is half of the tick marks
+ const ticksStyle = !!ticks ? undefined : { margin: `0 ${tickObject.percentageWidth / -2}%`, left: 0, right: 0 };
+
+ return (
+
+ {tickObject.sequence.map((tickValue) => {
+ const tickStyle = {};
+ let customTick;
+ if (ticks) {
+ customTick = ticks.find(o => o.value === tickValue);
+
+ if (customTick == null) {
+ return;
+ } else {
+ tickStyle.left = `${(customTick.value / max) * 100}%`;
+ }
+ } else {
+ tickStyle.width = `${tickObject.percentageWidth}%`;
+ }
+
+ const tickClasses = classNames(
+ 'euiRangeTick',
+ {
+ 'euiRangeTick--selected': value === tickValue,
+ 'euiRangeTick--isCustom': customTick,
+ }
+ );
+
+ return (
+
+ {customTick ? customTick.label : tickValue}
+
+ );
+ })}
+
+ );
+};
+
+EuiRangeTicks.propTypes = {
+ disabled: PropTypes.bool,
+ onChange: PropTypes.func,
+ ticks: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.number.isRequired,
+ label: PropTypes.node.isRequired,
+ }),
+ ),
+ tickObject: PropTypes.shape({
+ decimalWidth: PropTypes.number,
+ percentageWidth: PropTypes.number,
+ sequence: PropTypes.arrayOf(PropTypes.number),
+ }).isRequired,
+ value: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string]))
+ ]),
+ max: PropTypes.number.isRequired
+};
diff --git a/src/components/form/range/range_tooltip.js b/src/components/form/range/range_tooltip.js
new file mode 100644
index 00000000000..78e66aaa45c
--- /dev/null
+++ b/src/components/form/range/range_tooltip.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export const EuiRangeTooltip = ({ value, valueAppend, max, min, name, showTicks }) => {
+ // Calculate the left position based on value
+ const decimal = (value - min) / (max - min);
+ // Must be between 0-100%
+ let valuePosition = decimal <= 1 ? decimal : 1;
+ valuePosition = valuePosition >= 0 ? valuePosition : 0;
+
+ let valuePositionSide;
+ if (valuePosition > .5) {
+ valuePositionSide = 'left';
+ } else {
+ valuePositionSide = 'right';
+ }
+
+ const valuePositionStyle = { left: `${valuePosition * 100}%` };
+
+ // Change left/right position based on value (half way point)
+ const valueClasses = classNames(
+ 'euiRangeTooltip__value',
+ `euiRangeTooltip__value--${valuePositionSide}`,
+ {
+ 'euiRangeTooltip__value--hasTicks': showTicks
+ }
+ );
+
+ return (
+
+
+ {value}{valueAppend}
+
+
+ );
+};
+
+EuiRangeTooltip.propTypes = {
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ valueAppend: PropTypes.string,
+ max: PropTypes.number.isRequired,
+ min: PropTypes.number.isRequired,
+ name: PropTypes.string
+};
diff --git a/src/components/form/range/range_track.js b/src/components/form/range/range_track.js
new file mode 100644
index 00000000000..a09fb8d75eb
--- /dev/null
+++ b/src/components/form/range/range_track.js
@@ -0,0 +1,121 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import { range } from 'lodash';
+
+import { EuiRangeLevels, LEVEL_COLORS } from './range_levels';
+import { EuiRangeTicks } from './range_ticks';
+
+export { LEVEL_COLORS };
+
+export class EuiRangeTrack extends Component {
+
+ calculateTicksObject = (min, max, interval) => {
+ // Calculate the width of each tick mark
+ const tickWidthDecimal = (interval / ((max - min) + interval));
+ const tickWidthPercentage = tickWidthDecimal * 100;
+
+ // Loop from min to max, creating ticks at each interval
+ // (adds a very small number to the max since `range` is not inclusive of the max value)
+ const toBeInclusive = .000000001;
+ const sequence = range(min, max + toBeInclusive, interval);
+
+ return (
+ {
+ decimalWidth: tickWidthDecimal,
+ percentageWidth: tickWidthPercentage,
+ sequence: sequence,
+ }
+ );
+ }
+
+ render() {
+ const {
+ children,
+ disabled,
+ max,
+ min,
+ step,
+ showTicks,
+ tickInterval,
+ ticks, // eslint-disable-line no-unused-vars
+ levels,
+ onChange,
+ value
+ } = this.props;
+
+ let tickObject;
+ const inputWrapperStyle = {};
+ if (showTicks) {
+ tickObject = this.calculateTicksObject(min, max, tickInterval || step || 1);
+
+ // Calculate if any extra margin should be added to the inputWrapper
+ // because of longer tick labels on the ends
+ const lengthOfMinLabel = String(tickObject.sequence[0]).length;
+ const lenghtOfMaxLabel = String(tickObject.sequence[tickObject.sequence.length - 1]).length;
+ const isLastTickTheMax = tickObject.sequence[tickObject.sequence.length - 1] === max;
+ if (lengthOfMinLabel > 2) {
+ inputWrapperStyle.marginLeft = `${(lengthOfMinLabel / 5)}em`;
+ }
+ if (isLastTickTheMax && lenghtOfMaxLabel > 2) {
+ inputWrapperStyle.marginRight = `${(lenghtOfMaxLabel / 5)}em`;
+ }
+ }
+
+ const trackClasses = classNames('euiRangeTrack', {
+ 'euiRangeTrack--disabled': disabled
+ });
+
+ return (
+
+ {children}
+ {!!levels.length && (
+
+ )}
+ {showTicks && (
+
+ )}
+
+ );
+ }
+}
+
+EuiRangeTrack.propTypes = {
+ min: PropTypes.number.isRequired,
+ max: PropTypes.number.isRequired,
+ step: PropTypes.number,
+ value: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string]))
+ ]),
+ showTicks: PropTypes.bool,
+ tickInterval: PropTypes.number,
+ ticks: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.number.isRequired,
+ label: PropTypes.node.isRequired,
+ }),
+ ),
+ onChange: PropTypes.func,
+ levels: PropTypes.arrayOf(
+ PropTypes.shape({
+ min: PropTypes.number,
+ max: PropTypes.number,
+ color: PropTypes.oneOf(LEVEL_COLORS),
+ }),
+ ),
+};
diff --git a/src/components/form/range/range_wrapper.js b/src/components/form/range/range_wrapper.js
new file mode 100644
index 00000000000..49a11589ce8
--- /dev/null
+++ b/src/components/form/range/range_wrapper.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export const LEVEL_COLORS = ['primary', 'success', 'warning', 'danger'];
+
+export const EuiRangeWrapper = ({
+ children,
+ className,
+ fullWidth
+}) => {
+
+ const classes = classNames(
+ 'euiRangeWrapper',
+ {
+ 'euiRangeWrapper--fullWidth': fullWidth
+ },
+ className
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+EuiRangeWrapper.propTypes = {
+ fullWidth: PropTypes.bool
+};
diff --git a/src/components/index.js b/src/components/index.js
index b62b5953f65..795d2997184 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -132,6 +132,7 @@ export {
EuiCheckbox,
EuiCheckboxGroup,
EuiDescribedFormGroup,
+ EuiDualRange,
EuiFieldNumber,
EuiFieldPassword,
EuiFieldSearch,
diff --git a/src/services/index.ts b/src/services/index.ts
index ad29ebe8775..60faa4b159a 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -33,6 +33,8 @@ export {
formatText,
} from './format';
+export { isWithinRange } from './number';
+
export { Pager } from './paging';
export { Random } from './random';
diff --git a/src/services/number/index.ts b/src/services/number/index.ts
new file mode 100644
index 00000000000..3238b292cb6
--- /dev/null
+++ b/src/services/number/index.ts
@@ -0,0 +1 @@
+export * from './number';
diff --git a/src/services/number/number.test.tsx b/src/services/number/number.test.tsx
new file mode 100644
index 00000000000..af8a51aa993
--- /dev/null
+++ b/src/services/number/number.test.tsx
@@ -0,0 +1,21 @@
+import { isWithinRange } from './number';
+
+describe('numbers', () => {
+ test('isWithinRange', () => {
+ // True
+ expect(isWithinRange(0, 100, 50)).toBe(true);
+ expect(isWithinRange('0', 100, 50)).toBe(true);
+ expect(isWithinRange(0, '100', 50)).toBe(true);
+ expect(isWithinRange(0, 100, '50')).toBe(true);
+ expect(isWithinRange(0, 100, 0)).toBe(true);
+ expect(isWithinRange(0, 100, 100)).toBe(true);
+ expect(isWithinRange(-10, 10, 5)).toBe(true);
+ expect(isWithinRange(-10, 10, -5)).toBe(true);
+ expect(isWithinRange('-10', 10, '-5')).toBe(true);
+ // False
+ expect(isWithinRange(0, 100, 101)).toBe(false);
+ expect(isWithinRange(10, 100, 0)).toBe(false);
+ expect(isWithinRange(0, 100, -10)).toBe(false);
+ expect(isWithinRange(0, 100, '')).toBe(false);
+ });
+});
diff --git a/src/services/number/number.ts b/src/services/number/number.ts
new file mode 100644
index 00000000000..27e02f917c6
--- /dev/null
+++ b/src/services/number/number.ts
@@ -0,0 +1,12 @@
+export const isWithinRange = (
+ min: number | string,
+ max: number | string,
+ value: number | string
+) => {
+ if (min === '' || max === '' || value === '') {
+ return false;
+ }
+
+ const val = Number(value);
+ return Number(min) <= val && val <= Number(max);
+};