diff --git a/CHANGELOG.md b/CHANGELOG.md index 64d77755643..330d53f14a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Created `EuiDualRange` using components from modularized, refactored `EuiRange`. New util service `isWithinRange` is the first in the number category. ([#1485](https://github.com/elastic/eui/pull/1485)) + **Bug fixes** - Fixed `EuiSearchBar.Query` match_all query string must be `*` ([#1521](https://github.com/elastic/eui/pull/1521)) @@ -26,13 +28,14 @@ ## [`6.8.0`](https://github.com/elastic/eui/tree/v6.8.0) - Changed `flex-basis` value on `EuiPageBody` for better cross-browser support ([#1497](https://github.com/elastic/eui/pull/1497)) -- Converted a number of components to support text localization ([#1485](https://github.com/elastic/eui/pull/1485)) +- Converted a number of components to support text localization ([#1450](https://github.com/elastic/eui/pull/1450)) - Added a seconds option to the refresh interval selection in `EuiSuperDatePicker` ([#1503](https://github.com/elastic/eui/pull/1503)) -- Changed to conditionally render `EuiModalBody` if `EuiConfirmModal` has no `children` ([#1505](https://github.com/elastic/eui/pull/1505)) +- Changed to conditionally render `EuiModalBody` if `EuiConfirmModal` has no `children` ([#1500](https://github.com/elastic/eui/pull/1500)) + **Bug fixes** -- Remove `font-features` setting on `@euiFont` mixin to prevent breaks in ACE editor ([#1497](https://github.com/elastic/eui/pull/1497)) +- Remove `font-features` setting on `@euiFont` mixin to prevent breaks in ACE editor ([#1505](https://github.com/elastic/eui/pull/1505)) ## [`6.7.4`](https://github.com/elastic/eui/tree/v6.7.4) diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 6db4e77e6c6..762d21fb05a 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -201,6 +201,9 @@ import { PortalExample } import { ProgressExample } from './views/progress/progress_example'; +import { RangeControlExample } + from './views/range/range_example'; + import { ResponsiveExample } from './views/responsive/responsive_example'; @@ -400,6 +403,7 @@ const navigation = [{ DatePickerExample, ExpressionExample, FilterGroupExample, + RangeControlExample, SearchBarExample, ].map(example => createExample(example)), }, diff --git a/src-docs/src/views/form_controls/form_controls_example.js b/src-docs/src/views/form_controls/form_controls_example.js index 7cf3be3554e..317d3253ddb 100644 --- a/src-docs/src/views/form_controls/form_controls_example.js +++ b/src-docs/src/views/form_controls/form_controls_example.js @@ -25,7 +25,6 @@ import { EuiLink, EuiRadio, EuiRadioGroup, - EuiRange, EuiSelect, EuiSwitch, EuiTextArea, @@ -75,10 +74,6 @@ import RadioGroup from './radio_group'; const radioGroupSource = require('!!raw-loader!./radio_group'); const radioGroupHtml = renderToHtml(RadioGroup); -import RangeExample from './range'; -const rangeSource = require('!!raw-loader!./range'); -const rangeHtml = renderToHtml(RangeExample); - import Switch from './switch'; const switchSource = require('!!raw-loader!./switch'); const switchHtml = renderToHtml(Switch); @@ -250,36 +245,6 @@ export const FormControlsExample = { EuiRadioGroup, }, demo: , - }, { - title: 'Range', - text: ( - - -

- The base slider should only be used - when the precise value is not considered important. If - the precise value does matter, add the showInput prop or use - a EuiFieldNumber instead. -

-
-
-

- While currently considered optional, the showLabels property should - be added to explicitly state the range to the user. -

-
- ), - source: [{ - type: GuideSectionTypes.JS, - code: rangeSource, - }, { - type: GuideSectionTypes.HTML, - code: rangeHtml, - }], - props: { - EuiRange, - }, - demo: , }, { title: 'Switch', source: [{ diff --git a/src-docs/src/views/range/dual_range.js b/src-docs/src/views/range/dual_range.js new file mode 100644 index 00000000000..0d7663fef5d --- /dev/null +++ b/src-docs/src/views/range/dual_range.js @@ -0,0 +1,136 @@ +import React, { + Component, + Fragment, +} from 'react'; + +import { + EuiDualRange, + EuiSpacer, + EuiFormHelpText, +} from '../../../../src/components'; + +import makeId from '../../../../src/components/form/form_row/make_id'; + +export default class extends Component { + constructor(props) { + super(props); + + this.levels = [ + { + min: 0, + max: 600, + color: 'danger' + }, + { + min: 600, + max: 2000, + color: 'success' + } + ]; + + this.state = { + value: [120, 480] + }; + } + + onChange = (value) => { + this.setState({ + value + }); + }; + + render() { + return ( + + + + + + + + + + + + + + + + Recommended levels are 600 and above. + + + + + + + + + + ); + } +} diff --git a/src-docs/src/views/form_controls/range.js b/src-docs/src/views/range/range.js similarity index 99% rename from src-docs/src/views/form_controls/range.js rename to src-docs/src/views/range/range.js index d071610942a..8c1dc43695d 100644 --- a/src-docs/src/views/form_controls/range.js +++ b/src-docs/src/views/range/range.js @@ -29,7 +29,7 @@ export default class extends Component { ]; this.state = { - value: '120', + value: '120' }; } diff --git a/src-docs/src/views/range/range_example.js b/src-docs/src/views/range/range_example.js new file mode 100644 index 00000000000..10ba13cb139 --- /dev/null +++ b/src-docs/src/views/range/range_example.js @@ -0,0 +1,94 @@ +import React, { Fragment } from 'react'; + +import { renderToHtml } from '../../services'; + +import { + GuideSectionTypes, +} from '../../components'; + +import { + EuiCallOut, + EuiDualRange, + EuiRange, + EuiSpacer +} from '../../../../src/components'; + +import DualRangeExample from './dual_range'; +const dualRangeSource = require('!!raw-loader!./dual_range'); +const dualRangeHtml = renderToHtml(DualRangeExample); + +import RangeExample from './range'; +const rangeSource = require('!!raw-loader!./range'); +const rangeHtml = renderToHtml(RangeExample); + +export const RangeControlExample = { + title: 'Range', + intro: ( + + +

+ Range sliders should only be used + when the precise value is not considered important. If + the precise value does matter, add the showInput prop or use + a EuiFieldNumber instead. +

+

+ While currently considered optional, the showLabels property should + be added to explicitly state the range to the user. +

+
+ +
+ ), + sections: [ + { + title: 'Range', + source: [{ + type: GuideSectionTypes.JS, + code: rangeSource, + }, { + type: GuideSectionTypes.HTML, + code: rangeHtml, + }], + props: { + EuiRange, + }, + demo: , + }, + { + title: 'DualRange', + text: ( + + +

+ Two-value input[type=range] elements are not part of the HTML5 specification. + Because of this support gap, EuiDualRange cannot expose a native value property + for native form to consumption. + + The React onChange prop is the recommended method + for retrieving the upper and lower values. + +

+

+ EuiDualRange does use native inputs to help validate step values + and range limits. These may be used as form values when showInput is in use. + The alternative is to store values in input[type=hidden]. +

+
+ +
+ ), + source: [{ + type: GuideSectionTypes.JS, + code: dualRangeSource, + }, { + type: GuideSectionTypes.HTML, + code: dualRangeHtml, + }], + props: { + EuiDualRange, + }, + demo: , + } + ] +}; diff --git a/src/components/form/index.js b/src/components/form/index.js index a0dca1c8bb8..2dd6490a490 100644 --- a/src/components/form/index.js +++ b/src/components/form/index.js @@ -18,7 +18,7 @@ export { EuiRadio, EuiRadioGroup, } from './radio'; -export { EuiRange } from './range'; +export { EuiDualRange, EuiRange } from './range'; export { EuiSelect } from './select'; export { EuiSuperSelect, diff --git a/src/components/form/range/__snapshots__/dual_range.test.js.snap b/src/components/form/range/__snapshots__/dual_range.test.js.snap new file mode 100644 index 00000000000..1ee613624ec --- /dev/null +++ b/src/components/form/range/__snapshots__/dual_range.test.js.snap @@ -0,0 +1,360 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiDualRange allows value prop to accept numbers 1`] = ` +
+
+ +
+
+
+
+
+`; + +exports[`EuiDualRange is rendered 1`] = ` +
+
+ +
+
+
+
+
+`; + +exports[`EuiDualRange props compressed should render 1`] = ` +
+
+ +
+
+
+
+
+`; + +exports[`EuiDualRange props fullWidth should render 1`] = ` +
+
+ +
+
+
+
+
+`; + +exports[`EuiDualRange props inputs should render 1`] = ` +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+
+`; + +exports[`EuiDualRange props labels should render 1`] = ` +
+ +
+ +
+
+
+
+ +
+`; + +exports[`EuiDualRange props levels should render 1`] = ` +
+
+ +
+
+
+
+ + +
+
+
+`; + +exports[`EuiDualRange props range should render 1`] = ` +
+
+ +
+
+
+
+
+`; + +exports[`EuiDualRange props ticks should render 1`] = ` +
+
+ +
+
+
+
+ + + + + +
+
+
+`; diff --git a/src/components/form/range/__snapshots__/range.test.js.snap b/src/components/form/range/__snapshots__/range.test.js.snap index de45d8e5ab5..2c23ede4230 100644 --- a/src/components/form/range/__snapshots__/range.test.js.snap +++ b/src/components/form/range/__snapshots__/range.test.js.snap @@ -2,23 +2,23 @@ exports[`EuiRange allows value prop to accept a number 1`] = `
8 @@ -30,14 +30,14 @@ exports[`EuiRange allows value prop to accept a number 1`] = ` exports[`EuiRange is rendered 1`] = `
@@ -111,13 +111,13 @@ exports[`EuiRange props extra input should render 1`] = ` exports[`EuiRange props fullWidth should render 1`] = `
@@ -155,26 +155,26 @@ exports[`EuiRange props labels should render 1`] = ` exports[`EuiRange props levels should render 1`] = `
@@ -184,23 +184,24 @@ exports[`EuiRange props levels should render 1`] = ` exports[`EuiRange props range should render 1`] = `
@@ -209,23 +210,23 @@ exports[`EuiRange props range should render 1`] = ` exports[`EuiRange props ticks should render 1`] = `
- ); - })} -
- ); - } - - 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 ( + + ); +}; + +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 ( + + ); + })} +
+ ); +}; + +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); +};