├── .gitattributes
├── .npmrc
├── src
├── scss
│ ├── index.scss
│ └── input-range
│ │ ├── _input-range-label-container.scss
│ │ ├── input-range.scss
│ │ ├── _input-range-label.scss
│ │ ├── _input-range-track.scss
│ │ ├── _input-range-slider.scss
│ │ └── _input-range-variables.scss
└── js
│ ├── utils
│ ├── is-number.js
│ ├── is-defined.js
│ ├── is-object.js
│ ├── captialize.js
│ ├── length.js
│ ├── clamp.js
│ ├── index.js
│ └── distance-to.js
│ ├── input-range
│ ├── key-codes.js
│ ├── range-prop-type.js
│ ├── default-class-names.js
│ ├── value-prop-type.js
│ ├── label.jsx
│ ├── value-transformer.js
│ ├── track.jsx
│ ├── slider.jsx
│ └── input-range.jsx
│ └── index.js
├── .gitignore
├── .travis.yml
├── test
├── index.js
└── input-range
│ └── input-range.spec.jsx
├── .npmignore
├── postcss.config.js
├── example
├── scss
│ └── index.scss
├── js
│ ├── index.jsx
│ └── example-app.jsx
└── index.html
├── .babelrc
├── .editorconfig
├── .esdoc.json
├── .eslintrc
├── .sass-lint.yml
├── react-input-range.d.ts
├── LICENSE
├── karma.conf.js
├── webpack-example.config.babel.js
├── webpack.config.babel.js
├── package.json
├── CHANGELOG.md
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix = "v"
2 |
--------------------------------------------------------------------------------
/src/scss/index.scss:
--------------------------------------------------------------------------------
1 | @import 'input-range/input-range';
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | build
3 | coverage
4 | docs
5 | lib
6 | node_modules
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 6
4 | sudo: false
5 | cache: yarn
6 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import 'dom4';
2 |
3 | const context = require.context('.', true, /\.spec$/);
4 |
5 | context.keys().forEach(context);
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.conf.js
2 | *.config.js
3 | *.log
4 | **/.*
5 | build
6 | coverage
7 | docs
8 | example
9 | node_modules
10 | test
11 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const autoprefixer = require('autoprefixer');
2 |
3 | const postCssConfig = {
4 | plugins: [
5 | autoprefixer(),
6 | ],
7 | };
8 |
9 | module.exports = postCssConfig;
10 |
--------------------------------------------------------------------------------
/src/scss/input-range/_input-range-label-container.scss:
--------------------------------------------------------------------------------
1 | .input-range__label-container {
2 | left: -50%;
3 | position: relative;
4 |
5 | .input-range__label--max & {
6 | left: 50%;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/scss/index.scss:
--------------------------------------------------------------------------------
1 | .form {
2 | margin: 0 auto;
3 | padding: 100px 30px 0;
4 |
5 | @media (min-width: 800px) {
6 | max-width: 60%;
7 | }
8 | }
9 |
10 | .input-range {
11 | margin-bottom: 160px;
12 | }
13 |
--------------------------------------------------------------------------------
/src/js/utils/is-number.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if a value is a number
3 | * @ignore
4 | * @param {*} value
5 | * @return {boolean}
6 | */
7 | export default function isNumber(value) {
8 | return typeof value === 'number';
9 | }
10 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "add-module-exports",
4 | "transform-object-rest-spread",
5 | "transform-decorators-legacy",
6 | "transform-react-jsx"
7 | ],
8 | "presets": ["es2015", "es2016", "react"]
9 | }
10 |
--------------------------------------------------------------------------------
/src/js/utils/is-defined.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if a value is defined
3 | * @ignore
4 | * @param {*} value
5 | * @return {boolean}
6 | */
7 | export default function isDefined(value) {
8 | return value !== undefined && value !== null;
9 | }
10 |
--------------------------------------------------------------------------------
/src/js/utils/is-object.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if a value is an object
3 | * @ignore
4 | * @param {*} value
5 | * @return {boolean}
6 | */
7 | export default function isObject(value) {
8 | return value !== null && typeof value === 'object';
9 | }
10 |
--------------------------------------------------------------------------------
/src/js/utils/captialize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Captialize a string
3 | * @ignore
4 | * @param {string} string
5 | * @return {string}
6 | */
7 | export default function captialize(string) {
8 | return string.charAt(0).toUpperCase() + string.slice(1);
9 | }
10 |
--------------------------------------------------------------------------------
/src/js/input-range/key-codes.js:
--------------------------------------------------------------------------------
1 | /** @ignore */
2 | export const DOWN_ARROW = 40;
3 |
4 | /** @ignore */
5 | export const LEFT_ARROW = 37;
6 |
7 | /** @ignore */
8 | export const RIGHT_ARROW = 39;
9 |
10 | /** @ignore */
11 | export const UP_ARROW = 38;
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/src/js/utils/length.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Calculate the absolute difference between two numbers
3 | * @ignore
4 | * @param {number} numA
5 | * @param {number} numB
6 | * @return {number}
7 | */
8 | export default function length(numA, numB) {
9 | return Math.abs(numA - numB);
10 | }
11 |
--------------------------------------------------------------------------------
/example/js/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import ExampleApp from './example-app';
4 | import '../../src/scss/index.scss';
5 | import '../scss/index.scss';
6 |
7 | ReactDOM.render(
8 | ,
9 | document.getElementById('app'),
10 | );
11 |
--------------------------------------------------------------------------------
/src/js/utils/clamp.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Clamp a value between a min and max value
3 | * @ignore
4 | * @param {number} value
5 | * @param {number} min
6 | * @param {number} max
7 | * @return {number}
8 | */
9 | export default function clamp(value, min, max) {
10 | return Math.min(Math.max(value, min), max);
11 | }
12 |
--------------------------------------------------------------------------------
/src/scss/input-range/input-range.scss:
--------------------------------------------------------------------------------
1 | @import 'input-range-variables';
2 | @import 'input-range-slider';
3 | @import 'input-range-label';
4 | @import 'input-range-label-container';
5 | @import 'input-range-track';
6 |
7 | .input-range {
8 | height: $input-range-slider-height;
9 | position: relative;
10 | width: 100%;
11 | }
12 |
--------------------------------------------------------------------------------
/src/js/utils/index.js:
--------------------------------------------------------------------------------
1 | export { default as captialize } from './captialize';
2 | export { default as clamp } from './clamp';
3 | export { default as distanceTo } from './distance-to';
4 | export { default as isDefined } from './is-defined';
5 | export { default as isNumber } from './is-number';
6 | export { default as isObject } from './is-object';
7 | export { default as length } from './length';
8 |
--------------------------------------------------------------------------------
/src/js/utils/distance-to.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Calculate the distance between pointA and pointB
3 | * @ignore
4 | * @param {Point} pointA
5 | * @param {Point} pointB
6 | * @return {number} Distance
7 | */
8 | export default function distanceTo(pointA, pointB) {
9 | const xDiff = (pointB.x - pointA.x) ** 2;
10 | const yDiff = (pointB.y - pointA.y) ** 2;
11 |
12 | return Math.sqrt(xDiff + yDiff);
13 | }
14 |
--------------------------------------------------------------------------------
/.esdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": "./src",
3 | "destination": "./docs",
4 | "includes": ["\\.(jsx?)$"],
5 | "plugins": [
6 | {
7 | "name": "esdoc-importpath-plugin",
8 | "option": {
9 | "replaces": [
10 | {
11 | "from": "^src/js/",
12 | "to": ""
13 | },
14 | {
15 | "from": "\\.jsx$",
16 | "to": ".js"
17 | }
18 | ]
19 | }
20 | }
21 | ],
22 | "experimentalProposal": {
23 | "decorators": true,
24 | "objectRestSpread": true
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/js/input-range/range-prop-type.js:
--------------------------------------------------------------------------------
1 | import { isNumber } from '../utils';
2 |
3 | /**
4 | * @ignore
5 | * @param {Object} props - React component props
6 | * @return {?Error} Return Error if validation fails
7 | */
8 | export default function rangePropType(props) {
9 | const { maxValue, minValue } = props;
10 |
11 | if (!isNumber(minValue) || !isNumber(maxValue)) {
12 | return new Error('"minValue" and "maxValue" must be a number');
13 | }
14 |
15 | if (minValue >= maxValue) {
16 | return new Error('"minValue" must be smaller than "maxValue"');
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/scss/input-range/_input-range-label.scss:
--------------------------------------------------------------------------------
1 | .input-range__label {
2 | color: $input-range-label-color;
3 | font-family: $input-range-font-family;
4 | font-size: $input-range-label-font-size;
5 | transform: translateZ(0);
6 | white-space: nowrap;
7 | }
8 |
9 | .input-range__label--min,
10 | .input-range__label--max {
11 | bottom: $input-range-label-position-bottom;
12 | position: absolute;
13 | }
14 |
15 | .input-range__label--min {
16 | left: 0;
17 | }
18 |
19 | .input-range__label--max {
20 | right: 0;
21 | }
22 |
23 | .input-range__label--value {
24 | position: absolute;
25 | top: $input-range-label-value-position-top;
26 | }
27 |
--------------------------------------------------------------------------------
/src/scss/input-range/_input-range-track.scss:
--------------------------------------------------------------------------------
1 | .input-range__track {
2 | background: $input-range-track-background;
3 | border-radius: $input-range-track-height;
4 | cursor: pointer;
5 | display: block;
6 | height: $input-range-track-height;
7 | position: relative;
8 | transition: $input-range-track-transition;
9 |
10 | .input-range--disabled & {
11 | background: $input-range-track-disabled-background;
12 | }
13 | }
14 |
15 | .input-range__track--background {
16 | left: 0;
17 | margin-top: -0.5 * $input-range-track-height;
18 | position: absolute;
19 | right: 0;
20 | top: 50%;
21 | }
22 |
23 | .input-range__track--active {
24 | background: $input-range-track-active-background;
25 | }
26 |
--------------------------------------------------------------------------------
/src/js/input-range/default-class-names.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Default CSS class names
3 | * @ignore
4 | * @type {InputRangeClassNames}
5 | */
6 | const DEFAULT_CLASS_NAMES = {
7 | activeTrack: 'input-range__track input-range__track--active',
8 | disabledInputRange: 'input-range input-range--disabled',
9 | inputRange: 'input-range',
10 | labelContainer: 'input-range__label-container',
11 | maxLabel: 'input-range__label input-range__label--max',
12 | minLabel: 'input-range__label input-range__label--min',
13 | slider: 'input-range__slider',
14 | sliderContainer: 'input-range__slider-container',
15 | track: 'input-range__track input-range__track--background',
16 | valueLabel: 'input-range__label input-range__label--value',
17 | };
18 |
19 | export default DEFAULT_CLASS_NAMES;
20 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | rules:
2 | class-methods-use-this:
3 | - 1
4 | consistent-return:
5 | - 0
6 | import/prefer-default-export:
7 | - 0
8 | indent:
9 | - 2
10 | - 2
11 | max-len:
12 | - 0
13 | no-plusplus:
14 | - 2
15 | - allowForLoopAfterthoughts: true
16 | quotes:
17 | - 2
18 | - single
19 | semi:
20 | - 2
21 | - always
22 | jsx-a11y/no-static-element-interactions:
23 | - 0
24 | react/jsx-closing-bracket-location:
25 | - 2
26 | - after-props
27 | react/require-default-props:
28 | - 0
29 | env:
30 | es6: true
31 | browser: true
32 | jasmine: true
33 | extends: airbnb
34 | ecmaFeatures:
35 | jsx: true
36 | experimentalObjectRestSpread: true
37 | modules: true
38 | parser: babel-eslint
39 | parserOptions:
40 | ecmaFeatures:
41 | jsx: true
42 | plugins:
43 | - react
44 |
--------------------------------------------------------------------------------
/.sass-lint.yml:
--------------------------------------------------------------------------------
1 | files:
2 | include: './src/scss/**/*.scss'
3 | options:
4 | formatter: stylish
5 | rules:
6 | class-name-format:
7 | - 2
8 | - convention: hyphenatedbem
9 | empty-line-between-blocks:
10 | - 2
11 | - ignore-single-line-rulesets: false
12 | force-attribute-nesting: 0
13 | force-element-nesting: 0
14 | force-pseudo-nesting: 0
15 | function-name-format: 2
16 | hex-length:
17 | - 2
18 | - style: long
19 | indentation:
20 | - 2
21 | - size: 2
22 | leading-zero:
23 | - 2
24 | - include: true
25 | mixin-name-format: 1
26 | nesting-depth:
27 | - 2
28 | - max-depth: 2
29 | no-ids: 2
30 | no-important: 2
31 | placeholder-name-format:
32 | - 2
33 | - convention: hyphenatedbem
34 | quotes:
35 | - 2
36 | - style: single
37 | variable-name-format:
38 | - 2
39 | - convention: hyphenatedlowercase
40 |
--------------------------------------------------------------------------------
/src/js/input-range/value-prop-type.js:
--------------------------------------------------------------------------------
1 | import { isNumber, isObject } from '../utils';
2 |
3 | /**
4 | * @ignore
5 | * @param {Object} props
6 | * @return {?Error} Return Error if validation fails
7 | */
8 | export default function valuePropType(props, propName) {
9 | const { maxValue, minValue } = props;
10 | const value = props[propName];
11 |
12 | if (!isNumber(value) && (!isObject(value) || !isNumber(value.min) || !isNumber(value.max))) {
13 | return new Error(`"${propName}" must be a number or a range object`);
14 | }
15 |
16 | if (isNumber(value) && (value < minValue || value > maxValue)) {
17 | return new Error(`"${propName}" must be in between "minValue" and "maxValue"`);
18 | }
19 |
20 | if (isObject(value) && (value.min < minValue || value.min > maxValue || value.max < minValue || value.max > maxValue)) {
21 | return new Error(`"${propName}" must be in between "minValue" and "maxValue"`);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/js/input-range/label.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | /**
5 | * @ignore
6 | * @param {Object} props
7 | * @param {InputRangeClassNames} props.classNames
8 | * @param {Function} props.formatLabel
9 | * @param {string} props.type
10 | */
11 | export default function Label(props) {
12 | const labelValue = props.formatLabel ? props.formatLabel(props.children, props.type) : props.children;
13 |
14 | return (
15 |
16 |
17 | {labelValue}
18 |
19 |
20 | );
21 | }
22 |
23 | /**
24 | * @type {Object}
25 | * @property {Function} children
26 | * @property {Function} classNames
27 | * @property {Function} formatLabel
28 | * @property {Function} type
29 | */
30 | Label.propTypes = {
31 | children: PropTypes.node.isRequired,
32 | classNames: PropTypes.objectOf(PropTypes.string).isRequired,
33 | formatLabel: PropTypes.func,
34 | type: PropTypes.string.isRequired,
35 | };
36 |
--------------------------------------------------------------------------------
/react-input-range.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export interface Range {
4 | max: number;
5 | min: number;
6 | }
7 |
8 | export interface InputRangeClassNames {
9 | activeTrack: string;
10 | disabledInputRange: string;
11 | inputRange: string;
12 | labelContainer: string;
13 | maxLabel: string;
14 | minLabel: string;
15 | slider: string;
16 | sliderContainer: string;
17 | track: string;
18 | valueLabel: string;
19 | }
20 |
21 | export interface InputRangeProps {
22 | allowSameValues?: boolean;
23 | ariaLabelledby?: string;
24 | ariaControls?: string;
25 | classNames?: InputRangeClassNames;
26 | disabled?: boolean;
27 | draggableTrack?: boolean;
28 | formatLabel?: (value: number, type: string) => string;
29 | maxValue?: number;
30 | minValue?: number;
31 | name?: string;
32 | onChange: (value: Range | number) => void;
33 | onChangeStart?: (value: Range | number) => void;
34 | onChangeComplete?: (value: Range | number) => void;
35 | step?: number;
36 | value: Range | number;
37 | }
38 |
39 | export default class InputRange extends React.Component {
40 | }
41 |
--------------------------------------------------------------------------------
/src/scss/input-range/_input-range-slider.scss:
--------------------------------------------------------------------------------
1 | .input-range__slider {
2 | appearance: none;
3 | background: $input-range-slider-background;
4 | border: $input-range-slider-border;
5 | border-radius: 100%;
6 | cursor: pointer;
7 | display: block;
8 | height: $input-range-slider-height;
9 | margin-left: $input-range-slider-width / -2;
10 | margin-top: $input-range-slider-height / -2 + $input-range-track-height / -2;
11 | outline: none;
12 | position: absolute;
13 | top: 50%;
14 | transition: $input-range-slider-transition;
15 | width: $input-range-slider-width;
16 |
17 | &:active {
18 | transform: $input-range-slider-active-transform;
19 | }
20 |
21 | &:focus {
22 | box-shadow: 0 0 0 $input-range-slider-focus-box-shadow-radius $input-range-slider-focus-box-shadow-color;
23 | }
24 |
25 | .input-range--disabled & {
26 | background: $input-range-slider-disabled-background;
27 | border: $input-range-slider-disabled-border;
28 | box-shadow: none;
29 | transform: none;
30 | }
31 | }
32 |
33 | .input-range__slider-container {
34 | transition: $input-range-slider-container-transition;
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 David Chin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import InputRange from './input-range/input-range';
2 |
3 | /**
4 | * @ignore
5 | * @typedef {Object} ClientRect
6 | * @property {number} height
7 | * @property {number} left
8 | * @property {number} top
9 | * @property {number} width
10 | */
11 |
12 | /**
13 | * @typedef {Object} InputRangeClassNames
14 | * @property {string} activeTrack
15 | * @property {string} disabledInputRange
16 | * @property {string} inputRange
17 | * @property {string} labelContainer
18 | * @property {string} maxLabel
19 | * @property {string} minLabel
20 | * @property {string} slider
21 | * @property {string} sliderContainer
22 | * @property {string} track
23 | * @property {string} valueLabel
24 | */
25 |
26 | /**
27 | * @typedef {Function} LabelFormatter
28 | * @param {number} value
29 | * @param {string} type
30 | * @return {string}
31 | */
32 |
33 | /**
34 | * @ignore
35 | * @typedef {Object} Point
36 | * @property {number} x
37 | * @property {number} y
38 | */
39 |
40 | /**
41 | * @typedef {Object} Range
42 | * @property {number} min - Min value
43 | * @property {number} max - Max value
44 | */
45 |
46 | export default InputRange;
47 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | function configureKarma(config) {
4 | config.set({
5 | basePath: __dirname,
6 | browsers: ['PhantomJS'],
7 | coverageReporter: {
8 | reporters: [
9 | { type: 'html' },
10 | { type: 'text' },
11 | ],
12 | },
13 | frameworks: ['jasmine'],
14 | files: ['test/index.js'],
15 | preprocessors: {
16 | 'src/index.js': ['coverage'],
17 | 'test/index.js': ['webpack', 'sourcemap'],
18 | },
19 | reporters: ['mocha', 'coverage'],
20 | webpack: {
21 | devtool: 'inline-source-map',
22 | externals: {
23 | 'cheerio': 'window',
24 | 'react/addons': true,
25 | 'react/lib/ExecutionEnvironment': true,
26 | 'react/lib/ReactContext': true,
27 | },
28 | module: {
29 | rules: [
30 | {
31 | include: [
32 | path.resolve(__dirname, 'src'),
33 | path.resolve(__dirname, 'test'),
34 | ],
35 | loader: 'babel-loader',
36 | query: {
37 | plugins: ['istanbul'],
38 | },
39 | test: /\.jsx?$/,
40 | },
41 | {
42 | include: path.resolve(__dirname, 'src'),
43 | loaders: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
44 | test: /\.scss$/,
45 | },
46 | ],
47 | },
48 | resolve: {
49 | modules: ['node_modules'],
50 | extensions: ['.js', '.jsx'],
51 | },
52 | },
53 | });
54 | }
55 |
56 | module.exports = configureKarma;
57 |
--------------------------------------------------------------------------------
/webpack-example.config.babel.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
3 | import SasslintPlugin from 'sasslint-webpack-plugin';
4 | import path from 'path';
5 |
6 | const webpackExampleConfig = {
7 | context: __dirname,
8 | devtool: 'source-map',
9 | target: 'web',
10 | entry: {
11 | example: './example/js/index.jsx',
12 | },
13 | output: {
14 | filename: '[name].js',
15 | },
16 | module: {
17 | rules: [
18 | {
19 | include: [
20 | path.resolve(__dirname, 'src'),
21 | path.resolve(__dirname, 'example'),
22 | ],
23 | loader: 'babel-loader',
24 | test: /\.jsx?$/,
25 | }, {
26 | include: [
27 | path.resolve(__dirname, 'src'),
28 | path.resolve(__dirname, 'example'),
29 | ],
30 | loader: ExtractTextPlugin.extract({
31 | fallback: 'style-loader',
32 | use: ['css-loader', 'postcss-loader', 'sass-loader'],
33 | }),
34 | test: /\.scss$/,
35 | }, {
36 | include: [
37 | path.resolve(__dirname, 'src'),
38 | path.resolve(__dirname, 'example'),
39 | ],
40 | loader: 'eslint-loader',
41 | test: /\.jsx?$/,
42 | enforce: 'pre',
43 | },
44 | ],
45 | },
46 | plugins: [
47 | new ExtractTextPlugin('[name].css'),
48 | new SasslintPlugin({
49 | glob: './src/scss/**/*.scss',
50 | ignorePlugins: ['extract-text-webpack-plugin'],
51 | }),
52 | ],
53 | resolve: {
54 | modules: ['node_modules'],
55 | extensions: ['.js', '.jsx'],
56 | },
57 | };
58 |
59 | export default webpackExampleConfig;
60 |
--------------------------------------------------------------------------------
/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
2 | import UglifyJsPlugin from 'uglifyjs-webpack-plugin';
3 | import path from 'path';
4 |
5 | const webpackConfig = {
6 | context: __dirname,
7 | devtool: 'source-map',
8 | target: 'web',
9 | entry: {
10 | 'react-input-range.css': './src/scss/index.scss',
11 | 'react-input-range.js': './src/js/index.js',
12 | 'react-input-range.min.js': './src/js/index.js',
13 | },
14 | output: {
15 | filename: '[name]',
16 | path: path.resolve(__dirname, 'lib/bundle'),
17 | library: 'InputRange',
18 | libraryTarget: 'umd',
19 | },
20 | module: {
21 | rules: [
22 | {
23 | include: path.resolve(__dirname, 'src'),
24 | loader: 'babel-loader',
25 | test: /\.jsx?$/,
26 | },
27 | {
28 | include: path.resolve(__dirname, 'src'),
29 | loader: ExtractTextPlugin.extract({
30 | use: ['css-loader', 'postcss-loader', 'sass-loader'],
31 | }),
32 | test: /\.scss$/,
33 | },
34 | ],
35 | },
36 | plugins: [
37 | new ExtractTextPlugin('[name]'),
38 | new UglifyJsPlugin({
39 | sourceMap: true,
40 | test: /\.min\.js$/,
41 | }),
42 | ],
43 | resolve: {
44 | modules: ['node_modules'],
45 | extensions: ['.js', '.jsx'],
46 | },
47 | externals: {
48 | react: {
49 | amd: 'react',
50 | commonjs: 'react',
51 | commonjs2: 'react',
52 | root: 'React',
53 | },
54 | 'react-dom': {
55 | amd: 'react-dom',
56 | commonjs: 'react-dom',
57 | commonjs2: 'react-dom',
58 | root: 'ReactDOM',
59 | },
60 | },
61 | };
62 |
63 | export default webpackConfig;
64 |
--------------------------------------------------------------------------------
/src/scss/input-range/_input-range-variables.scss:
--------------------------------------------------------------------------------
1 | $input-range-font-family: 'Helvetica Neue', san-serif !default;
2 | $input-range-primary-color: #3f51b5 !default;
3 | $input-range-neutral-color: #aaaaaa !default;
4 | $input-range-neutral-light-color: #eeeeee !default;
5 | $input-range-disabled-color: #cccccc !default;
6 |
7 | // input-range-slider
8 | $input-range-slider-background: $input-range-primary-color !default;
9 | $input-range-slider-border: 1px solid $input-range-primary-color !default;
10 | $input-range-slider-focus-box-shadow-radius: 5px !default;
11 | $input-range-slider-focus-box-shadow-color: transparentize($input-range-slider-background, 0.8) !default;
12 | $input-range-slider-height: 1rem !default;
13 | $input-range-slider-width: 1rem !default;
14 | $input-range-slider-transition: transform 0.3s ease-out, box-shadow 0.3s ease-out !default;
15 | $input-range-slider-container-transition: left 0.3s ease-out !default;
16 | $input-range-slider-active-transform: scale(1.3) !default;
17 | $input-range-slider-disabled-background: $input-range-disabled-color !default;
18 | $input-range-slider-disabled-border: 1px solid $input-range-disabled-color !default;
19 |
20 | // input-range-label
21 | $input-range-label-color: $input-range-neutral-color !default;
22 | $input-range-label-font-size: 0.8rem !default;
23 | $input-range-label-position-bottom: -1.4rem !default;
24 | $input-range-label-value-position-top: -1.8rem !default;
25 |
26 | // input-range-track
27 | $input-range-track-background: $input-range-neutral-light-color !default;
28 | $input-range-track-height: 0.3rem !default;
29 | $input-range-track-transition: left 0.3s ease-out, width 0.3s ease-out !default;
30 | $input-range-track-active-background: $input-range-primary-color !default;
31 | $input-range-track-disabled-background: $input-range-neutral-light-color !default;
32 |
--------------------------------------------------------------------------------
/example/js/example-app.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this, no-console */
2 |
3 | import React from 'react';
4 | import InputRange from '../../src/js';
5 |
6 | export default class ExampleApp extends React.Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = {
11 | value: 5,
12 | value2: 10,
13 | value3: 10,
14 | value4: {
15 | min: 5,
16 | max: 10,
17 | },
18 | value5: {
19 | min: 3,
20 | max: 7,
21 | },
22 | value6: {
23 | min: 3,
24 | max: 7,
25 | },
26 | };
27 | }
28 |
29 | render() {
30 | return (
31 |
81 | );
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-input-range",
3 | "version": "1.3.0",
4 | "description": "React component for inputting numeric values within a range",
5 | "keywords": [
6 | "react",
7 | "react-component",
8 | "range-slider",
9 | "input-range",
10 | "range",
11 | "slider",
12 | "form",
13 | "input"
14 | ],
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/davidchin/react-input-range.git"
18 | },
19 | "main": "lib/js/index.js",
20 | "types": "react-input-range.d.ts",
21 | "author": "David Chin",
22 | "license": "MIT",
23 | "devDependencies": {
24 | "@types/react": "^15.0.1",
25 | "autoprefixer": "^6.6.1",
26 | "babel-cli": "^6.22.2",
27 | "babel-core": "^6.22.1",
28 | "babel-eslint": "^7.1.1",
29 | "babel-loader": "^6.2.10",
30 | "babel-plugin-add-module-exports": "^0.2.1",
31 | "babel-plugin-istanbul": "^3.1.2",
32 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
33 | "babel-plugin-transform-object-rest-spread": "^6.22.0",
34 | "babel-plugin-transform-react-jsx": "^6.22.0",
35 | "babel-preset-es2015": "^6.22.0",
36 | "babel-preset-es2016": "^6.22.0",
37 | "babel-preset-react": "^6.22.0",
38 | "conventional-changelog-cli": "~1.2.0",
39 | "css-loader": "^0.27.3",
40 | "dom4": "^1.8.3",
41 | "enzyme": "^2.7.1",
42 | "esdoc": "^0.5.2",
43 | "esdoc-importpath-plugin": "^0.1.0",
44 | "eslint": "^3.14.0",
45 | "eslint-config-airbnb": "^14.0.0",
46 | "eslint-loader": "^1.6.1",
47 | "eslint-plugin-import": "^2.2.0",
48 | "eslint-plugin-jsx-a11y": "^4.0.0",
49 | "eslint-plugin-react": "^6.9.0",
50 | "extract-text-webpack-plugin": "^2.1.0",
51 | "jasmine-core": "^2.5.2",
52 | "karma": "^1.4.0",
53 | "karma-babel-preprocessor": "^6.0.1",
54 | "karma-coverage": "^1.1.1",
55 | "karma-jasmine": "^1.1.0",
56 | "karma-mocha-reporter": "^2.2.2",
57 | "karma-phantomjs-launcher": "^1.0.2",
58 | "karma-sourcemap-loader": "^0.3.7",
59 | "karma-webpack": "^2.0.1",
60 | "node-sass": "^4.3.0",
61 | "postcss-loader": "^1.2.2",
62 | "react": "^15.5.4",
63 | "react-addons-test-utils": "^15.4.2",
64 | "react-dom": "^15.5.4",
65 | "sass-lint": "^1.10.2",
66 | "sass-loader": "^6.0.3",
67 | "sasslint-webpack-plugin": "^1.0.4",
68 | "style-loader": "^0.14.0",
69 | "uglify-js": "^2.8.16",
70 | "uglifyjs-webpack-plugin": "^0.3.1",
71 | "webpack": "^2.2.1",
72 | "webpack-dev-server": "^2.4.2"
73 | },
74 | "peerDependencies": {
75 | "react": "^15.0.0 || ^16.0.0",
76 | "react-dom": "^15.0.0 || ^16.0.0"
77 | },
78 | "scripts": {
79 | "prebuild": "npm test && rm -rf lib",
80 | "build": "npm run build:lib && npm run build:scss && npm run build:bundle",
81 | "build:bundle": "NODE_ENV=production webpack",
82 | "build:lib": "NODE_ENV=production babel src/js --out-dir lib/js --source-maps",
83 | "build:scss": "NODE_ENV=production node-sass src/scss --recursive --source-map true --output lib/css",
84 | "changelog": "conventional-changelog --preset angular --infile CHANGELOG.md --same-file",
85 | "dev": "webpack-dev-server --inline --config webpack-example.config.babel.js --content-base example",
86 | "docs": "esdoc",
87 | "lint": "npm run lint:js && npm run lint:scss",
88 | "lint:js": "eslint src/js/ --ext .js --ext .jsx",
89 | "lint:scss": "sass-lint --verbose --no-exit",
90 | "prepublish": "npm run build",
91 | "pretest": "npm run lint",
92 | "test": "karma start --single-run",
93 | "preversion": "npm test",
94 | "version": "npm run changelog && git add CHANGELOG.md"
95 | },
96 | "dependencies": {
97 | "autobind-decorator": "^1.3.4",
98 | "prop-types": "^15.5.8"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/js/input-range/value-transformer.js:
--------------------------------------------------------------------------------
1 | import { clamp } from '../utils';
2 |
3 | /**
4 | * Convert a point into a percentage value
5 | * @ignore
6 | * @param {Point} position
7 | * @param {ClientRect} clientRect
8 | * @return {number} Percentage value
9 | */
10 | export function getPercentageFromPosition(position, clientRect) {
11 | const length = clientRect.width;
12 | const sizePerc = position.x / length;
13 |
14 | return sizePerc || 0;
15 | }
16 |
17 | /**
18 | * Convert a point into a model value
19 | * @ignore
20 | * @param {Point} position
21 | * @param {number} minValue
22 | * @param {number} maxValue
23 | * @param {ClientRect} clientRect
24 | * @return {number}
25 | */
26 | export function getValueFromPosition(position, minValue, maxValue, clientRect) {
27 | const sizePerc = getPercentageFromPosition(position, clientRect);
28 | const valueDiff = maxValue - minValue;
29 |
30 | return minValue + (valueDiff * sizePerc);
31 | }
32 |
33 | /**
34 | * Convert props into a range value
35 | * @ignore
36 | * @param {Object} props
37 | * @param {boolean} isMultiValue
38 | * @return {Range}
39 | */
40 | export function getValueFromProps(props, isMultiValue) {
41 | if (isMultiValue) {
42 | return { ...props.value };
43 | }
44 |
45 | return {
46 | min: props.minValue,
47 | max: props.value,
48 | };
49 | }
50 |
51 | /**
52 | * Convert a model value into a percentage value
53 | * @ignore
54 | * @param {number} value
55 | * @param {number} minValue
56 | * @param {number} maxValue
57 | * @return {number}
58 | */
59 | export function getPercentageFromValue(value, minValue, maxValue) {
60 | const validValue = clamp(value, minValue, maxValue);
61 | const valueDiff = maxValue - minValue;
62 | const valuePerc = (validValue - minValue) / valueDiff;
63 |
64 | return valuePerc || 0;
65 | }
66 |
67 | /**
68 | * Convert model values into percentage values
69 | * @ignore
70 | * @param {Range} values
71 | * @param {number} minValue
72 | * @param {number} maxValue
73 | * @return {Range}
74 | */
75 | export function getPercentagesFromValues(values, minValue, maxValue) {
76 | return {
77 | min: getPercentageFromValue(values.min, minValue, maxValue),
78 | max: getPercentageFromValue(values.max, minValue, maxValue),
79 | };
80 | }
81 |
82 | /**
83 | * Convert a value into a point
84 | * @ignore
85 | * @param {number} value
86 | * @param {number} minValue
87 | * @param {number} maxValue
88 | * @param {ClientRect} clientRect
89 | * @return {Point} Position
90 | */
91 | export function getPositionFromValue(value, minValue, maxValue, clientRect) {
92 | const length = clientRect.width;
93 | const valuePerc = getPercentageFromValue(value, minValue, maxValue);
94 | const positionValue = valuePerc * length;
95 |
96 | return {
97 | x: positionValue,
98 | y: 0,
99 | };
100 | }
101 |
102 | /**
103 | * Convert a range of values into points
104 | * @ignore
105 | * @param {Range} values
106 | * @param {number} minValue
107 | * @param {number} maxValue
108 | * @param {ClientRect} clientRect
109 | * @return {Range}
110 | */
111 | export function getPositionsFromValues(values, minValue, maxValue, clientRect) {
112 | return {
113 | min: getPositionFromValue(values.min, minValue, maxValue, clientRect),
114 | max: getPositionFromValue(values.max, minValue, maxValue, clientRect),
115 | };
116 | }
117 |
118 | /**
119 | * Convert an event into a point
120 | * @ignore
121 | * @param {Event} event
122 | * @param {ClientRect} clientRect
123 | * @return {Point}
124 | */
125 | export function getPositionFromEvent(event, clientRect) {
126 | const length = clientRect.width;
127 | const { clientX } = event.touches ? event.touches[0] : event;
128 |
129 | return {
130 | x: clamp(clientX - clientRect.left, 0, length),
131 | y: 0,
132 | };
133 | }
134 |
135 | /**
136 | * Convert a value into a step value
137 | * @ignore
138 | * @param {number} value
139 | * @param {number} valuePerStep
140 | * @return {number}
141 | */
142 | export function getStepValueFromValue(value, valuePerStep) {
143 | return Math.round(value / valuePerStep) * valuePerStep;
144 | }
145 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # [1.3.0](https://github.com/davidchin/react-input-range/compare/v1.2.2...v1.3.0) (2018-01-06)
3 |
4 |
5 | ### Features
6 |
7 | * Allow min and max to have same value ([#114](https://github.com/davidchin/react-input-range/issues/114)) ([8b36de7](https://github.com/davidchin/react-input-range/commit/8b36de7))
8 |
9 |
10 |
11 |
12 | ## [1.2.1](https://github.com/davidchin/react-input-range/compare/v1.2.0...v1.2.1) (2017-07-14)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * Always set slider dragging back to false ([#98](https://github.com/davidchin/react-input-range/issues/98)) ([d22fa26](https://github.com/davidchin/react-input-range/commit/d22fa26))
18 |
19 |
20 |
21 |
22 | # [1.2.0](https://github.com/davidchin/react-input-range/compare/v1.1.5...v1.2.0) (2017-07-09)
23 |
24 |
25 | ### Features
26 |
27 | * Add track dragging functionality ([#91](https://github.com/davidchin/react-input-range/issues/91)) ([4a8ca26](https://github.com/davidchin/react-input-range/commit/4a8ca26))
28 |
29 |
30 |
31 |
32 | ## [1.1.5](https://github.com/davidchin/react-input-range/compare/v1.1.4...v1.1.5) (2017-07-09)
33 |
34 |
35 | ### Bug Fixes
36 |
37 | * Fix Typescript definition file ([4935745](https://github.com/davidchin/react-input-range/commit/4935745))
38 |
39 |
40 |
41 |
42 | ## [1.1.4](https://github.com/davidchin/react-input-range/compare/v1.1.3...v1.1.4) (2017-05-20)
43 |
44 |
45 | ### Bug Fixes
46 |
47 | * Remove event listener handleTouchEnd when Slider unmount ([#89](https://github.com/davidchin/react-input-range/issues/89)) ([660fa5c](https://github.com/davidchin/react-input-range/commit/660fa5c))
48 |
49 |
50 |
51 |
52 | ## [1.1.3](https://github.com/davidchin/react-input-range/compare/v1.1.2...v1.1.3) (2017-05-03)
53 |
54 |
55 | ### Changes
56 |
57 | * Include prop-types package to support React 15.5 ([1939f6c](https://github.com/davidchin/react-input-range/commit/1939f6c))
58 |
59 |
60 |
61 |
62 | ## [1.1.2](https://github.com/davidchin/react-input-range/compare/v1.1.1...v1.1.2) (2017-03-30)
63 |
64 |
65 | ### Bug Fixes
66 |
67 | * **build:** Fix requiring React as an external dependency ([903eadb](https://github.com/davidchin/react-input-range/commit/903eadb))
68 |
69 |
70 |
71 |
72 | ## [1.1.1](https://github.com/davidchin/react-input-range/compare/v1.1.0...v1.1.1) (2017-03-28)
73 |
74 |
75 | ### Bug Fixes
76 |
77 | * Only uglify and minify min.js files ([c73a491](https://github.com/davidchin/react-input-range/commit/c73a491))
78 |
79 |
80 |
81 | # [1.1.0](https://github.com/davidchin/react-input-range/compare/v1.0.2...v1.1.0) (2017-03-26)
82 |
83 |
84 | ### Features
85 |
86 | * Add a callback prop responsible for notifying the start of any interaction ([#66](https://github.com/davidchin/react-input-range/issues/66)) ([4ca6ea2](https://github.com/davidchin/react-input-range/commit/4ca6ea2))
87 |
88 |
89 |
90 |
91 | ## [1.0.2](https://github.com/davidchin/react-input-range/compare/v1.0.1...v1.0.2) (2017-02-01)
92 |
93 |
94 | ### Bug Fixes
95 |
96 | * Fix the display glitch affecting moving sliders in Safari ([69c9511](https://github.com/davidchin/react-input-range/commit/69c9511))
97 |
98 |
99 |
100 |
101 | ## [1.0.1](https://github.com/davidchin/react-input-range/compare/v1.0.0...v1.0.1) (2017-01-31)
102 |
103 |
104 | ### Bug Fixes
105 |
106 | * Fix CommonJS and global default exports ([ff39e9d](https://github.com/davidchin/react-input-range/commit/ff39e9d))
107 |
108 |
109 |
110 |
111 | # [1.0.0](https://github.com/davidchin/react-input-range/compare/v0.10.0...v1.0.0) (2017-01-30)
112 |
113 |
114 | ### Bug Fixes
115 |
116 | * Fix a display glitch affecting labels on mobile devices ([d809046](https://github.com/davidchin/react-input-range/commit/d809046))
117 | * Render hidden inputs with values ([57c44f8](https://github.com/davidchin/react-input-range/commit/57c44f8))
118 |
119 |
120 | ### Breaking
121 |
122 | * Bump React version to `^15.0.0` ([d741a58](https://github.com/davidchin/react-input-range/commit/d741a58))
123 | * Remove Bower support ([7a28c64](https://github.com/davidchin/react-input-range/commit/7a28c64))
124 | * Change `onChange` and `onChangeComplete` callback signature. They no longer pass the component as a parameter ([c824064](https://github.com/davidchin/react-input-range/commit/c824064))
125 | * Remove `labelPrefix` and `labelSuffix` props. Use `formatLabel` prop to format labels instead. Remove `defaultValue` prop. Use `value` prop to set an initial value instead ([bb40806](https://github.com/davidchin/react-input-range/commit/bb40806))
126 | * Change the naming convention of CSS classes to BEM ([9e22025](https://github.com/davidchin/react-input-range/commit/9e22025))
127 | * Change `classNames` prop to accept a different set of keys ([92277fe](https://github.com/davidchin/react-input-range/commit/92277fe))
128 |
--------------------------------------------------------------------------------
/src/js/input-range/track.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import autobind from 'autobind-decorator';
4 |
5 | /**
6 | * @ignore
7 | */
8 | export default class Track extends React.Component {
9 | /**
10 | * @override
11 | * @return {Object}
12 | * @property {Function} children
13 | * @property {Function} classNames
14 | * @property {Boolean} draggableTrack
15 | * @property {Function} onTrackDrag
16 | * @property {Function} onTrackMouseDown
17 | * @property {Function} percentages
18 | */
19 | static get propTypes() {
20 | return {
21 | children: PropTypes.node.isRequired,
22 | classNames: PropTypes.objectOf(PropTypes.string).isRequired,
23 | draggableTrack: PropTypes.bool,
24 | onTrackDrag: PropTypes.func,
25 | onTrackMouseDown: PropTypes.func.isRequired,
26 | percentages: PropTypes.objectOf(PropTypes.number).isRequired,
27 | };
28 | }
29 |
30 | /**
31 | * @param {Object} props
32 | * @param {InputRangeClassNames} props.classNames
33 | * @param {Boolean} props.draggableTrack
34 | * @param {Function} props.onTrackDrag
35 | * @param {Function} props.onTrackMouseDown
36 | * @param {number} props.percentages
37 | */
38 | constructor(props) {
39 | super(props);
40 |
41 | /**
42 | * @private
43 | * @type {?Component}
44 | */
45 | this.node = null;
46 | this.trackDragEvent = null;
47 | }
48 |
49 | /**
50 | * @private
51 | * @return {ClientRect}
52 | */
53 | getClientRect() {
54 | return this.node.getBoundingClientRect();
55 | }
56 |
57 | /**
58 | * @private
59 | * @return {Object} CSS styles
60 | */
61 | getActiveTrackStyle() {
62 | const width = `${(this.props.percentages.max - this.props.percentages.min) * 100}%`;
63 | const left = `${this.props.percentages.min * 100}%`;
64 |
65 | return { left, width };
66 | }
67 |
68 | /**
69 | * Listen to mousemove event
70 | * @private
71 | * @return {void}
72 | */
73 | addDocumentMouseMoveListener() {
74 | this.removeDocumentMouseMoveListener();
75 | this.node.ownerDocument.addEventListener('mousemove', this.handleMouseMove);
76 | }
77 |
78 | /**
79 | * Listen to mouseup event
80 | * @private
81 | * @return {void}
82 | */
83 | addDocumentMouseUpListener() {
84 | this.removeDocumentMouseUpListener();
85 | this.node.ownerDocument.addEventListener('mouseup', this.handleMouseUp);
86 | }
87 |
88 | /**
89 | * @private
90 | * @return {void}
91 | */
92 | removeDocumentMouseMoveListener() {
93 | this.node.ownerDocument.removeEventListener('mousemove', this.handleMouseMove);
94 | }
95 |
96 | /**
97 | * @private
98 | * @return {void}
99 | */
100 | removeDocumentMouseUpListener() {
101 | this.node.ownerDocument.removeEventListener('mouseup', this.handleMouseUp);
102 | }
103 |
104 | /**
105 | * @private
106 | * @param {SyntheticEvent} event
107 | * @return {void}
108 | */
109 | @autobind
110 | handleMouseMove(event) {
111 | if (!this.props.draggableTrack) {
112 | return;
113 | }
114 |
115 | if (this.trackDragEvent !== null) {
116 | this.props.onTrackDrag(event, this.trackDragEvent);
117 | }
118 |
119 | this.trackDragEvent = event;
120 | }
121 |
122 | /**
123 | * @private
124 | * @return {void}
125 | */
126 | @autobind
127 | handleMouseUp() {
128 | if (!this.props.draggableTrack) {
129 | return;
130 | }
131 |
132 | this.removeDocumentMouseMoveListener();
133 | this.removeDocumentMouseUpListener();
134 | this.trackDragEvent = null;
135 | }
136 |
137 | /**
138 | * @private
139 | * @param {SyntheticEvent} event - User event
140 | */
141 | @autobind
142 | handleMouseDown(event) {
143 | const clientX = event.touches ? event.touches[0].clientX : event.clientX;
144 | const trackClientRect = this.getClientRect();
145 | const position = {
146 | x: clientX - trackClientRect.left,
147 | y: 0,
148 | };
149 |
150 | this.props.onTrackMouseDown(event, position);
151 |
152 | if (this.props.draggableTrack) {
153 | this.addDocumentMouseMoveListener();
154 | this.addDocumentMouseUpListener();
155 | }
156 | }
157 |
158 | /**
159 | * @private
160 | * @param {SyntheticEvent} event - User event
161 | */
162 | @autobind
163 | handleTouchStart(event) {
164 | event.preventDefault();
165 |
166 | this.handleMouseDown(event);
167 | }
168 |
169 | /**
170 | * @override
171 | * @return {JSX.Element}
172 | */
173 | render() {
174 | const activeTrackStyle = this.getActiveTrackStyle();
175 |
176 | return (
177 | { this.node = node; }}>
182 |
185 | {this.props.children}
186 |
187 | );
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-input-range
2 |
3 | `InputRange` is a React component allowing users to input numeric values within a specific range. It can accept a single value, or a range of values (min/max). By default, basic styles are applied, but can be overridden depending on your design requirements.
4 |
5 | [](https://travis-ci.org/davidchin/react-input-range)
6 |
7 | ## Demo
8 | A CodePen demo is available [here](http://codepen.io/davidchin/full/GpNvqw/).
9 |
10 | ## Installation
11 |
12 | 1. Install `react-input-range` using npm (or [yarn]). `npm install react-input-range`
13 | 2. Import `react-input-range` to use `InputRange` component.
14 | 3. Optionally, import `react-input-range/lib/css/index.css` if you want to apply the default styling.
15 |
16 | ## Usage
17 |
18 | To accept min/max value:
19 | ```jsx
20 | import React from 'react';
21 | import ReactDOM from 'react-dom';
22 | import InputRange from 'react-input-range';
23 |
24 | class App extends React.Component {
25 | constructor(props) {
26 | super(props);
27 |
28 | this.state = {
29 | value: { min: 2, max: 10 },
30 | };
31 | }
32 |
33 | render() {
34 | return (
35 | this.setState({ value })} />
40 | );
41 | }
42 | }
43 |
44 | ReactDOM.render(
45 | ,
46 | document.getElementById('app')
47 | );
48 | ```
49 |
50 | To accept a single value:
51 | ```jsx
52 | class App extends React.Component {
53 | constructor(props) {
54 | super(props);
55 |
56 | this.state = { value: 10 };
57 | }
58 |
59 | render() {
60 | return (
61 | this.setState({ value })} />
66 | );
67 | }
68 | }
69 | ```
70 |
71 | To format labels:
72 | ```jsx
73 | `${value}cm`}
75 | value={this.state.value}
76 | onChange={value => this.setState({ value })} />
77 | ```
78 |
79 | To specify the amount of increment/decrement
80 | ```jsx
81 | this.setState({ value })} />
85 | ```
86 |
87 | ## API
88 |
89 | ### InputRange#props
90 |
91 | #### allowSameValues: boolean
92 |
93 | Set to `true` to allow `minValue` and `maxValue` to be the same.
94 |
95 | #### ariaLabelledby: string
96 |
97 | Set `aria-labelledby` attribute to your component.
98 |
99 | #### ariaControls: string
100 |
101 | Set `aria-controls` attribute to your component.
102 |
103 | #### classNames: InputRangeClassNames
104 |
105 | Override the default CSS classes applied to your component and its sub-components.
106 |
107 | #### disabled: boolean
108 |
109 | If this property is set to true, your component is disabled. This means you'll not able to interact with it.
110 |
111 | #### draggableTrack: boolean
112 |
113 | If this property is set to true, you can drag the entire track.
114 |
115 | #### formatLabel: (value: number, type: string): string
116 |
117 | By default, value labels are displayed as plain numbers. If you want to change the display, you can do so by passing in a function. The function can return something different, i.e.: append a unit, reduce the precision of a number.
118 |
119 | #### maxValue: number
120 |
121 | Set a maximum value for your component. You cannot drag your slider beyond this value.
122 |
123 | #### minValue: number
124 |
125 | Set a minimum value for your component. You cannot drag your slider under this value.
126 |
127 | #### name: string
128 |
129 | Set a name for your form component.
130 |
131 | #### onChange: (value: number | Range): void
132 |
133 | Whenever your user interacts with your component (i.e.: dragging a slider), this function gets called. Inside the function, you should assign the new value to your component.
134 |
135 | #### onChangeStart: (value: number | Range): void
136 |
137 | Whenever your user starts interacting with your component (i.e.: `onMouseDown`, or `onTouchStart`), this function gets called.
138 |
139 | #### onChangeComplete: (value: number | Range): void
140 |
141 | Every mouse / touch event can trigger multiple updates, therefore causing `onChange` callback to fire multiple times. On the other hand, `onChangeComplete` callback only gets called when the user stops dragging.
142 |
143 | #### step: number
144 |
145 | The default increment/decrement of your component is 1. You can change that by setting a different number to this property.
146 |
147 | #### value: number | Range
148 |
149 | Set the current value for your component. If only a single number is provided, only a single slider will get rendered. If a range object (min/max) is provided, two sliders will get rendered
150 |
151 | ### Types
152 |
153 | #### InputRangeClassNames
154 | * activeTrack: string
155 | * disabledInputRange: string
156 | * inputRange: string
157 | * labelContainer: string
158 | * maxLabel: string
159 | * minLabel: string
160 | * slider: string
161 | * sliderContainer: string
162 | * track: string
163 | * valueLabel: string
164 |
165 | #### Range
166 | * max: number
167 | * min: number
168 |
169 | ## Development
170 |
171 | If you want to work on this project locally, you need to grab all of its dependencies, for which
172 | we recommend using [yarn]. You can find the instructions to setup yarn [here](https://yarnpkg.com/docs/install).
173 | ```
174 | yarn install
175 | ```
176 |
177 | After that, you should be able run to preview
178 | ```
179 | yarn dev
180 | ```
181 |
182 | To test
183 | ```
184 | yarn test
185 | ```
186 |
187 | Contributions are welcome. :)
188 |
189 | [yarn]: https://yarnpkg.com/
190 |
--------------------------------------------------------------------------------
/src/js/input-range/slider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import autobind from 'autobind-decorator';
4 | import Label from './label';
5 |
6 | /**
7 | * @ignore
8 | */
9 | export default class Slider extends React.Component {
10 | /**
11 | * Accepted propTypes of Slider
12 | * @override
13 | * @return {Object}
14 | * @property {Function} ariaLabelledby
15 | * @property {Function} ariaControls
16 | * @property {Function} className
17 | * @property {Function} formatLabel
18 | * @property {Function} maxValue
19 | * @property {Function} minValue
20 | * @property {Function} onSliderDrag
21 | * @property {Function} onSliderKeyDown
22 | * @property {Function} percentage
23 | * @property {Function} type
24 | * @property {Function} value
25 | */
26 | static get propTypes() {
27 | return {
28 | ariaLabelledby: PropTypes.string,
29 | ariaControls: PropTypes.string,
30 | classNames: PropTypes.objectOf(PropTypes.string).isRequired,
31 | formatLabel: PropTypes.func,
32 | maxValue: PropTypes.number,
33 | minValue: PropTypes.number,
34 | onSliderDrag: PropTypes.func.isRequired,
35 | onSliderKeyDown: PropTypes.func.isRequired,
36 | percentage: PropTypes.number.isRequired,
37 | type: PropTypes.string.isRequired,
38 | value: PropTypes.number.isRequired,
39 | };
40 | }
41 |
42 | /**
43 | * @param {Object} props
44 | * @param {string} [props.ariaLabelledby]
45 | * @param {string} [props.ariaControls]
46 | * @param {InputRangeClassNames} props.classNames
47 | * @param {Function} [props.formatLabel]
48 | * @param {number} [props.maxValue]
49 | * @param {number} [props.minValue]
50 | * @param {Function} props.onSliderKeyDown
51 | * @param {Function} props.onSliderDrag
52 | * @param {number} props.percentage
53 | * @param {number} props.type
54 | * @param {number} props.value
55 | */
56 | constructor(props) {
57 | super(props);
58 |
59 | /**
60 | * @private
61 | * @type {?Component}
62 | */
63 | this.node = null;
64 | }
65 |
66 | /**
67 | * @ignore
68 | * @override
69 | * @return {void}
70 | */
71 | componentWillUnmount() {
72 | this.removeDocumentMouseMoveListener();
73 | this.removeDocumentMouseUpListener();
74 | this.removeDocumentTouchEndListener();
75 | this.removeDocumentTouchMoveListener();
76 | }
77 |
78 | /**
79 | * @private
80 | * @return {Object}
81 | */
82 | getStyle() {
83 | const perc = (this.props.percentage || 0) * 100;
84 | const style = {
85 | position: 'absolute',
86 | left: `${perc}%`,
87 | };
88 |
89 | return style;
90 | }
91 |
92 | /**
93 | * Listen to mousemove event
94 | * @private
95 | * @return {void}
96 | */
97 | addDocumentMouseMoveListener() {
98 | this.removeDocumentMouseMoveListener();
99 | this.node.ownerDocument.addEventListener('mousemove', this.handleMouseMove);
100 | }
101 |
102 | /**
103 | * Listen to mouseup event
104 | * @private
105 | * @return {void}
106 | */
107 | addDocumentMouseUpListener() {
108 | this.removeDocumentMouseUpListener();
109 | this.node.ownerDocument.addEventListener('mouseup', this.handleMouseUp);
110 | }
111 |
112 | /**
113 | * Listen to touchmove event
114 | * @private
115 | * @return {void}
116 | */
117 | addDocumentTouchMoveListener() {
118 | this.removeDocumentTouchMoveListener();
119 | this.node.ownerDocument.addEventListener('touchmove', this.handleTouchMove);
120 | }
121 |
122 | /**
123 | * Listen to touchend event
124 | * @private
125 | * @return {void}
126 | */
127 | addDocumentTouchEndListener() {
128 | this.removeDocumentTouchEndListener();
129 | this.node.ownerDocument.addEventListener('touchend', this.handleTouchEnd);
130 | }
131 |
132 | /**
133 | * @private
134 | * @return {void}
135 | */
136 | removeDocumentMouseMoveListener() {
137 | this.node.ownerDocument.removeEventListener('mousemove', this.handleMouseMove);
138 | }
139 |
140 | /**
141 | * @private
142 | * @return {void}
143 | */
144 | removeDocumentMouseUpListener() {
145 | this.node.ownerDocument.removeEventListener('mouseup', this.handleMouseUp);
146 | }
147 |
148 | /**
149 | * @private
150 | * @return {void}
151 | */
152 | removeDocumentTouchMoveListener() {
153 | this.node.ownerDocument.removeEventListener('touchmove', this.handleTouchMove);
154 | }
155 |
156 | /**
157 | * @private
158 | * @return {void}
159 | */
160 | removeDocumentTouchEndListener() {
161 | this.node.ownerDocument.removeEventListener('touchend', this.handleTouchEnd);
162 | }
163 |
164 | /**
165 | * @private
166 | * @return {void}
167 | */
168 | @autobind
169 | handleMouseDown() {
170 | this.addDocumentMouseMoveListener();
171 | this.addDocumentMouseUpListener();
172 | }
173 |
174 | /**
175 | * @private
176 | * @return {void}
177 | */
178 | @autobind
179 | handleMouseUp() {
180 | this.removeDocumentMouseMoveListener();
181 | this.removeDocumentMouseUpListener();
182 | }
183 |
184 | /**
185 | * @private
186 | * @param {SyntheticEvent} event
187 | * @return {void}
188 | */
189 | @autobind
190 | handleMouseMove(event) {
191 | this.props.onSliderDrag(event, this.props.type);
192 | }
193 |
194 | /**
195 | * @private
196 | * @return {void}
197 | */
198 | @autobind
199 | handleTouchStart() {
200 | this.addDocumentTouchEndListener();
201 | this.addDocumentTouchMoveListener();
202 | }
203 |
204 | /**
205 | * @private
206 | * @param {SyntheticEvent} event
207 | * @return {void}
208 | */
209 | @autobind
210 | handleTouchMove(event) {
211 | this.props.onSliderDrag(event, this.props.type);
212 | }
213 |
214 | /**
215 | * @private
216 | * @return {void}
217 | */
218 | @autobind
219 | handleTouchEnd() {
220 | this.removeDocumentTouchMoveListener();
221 | this.removeDocumentTouchEndListener();
222 | }
223 |
224 | /**
225 | * @private
226 | * @param {SyntheticEvent} event
227 | * @return {void}
228 | */
229 | @autobind
230 | handleKeyDown(event) {
231 | this.props.onSliderKeyDown(event, this.props.type);
232 | }
233 |
234 | /**
235 | * @override
236 | * @return {JSX.Element}
237 | */
238 | render() {
239 | const style = this.getStyle();
240 |
241 | return (
242 | { this.node = node; }}
245 | style={style}>
246 |
252 |
253 |
266 |
267 | );
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/test/input-range/input-range.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InputRange from '../../src/js';
3 | import { mount, shallow } from 'enzyme';
4 |
5 | describe('InputRange', () => {
6 | let container;
7 | let requestAnimationFrame;
8 |
9 | beforeEach(() => {
10 | requestAnimationFrame = window.requestAnimationFrame;
11 | window.requestAnimationFrame = callback => callback();
12 |
13 | container = document.createElement('div');
14 | document.body.appendChild(container);
15 | });
16 |
17 | afterEach(() => {
18 | window.requestAnimationFrame = requestAnimationFrame;
19 |
20 | document.body.removeChild(container);
21 | });
22 |
23 | it('updates the current value when the user tries to drag the slider', () => {
24 | const jsx = (
25 | component.setProps({ value })}
30 | />
31 | );
32 | const component = mount(jsx, { attachTo: container });
33 | const minSlider = component.find(`Slider [onMouseDown]`).at(0);
34 | const maxSlider = component.find(`Slider [onMouseDown]`).at(1);
35 |
36 | minSlider.simulate('mouseDown', { clientX: 50, clientY: 50 });
37 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 50 }));
38 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 100, clientY: 50 }));
39 | expect(component.props().value).toEqual({ min: 5, max: 10 });
40 |
41 | maxSlider.simulate('mouseDown', { clientX: 210, clientY: 50 });
42 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 260, clientY: 50 }));
43 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 260, clientY: 50 }));
44 | expect(component.props().value).toEqual({ min: 5, max: 13 });
45 |
46 | component.detach();
47 | });
48 |
49 | it('updates the current value when the user clicks on the track', () => {
50 | const jsx = (
51 | component.setProps({ value })}
56 | />
57 | );
58 | const component = mount(jsx, { attachTo: container });
59 | const track = component.find(`Track [onMouseDown]`).first();
60 |
61 | track.simulate('mouseDown', { clientX: 150, clientY: 50 });
62 | expect(component.props().value).toEqual({ min: 2, max: 7 });
63 |
64 | track.simulate('mouseDown', { clientX: 20, clientY: 50 });
65 | expect(component.props().value).toEqual({ min: 1, max: 7 });
66 |
67 | component.detach();
68 | });
69 |
70 | it('updates the current value when the user touches on the track', () => {
71 | const jsx = (
72 | component.setProps({ value })}
77 | />
78 | );
79 | const component = mount(jsx, { attachTo: container });
80 | const track = component.find(`Track [onTouchStart]`).first();
81 |
82 | track.simulate('touchStart', { touches: [{ clientX: 150, clientY: 50 }] });
83 | expect(component.props().value).toEqual({ min: 2, max: 7 });
84 |
85 | component.detach();
86 | });
87 |
88 | it('updates the current value by a predefined increment', () => {
89 | const jsx = (
90 | component.setProps({ value })}
95 | step={2}
96 | />
97 | );
98 | const component = mount(jsx, { attachTo: container });
99 | const slider = component.find(`Slider [onMouseDown]`).first();
100 |
101 | slider.simulate('mouseDown', { clientX: 50, clientY: 50 });
102 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 60, clientY: 50 }));
103 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 60, clientY: 50 }));
104 | expect(component.props().value).toEqual({ min: 2, max: 10 });
105 |
106 | slider.simulate('mouseDown', { clientX: 50, clientY: 50 });
107 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 70, clientY: 50 }));
108 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 70, clientY: 50 }));
109 | expect(component.props().value).toEqual({ min: 4, max: 10 });
110 |
111 | component.detach();
112 | });
113 |
114 | it('updates the current value when the user hits one of the arrow keys', () => {
115 | const jsx = (
116 | component.setProps({ value })}
121 | />
122 | );
123 | const component = mount(jsx);
124 | const slider = component.find(`Slider [onKeyDown]`).first();
125 |
126 | slider.simulate('keyDown', { keyCode: 37 });
127 | slider.simulate('keyUp', { keyCode: 37 });
128 | expect(component.props().value).toEqual({ min: 1, max: 10 });
129 |
130 | slider.simulate('keyDown', { keyCode: 39 });
131 | slider.simulate('keyUp', { keyCode: 39 });
132 | expect(component.props().value).toEqual({ min: 2, max: 10 });
133 | });
134 |
135 | it('does not respond to keyboard events other than arrow keys', () => {
136 | const jsx = (
137 | component.setProps({ value })}
142 | />
143 | );
144 | const component = mount(jsx);
145 | const slider = component.find(`Slider [onKeyDown]`).first();
146 |
147 | slider.simulate('keyDown', { keyCode: 65 });
148 | slider.simulate('keyUp', { keyCode: 65 });
149 | expect(component.props().value).toEqual({ min: 2, max: 10 });
150 | });
151 |
152 | it('does not respond to mouse event when it is disabled', () => {
153 | const jsx = (
154 | component.setProps({ value })}
158 | />
159 | );
160 | const component = mount(jsx, { attachTo: container });
161 | const slider = component.find(`Slider [onMouseDown]`).at(0);
162 |
163 | slider.simulate('mouseDown', { clientX: 50, clientY: 50 });
164 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 50 }));
165 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 100, clientY: 50 }));
166 | expect(component.props().value).toEqual({ min: 2, max: 10 });
167 |
168 | component.detach();
169 | });
170 |
171 | it('does not respond to keyboard event when it is disabled', () => {
172 | const jsx = (
173 | component.setProps({ value })}
177 | />
178 | );
179 | const component = mount(jsx);
180 | const slider = component.find(`Slider [onKeyDown]`).first();
181 |
182 | slider.simulate('keyDown', { keyCode: 37 });
183 | slider.simulate('keyUp', { keyCode: 37 });
184 | expect(component.props().value).toEqual(2);
185 | });
186 |
187 | it('prevents the min/max value from exceeding the min/max range', () => {
188 | const jsx = (
189 | component.setProps({ value })}
194 | />
195 | );
196 | const component = mount(jsx, { attachTo: container });
197 | const minSlider = component.find(`Slider [onMouseDown]`).at(0);
198 | const maxSlider = component.find(`Slider [onMouseDown]`).at(1);
199 |
200 | minSlider.simulate('mouseDown', { clientX: 50, clientY: 50 });
201 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: -20, clientY: 50 }));
202 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: -20, clientY: 50 }));
203 | expect(component.props().value).toEqual({ min: 0, max: 10 });
204 |
205 | maxSlider.simulate('mouseDown', { clientX: 210, clientY: 50 });
206 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 600, clientY: 50 }));
207 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 600, clientY: 50 }));
208 | expect(component.props().value).toEqual({ min: 0, max: 20 });
209 |
210 | component.detach();
211 | });
212 |
213 | it('prevents the current value from exceeding the min/max range', () => {
214 | const jsx = (
215 | component.setProps({ value })}
220 | />
221 | );
222 | const component = mount(jsx, { attachTo: container });
223 | const slider = component.find(`Slider [onMouseDown]`).first();
224 |
225 | slider.simulate('mouseDown', { clientX: 50, clientY: 50 });
226 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: -20, clientY: 50 }));
227 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: -20, clientY: 50 }));
228 | expect(component.props().value).toEqual(0);
229 |
230 | slider.simulate('mouseDown', { clientX: 0, clientY: 50 });
231 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 600, clientY: 50 }));
232 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 600, clientY: 50 }));
233 | expect(component.props().value).toEqual(20);
234 |
235 | component.detach();
236 | });
237 |
238 | it('prevents the minimum value from exceeding the maximum value', () => {
239 | const jsx = (
240 | component.setProps({ value })}
245 | />
246 | );
247 | const component = mount(jsx, { attachTo: container });
248 | const slider = component.find(`Slider [onMouseDown]`).first();
249 |
250 | slider.simulate('mouseDown', { clientX: 50, clientY: 50 });
251 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 190, clientY: 50 }));
252 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 190, clientY: 50 }));
253 | expect(component.props().value).toEqual({ min: 9, max: 10 });
254 |
255 | component.detach();
256 | });
257 |
258 | it('allows the min value to equal the max value', () => {
259 | const jsx = (
260 | component.setProps({ value })}
266 | />
267 | );
268 | const component = mount(jsx, { attachTo: container });
269 | const minSlider = component.find(`Slider [onMouseDown]`).at(0);
270 |
271 | minSlider.simulate('mouseDown', { clientX: 50, clientY: 50 });
272 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 200, clientY: 50 }));
273 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 200, clientY: 50 }));
274 |
275 | expect(component.props().value).toEqual({ min: 10, max: 10 });
276 |
277 | component.detach();
278 | });
279 |
280 | it('does not allow the min value to equal the max value', () => {
281 | const jsx = (
282 | component.setProps({ value })}
287 | />
288 | );
289 | const component = mount(jsx, { attachTo: container });
290 | const minSlider = component.find(`Slider [onMouseDown]`).at(0);
291 |
292 | minSlider.simulate('mouseDown', { clientX: 50, clientY: 50 });
293 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 200, clientY: 50 }));
294 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 200, clientY: 50 }));
295 |
296 | expect(component.props().value).toEqual({ min: 2, max: 10 });
297 |
298 | component.detach();
299 | });
300 |
301 | it('notifies the parent component when dragging starts', () => {
302 | const onChange = jasmine.createSpy('onChange').and.callFake(value => component.setProps({ value }));
303 | const onChangeStart = jasmine.createSpy('onChangeStart');
304 | const jsx = (
305 | component.setProps({ value })}
310 | onChangeStart={onChangeStart}
311 | />
312 | );
313 | const component = mount(jsx, { attachTo: container });
314 | const slider = component.find(`Slider [onMouseDown]`).first();
315 |
316 | slider.simulate('mouseDown', { clientX: 50, clientY: 50 });
317 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 50 }));
318 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 150, clientY: 50 }));
319 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 150, clientY: 50 }));
320 | expect(onChangeStart.calls.count()).toEqual(1);
321 |
322 | component.detach();
323 | });
324 |
325 | it('notifies the parent component when dragging stops', () => {
326 | const onChange = jasmine.createSpy('onChange').and.callFake(value => component.setProps({ value }));
327 | const onChangeComplete = jasmine.createSpy('onChangeComplete');
328 | const jsx = (
329 |
336 | );
337 | const component = mount(jsx, { attachTo: container });
338 | const slider = component.find(`Slider [onMouseDown]`).first();
339 |
340 | slider.simulate('mouseDown', { clientX: 50, clientY: 50 });
341 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 50 }));
342 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 150, clientY: 50 }));
343 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 150, clientY: 50 }));
344 | expect(onChange.calls.count()).toEqual(2);
345 | expect(onChangeComplete.calls.count()).toEqual(1);
346 |
347 | component.detach();
348 | });
349 |
350 | it('does not notify the parent component if there is no change', () => {
351 | const onChange = jasmine.createSpy('onChange').and.callFake(value => component.setProps({ value }));
352 | const onChangeComplete = jasmine.createSpy('onChangeComplete');
353 | const jsx = (
354 |
361 | );
362 | const component = mount(jsx, { attachTo: container });
363 | const slider = component.find(`Slider [onMouseDown]`).first();
364 |
365 | slider.simulate('mouseDown', { clientX: 50, clientY: 50 });
366 | document.dispatchEvent(new MouseEvent('mousemove', { clientX: 51, clientY: 50 }));
367 | document.dispatchEvent(new MouseEvent('mouseup', { clientX: 51, clientY: 50 }));
368 | expect(onChange).not.toHaveBeenCalled();
369 | expect(onChangeComplete).not.toHaveBeenCalled();
370 |
371 | component.detach();
372 | });
373 |
374 | it('displays the current value as a label', () => {
375 | const jsx = (
376 | component.setProps({ value })}
379 | />
380 | );
381 | const component = mount(jsx);
382 | const label = component.find('Slider Label').first();
383 |
384 | expect(label.text()).toEqual('2');
385 | });
386 |
387 | it('displays the current value as a formatted label', () => {
388 | const jsx = (
389 | `${value}cm`}
392 | onChange={value => component.setProps({ value })}
393 | />
394 | );
395 | const component = mount(jsx);
396 | const label = component.find('Slider Label').first();
397 |
398 | expect(label.text()).toEqual('2cm');
399 | });
400 |
401 | it('displays the current value for screen readers', () => {
402 | const jsx = (
403 | component.setProps({ value })}
406 | />
407 | );
408 | const component = mount(jsx);
409 | const sliderHandle = component.find('Slider [role="slider"]').first();
410 |
411 | expect(sliderHandle.getDOMNode().getAttribute('aria-valuenow')).toEqual('2');
412 | });
413 |
414 | it('renders a pair of sliders if the input value is a range', () => {
415 | const jsx = (
416 | {}}
419 | />
420 | );
421 | const component = mount(jsx);
422 |
423 | expect(component.find('Slider').length).toEqual(2);
424 | });
425 |
426 | it('renders a single slider if the input value is a number', () => {
427 | const jsx = (
428 | {}}
431 | />
432 | );
433 | const component = mount(jsx);
434 |
435 | expect(component.find('Slider').length).toEqual(1);
436 | });
437 |
438 | it('renders a pair of hidden inputs containing the current min/max value', () => {
439 | const jsx = (
440 | {}}
444 | />
445 | );
446 | const component = mount(jsx);
447 | const minInput = component.find('[name="priceMin"][type="hidden"]');
448 | const maxInput = component.find('[name="priceMax"][type="hidden"]');
449 |
450 | expect(minInput.getDOMNode().getAttribute('value')).toEqual('2');
451 | expect(maxInput.getDOMNode().getAttribute('value')).toEqual('10');
452 | });
453 |
454 | it('renders a hidden input containing the current value', () => {
455 | const jsx = (
456 | {}}
460 | />
461 | );
462 | const component = mount(jsx);
463 | const hiddenInput = component.find('[name="price"][type="hidden"]');
464 |
465 | expect(hiddenInput.getDOMNode().getAttribute('value')).toEqual('5');
466 | });
467 |
468 | it('returns an error if the max/min range is invalid', () => {
469 | const sampleProps = [
470 | { minValue: '2', maxValue: '10' },
471 | { minValue: 10, maxValue: 2 },
472 | ];
473 |
474 | sampleProps.forEach(props => {
475 | expect(InputRange.propTypes.minValue(props)).toEqual(jasmine.any(Error));
476 | expect(InputRange.propTypes.maxValue(props)).toEqual(jasmine.any(Error));
477 | });
478 | });
479 |
480 | it('returns an error if the current value is not in the expected format', () => {
481 | const sampleProps = [
482 | { value: { a: 3, b: 6 }, minValue: 2, maxValue: 10 },
483 | { value: { min: 1, max: 6 }, minValue: 2, maxValue: 10 },
484 | { value: { min: 2, max: 11 }, minValue: 2, maxValue: 10 },
485 | { value: 11, minValue: 2, maxValue: 10 },
486 | { value: null, minValue: 2, maxValue: 10 },
487 | ];
488 |
489 | sampleProps.forEach(props => {
490 | expect(InputRange.propTypes.value(props, 'value')).toEqual(jasmine.any(Error));
491 | });
492 | });
493 | });
494 |
--------------------------------------------------------------------------------
/src/js/input-range/input-range.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import autobind from 'autobind-decorator';
4 | import * as valueTransformer from './value-transformer';
5 | import DEFAULT_CLASS_NAMES from './default-class-names';
6 | import Label from './label';
7 | import rangePropType from './range-prop-type';
8 | import valuePropType from './value-prop-type';
9 | import Slider from './slider';
10 | import Track from './track';
11 | import { captialize, distanceTo, isDefined, isObject, length } from '../utils';
12 | import { DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from './key-codes';
13 |
14 | /**
15 | * A React component that allows users to input numeric values within a range
16 | * by dragging its sliders.
17 | */
18 | export default class InputRange extends React.Component {
19 | /**
20 | * @ignore
21 | * @override
22 | * @return {Object}
23 | */
24 | static get propTypes() {
25 | return {
26 | allowSameValues: PropTypes.bool,
27 | ariaLabelledby: PropTypes.string,
28 | ariaControls: PropTypes.string,
29 | classNames: PropTypes.objectOf(PropTypes.string),
30 | disabled: PropTypes.bool,
31 | draggableTrack: PropTypes.bool,
32 | formatLabel: PropTypes.func,
33 | maxValue: rangePropType,
34 | minValue: rangePropType,
35 | name: PropTypes.string,
36 | onChangeStart: PropTypes.func,
37 | onChange: PropTypes.func.isRequired,
38 | onChangeComplete: PropTypes.func,
39 | step: PropTypes.number,
40 | value: valuePropType,
41 | };
42 | }
43 |
44 | /**
45 | * @ignore
46 | * @override
47 | * @return {Object}
48 | */
49 | static get defaultProps() {
50 | return {
51 | allowSameValues: false,
52 | classNames: DEFAULT_CLASS_NAMES,
53 | disabled: false,
54 | maxValue: 10,
55 | minValue: 0,
56 | step: 1,
57 | };
58 | }
59 |
60 | /**
61 | * @param {Object} props
62 | * @param {boolean} [props.allowSameValues]
63 | * @param {string} [props.ariaLabelledby]
64 | * @param {string} [props.ariaControls]
65 | * @param {InputRangeClassNames} [props.classNames]
66 | * @param {boolean} [props.disabled = false]
67 | * @param {Function} [props.formatLabel]
68 | * @param {number|Range} [props.maxValue = 10]
69 | * @param {number|Range} [props.minValue = 0]
70 | * @param {string} [props.name]
71 | * @param {string} props.onChange
72 | * @param {Function} [props.onChangeComplete]
73 | * @param {Function} [props.onChangeStart]
74 | * @param {number} [props.step = 1]
75 | * @param {number|Range} props.value
76 | */
77 | constructor(props) {
78 | super(props);
79 |
80 | /**
81 | * @private
82 | * @type {?number}
83 | */
84 | this.startValue = null;
85 |
86 | /**
87 | * @private
88 | * @type {?Component}
89 | */
90 | this.node = null;
91 |
92 | /**
93 | * @private
94 | * @type {?Component}
95 | */
96 | this.trackNode = null;
97 |
98 | /**
99 | * @private
100 | * @type {bool}
101 | */
102 | this.isSliderDragging = false;
103 |
104 | /**
105 | * @private
106 | * @type {?string}
107 | */
108 | this.lastKeyMoved = null;
109 | }
110 |
111 | /**
112 | * @ignore
113 | * @override
114 | * @return {void}
115 | */
116 | componentWillUnmount() {
117 | this.removeDocumentMouseUpListener();
118 | this.removeDocumentTouchEndListener();
119 | }
120 |
121 | /**
122 | * Return the CSS class name of the component
123 | * @private
124 | * @return {string}
125 | */
126 | getComponentClassName() {
127 | if (!this.props.disabled) {
128 | return this.props.classNames.inputRange;
129 | }
130 |
131 | return this.props.classNames.disabledInputRange;
132 | }
133 |
134 | /**
135 | * Return the bounding rect of the track
136 | * @private
137 | * @return {ClientRect}
138 | */
139 | getTrackClientRect() {
140 | return this.trackNode.getClientRect();
141 | }
142 |
143 | /**
144 | * Return the slider key closest to a point
145 | * @private
146 | * @param {Point} position
147 | * @return {string}
148 | */
149 | getKeyByPosition(position) {
150 | const values = valueTransformer.getValueFromProps(this.props, this.isMultiValue());
151 | const positions = valueTransformer.getPositionsFromValues(values, this.props.minValue, this.props.maxValue, this.getTrackClientRect());
152 |
153 | if (this.isMultiValue()) {
154 | const distanceToMin = distanceTo(position, positions.min);
155 | const distanceToMax = distanceTo(position, positions.max);
156 |
157 | if (distanceToMin < distanceToMax) {
158 | return 'min';
159 | }
160 | }
161 |
162 | return 'max';
163 | }
164 |
165 | /**
166 | * Return all the slider keys
167 | * @private
168 | * @return {string[]}
169 | */
170 | getKeys() {
171 | if (this.isMultiValue()) {
172 | return ['min', 'max'];
173 | }
174 |
175 | return ['max'];
176 | }
177 |
178 | /**
179 | * Return true if the difference between the new and the current value is
180 | * greater or equal to the step amount of the component
181 | * @private
182 | * @param {Range} values
183 | * @return {boolean}
184 | */
185 | hasStepDifference(values) {
186 | const currentValues = valueTransformer.getValueFromProps(this.props, this.isMultiValue());
187 |
188 | return length(values.min, currentValues.min) >= this.props.step ||
189 | length(values.max, currentValues.max) >= this.props.step;
190 | }
191 |
192 | /**
193 | * Return true if the component accepts a min and max value
194 | * @private
195 | * @return {boolean}
196 | */
197 | isMultiValue() {
198 | return isObject(this.props.value);
199 | }
200 |
201 | /**
202 | * Return true if the range is within the max and min value of the component
203 | * @private
204 | * @param {Range} values
205 | * @return {boolean}
206 | */
207 | isWithinRange(values) {
208 | if (this.isMultiValue()) {
209 | return values.min >= this.props.minValue &&
210 | values.max <= this.props.maxValue &&
211 | this.props.allowSameValues
212 | ? values.min <= values.max
213 | : values.min < values.max;
214 | }
215 |
216 | return values.max >= this.props.minValue && values.max <= this.props.maxValue;
217 | }
218 |
219 | /**
220 | * Return true if the new value should trigger a render
221 | * @private
222 | * @param {Range} values
223 | * @return {boolean}
224 | */
225 | shouldUpdate(values) {
226 | return this.isWithinRange(values) && this.hasStepDifference(values);
227 | }
228 |
229 | /**
230 | * Update the position of a slider
231 | * @private
232 | * @param {string} key
233 | * @param {Point} position
234 | * @return {void}
235 | */
236 | updatePosition(key, position) {
237 | const values = valueTransformer.getValueFromProps(this.props, this.isMultiValue());
238 | const positions = valueTransformer.getPositionsFromValues(values, this.props.minValue, this.props.maxValue, this.getTrackClientRect());
239 |
240 | positions[key] = position;
241 | this.lastKeyMoved = key;
242 |
243 | this.updatePositions(positions);
244 | }
245 |
246 | /**
247 | * Update the positions of multiple sliders
248 | * @private
249 | * @param {Object} positions
250 | * @param {Point} positions.min
251 | * @param {Point} positions.max
252 | * @return {void}
253 | */
254 | updatePositions(positions) {
255 | const values = {
256 | min: valueTransformer.getValueFromPosition(positions.min, this.props.minValue, this.props.maxValue, this.getTrackClientRect()),
257 | max: valueTransformer.getValueFromPosition(positions.max, this.props.minValue, this.props.maxValue, this.getTrackClientRect()),
258 | };
259 |
260 | const transformedValues = {
261 | min: valueTransformer.getStepValueFromValue(values.min, this.props.step),
262 | max: valueTransformer.getStepValueFromValue(values.max, this.props.step),
263 | };
264 |
265 | this.updateValues(transformedValues);
266 | }
267 |
268 | /**
269 | * Update the value of a slider
270 | * @private
271 | * @param {string} key
272 | * @param {number} value
273 | * @return {void}
274 | */
275 | updateValue(key, value) {
276 | const values = valueTransformer.getValueFromProps(this.props, this.isMultiValue());
277 |
278 | values[key] = value;
279 |
280 | this.updateValues(values);
281 | }
282 |
283 | /**
284 | * Update the values of multiple sliders
285 | * @private
286 | * @param {Range|number} values
287 | * @return {void}
288 | */
289 | updateValues(values) {
290 | if (!this.shouldUpdate(values)) {
291 | return;
292 | }
293 |
294 | this.props.onChange(this.isMultiValue() ? values : values.max);
295 | }
296 |
297 | /**
298 | * Increment the value of a slider by key name
299 | * @private
300 | * @param {string} key
301 | * @return {void}
302 | */
303 | incrementValue(key) {
304 | const values = valueTransformer.getValueFromProps(this.props, this.isMultiValue());
305 | const value = values[key] + this.props.step;
306 |
307 | this.updateValue(key, value);
308 | }
309 |
310 | /**
311 | * Decrement the value of a slider by key name
312 | * @private
313 | * @param {string} key
314 | * @return {void}
315 | */
316 | decrementValue(key) {
317 | const values = valueTransformer.getValueFromProps(this.props, this.isMultiValue());
318 | const value = values[key] - this.props.step;
319 |
320 | this.updateValue(key, value);
321 | }
322 |
323 | /**
324 | * Listen to mouseup event
325 | * @private
326 | * @return {void}
327 | */
328 | addDocumentMouseUpListener() {
329 | this.removeDocumentMouseUpListener();
330 | this.node.ownerDocument.addEventListener('mouseup', this.handleMouseUp);
331 | }
332 |
333 | /**
334 | * Listen to touchend event
335 | * @private
336 | * @return {void}
337 | */
338 | addDocumentTouchEndListener() {
339 | this.removeDocumentTouchEndListener();
340 | this.node.ownerDocument.addEventListener('touchend', this.handleTouchEnd);
341 | }
342 |
343 | /**
344 | * Stop listening to mouseup event
345 | * @private
346 | * @return {void}
347 | */
348 | removeDocumentMouseUpListener() {
349 | this.node.ownerDocument.removeEventListener('mouseup', this.handleMouseUp);
350 | }
351 |
352 | /**
353 | * Stop listening to touchend event
354 | * @private
355 | * @return {void}
356 | */
357 | removeDocumentTouchEndListener() {
358 | this.node.ownerDocument.removeEventListener('touchend', this.handleTouchEnd);
359 | }
360 |
361 | /**
362 | * Handle any "mousemove" event received by the slider
363 | * @private
364 | * @param {SyntheticEvent} event
365 | * @param {string} key
366 | * @return {void}
367 | */
368 | @autobind
369 | handleSliderDrag(event, key) {
370 | if (this.props.disabled) {
371 | return;
372 | }
373 |
374 | const position = valueTransformer.getPositionFromEvent(event, this.getTrackClientRect());
375 | this.isSliderDragging = true;
376 | requestAnimationFrame(() => this.updatePosition(key, position));
377 | }
378 |
379 | /**
380 | * Handle any "mousemove" event received by the track
381 | * @private
382 | * @param {SyntheticEvent} event
383 | * @return {void}
384 | */
385 | @autobind
386 | handleTrackDrag(event, prevEvent) {
387 | if (this.props.disabled || !this.props.draggableTrack || this.isSliderDragging) {
388 | return;
389 | }
390 |
391 | const {
392 | maxValue,
393 | minValue,
394 | value: { max, min },
395 | } = this.props;
396 |
397 | const position = valueTransformer.getPositionFromEvent(event, this.getTrackClientRect());
398 | const value = valueTransformer.getValueFromPosition(position, minValue, maxValue, this.getTrackClientRect());
399 | const stepValue = valueTransformer.getStepValueFromValue(value, this.props.step);
400 |
401 | const prevPosition = valueTransformer.getPositionFromEvent(prevEvent, this.getTrackClientRect());
402 | const prevValue = valueTransformer.getValueFromPosition(prevPosition, minValue, maxValue, this.getTrackClientRect());
403 | const prevStepValue = valueTransformer.getStepValueFromValue(prevValue, this.props.step);
404 |
405 | const offset = prevStepValue - stepValue;
406 |
407 | const transformedValues = {
408 | min: min - offset,
409 | max: max - offset,
410 | };
411 |
412 | this.updateValues(transformedValues);
413 | }
414 |
415 | /**
416 | * Handle any "keydown" event received by the slider
417 | * @private
418 | * @param {SyntheticEvent} event
419 | * @param {string} key
420 | * @return {void}
421 | */
422 | @autobind
423 | handleSliderKeyDown(event, key) {
424 | if (this.props.disabled) {
425 | return;
426 | }
427 |
428 | switch (event.keyCode) {
429 | case LEFT_ARROW:
430 | case DOWN_ARROW:
431 | event.preventDefault();
432 | this.decrementValue(key);
433 | break;
434 |
435 | case RIGHT_ARROW:
436 | case UP_ARROW:
437 | event.preventDefault();
438 | this.incrementValue(key);
439 | break;
440 |
441 | default:
442 | break;
443 | }
444 | }
445 |
446 | /**
447 | * Handle any "mousedown" event received by the track
448 | * @private
449 | * @param {SyntheticEvent} event
450 | * @param {Point} position
451 | * @return {void}
452 | */
453 | @autobind
454 | handleTrackMouseDown(event, position) {
455 | if (this.props.disabled) {
456 | return;
457 | }
458 |
459 | const {
460 | maxValue,
461 | minValue,
462 | value: { max, min },
463 | } = this.props;
464 |
465 | event.preventDefault();
466 |
467 | const value = valueTransformer.getValueFromPosition(position, minValue, maxValue, this.getTrackClientRect());
468 | const stepValue = valueTransformer.getStepValueFromValue(value, this.props.step);
469 |
470 | if (!this.props.draggableTrack || stepValue > max || stepValue < min) {
471 | this.updatePosition(this.getKeyByPosition(position), position);
472 | }
473 | }
474 |
475 | /**
476 | * Handle the start of any mouse/touch event
477 | * @private
478 | * @return {void}
479 | */
480 | @autobind
481 | handleInteractionStart() {
482 | if (this.props.onChangeStart) {
483 | this.props.onChangeStart(this.props.value);
484 | }
485 |
486 | if (this.props.onChangeComplete && !isDefined(this.startValue)) {
487 | this.startValue = this.props.value;
488 | }
489 | }
490 |
491 | /**
492 | * Handle the end of any mouse/touch event
493 | * @private
494 | * @return {void}
495 | */
496 | @autobind
497 | handleInteractionEnd() {
498 | if (this.isSliderDragging) {
499 | this.isSliderDragging = false;
500 | }
501 |
502 | if (!this.props.onChangeComplete || !isDefined(this.startValue)) {
503 | return;
504 | }
505 |
506 | if (this.startValue !== this.props.value) {
507 | this.props.onChangeComplete(this.props.value);
508 | }
509 |
510 | this.startValue = null;
511 | }
512 |
513 | /**
514 | * Handle any "keydown" event received by the component
515 | * @private
516 | * @param {SyntheticEvent} event
517 | * @return {void}
518 | */
519 | @autobind
520 | handleKeyDown(event) {
521 | this.handleInteractionStart(event);
522 | }
523 |
524 | /**
525 | * Handle any "keyup" event received by the component
526 | * @private
527 | * @param {SyntheticEvent} event
528 | * @return {void}
529 | */
530 | @autobind
531 | handleKeyUp(event) {
532 | this.handleInteractionEnd(event);
533 | }
534 |
535 | /**
536 | * Handle any "mousedown" event received by the component
537 | * @private
538 | * @param {SyntheticEvent} event
539 | * @return {void}
540 | */
541 | @autobind
542 | handleMouseDown(event) {
543 | this.handleInteractionStart(event);
544 | this.addDocumentMouseUpListener();
545 | }
546 |
547 | /**
548 | * Handle any "mouseup" event received by the component
549 | * @private
550 | * @param {SyntheticEvent} event
551 | */
552 | @autobind
553 | handleMouseUp(event) {
554 | this.handleInteractionEnd(event);
555 | this.removeDocumentMouseUpListener();
556 | }
557 |
558 | /**
559 | * Handle any "touchstart" event received by the component
560 | * @private
561 | * @param {SyntheticEvent} event
562 | * @return {void}
563 | */
564 | @autobind
565 | handleTouchStart(event) {
566 | this.handleInteractionStart(event);
567 | this.addDocumentTouchEndListener();
568 | }
569 |
570 | /**
571 | * Handle any "touchend" event received by the component
572 | * @private
573 | * @param {SyntheticEvent} event
574 | */
575 | @autobind
576 | handleTouchEnd(event) {
577 | this.handleInteractionEnd(event);
578 | this.removeDocumentTouchEndListener();
579 | }
580 |
581 | /**
582 | * Return JSX of sliders
583 | * @private
584 | * @return {JSX.Element}
585 | */
586 | renderSliders() {
587 | const values = valueTransformer.getValueFromProps(this.props, this.isMultiValue());
588 | const percentages = valueTransformer.getPercentagesFromValues(values, this.props.minValue, this.props.maxValue);
589 | const keys = this.props.allowSameValues &&
590 | this.lastKeyMoved === 'min'
591 | ? this.getKeys().reverse()
592 | : this.getKeys();
593 |
594 | return keys.map((key) => {
595 | const value = values[key];
596 | const percentage = percentages[key];
597 |
598 | let { maxValue, minValue } = this.props;
599 |
600 | if (key === 'min') {
601 | maxValue = values.max;
602 | } else {
603 | minValue = values.min;
604 | }
605 |
606 | const slider = (
607 |
620 | );
621 |
622 | return slider;
623 | });
624 | }
625 |
626 | /**
627 | * Return JSX of hidden inputs
628 | * @private
629 | * @return {JSX.Element}
630 | */
631 | renderHiddenInputs() {
632 | if (!this.props.name) {
633 | return [];
634 | }
635 |
636 | const isMultiValue = this.isMultiValue();
637 | const values = valueTransformer.getValueFromProps(this.props, isMultiValue);
638 |
639 | return this.getKeys().map((key) => {
640 | const value = values[key];
641 | const name = isMultiValue ? `${this.props.name}${captialize(key)}` : this.props.name;
642 |
643 | return (
644 |
645 | );
646 | });
647 | }
648 |
649 | /**
650 | * @ignore
651 | * @override
652 | * @return {JSX.Element}
653 | */
654 | render() {
655 | const componentClassName = this.getComponentClassName();
656 | const values = valueTransformer.getValueFromProps(this.props, this.isMultiValue());
657 | const percentages = valueTransformer.getPercentagesFromValues(values, this.props.minValue, this.props.maxValue);
658 |
659 | return (
660 | { this.node = node; }}
663 | className={componentClassName}
664 | onKeyDown={this.handleKeyDown}
665 | onKeyUp={this.handleKeyUp}
666 | onMouseDown={this.handleMouseDown}
667 | onTouchStart={this.handleTouchStart}>
668 |
674 |
675 |
685 |
686 |
692 |
693 | {this.renderHiddenInputs()}
694 |
695 | );
696 | }
697 | }
698 |
--------------------------------------------------------------------------------