├── .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 |
32 | this.setState({ value })} 37 | onChangeComplete={value => console.log(value)} /> 38 | 39 | this.setState({ value })} 45 | onChangeComplete={value => console.log(value)} /> 46 | 47 | value.toFixed(2)} 51 | value={this.state.value3} 52 | onChange={value => this.setState({ value3: value })} 53 | onChangeStart={value => console.log('onChangeStart with value =', value)} 54 | onChangeComplete={value => console.log(value)} /> 55 | 56 | `${value}kg`} 60 | value={this.state.value4} 61 | onChange={value => this.setState({ value4: value })} 62 | onChangeComplete={value => console.log(value)} /> 63 | 64 | this.setState({ value5: value })} 69 | onChangeComplete={value => console.log(value)} 70 | value={this.state.value5} /> 71 | 72 | this.setState({ value6: value })} 78 | onChangeComplete={value => console.log(value)} 79 | value={this.state.value6} /> 80 | 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 | [![Build Status](https://travis-ci.org/davidchin/react-input-range.svg?branch=master)](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 | { this.trackNode = trackNode; }} 679 | percentages={percentages} 680 | onTrackDrag={this.handleTrackDrag} 681 | onTrackMouseDown={this.handleTrackMouseDown}> 682 | 683 | {this.renderSliders()} 684 | 685 | 686 | 692 | 693 | {this.renderHiddenInputs()} 694 |
695 | ); 696 | } 697 | } 698 | --------------------------------------------------------------------------------