├── .github └── CONTRIBUTING.md ├── .eslintignore ├── .npmignore ├── components ├── utils │ ├── noop.js │ ├── lastElement.js │ ├── getSelectedIds.js │ ├── autoBind.js │ └── debounce.js ├── Count │ ├── index.js │ ├── Count.scss │ └── Count.jsx ├── Group │ ├── index.js │ └── Group.jsx ├── Slider │ ├── index.js │ ├── utils │ │ ├── isVertical.js │ │ ├── suppress.js │ │ ├── capitalize.js │ │ ├── hasStepDifference.js │ │ ├── removeClass.js │ │ ├── isWithinRange.js │ │ ├── isEqual.js │ │ ├── index.js │ │ ├── formatValue.js │ │ └── formatNumber.js │ ├── helpers │ │ ├── getPositionFromValue.js │ │ ├── index.js │ │ ├── getValueFromPosition.js │ │ ├── getRelativePosition.js │ │ └── getNearestValue.js │ ├── constants.js │ ├── Rail.js │ ├── Steps.js │ ├── Slider.scss │ ├── Control.js │ └── Slider.jsx ├── Toggle │ ├── index.js │ ├── Toggle.scss │ └── Toggle.jsx ├── InputRange │ ├── index.js │ ├── InputRange.scss │ └── InputRange.jsx ├── AutoComplete │ ├── index.js │ ├── Suggestions.js │ ├── Tag.js │ ├── AutoComplete.scss │ ├── SearchBox.js │ └── AutoComplete.jsx ├── styles.scss ├── mixins.scss ├── index.js ├── theme.scss └── icons.scss ├── assets ├── logo.png └── fonts │ ├── reactfilters.eot │ ├── reactfilters.ttf │ ├── reactfilters.woff │ └── reactfilters.svg ├── .babelrc ├── docs ├── cd369ac486335815aba6faf43e3476e9.eot ├── eefe50b894a27050a86bd6ae7a822682.ttf ├── static │ ├── preview.bundle.js.map │ └── manager.bundle.js.map ├── iframe.html ├── index.html └── 0ab516c6e00c001e01c647f19720b2e8.svg ├── .gitignore ├── test ├── Slider │ └── utils │ │ ├── capitalize.test.js │ │ ├── removeClass.test.js │ │ ├── isEqual.test.js │ │ ├── hasStepDifference.test.js │ │ ├── formatNumber.test.js │ │ └── isWithinRange.test.js ├── utils │ ├── lastElement.test.js │ └── debounce.test.js ├── config │ └── setup.js ├── Count.test.js ├── Group.test.js └── Toggle.test.js ├── .storybook ├── base.scss ├── webpack.config.js └── config.js ├── .travis.yml ├── stories ├── CheckBox.story.js ├── InputRange.story.js ├── Count.story.js ├── Switch.story.js ├── Slider.value.story.js ├── Radio.story.js ├── Container.js ├── Slider.range.story.js ├── Group.story.js └── AutoComplete.story.js ├── mdDocs ├── InputRange.md ├── Group.md ├── Count.md ├── Slider.md ├── AutoComplete.md └── Toggle.md ├── .eslintrc ├── .editorconfig ├── LICENSE.md ├── scripts ├── buildDocs.js └── generateMarkdown.js ├── README.md ├── package.json └── CHANGELOG.md /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | **/node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | tests 4 | coverage 5 | -------------------------------------------------------------------------------- /components/utils/noop.js: -------------------------------------------------------------------------------- 1 | export default function () {} 2 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritz078/react-filters/HEAD/assets/logo.png -------------------------------------------------------------------------------- /components/Count/index.js: -------------------------------------------------------------------------------- 1 | import Count from './Count'; 2 | export default Count; 3 | -------------------------------------------------------------------------------- /components/Group/index.js: -------------------------------------------------------------------------------- 1 | import Group from './Group'; 2 | export default Group; 3 | -------------------------------------------------------------------------------- /components/Slider/index.js: -------------------------------------------------------------------------------- 1 | import Slider from './Slider'; 2 | export default Slider; 3 | -------------------------------------------------------------------------------- /components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | import Toggle from './Toggle'; 2 | export default Toggle; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /components/InputRange/index.js: -------------------------------------------------------------------------------- 1 | import InputRange from './InputRange'; 2 | export default InputRange; 3 | -------------------------------------------------------------------------------- /components/AutoComplete/index.js: -------------------------------------------------------------------------------- 1 | import AutoComplete from './AutoComplete'; 2 | export default AutoComplete; 3 | -------------------------------------------------------------------------------- /assets/fonts/reactfilters.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritz078/react-filters/HEAD/assets/fonts/reactfilters.eot -------------------------------------------------------------------------------- /assets/fonts/reactfilters.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritz078/react-filters/HEAD/assets/fonts/reactfilters.ttf -------------------------------------------------------------------------------- /assets/fonts/reactfilters.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritz078/react-filters/HEAD/assets/fonts/reactfilters.woff -------------------------------------------------------------------------------- /components/Slider/utils/isVertical.js: -------------------------------------------------------------------------------- 1 | export default function (orientation) { 2 | return orientation === 'vertical'; 3 | } 4 | -------------------------------------------------------------------------------- /components/Slider/utils/suppress.js: -------------------------------------------------------------------------------- 1 | export default function (e) { 2 | e.stopPropagation(); 3 | e.preventDefault(); 4 | } 5 | -------------------------------------------------------------------------------- /components/Slider/utils/capitalize.js: -------------------------------------------------------------------------------- 1 | export default function (str) { 2 | return str.charAt(0).toUpperCase() + str.substr(1); 3 | } 4 | -------------------------------------------------------------------------------- /docs/cd369ac486335815aba6faf43e3476e9.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritz078/react-filters/HEAD/docs/cd369ac486335815aba6faf43e3476e9.eot -------------------------------------------------------------------------------- /docs/eefe50b894a27050a86bd6ae7a822682.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritz078/react-filters/HEAD/docs/eefe50b894a27050a86bd6ae7a822682.ttf -------------------------------------------------------------------------------- /components/Slider/utils/hasStepDifference.js: -------------------------------------------------------------------------------- 1 | export default function (newValue, oldValue, step) { 2 | return Math.abs(newValue - oldValue) >= step; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | dist 5 | lib 6 | coverage 7 | .idea 8 | examples/bundle.js 9 | examples/style.css 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /components/Slider/helpers/getPositionFromValue.js: -------------------------------------------------------------------------------- 1 | export default function (props) { 2 | const { min, max, value } = props; 3 | return ((value / (max - min))) * 100; 4 | } 5 | -------------------------------------------------------------------------------- /components/utils/lastElement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * returns the last element of the array 3 | * @param arr 4 | */ 5 | export default function (arr) { 6 | return arr.length ? arr[arr.length - 1] : null; 7 | } 8 | -------------------------------------------------------------------------------- /components/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./icons"; 2 | @import "./Count/Count"; 3 | @import "./Toggle/Toggle"; 4 | @import "./Slider/Slider"; 5 | @import "./AutoComplete/AutoComplete"; 6 | @import "./InputRange/InputRange"; 7 | 8 | -------------------------------------------------------------------------------- /components/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin no-select { 2 | -webkit-touch-callout: none; 3 | -webkit-user-select: none; 4 | -khtml-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | } 9 | -------------------------------------------------------------------------------- /docs/static/preview.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/preview.bundle.js","sources":["webpack:///static/preview.bundle.js","webpack:///"],"mappings":"AAAA;ACynIA;AAopIA;AA0zIA;AAk8EA;AAs5EA;AA+4GA;AA4gFA;AA4nIA;AAg/GA;AAw4GA;AA2oEA;AA2+DA;AAq4FA","sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/manager.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/manager.bundle.js","sources":["webpack:///static/manager.bundle.js","webpack:///?d41d"],"mappings":"AAAA;AC2kIA;AAu6IA;AAywIA;AA4zEA;AAw1EA;AAsoHA;AAgzFA;AA8wHA;AA0mHA;AAqvHA;AAotFA;AAk9CA;AAqtGA","sourceRoot":""} -------------------------------------------------------------------------------- /components/Slider/utils/removeClass.js: -------------------------------------------------------------------------------- 1 | export default function (element, className) { 2 | const classes = element.className.split(' '); 3 | const i = classes.indexOf(className); 4 | if (i !== -1) { 5 | classes.splice(i, 1); 6 | } 7 | return classes.join(' '); 8 | } 9 | -------------------------------------------------------------------------------- /components/Slider/constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | horizontal: { 3 | coordinate: 'x', 4 | dimension: 'width', 5 | direction: 'left' 6 | }, 7 | vertical: { 8 | coordinate: 'y', 9 | dimension: 'height', 10 | direction: 'top' 11 | } 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | import AutoComplete from './AutoComplete'; 2 | import Count from './Count'; 3 | import Slider from './Slider'; 4 | import Toggle from './Toggle'; 5 | import InputRange from './InputRange'; 6 | import Group from './Group'; 7 | 8 | export { AutoComplete, Count, Slider, Toggle, InputRange, Group }; 9 | -------------------------------------------------------------------------------- /components/InputRange/InputRange.scss: -------------------------------------------------------------------------------- 1 | .rf-input-range{ 2 | width: 300px; 3 | 4 | .rf-autocomplete{ 5 | width: 47%; 6 | display: inline-block; 7 | vertical-align: top; 8 | } 9 | 10 | .ir-separator{ 11 | width: 6%; 12 | display: inline-block; 13 | vertical-align: top; 14 | text-align: center; 15 | line-height: 30px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/Slider/utils/capitalize.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, __base } = global; 2 | const { capitalize } = require(`${__base}components/Slider/utils/`); 3 | import { expect } from 'chai'; 4 | 5 | describe('capitalize util method()', () => { 6 | it('should return capitalized string', () => { 7 | const str = 'foo bar'; 8 | expect(capitalize(str)).to.equal('Foo bar'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /components/Slider/helpers/index.js: -------------------------------------------------------------------------------- 1 | import getPositionFromValue from './getPositionFromValue'; 2 | import getValueFromPosition from './getValueFromPosition'; 3 | import getRelativePosition from './getRelativePosition'; 4 | import getNearestValue from './getNearestValue'; 5 | 6 | export { 7 | getPositionFromValue, 8 | getValueFromPosition, 9 | getRelativePosition, 10 | getNearestValue 11 | }; 12 | -------------------------------------------------------------------------------- /components/Slider/utils/isWithinRange.js: -------------------------------------------------------------------------------- 1 | export default function (props, value, position) { 2 | if (position) return position <= props.trackLength; 3 | const { min, max, step } = props; 4 | if (typeof value === 'object') { // if Array 5 | return (value[1] - value[0] >= step) && value[0] >= min && value[1] <= max; 6 | } else { 7 | // TODO : is step needed here ? 8 | return (value >= min && value <= max); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /components/Slider/helpers/getValueFromPosition.js: -------------------------------------------------------------------------------- 1 | import { formatNumber, isVertical } from '../utils'; 2 | import constants from '../constants'; 3 | 4 | export default function (props, position) { 5 | const { min, max, trackOffset, step, orientation } = props; 6 | const ratio = (max - min) / trackOffset[constants[orientation].dimension]; 7 | if (isVertical(orientation)) position = trackOffset.height - position; 8 | return formatNumber(position * ratio, step, min); 9 | } 10 | -------------------------------------------------------------------------------- /test/utils/lastElement.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, __base } = global; 2 | import { expect } from 'chai'; 3 | 4 | const lastElement = require(`${__base}components/utils/lastElement`).default; 5 | 6 | describe('lastElement', () => { 7 | it('should return the last element of an Array', () => { 8 | expect(lastElement([1, 2, 3])).to.equal(3); 9 | }); 10 | 11 | it('should return null if empty array is passed', () => { 12 | expect(lastElement([])).to.equal(null); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/Slider/utils/removeClass.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, __base } = global; 2 | const { removeClass } = require(`${__base}components/Slider/utils/`); 3 | import { expect } from 'chai'; 4 | 5 | describe('removeClass utility method', () => { 6 | it('should remove return the class string after removing the specified string', () => { 7 | const element = { 8 | className: 'foo bar hello' 9 | }; 10 | 11 | expect(removeClass(element, 'bar')).to.equal('foo hello'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.storybook/base.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lato); 2 | @import "../components/styles"; 3 | 4 | body{ 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 6 | } 7 | 8 | .ac-suggestion{ 9 | span{ 10 | float: right; 11 | } 12 | } 13 | 14 | .range-container{ 15 | width: 80%; 16 | } 17 | 18 | .rf-autocomplete{ 19 | width: 80%; 20 | } 21 | 22 | .group{ 23 | width: 200px; 24 | } 25 | -------------------------------------------------------------------------------- /components/Slider/utils/isEqual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tells if two values are exactly similar 3 | * @param val1 4 | * @param val2 5 | * @returns {boolean} 6 | */ 7 | export default function (val1, val2) { 8 | let isSame = true; 9 | if (typeof val1 === 'object' && typeof val2 === 'object') { 10 | if (val1.length !== val2.length) return false; 11 | val1.forEach((val, i) => { 12 | if (val2[i] !== val) isSame = false; 13 | }); 14 | } else { 15 | isSame = (val1 === val2); 16 | } 17 | 18 | return isSame; 19 | } 20 | -------------------------------------------------------------------------------- /components/utils/getSelectedIds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an array of ids which are selected 3 | * if arr = [{key: 1, value:true}, {key:2, value:false}, {key: 3, value: true}] and key = 'key' 4 | * it will return [1, 3] 5 | * @param arr 6 | * @param key 7 | * @param number whether return number or Array 8 | */ 9 | export default function (arr, key, number = false) { 10 | const selected = []; 11 | arr.forEach(val => { 12 | if (val.value) selected.push(val[key]); 13 | }); 14 | return number ? selected[0] : selected; 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | script: 15 | - npm run test:cover 16 | after_success: 17 | - npm run test:report 18 | - npm run semantic-release 19 | branches: 20 | except: 21 | - "/^v\\d+\\.\\d+\\.\\d+$/" 22 | addons: 23 | code_climate: 24 | repo_token: 842be7e8b1dee369a0c5077a36d43ae401a153731331283c570c725148a10fb5 25 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | module: { 5 | loaders: [ 6 | { 7 | test: /\.scss$/, 8 | loaders: ["style", "css", "sass"], 9 | include: path.resolve(__dirname, '../') 10 | }, 11 | { 12 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 13 | loader: "url-loader?limit=10000&mimetype=application/font-woff" 14 | }, 15 | { 16 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 17 | loader: "file-loader" 18 | } 19 | ] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /components/utils/autoBind.js: -------------------------------------------------------------------------------- 1 | /** 2 | * binds all the methods with the passed context 3 | * Eg : autoBind(['method1', 'method2'], this) 4 | * is same as 5 | * this.method1 = this.method1.bind(this); 6 | * this.method2 = this.method2.bind(this); 7 | * 8 | * @param methods => An array of method names 9 | * @param context => the binding `this` context 10 | */ 11 | export default function (methods, context) { 12 | methods.forEach(method => { 13 | // eslint-disable-next-line no-param-reassign 14 | context[method] = context[method].bind(context); 15 | }); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /components/theme.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #6495ED; 2 | $secondary-color: #e4e4e4; 3 | $tertiary-color: darkgrey; 4 | 5 | // Slider 6 | $slider-track-width: 6px; 7 | $slider-control-width: 15px; 8 | $slider-step-width: 6px; 9 | 10 | //Toggle 11 | $toggle-height: 24px; 12 | $toggle-width: 44px; 13 | $toggle-padding: 2px; 14 | $toggle-btn-color: #ffffff; 15 | $toggle-count-color: grey; 16 | $toggle-label-font-size: $toggle-height * (11px / 22px); 17 | $toggle-label-line-height: $toggle-label-font-size * 1.6; 18 | 19 | // Count 20 | $count-width: 120px; 21 | $count-height: 40px; 22 | $count-padding: 4px; 23 | -------------------------------------------------------------------------------- /test/config/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | const path = require('path'); 3 | 4 | const jsdom = require('jsdom').jsdom; 5 | const exposedProperties = ['window', 'navigator', 'document']; 6 | 7 | global.document = jsdom(''); 8 | global.window = document.defaultView; 9 | Object.keys(document.defaultView).forEach((property) => { 10 | if (typeof global[property] === 'undefined') { 11 | exposedProperties.push(property); 12 | global[property] = document.defaultView[property]; 13 | } 14 | }); 15 | 16 | global.navigator = { 17 | userAgent: 'node.js' 18 | }; 19 | global.__base = `${path.resolve()}/`; 20 | -------------------------------------------------------------------------------- /components/Slider/utils/index.js: -------------------------------------------------------------------------------- 1 | import hasStepDifference from './hasStepDifference'; 2 | import capitalize from './capitalize'; 3 | import formatNumber from './formatNumber'; 4 | import suppress from './suppress'; 5 | import isWithinRange from './isWithinRange'; 6 | import isEqual from './isEqual'; 7 | import removeClass from './removeClass'; 8 | import isVertical from './isVertical'; 9 | import formatValue from './formatValue'; 10 | 11 | export { 12 | capitalize, 13 | hasStepDifference, 14 | formatNumber, 15 | suppress, 16 | isWithinRange, 17 | isEqual, 18 | removeClass, 19 | isVertical, 20 | formatValue 21 | }; 22 | -------------------------------------------------------------------------------- /stories/CheckBox.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | import { Toggle } from '../components'; 4 | import Container from './Container'; 5 | 6 | storiesOf('Checkbox Button', module) 7 | .addDecorator((story) => {story()}) 8 | .add('with a text', () => ( 9 | 13 | )) 14 | .add('with no text', () => ( 15 | 22 | )); 23 | -------------------------------------------------------------------------------- /test/Slider/utils/isEqual.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, __base } = global; 2 | const { isEqual } = require(`${__base}components/Slider/utils/`); 3 | import { expect } from 'chai'; 4 | 5 | describe('isArrayEqual Method', () => { 6 | it('should return true if two arrays are exactly same', () => { 7 | const array1 = [2, 'hello']; 8 | const array2 = [2, 'hello']; 9 | expect(isEqual(array1, array2)).to.equal(true); 10 | 11 | const array3 = ['2', 'hello']; 12 | expect(isEqual(array1, array3)).to.equal(false); 13 | 14 | const array4 = [2, 'hello', 2]; 15 | expect(isEqual(array1, array4)).to.equal(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /stories/InputRange.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@kadira/storybook'; 3 | import { InputRange } from '../components'; 4 | 5 | const list = [{ 6 | id: 1, 7 | title: 'The Great Gatsby', 8 | author: 'F. Scott Fitzgerald' 9 | }, { 10 | id: 2, 11 | title: 'The DaVinci Code', 12 | author: 'Dan Brown' 13 | }, { 14 | id: 3, 15 | title: 'Angels & Demons', 16 | author: 'Dan Brown' 17 | }]; 18 | 19 | const suggestions = [list, list]; 20 | 21 | storiesOf('InputRange', module) 22 | .add('Basic', () => ( 23 | 27 | )); 28 | -------------------------------------------------------------------------------- /test/Slider/utils/hasStepDifference.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, __base } = global; 2 | const { hasStepDifference } = require(__base + 'components/Slider/utils/'); 3 | import { expect } from 'chai'; 4 | 5 | describe('hasStepDifference method', () => { 6 | it('should tell whether the lower and upper value difference is multiple of steps or > step', 7 | () => { 8 | const oldValue = 6; 9 | const newValue = 9; 10 | expect(hasStepDifference(newValue, oldValue, 2)).to.equal(true); 11 | expect(hasStepDifference(newValue, oldValue, 3)).to.equal(true); 12 | expect(hasStepDifference(2, 1, 2)).to.equal(false); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /docs/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | React Storybook 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /stories/Count.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | 4 | import { Count } from '../components'; 5 | import Container from './Container'; 6 | 7 | storiesOf('Count', module) 8 | .addDecorator((story) => {story()}) 9 | .add('Basic', () => ( 10 | 11 | )) 12 | .add('Disabled', () => ( 13 | 14 | )) 15 | .add('Define Range', () => ( 16 | 17 | )) 18 | .add('Stepped change', () => ( 19 | 20 | )); 21 | -------------------------------------------------------------------------------- /test/Slider/utils/formatNumber.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, __base } = global; 2 | const { formatNumber } = require(`${__base}components/Slider/utils/`); 3 | import { expect } from 'chai'; 4 | 5 | describe('formatNumber', () => { 6 | it('should return nearest valid integer if min = 0', () => { 7 | expect(formatNumber(24.6, 1, 0)).to.equal(25); 8 | expect(formatNumber(24.6, 2, 0)).to.equal(24); 9 | expect(formatNumber(24.8, 3, 0)).to.equal(24); 10 | }); 11 | 12 | it('should return nearest valid integer if min != 0', () => { 13 | expect(formatNumber(25.8796, 2, 1)).to.equal(25); 14 | expect(formatNumber(25.4, 3, 2)).to.equal(26); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /stories/Switch.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | import { Toggle } from '../components'; 4 | import Container from './Container'; 5 | 6 | storiesOf('Switch Button') 7 | .addDecorator((story) => {story()}) 8 | .add('Basic', () => ( 9 | 10 | )) 11 | .add('Label and count', () => ( 12 | 17 | )) 18 | .add('Icon Label', () => ( 19 | 23 | )); 24 | -------------------------------------------------------------------------------- /components/Slider/utils/formatValue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the new value Array or string based on which control is changed and 3 | * type of slider 4 | * @param currentVal : current value of the slider (Array | Number) 5 | * @param newValue : value of one control (Number) 6 | * @param changed : 'lower' or 'upper' 7 | * @param type : 'range' or 'value' 8 | * @returns {*} 9 | */ 10 | export default function (currentVal, newValue, changed, type) { 11 | let val = newValue; 12 | if (type === 'range') { 13 | if (typeof newValue === 'object') return newValue; 14 | val = (changed === 'lower') ? [newValue, currentVal[1]] : [currentVal[0], newValue]; 15 | } 16 | return val; 17 | } 18 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from '@kadira/storybook'; 2 | import './base.scss'; 3 | import centered from '@kadira/react-storybook-decorator-centered'; 4 | 5 | addDecorator(centered); 6 | 7 | function loadStories () { 8 | require('../stories/Switch.story'); 9 | require('../stories/Radio.story'); 10 | require('../stories/Slider.range.story.js'); 11 | require('../stories/CheckBox.story'); 12 | require('../stories/AutoComplete.story'); 13 | require('../stories/Count.story'); 14 | require('../stories/InputRange.story'); 15 | require('../stories/Group.story'); 16 | require('../stories/Slider.value.story.js'); 17 | } 18 | 19 | configure(loadStories, module); 20 | -------------------------------------------------------------------------------- /mdDocs/InputRange.md: -------------------------------------------------------------------------------- 1 | `InputRange` (component) 2 | ======================== 3 | 4 | 5 | 6 | Props 7 | ----- 8 | 9 | prop name | isRequired | type 10 | -------|------|------ 11 | [disabled](#disabled)| |`bool` 12 | [name](#name)| |`string` 13 | [onSelect](#onselect)|✔️|`func` 14 | [placeholders](#placeholders)| |`arrayOf[object Object]` 15 | [suggestions](#suggestions)| |`array` 16 | ### `disabled` 17 | type: `bool` 18 | defaultValue: `false` 19 | 20 | 21 | 22 | ### `name` 23 | type: `string` 24 | 25 | 26 | 27 | ### `onSelect` (required) 28 | type: `func` 29 | defaultValue: `noop` 30 | 31 | 32 | 33 | ### `placeholders` 34 | type: `arrayOf[object Object]` 35 | 36 | 37 | 38 | ### `suggestions` 39 | type: `array` 40 | 41 | 42 | -------------------------------------------------------------------------------- /components/utils/debounce.js: -------------------------------------------------------------------------------- 1 | // Returns a function, that, as long as it continues to be invoked, will not 2 | // be triggered. The function will be called after it stops being called for 3 | // N milliseconds. If `immediate` is passed, trigger the function on the 4 | // leading edge, instead of the trailing. 5 | export default function (func, wait, immediate) { 6 | let timeout; 7 | return function ({ ...args }) { 8 | const context = this; 9 | const later = () => { 10 | timeout = null; 11 | if (!immediate) func.apply(context, args); 12 | }; 13 | const callNow = immediate && !timeout; 14 | clearTimeout(timeout); 15 | timeout = setTimeout(later, wait); 16 | if (callNow) func.apply(context, args); 17 | }; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /components/Slider/utils/formatNumber.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the closest integer value to the value calculated by cursor position 3 | * @param value non-formatted value calculated by cursor position 4 | * @param step step difference allowed in the slider 5 | * @param min minimum value on the slider 6 | * @returns {*} the nearest rounded valid number 7 | * eg: if value = 24.6 and step = 2 the closest valid number is 24 and not 25 since 8 | * the next valid number is 26 which is farther than 24. 9 | */ 10 | export default function (value, step, min) { 11 | const remainder = value % step; 12 | const prevNumber = (value - remainder) + min; 13 | const nextNumber = prevNumber + step; 14 | return (value - prevNumber) >= (nextNumber - value) ? nextNumber : prevNumber; 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "plugins": [ 10 | "react" 11 | ], 12 | "globals": { 13 | "jest": false 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true, 17 | "globalReturn": false 18 | }, 19 | "rules": { 20 | "comma-dangle" : [1, "never"], 21 | "func-names" : 0, 22 | "key-spacing" : 0, 23 | "space-before-function-paren" : [2, "always"], 24 | "no-else-return" : 0, 25 | "no-multi-spaces" : 0, 26 | "quotes" : [2, "single"], 27 | "jsx-quotes" : [2, "prefer-single"], 28 | "one-var" : 0, 29 | "react/prefer-template" : 0, 30 | "no-underscore-dangle" : 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/Slider/utils/isWithinRange.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, __base } = global; 2 | const { isWithinRange } = require(`${__base}components/Slider/utils/`); 3 | import { expect } from 'chai'; 4 | 5 | describe('isWithinRange utility method', () => { 6 | const props = { 7 | trackLength : 400, 8 | min: 5, 9 | max: 15, 10 | step: 3 11 | }; 12 | 13 | it('should tell if the value is within range for a number', () => { 14 | expect(isWithinRange(props, 8)).to.equal(true); 15 | expect(isWithinRange(props, 16)).to.equal(false); 16 | expect(isWithinRange(props, 4)).to.equal(false); 17 | }); 18 | 19 | it('should tell if the values are withing range and following steps for Array', () => { 20 | expect(isWithinRange(props, [8, 11])).to.equal(true); 21 | expect(isWithinRange(props, [4, 14])).to.equal(false); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /components/AutoComplete/Suggestions.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export default function Suggestions (props) { 5 | const resultsTemplate = props.results.map((val, i) => 6 | props.resultsTemplate(val, i, props.selectedIndex)); 7 | return ( 8 |
9 | {resultsTemplate} 10 |
11 | ); 12 | } 13 | 14 | Suggestions.defaultResultsTemplate = function (val, i, selectedIndex) { 15 | const className = classNames('ac-suggestion', { 16 | 'ac-suggestion-active': i === selectedIndex 17 | }); 18 | return
{val.title}
; 19 | }; 20 | 21 | Suggestions.propTypes = { 22 | results: PropTypes.array.isRequired, 23 | resultsTemplate: PropTypes.func, 24 | selectedIndex: PropTypes.number 25 | }; 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /components/Count/Count.scss: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | 3 | .rf-count { 4 | width: $count-width; 5 | background-color: #f6f6f6; 6 | padding: $count-padding; 7 | overflow: auto; 8 | 9 | &.disabled{ 10 | opacity: 0.8; 11 | pointer-events: none; 12 | } 13 | } 14 | 15 | .count-button { 16 | border: 1px solid #e5e5e5; 17 | background-color: #ffffff; 18 | outline: none; 19 | border-radius: 2px; 20 | padding: 8px 10px; 21 | color: $primary-color; 22 | display: inline-block; 23 | vertical-align: top; 24 | float: left; 25 | height: $count-height - 2*$count-padding; 26 | width: $count-height - 2*$count-padding; 27 | cursor: pointer; 28 | } 29 | 30 | .count-value { 31 | display: inline-block; 32 | vertical-align: top; 33 | line-height: 32px; 34 | float: left; 35 | overflow: hidden; 36 | width: $count-width - 2*($count-height - 2*$count-padding); 37 | text-align: center; 38 | } 39 | -------------------------------------------------------------------------------- /components/Slider/helpers/getRelativePosition.js: -------------------------------------------------------------------------------- 1 | import { capitalize } from '../utils'; 2 | import constants from '../constants'; 3 | 4 | export default function (e, props, sliderWidth) { 5 | // Get the offset DIRECTION relative to the viewport 6 | 7 | const coordinate = constants[props.orientation].coordinate; 8 | const direction = constants[props.orientation].direction; 9 | const ucCoordinate = capitalize(coordinate); 10 | const trackPos = props.trackOffset[direction]; 11 | 12 | let btnPos = 0; 13 | 14 | if (typeof e[`page${ucCoordinate}`] !== 'undefined') { 15 | btnPos = e[`page${ucCoordinate}`]; 16 | } else if (e && typeof e[`client${ucCoordinate}`] !== 'undefined') { 17 | btnPos = e[`client${ucCoordinate}`]; 18 | } else if (e.touches && e.touches[0] && 19 | typeof e.touches[0][`client${ucCoordinate}`] !== 'undefined') { 20 | btnPos = e.touches[0][`client${ucCoordinate}`]; 21 | } 22 | 23 | return btnPos - trackPos - sliderWidth / 2; 24 | } 25 | -------------------------------------------------------------------------------- /stories/Slider.value.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | import { Slider } from '../components'; 4 | import Container from './Container'; 5 | 6 | storiesOf('Slider Component (Value)', module) 7 | .addDecorator((story) => ( 8 | 13 | {story()} 14 | )) 15 | .add('Default', () => ( 16 | 17 | )) 18 | .add('Read only', () => ( 19 | 20 | )) 21 | .add('Disabled', () => ( 22 | 23 | )) 24 | .add('Steps', () => ( 25 | 26 | )) 27 | .add('Vertical', () => ( 28 | 35 | )); 36 | -------------------------------------------------------------------------------- /stories/Radio.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | import { Toggle } from '../components'; 4 | import Container from './Container'; 5 | 6 | storiesOf('Radio Button', module) 7 | .addDecorator((story) => {story()}) 8 | .add('Default', () => ( 9 | 14 | )) 15 | .add('Disabled', () => ( 16 | 23 | )) 24 | .add('Tag', () => ( 25 | 32 | )) 33 | .add('Tag Disabled', () => ( 34 | 42 | )); 43 | -------------------------------------------------------------------------------- /mdDocs/Group.md: -------------------------------------------------------------------------------- 1 | `Group` (component) 2 | =================== 3 | 4 | 5 | 6 | Props 7 | ----- 8 | 9 | prop name | isRequired | type 10 | -------|------|------ 11 | [attributes](#attributes)| |`object` 12 | [className](#classname)| |`string` 13 | [id](#id)| |`string` 14 | [mode](#mode)| |`enum('normal'|'tag')` 15 | [name](#name)|✔️|`string` 16 | [onChange](#onchange)|✔️|`func` 17 | [selectedIds](#selectedids)| |`array` 18 | [type](#type)| |`enum('radio'|'checkbox'|'switch')` 19 | [value](#value)| |`arrayOf[object Object]` 20 | ### `attributes` 21 | type: `object` 22 | 23 | 24 | 25 | ### `className` 26 | type: `string` 27 | 28 | 29 | 30 | ### `id` 31 | type: `string` 32 | defaultValue: `'id'` 33 | 34 | 35 | 36 | ### `mode` 37 | type: `enum('normal'|'tag')` 38 | 39 | 40 | 41 | ### `name` (required) 42 | type: `string` 43 | 44 | 45 | 46 | ### `onChange` (required) 47 | type: `func` 48 | 49 | 50 | 51 | ### `selectedIds` 52 | type: `array` 53 | 54 | 55 | 56 | ### `type` 57 | type: `enum('radio'|'checkbox'|'switch')` 58 | 59 | 60 | 61 | ### `value` 62 | type: `arrayOf[object Object]` 63 | 64 | 65 | -------------------------------------------------------------------------------- /stories/Container.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | 3 | export default class Container extends Component { 4 | constructor (props) { 5 | super(props); 6 | this.state = { 7 | value: props.value 8 | }; 9 | this.handleChange = this.handleChange.bind(this); 10 | } 11 | 12 | handleChange (data) { 13 | this.setState({ 14 | value: data.value 15 | }, () => { 16 | this.props.action('data changed')(data); 17 | }); 18 | } 19 | 20 | render () { 21 | const self = this; 22 | const children = React.Children.map(this.props.children, (child) => ( 23 | React.cloneElement(child, { 24 | value: self.state.value, 25 | onChange: self.handleChange 26 | })) 27 | ); 28 | 29 | return ( 30 |
31 | {children} 32 |
33 | ); 34 | } 35 | } 36 | 37 | Container.propTypes = { 38 | action: PropTypes.func, 39 | value: PropTypes.oneOfType( 40 | [PropTypes.array, PropTypes.bool] 41 | ), 42 | children: PropTypes.element, 43 | className: PropTypes.string 44 | }; 45 | -------------------------------------------------------------------------------- /stories/Slider.range.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | import { Slider } from '../components'; 4 | import Container from './Container'; 5 | 6 | storiesOf('Slider Component (Range)', module) 7 | .addDecorator((story) => ( 8 | 13 | {story()} 14 | )) 15 | .add('Default', () => ( 16 | 17 | )) 18 | .add('Read Only', () => ( 19 | 20 | )) 21 | .add('Disabled', () => ( 22 | 23 | )) 24 | .add('Steps', () => ( 25 | 26 | )) 27 | .add('Vertical', () => ( 28 | 36 | )); 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ritesh Kumar 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/AutoComplete/Tag.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import autoBind from '../utils/autoBind'; 4 | 5 | export default class Tag extends Component { 6 | constructor (props) { 7 | super(props); 8 | 9 | autoBind([ 10 | 'handleRemove' 11 | ], this); 12 | } 13 | 14 | handleRemove () { 15 | this.props.onRemove({ 16 | id: this.props.id, 17 | text: this.props.text 18 | }); 19 | } 20 | 21 | render () { 22 | const { showRemove, id, text } = this.props; 23 | return ( 24 |
25 | {text} 26 | {showRemove && 27 | 31 | ✕ 32 | 33 | } 34 |
35 | ); 36 | } 37 | } 38 | 39 | Tag.propTypes = { 40 | id: PropTypes.oneOfType([ 41 | PropTypes.number, PropTypes.string 42 | ]), 43 | onRemove: PropTypes.func, 44 | showRemove: PropTypes.bool, 45 | text: PropTypes.string.isRequired 46 | }; 47 | -------------------------------------------------------------------------------- /scripts/buildDocs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example script expects a JSON blob generated by react-docgen as input, 3 | * e.g. react-docgen components/* | buildDocs.sh 4 | */ 5 | 6 | var fs = require('fs'); 7 | var generateMarkdown = require('./generateMarkdown'); 8 | var path = require('path'); 9 | 10 | var json = ''; 11 | process.stdin.setEncoding('utf8'); 12 | process.stdin.on('readable', function() { 13 | var chunk = process.stdin.read(); 14 | if (chunk !== null) { 15 | json += chunk; 16 | } 17 | }); 18 | 19 | process.stdin.on('end', function() { 20 | buildDocs(JSON.parse(json)); 21 | }); 22 | 23 | function buildDocs(api) { 24 | // api is an object keyed by filepath. We use the file name as component name. 25 | for (var filepath in api) { 26 | var name = getComponentName(filepath); 27 | var markdown = generateMarkdown(name, api[filepath]); 28 | fs.writeFileSync('mdDocs/' + name + '.md', markdown); 29 | process.stdout.write(filepath + ' -> ' + name + '.md\n'); 30 | } 31 | } 32 | 33 | function getComponentName(filepath) { 34 | var name = path.basename(filepath); 35 | var ext; 36 | while ((ext = path.extname(name))) { 37 | name = name.substring(0, name.length - ext.length); 38 | } 39 | return name; 40 | } 41 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Storybook 8 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /stories/Group.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | 4 | import { Group } from '../components'; 5 | import Container from './Container'; 6 | 7 | const value = [{ 8 | id: 1, 9 | label: 'a', 10 | count: 6 11 | }, { 12 | id: 2, 13 | label: 'b' 14 | }, { 15 | id: 3, 16 | label: 'c' 17 | }]; 18 | 19 | const value2 = [{ 20 | id: 1, 21 | label: 'a', 22 | count: 6 23 | }, { 24 | id: 2, 25 | label: 'b' 26 | }, { 27 | id: 3, 28 | label: 'c' 29 | }]; 30 | 31 | storiesOf('Group Button', module) 32 | .addDecorator((story) => {story()}) 33 | .add('Switch', () => ( 34 | 39 | )) 40 | .add('Radio', () => ( 41 | 46 | )) 47 | .add('Checkbox', () => ( 48 | 53 | )) 54 | .add('Radio Tag', () => ( 55 | 61 | )) 62 | .add('Checkbox Tag', () => ( 63 | 69 | )); 70 | -------------------------------------------------------------------------------- /components/icons.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'react-filters'; 3 | src: url('../assets/fonts/reactfilters.eot'); 4 | src: url('../assets/fonts/reactfilters.eot?#iefix') format('embedded-opentype'), 5 | url('../assets/fonts/reactfilters.ttf') format('truetype'), 6 | url('../assets/fonts/reactfilters.woff') format('woff'), 7 | url('../assets/fonts/reactfilters.svg') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'react-filters' !important; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .icon-add:before { 28 | content: "\e145"; 29 | } 30 | 31 | .icon-arrow-drop-down:before { 32 | content: "\e5c5"; 33 | } 34 | 35 | .icon-arrow-drop-up:before { 36 | content: "\e5c7"; 37 | } 38 | 39 | .icon-cancel:before { 40 | content: "\e5c9"; 41 | } 42 | 43 | .icon-check-box:before { 44 | content: "\e834"; 45 | } 46 | 47 | .icon-check-box-outline-blank:before { 48 | content: "\e835"; 49 | } 50 | 51 | .icon-check-circle:before { 52 | content: "\e86c"; 53 | } 54 | 55 | .icon-radio-button-unchecked:before { 56 | content: "\e836"; 57 | } 58 | 59 | .icon-radio-button-checked:before { 60 | content: "\e837"; 61 | } 62 | 63 | .icon-remove:before { 64 | content: "\e15b"; 65 | } 66 | -------------------------------------------------------------------------------- /components/Slider/helpers/getNearestValue.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | import { capitalize, isVertical } from '../utils'; 3 | 4 | /** 5 | * Returns the nearest value that can be obtained after clicking on a 6 | * particular position on the track. Technically finds the nearest 7 | * slider (upper or lower) and changes the value based on whether the lower or upper 8 | * slider should move to that position. 9 | * @param e [Synthetic Event] 10 | * @param props React Props 11 | * @param trackOffset cached track.getBoundingClientRect() 12 | * @returns {*} 13 | */ 14 | export default function (e, props, trackOffset) { 15 | const { value, max, min, step, type, orientation } = props; 16 | 17 | let relativeOffset = e[`page${capitalize(constants[orientation].coordinate)}`] 18 | - trackOffset[constants[orientation].direction]; 19 | 20 | if (isVertical(orientation)) relativeOffset = trackOffset.height - relativeOffset; 21 | 22 | const positionOffset = trackOffset[constants[orientation].dimension] / (max - min); 23 | const nearestIntegerValue = Math.round(relativeOffset / positionOffset); 24 | const nearestValue = nearestIntegerValue - (nearestIntegerValue % step); 25 | if (type === 'range') { 26 | const distancesFromValues = [ 27 | Math.abs(nearestValue - value[0]), 28 | Math.abs(nearestValue - value[1]) 29 | ]; 30 | return distancesFromValues[0] < distancesFromValues[1] ? ({ 31 | changed: 'lower', 32 | value: [nearestValue, value[1]] 33 | }) : ({ 34 | changed: 'upper', 35 | value: [value[0], nearestValue] 36 | }); 37 | } else { 38 | return { value: nearestValue }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/utils/debounce.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, before, after, __base } = global; 2 | import { expect } from 'chai'; 3 | import sinon from 'sinon'; 4 | 5 | const debounce = require(`${__base}components/utils/debounce`).default; 6 | 7 | describe('debounce()', () => { 8 | it('shouldnt execute immediately if immediate is not passed', () => { 9 | const callback = sinon.spy(); 10 | 11 | const fn = debounce(callback, 100); 12 | 13 | expect(callback.calledOnce).to.equal(false); 14 | fn(); 15 | expect(callback.calledOnce).to.equal(false); 16 | }); 17 | 18 | let clock; 19 | 20 | before(() => (clock = sinon.useFakeTimers())); 21 | after(() => (clock.restore())); 22 | 23 | it('should execute the function after delay', () => { 24 | const callback = sinon.spy(); 25 | 26 | const fn = debounce(callback, 100); 27 | 28 | fn(); 29 | 30 | clock.tick(99); 31 | expect(callback.calledOnce).to.equal(false); 32 | 33 | clock.tick(1); 34 | expect(callback.calledOnce).to.equal(true); 35 | }); 36 | 37 | it('should execute at start if immediate flag is passed', () => { 38 | const callback = sinon.spy(); 39 | const fn = debounce(callback, 100, true); 40 | 41 | fn(); 42 | 43 | expect(callback.calledOnce).to.equal(true); 44 | }); 45 | 46 | it('should be called once in interval', () => { 47 | const callback = sinon.spy(); 48 | const fn = debounce(callback, 100); 49 | 50 | fn(); 51 | fn(); 52 | fn(); 53 | clock.tick(99); 54 | expect(callback.calledOnce).to.equal(false); 55 | 56 | clock.tick(1); 57 | fn(); 58 | fn(); 59 | fn(); 60 | 61 | expect(callback.calledOnce).to.equal(true); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /components/InputRange/InputRange.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import autoBind from '../utils/autoBind'; 4 | import noop from '../utils/noop'; 5 | 6 | import AutoComplete from '../AutoComplete/AutoComplete'; 7 | 8 | export default class InputRange extends Component { 9 | constructor (props) { 10 | super(props); 11 | this.state = {}; 12 | 13 | autoBind([ 14 | 'onSelect' 15 | ], this); 16 | } 17 | 18 | onSelect (name, selected) { 19 | this.props.onSelect(name, selected); 20 | } 21 | 22 | render () { 23 | const { name, disabled, suggestions, placeholders } = this.props; 24 | const mainClass = classNames('react-filters', 'rf-input-range', name, { disabled }); 25 | return ( 26 |
27 | 34 | - 35 | 42 |
43 | ); 44 | } 45 | } 46 | 47 | InputRange.propTypes = { 48 | disabled: PropTypes.bool, 49 | name: PropTypes.string, 50 | onSelect: PropTypes.func.isRequired, 51 | placeholders: PropTypes.arrayOf(PropTypes.string), 52 | suggestions: PropTypes.array 53 | }; 54 | 55 | InputRange.defaultProps = { 56 | disabled: false, 57 | onSelect: noop 58 | }; 59 | -------------------------------------------------------------------------------- /mdDocs/Count.md: -------------------------------------------------------------------------------- 1 | `Count` (component) 2 | =================== 3 | 4 | 5 | 6 | Props 7 | ----- 8 | 9 | prop name | isRequired | type 10 | -------|------|------ 11 | [decrementElement](#decrementelement)| |`func` 12 | [disabled](#disabled)| |`bool` 13 | [incrementElement](#incrementelement)| |`func` 14 | [max](#max)| |`number` 15 | [min](#min)| |`number` 16 | [name](#name)|✔️|`string` 17 | [onChange](#onchange)|✔️|`func` 18 | [prefix](#prefix)| |`string` 19 | [step](#step)| |`number` 20 | [suffix](#suffix)| |`string` 21 | [value](#value)| |`number` 22 | ### `decrementElement` 23 | type: `func` 24 | 25 | defaultValue: 26 | ```js 27 | function() { 28 | return ( 29 | 32 | ); 33 | } 34 | ``` 35 | 36 | 37 | ### `disabled` 38 | type: `bool` 39 | defaultValue: `false` 40 | 41 | 42 | 43 | ### `incrementElement` 44 | type: `func` 45 | 46 | defaultValue: 47 | ```js 48 | function() { 49 | return ( 50 | 53 | ); 54 | } 55 | ``` 56 | 57 | 58 | ### `max` 59 | type: `number` 60 | defaultValue: `Number.POSITIVE_INFINITY` 61 | 62 | 63 | 64 | ### `min` 65 | type: `number` 66 | defaultValue: `Number.NEGATIVE_INFINITY` 67 | 68 | 69 | 70 | ### `name` (required) 71 | type: `string` 72 | 73 | 74 | 75 | ### `onChange` (required) 76 | type: `func` 77 | 78 | 79 | 80 | ### `prefix` 81 | type: `string` 82 | 83 | 84 | 85 | ### `step` 86 | type: `number` 87 | defaultValue: `1` 88 | 89 | 90 | 91 | ### `suffix` 92 | type: `string` 93 | 94 | 95 | 96 | ### `value` 97 | type: `number` 98 | defaultValue: `0` 99 | 100 | 101 | -------------------------------------------------------------------------------- /components/Slider/Rail.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { isVertical } from './utils'; 3 | import constants from './constants'; 4 | 5 | /** 6 | * Returns rail's position value of `left` for horizontal slider and `top` 7 | * for vertical slider 8 | * @param value 9 | * @param min 10 | * @param max 11 | * @param orientation 12 | * @returns {number} Value in Percentage 13 | */ 14 | function getDirectionPositionForRange (value, min, max, orientation) { 15 | return isVertical(orientation) ? ( 16 | // as upper value is used to calculate `top`; 17 | Math.round(((max - value[1]) / max - min) * 100) 18 | ) : ( 19 | Math.round((value[0] / max - min) * 100) 20 | ); 21 | } 22 | 23 | export default function Rail (props) { 24 | const { value, min, max, orientation, isRangeType } = props; 25 | 26 | const difference = isRangeType ? (value[1] - value[0]) : value; 27 | const dimensionValue = (difference / (max - min)) * 100; 28 | 29 | const directionValue = getDirectionPositionForRange(value, min, max, orientation); 30 | 31 | const railStyle = { 32 | [constants[orientation].direction]: `${directionValue}%`, 33 | [constants[orientation].dimension]: `${dimensionValue}%` 34 | }; 35 | 36 | if (!isRangeType) { 37 | railStyle[isVertical(orientation) ? 'bottom' : 'left'] = 0; 38 | if (isVertical(orientation)) { 39 | railStyle.top = `${100 - dimensionValue}%`; 40 | } else { 41 | railStyle.left = 0; 42 | } 43 | } 44 | 45 | return
; 46 | } 47 | 48 | Rail.propTypes = { 49 | isRangeType: PropTypes.bool.isRequired, 50 | max: PropTypes.number.isRequired, 51 | min: PropTypes.number.isRequired, 52 | orientation: PropTypes.string.isRequired, 53 | value: PropTypes.array.isRequired 54 | }; 55 | -------------------------------------------------------------------------------- /components/AutoComplete/AutoComplete.scss: -------------------------------------------------------------------------------- 1 | .rf-autocomplete { 2 | position: relative; 3 | } 4 | 5 | .ac-searchbox { 6 | outline: none; 7 | border-radius: 2px; 8 | border: 1px solid #eee; 9 | width: 100%; 10 | box-sizing: border-box; 11 | will-change: border-color; 12 | transition: border-color .2s ease-in; 13 | background-color: #ffffff; 14 | &:focus { 15 | border: 1px solid deepskyblue; 16 | } 17 | .disabled & { 18 | pointer-events: none; 19 | background-color: #fafafa; 20 | } 21 | &.tags{ 22 | padding-left: 10px; 23 | } 24 | } 25 | 26 | .ac-searchbox-input{ 27 | width: 100%; 28 | padding: 10px; 29 | font-size: 12px; 30 | line-height: 14px; 31 | border:none; 32 | outline: none; 33 | font-family: inherit; 34 | box-sizing: border-box; 35 | background-color: transparent; 36 | .tags &{ 37 | display: inline-block; 38 | max-width: 150px; 39 | vertical-align: top; 40 | padding-left: 0; 41 | } 42 | } 43 | 44 | .ac-suggestions-wrapper { 45 | border: 1px solid #eee; 46 | border-top: 0; 47 | background-color: #fff; 48 | position: absolute; 49 | width: 100%; 50 | box-sizing: border-box; 51 | } 52 | 53 | .ac-suggestion { 54 | font-size: 12px; 55 | line-height: 30px; 56 | padding: 0 10px; 57 | color: #666; 58 | border-bottom: 1px solid #eeeeee; 59 | &:last-of-type { 60 | border-bottom: none; 61 | } 62 | &.ac-suggestion-active { 63 | background-color: #fafafa; 64 | } 65 | } 66 | 67 | .ac-reset { 68 | position: absolute; 69 | right: 10px; 70 | top: 8px; 71 | cursor: pointer; 72 | } 73 | 74 | .ac-tag-wrapper{ 75 | display: inline-block; 76 | padding: 6px 0; 77 | float: left; 78 | font-size: 12px; 79 | } 80 | 81 | .ac-tag { 82 | display: inline-block; 83 | background-color: cornflowerblue; 84 | color: #fff; 85 | border-radius: 2px; 86 | padding: 0 5px; 87 | line-height: 22px; 88 | margin-right: 5px; 89 | } 90 | 91 | .ac-tag-remove { 92 | padding-left: 5px; 93 | cursor: pointer; 94 | border-left: 1px solid #a5c5ff; 95 | margin-left: 5px; 96 | } 97 | -------------------------------------------------------------------------------- /assets/fonts/reactfilters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/0ab516c6e00c001e01c647f19720b2e8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /components/Slider/Steps.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | import constants from './constants'; 4 | import { isVertical } from './utils'; 5 | 6 | /** 7 | * Tells whether a particular step comes in between two controls or not 8 | * @param stepValue value of the position where this step is located 9 | * @param value Array of control values 10 | * @param isRangeType 11 | * @returns {boolean} 12 | */ 13 | function isInActiveRange (stepValue, value, isRangeType) { 14 | if (isRangeType) { 15 | return stepValue > value[0] && stepValue < value[1]; 16 | } else { 17 | return stepValue < value; 18 | } 19 | } 20 | 21 | /** 22 | * Returns the step position in percentage 23 | * @param stepValue value of the position where this step is located 24 | * @param min minimum value of slider 25 | * @param max maximum value of slider 26 | * @returns {number} 27 | */ 28 | function getPositionInPercentage (stepValue, min, max) { 29 | return (stepValue / (max - min)) * 100; 30 | } 31 | 32 | 33 | /** 34 | * Array of step elements placed side by side 35 | * @param props 36 | * @returns {Array} 37 | */ 38 | function getSteps (props) { 39 | const { step, min, max, value, isRangeType, orientation } = props; 40 | 41 | const steps = []; 42 | const totalSteps = ((max - min) / step) + 1; 43 | 44 | for (let i = 0; i < totalSteps; i++) { 45 | let position = getPositionInPercentage(i * step, min, max); 46 | if (isVertical(orientation)) position = 100 - position; 47 | 48 | const style = { [constants[orientation].direction]: `${position}%` }; 49 | 50 | const className = classNames('slider-step', { 51 | 'slider-step-active': isInActiveRange(i * step, value, isRangeType) 52 | }); 53 | steps.push(); 54 | } 55 | return steps; 56 | } 57 | 58 | export default function Steps (props) { 59 | return ( 60 |
61 | {getSteps(props)} 62 |
63 | ); 64 | } 65 | 66 | Steps.propTypes = { 67 | isRangeType: PropTypes.bool.isRequired, 68 | max: PropTypes.number.isRequired, 69 | min: PropTypes.number.isRequired, 70 | onClick: PropTypes.func.isRequired, 71 | orientation: PropTypes.string.isRequired, 72 | step: PropTypes.number.isRequired 73 | }; 74 | 75 | -------------------------------------------------------------------------------- /mdDocs/Slider.md: -------------------------------------------------------------------------------- 1 | `Slider` (component) 2 | ==================== 3 | 4 | 5 | 6 | Props 7 | ----- 8 | 9 | prop name | isRequired | type 10 | -------|------|------ 11 | [attributes](#attributes)| |`object` 12 | [disabled](#disabled)| |`bool` 13 | [max](#max)| |`number` 14 | [min](#min)| |`number` 15 | [name](#name)|✔️|`string` 16 | [onChange](#onchange)| |`func` 17 | [onDragEnd](#ondragend)| |`func` 18 | [onDragStart](#ondragstart)| |`func` 19 | [orientation](#orientation)| |`enum('horizontal'|'vertical')` 20 | [rangeTemplate](#rangetemplate)| |`func` 21 | [readOnly](#readonly)| |`bool` 22 | [showSteps](#showsteps)| |`bool` 23 | [step](#step)| |`number` 24 | [toolTipTemplate](#tooltiptemplate)| |`func` 25 | [type](#type)| |`enum('value'|'range')` 26 | [value](#value)| |`union(array|number)` 27 | ### `attributes` 28 | type: `object` 29 | defaultValue: `{}` 30 | 31 | 32 | 33 | ### `disabled` 34 | type: `bool` 35 | defaultValue: `false` 36 | 37 | 38 | 39 | ### `max` 40 | type: `number` 41 | defaultValue: `20` 42 | 43 | 44 | 45 | ### `min` 46 | type: `number` 47 | defaultValue: `0` 48 | 49 | 50 | 51 | ### `name` (required) 52 | type: `string` 53 | 54 | 55 | 56 | ### `onChange` 57 | type: `func` 58 | 59 | 60 | 61 | ### `onDragEnd` 62 | type: `func` 63 | defaultValue: `noop` 64 | 65 | 66 | 67 | ### `onDragStart` 68 | type: `func` 69 | defaultValue: `noop` 70 | 71 | 72 | 73 | ### `orientation` 74 | type: `enum('horizontal'|'vertical')` 75 | defaultValue: `'horizontal'` 76 | 77 | 78 | 79 | ### `rangeTemplate` 80 | type: `func` 81 | 82 | defaultValue: 83 | ```js 84 | function(min, max) { 85 | return ( 86 |
87 |
{min}
88 |
{max}
89 |
90 | ); 91 | } 92 | ``` 93 | 94 | 95 | ### `readOnly` 96 | type: `bool` 97 | defaultValue: `false` 98 | 99 | 100 | 101 | ### `showSteps` 102 | type: `bool` 103 | defaultValue: `false` 104 | 105 | 106 | 107 | ### `step` 108 | type: `number` 109 | defaultValue: `1` 110 | 111 | 112 | 113 | ### `toolTipTemplate` 114 | type: `func` 115 | 116 | defaultValue: 117 | ```js 118 | function(value) { 119 | return value; 120 | } 121 | ``` 122 | 123 | 124 | ### `type` 125 | type: `enum('value'|'range')` 126 | defaultValue: `'value'` 127 | 128 | 129 | 130 | ### `value` 131 | type: `union(array|number)` 132 | defaultValue: `[5, 10]` 133 | 134 | 135 | -------------------------------------------------------------------------------- /scripts/generateMarkdown.js: -------------------------------------------------------------------------------- 1 | function stringOfLength(string, length) { 2 | var newString = ''; 3 | for (var i = 0; i < length; i++) { 4 | newString += string; 5 | } 6 | return newString; 7 | } 8 | 9 | function generateTitle(name) { 10 | var title = '`' + name + '` (component)'; 11 | return title + '\n' + stringOfLength('=', title.length) + '\n'; 12 | } 13 | 14 | function generateDesciption(description) { 15 | return description + '\n'; 16 | } 17 | 18 | function generatePropType(type, includeType = true) { 19 | var values; 20 | if (Array.isArray(type.value)) { 21 | values = '(' + 22 | type.value.map(function(typeValue) { 23 | return typeValue.name || typeValue.value; 24 | }).join('|') + 25 | ')'; 26 | } else { 27 | values = type.value; 28 | } 29 | 30 | return (includeType ? 'type: `' : '`') + type.name + (values ? values: '') + '`\n'; 31 | } 32 | 33 | function generatePropDefaultValue(value) { 34 | return value.value.indexOf('function') >= 0 ? 35 | '\ndefaultValue: \n```js\n' + value.value + '\n```' : 'defaultValue: `' + value.value + '`\n'; 36 | } 37 | 38 | function generateProp(propName, prop) { 39 | return ( 40 | '### `' + propName + '`' + (prop.required ? ' (required)' : '') + '\n' + 41 | (prop.type ? generatePropType(prop.type) : '') + 42 | (prop.defaultValue ? generatePropDefaultValue(prop.defaultValue) : '') + 43 | '\n' + 44 | (prop.description ? prop.description + '\n\n' : '') + 45 | '\n' 46 | ); 47 | } 48 | 49 | function propTable (propName, prop) { 50 | return ( 51 | '[' +propName + '](#'+ propName.toLowerCase() +')' + '|' + (prop.required ? '✔️' : ' ') + '|' + (prop.type ? generatePropType(prop.type, false) : '\n') 52 | ) 53 | } 54 | 55 | function generateProps(props) { 56 | var title = 'Props'; 57 | 58 | return ( 59 | title + '\n' + 60 | stringOfLength('-', title.length) + '\n' + 61 | '\n' + 62 | 'prop name | isRequired | type\n-------|------|------\n' + 63 | Object.keys(props).sort().map(function (propName) { 64 | return propTable(propName, props[propName]) 65 | }).join('')+ 66 | Object.keys(props).sort().map(function(propName) { 67 | return generateProp(propName, props[propName]); 68 | }).join('\n') 69 | ); 70 | } 71 | 72 | function generateMarkdown(name, reactAPI) { 73 | return generateTitle(name) + '\n' + 74 | generateDesciption(reactAPI.description) + '\n' + 75 | generateProps(reactAPI.props); 76 | } 77 | 78 | module.exports = generateMarkdown; 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | Build Status 6 | 7 | 8 | npm 9 | 10 | 11 | 12 | Codecov 13 | 14 | 15 | Twitter 16 | 17 |

18 | 19 | > A collection of Components like autocomplete, radio, checkbox, slider etc. written in React. 20 | 21 | ##Installation 22 | ``` 23 | npm install --save react-filters 24 | ``` 25 | 26 | ##Basic Usage 27 | import the component you need to use. 28 | 29 | ```js 30 | import { Slider, Toggle } from 'react-filters/dist'; 31 | ``` 32 | 33 | If you don't want to use all the components and are concerned about file size, you can just import the component you need. 34 | 35 | ```js 36 | import Slider from 'react-filters/dist/Slider'; 37 | import Toggle from 'react-filters/dist/Toggle'; 38 | ``` 39 | 40 | ##Components 41 | 42 | Click on the component name to go to their documentation. 43 | 44 | - [x] [Toggle](mdDocs/Toggle.md) (Switch, Checkbox, Radio) 45 | - [x] [Slider](mdDocs/Slider.md) 46 | - [x] [Autocomplete](mdDocs/AutoComplete.md) 47 | - [x] [Input Range](mdDocs/InputRange.md) 48 | - [x] [Count](mdDocs/Count.md) 49 | - [ ] Select 50 | - [ ] Dropdown 51 | 52 | ##Development 53 | 1. Clone the repo 54 | 1. Create a new branch. 55 | 1. Run `npm install && npm run storybook` 56 | 1. You can find the server running at **localhost:9002** 57 | 1. Add feature or fix bug. Add tests if required. 58 | 1. if commit fails make sure that there's no linting error or failed test by running `npm run test && npm run lint` 59 | 60 | 61 | ##License 62 | MIT @ Ritesh Kumar 63 | -------------------------------------------------------------------------------- /mdDocs/AutoComplete.md: -------------------------------------------------------------------------------- 1 | `AutoComplete` (component) 2 | ========================== 3 | 4 | 5 | 6 | Props 7 | ----- 8 | 9 | prop name | isRequired | type 10 | -------|------|------ 11 | [Reset](#reset)| |`func` 12 | [async](#async)| |`bool` 13 | [className](#classname)| |`string` 14 | [debounce](#debounce)| |`number` 15 | [disabled](#disabled)| |`bool` 16 | [fuzzyOptions](#fuzzyoptions)| |`shape[object Object]` 17 | [list](#list)| |`array` 18 | [multiSelect](#multiselect)| |`bool` 19 | [name](#name)|✔️|`string` 20 | [onBlur](#onblur)| |`func` 21 | [onChange](#onchange)| |`func` 22 | [onFocus](#onfocus)| |`func` 23 | [onSelect](#onselect)|✔️|`func` 24 | [placeholder](#placeholder)| |`string` 25 | [resultsTemplate](#resultstemplate)| |`func` 26 | [showInitialResults](#showinitialresults)| |`bool` 27 | [showTagRemove](#showtagremove)| |`bool` 28 | [tags](#tags)| | 29 | [valueKey](#valuekey)| |`string` 30 | [width](#width)| |`number` 31 | ### `Reset` 32 | type: `func` 33 | 34 | 35 | 36 | ### `async` 37 | type: `bool` 38 | defaultValue: `false` 39 | 40 | 41 | 42 | ### `className` 43 | type: `string` 44 | 45 | 46 | 47 | ### `debounce` 48 | type: `number` 49 | defaultValue: `250` 50 | 51 | 52 | 53 | ### `disabled` 54 | type: `bool` 55 | defaultValue: `false` 56 | 57 | 58 | 59 | ### `fuzzyOptions` 60 | type: `shape[object Object]` 61 | defaultValue: `{ 62 | caseSensitive: false, 63 | shouldSort: true, 64 | sortFn (a, b) { 65 | return a.score - b.score; 66 | }, 67 | threshold: 0.6, 68 | tokenize: false, 69 | verbose: false, 70 | distance: 100, 71 | include: [], 72 | location: 0 73 | }` 74 | 75 | 76 | 77 | ### `list` 78 | type: `array` 79 | 80 | 81 | 82 | ### `multiSelect` 83 | type: `bool` 84 | defaultValue: `false` 85 | 86 | 87 | 88 | ### `name` (required) 89 | type: `string` 90 | 91 | 92 | 93 | ### `onBlur` 94 | type: `func` 95 | 96 | 97 | 98 | ### `onChange` 99 | type: `func` 100 | 101 | 102 | 103 | ### `onFocus` 104 | type: `func` 105 | 106 | 107 | 108 | ### `onSelect` (required) 109 | type: `func` 110 | 111 | 112 | 113 | ### `placeholder` 114 | type: `string` 115 | defaultValue: `'Search'` 116 | 117 | 118 | 119 | ### `resultsTemplate` 120 | type: `func` 121 | defaultValue: `Suggestions.defaultResultsTemplate` 122 | 123 | 124 | 125 | ### `showInitialResults` 126 | type: `bool` 127 | defaultValue: `false` 128 | 129 | 130 | 131 | ### `showTagRemove` 132 | type: `bool` 133 | defaultValue: `true` 134 | 135 | 136 | 137 | ### `tags` 138 | defaultValue: `false` 139 | 140 | 141 | 142 | ### `valueKey` 143 | type: `string` 144 | defaultValue: `'title'` 145 | 146 | 147 | 148 | ### `width` 149 | type: `number` 150 | defaultValue: `430` 151 | 152 | 153 | -------------------------------------------------------------------------------- /test/Count.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const { describe, it, __base } = global; 3 | import { shallow } from 'enzyme'; 4 | import sinon from 'sinon'; 5 | import { expect } from 'chai'; 6 | 7 | const { Count } = require(`${__base}components`); 8 | 9 | describe('Count Component', () => { 10 | it('should call onChange function with correct args on click', () => { 11 | const onChange = sinon.spy(); 12 | const wrapper = shallow( 13 | 18 | ); 19 | 20 | const args1 = { 21 | name: 'count', 22 | value: 6, 23 | action: 'increased' 24 | }; 25 | 26 | const args2 = { 27 | name: 'count', 28 | value: 4, 29 | action: 'decreased' 30 | }; 31 | 32 | wrapper.find('.cb-upper').simulate('click'); 33 | expect(onChange.calledWith(args1)).to.equal(true); 34 | wrapper.find('.cb-lower').simulate('click'); 35 | expect(onChange.calledWith(args2)).to.equal(true); 36 | 37 | expect(onChange.calledTwice).to.equal(true); 38 | }); 39 | 40 | it('should be inactive when disabled', () => { 41 | const onChange = sinon.spy(); 42 | const wrapper = shallow( 43 | 49 | ); 50 | 51 | wrapper.find('.cb-lower').simulate('click'); 52 | expect(onChange.calledOnce).to.equal(false); 53 | }); 54 | 55 | it('should only be active in the provided range', () => { 56 | const onChange = sinon.spy(); 57 | const wrapper = shallow( 58 | 65 | ); 66 | 67 | wrapper.find('.cb-upper').simulate('click'); 68 | expect(onChange.calledOnce).to.equal(false); 69 | 70 | wrapper.find('.cb-lower').simulate('click'); 71 | expect(onChange.calledOnce).to.equal(true); 72 | }); 73 | 74 | it('should change with the provided step', () => { 75 | const onChange = sinon.spy(); 76 | const wrapper = shallow( 77 | 83 | ); 84 | 85 | wrapper.find('.cb-upper').simulate('click'); 86 | const args1 = { 87 | name: 'count', 88 | value: 7, 89 | action: 'increased' 90 | }; 91 | 92 | expect(onChange.calledWith(args1)).to.equal(true); 93 | 94 | wrapper.find('.cb-lower').simulate('click'); 95 | const args2 = { 96 | name: 'count', 97 | value: 3, 98 | action: 'decreased' 99 | }; 100 | 101 | expect(onChange.calledWith(args2)).to.equal(true); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /components/Toggle/Toggle.scss: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | @import "../mixins"; 3 | 4 | $toggleContainerWidth: 200px !default; 5 | 6 | .rf-toggle { 7 | display: inline-block; 8 | i{ 9 | color: $tertiary-color; 10 | transition: all .3s ease-out; 11 | } 12 | &.toggle-active i{ 13 | color: $primary-color; 14 | } 15 | &.toggle-tag.toggle-disabled{ 16 | background-color: $secondary-color; 17 | border: darken($secondary-color, 35%); 18 | } 19 | } 20 | 21 | .toggle-wrapper { 22 | background-color: darken($secondary-color, 30%); 23 | width: $toggle-width; 24 | height: $toggle-height; 25 | border-radius: $toggle-height/2; 26 | padding: $toggle-padding; 27 | display: inline-block; 28 | position: relative; 29 | transition: all .3s ease-out; 30 | box-sizing: border-box; 31 | .toggle-active & { 32 | background-color: $primary-color; 33 | } 34 | .toggle-disabled & { 35 | background-color: grey; 36 | opacity: 0.5; 37 | pointer-events: none; 38 | } 39 | } 40 | 41 | .toggle-tag{ 42 | border: 1px solid $primary-color; 43 | border-radius: $toggle-height; 44 | cursor: pointer; 45 | &:hover{ 46 | background-color: $secondary-color; 47 | } 48 | &.toggle-active{ 49 | background-color: $primary-color; 50 | &:hover{ 51 | background-color: darken($primary-color, 5%); 52 | } 53 | .toggle-count, .toggle-label{ 54 | color: #ffffff; 55 | } 56 | } 57 | } 58 | 59 | .toggle-btn { 60 | height: $toggle-height - ($toggle-padding * 2); 61 | width: $toggle-height - ($toggle-padding * 2); 62 | position: absolute; 63 | background-color: $toggle-btn-color; 64 | border-radius: 50%; 65 | cursor: pointer; 66 | left: $toggle-padding; 67 | display: inline-block; 68 | transition: all .3s ease-out; 69 | transform: translate3d(0, 0, 0); 70 | .toggle-active & { 71 | left: 100%; 72 | margin-left: -($toggle-height - $toggle-padding); 73 | } 74 | } 75 | 76 | .toggle-label { 77 | display: inline-block; 78 | vertical-align: top; 79 | width: $toggleContainerWidth - $toggle-width; 80 | @include no-select(); 81 | .after { 82 | float: right; 83 | } 84 | .toggle-tag &{ 85 | text-align: center; 86 | padding: 6px 0; 87 | line-height: 20px; 88 | } 89 | } 90 | 91 | .toggle-count { 92 | color: $toggle-count-color; 93 | margin-left: 5px; 94 | } 95 | 96 | .toggle-icon-label { 97 | font-size: $toggle-label-font-size; 98 | line-height: $toggle-label-line-height; 99 | width: $toggle-width - ($toggle-height + 2 * $toggle-padding); 100 | text-align: center; 101 | color: white; 102 | &.toggle-il-right { 103 | float: right; 104 | padding-right: $toggle-padding; 105 | } 106 | &.toggle-il-left { 107 | float: left; 108 | padding-left: $toggle-padding; 109 | } 110 | } 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /mdDocs/Toggle.md: -------------------------------------------------------------------------------- 1 | `Toggle` (component) 2 | ==================== 3 | 4 | Hello world 5 | 6 | Props 7 | ----- 8 | 9 | prop name | isRequired | type 10 | -------|------|------ 11 | [attributes](#attributes)| |`object` 12 | [className](#classname)| |`string` 13 | [count](#count)| |`number` 14 | [countElem](#countelem)| |`union(func|element)` 15 | [disabled](#disabled)| |`bool` 16 | [iconElement](#iconelement)| |`func` 17 | [iconLabel](#iconlabel)| |`array` 18 | [label](#label)| |`string` 19 | [labelPosition](#labelposition)| |`enum('before'|'after')` 20 | [mode](#mode)| |`enum('normal'|'tag')` 21 | [name](#name)|✔️|`string` 22 | [onChange](#onchange)| |`func` 23 | [type](#type)| |`enum('switch'|'radio'|'checkbox')` 24 | [value](#value)|✔️|`bool` 25 | ### `attributes` 26 | type: `object` 27 | 28 | Sometimes you may need to add some custom attributes to the root tag of the 29 | component. attributes will accept an object where the key and values will 30 | be those attributes and their value respectively. 31 | 32 | Eg : If you pass 33 | ```js 34 | attributes = { 35 | 'data-attr1' : 'val1', 36 | 'data-attr2' : 'val2' 37 | } 38 | ``` 39 | the root tag will have the attributes `data-attr1` and `data-attr2` with the 40 | corresponding values as `val1` and `val2` respectively 41 | 42 | 43 | 44 | ### `className` 45 | type: `string` 46 | 47 | Optional className to be added to the root tag of the component 48 | 49 | 50 | 51 | ### `count` 52 | type: `number` 53 | 54 | In case you want to show aggregation/count in front of label then pass the 55 | number in this option. This is generally useful for showing the items present 56 | corresponding to that filter option. 57 | 58 | 59 | 60 | ### `countElem` 61 | type: `union(func|element)` 62 | 63 | defaultValue: 64 | ```js 65 | function(p) { 66 | return ({p.count}); 67 | } 68 | ``` 69 | 70 | 71 | ### `disabled` 72 | type: `bool` 73 | defaultValue: `false` 74 | 75 | Set to `true` if you want to disable the component interactions. 76 | 77 | 78 | 79 | ### `iconElement` 80 | type: `func` 81 | 82 | 83 | 84 | ### `iconLabel` 85 | type: `array` 86 | 87 | 88 | 89 | ### `label` 90 | type: `string` 91 | 92 | The label text present in the component. If this option is not set only the 93 | icon element will render. 94 | 95 | 96 | 97 | ### `labelPosition` 98 | type: `enum('before'|'after')` 99 | defaultValue: `'before'` 100 | 101 | 102 | 103 | ### `mode` 104 | type: `enum('normal'|'tag')` 105 | defaultValue: `'normal'` 106 | 107 | 108 | 109 | ### `name` (required) 110 | type: `string` 111 | 112 | 113 | 114 | ### `onChange` 115 | type: `func` 116 | defaultValue: `noop` 117 | 118 | 119 | 120 | ### `type` 121 | type: `enum('switch'|'radio'|'checkbox')` 122 | defaultValue: `'switch'` 123 | 124 | 125 | 126 | ### `value` (required) 127 | type: `bool` 128 | defaultValue: `false` 129 | 130 | 131 | -------------------------------------------------------------------------------- /stories/AutoComplete.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | import classNames from 'classnames'; 4 | import { AutoComplete } from '../components'; 5 | 6 | 7 | const list = [{ 8 | id: 1, 9 | title: 'The Great Gatsby', 10 | author: 'F. Scott Fitzgerald' 11 | }, { 12 | id: 2, 13 | title: 'The DaVinci Code', 14 | author: 'Dan Brown' 15 | }, { 16 | id: 3, 17 | title: 'Angels & Demons', 18 | author: 'Dan Brown' 19 | }]; 20 | 21 | class AutoCompleteContainer extends React.Component { 22 | constructor (props) { 23 | super(props); 24 | this.state = { 25 | data: [] 26 | }; 27 | 28 | this.onChange = this.onChange.bind(this); 29 | this.onSelect = this.onSelect.bind(this); 30 | } 31 | 32 | onChange (query) { 33 | const url = `https://buy.housing.com/api/v0/search/suggest/?&source=web&polygon_uuid=a0fd32816f73961748cf&cursor=1&string=${query}`; 34 | if (query.length) { 35 | fetch(url) 36 | .then((x) => x.json()) 37 | .then((data) => { 38 | this.setState({ data }); 39 | }); 40 | } 41 | } 42 | 43 | onSelect (name, selected) { 44 | this.setState({ data: [] }); 45 | action('selected')(name, selected); 46 | } 47 | 48 | resultTemplate (val, i, selectedIndex) { 49 | const className = classNames('ac-suggestion', { 50 | 'ac-suggestion-active': i === selectedIndex 51 | }); 52 | return
{val.name} {val.type}
; 53 | } 54 | 55 | render () { 56 | return ( 57 | 68 | ); 69 | } 70 | } 71 | 72 | AutoCompleteContainer.propTypes = { 73 | multiSelect: React.PropTypes.bool 74 | }; 75 | 76 | storiesOf('AutoComplete', module) 77 | .add('Basic', () => ( 78 | 84 | )) 85 | .add('Disabled', () => ( 86 | 93 | )) 94 | .add('Async requests', () => ) 95 | .add('Fuzzy Search', () => ( 96 | 102 | )) 103 | .add('Initial Suggestions', () => ( 104 | 111 | )) 112 | .add('Tags', () => ( 113 | 114 | )); 115 | -------------------------------------------------------------------------------- /components/Count/Count.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import autoBind from '../utils/autoBind'; 5 | 6 | function inRange (value, min, max) { 7 | return value >= min && value <= max; 8 | } 9 | 10 | export default class Count extends Component { 11 | constructor (props) { 12 | super(props); 13 | 14 | autoBind([ 15 | 'handleIncrement', 16 | 'handleDecrement', 17 | 'onChange' 18 | ], this); 19 | } 20 | 21 | shouldComponentUpdate (newProps) { 22 | const { value, min, max } = newProps; 23 | return ( 24 | inRange(value, min, max) && 25 | (value !== this.props.value) 26 | ); 27 | } 28 | 29 | onChange (value, action) { 30 | const { name, onChange, min, max } = this.props; 31 | 32 | if (inRange(value, min, max)) { 33 | onChange({ 34 | action, 35 | name, 36 | value 37 | }); 38 | } 39 | } 40 | 41 | handleDecrement () { 42 | this.onChange(this.props.value - this.props.step, 'decreased'); 43 | } 44 | 45 | handleIncrement () { 46 | this.onChange(this.props.value + this.props.step, 'increased'); 47 | } 48 | 49 | render () { 50 | const { 51 | name, 52 | disabled, 53 | value, 54 | prefix, 55 | suffix, 56 | decrementElement, 57 | incrementElement 58 | } = this.props; 59 | 60 | const mainClass = classNames('react-filters', 'rf-count', name, { disabled }); 61 | return ( 62 |
63 |
67 | {decrementElement(this.props)} 68 |
69 | 70 |
71 | {prefix} 72 | {value} 73 | {suffix} 74 |
75 |
79 | {incrementElement(this.props)} 80 |
81 |
82 | ); 83 | } 84 | } 85 | 86 | Count.propTypes = { 87 | decrementElement: PropTypes.func, 88 | disabled: PropTypes.bool, 89 | incrementElement: PropTypes.func, 90 | max: PropTypes.number, 91 | min: PropTypes.number, 92 | name: PropTypes.string.isRequired, 93 | onChange: PropTypes.func.isRequired, 94 | prefix: PropTypes.string, 95 | step: PropTypes.number, 96 | suffix: PropTypes.string, 97 | value: PropTypes.number 98 | }; 99 | 100 | Count.defaultProps = { 101 | decrementElement () { 102 | return ( 103 | 106 | ); 107 | }, 108 | disabled: false, 109 | incrementElement () { 110 | return ( 111 | 114 | ); 115 | }, 116 | max: Number.POSITIVE_INFINITY, 117 | min: Number.NEGATIVE_INFINITY, 118 | step: 1, 119 | value: 0 120 | }; 121 | -------------------------------------------------------------------------------- /components/AutoComplete/SearchBox.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import Tag from './Tag'; 4 | 5 | import autoBind from '../utils/autoBind'; 6 | 7 | export default class SearchBox extends Component { 8 | constructor (props) { 9 | super(props); 10 | 11 | autoBind([ 12 | 'handleQueryChange', 13 | 'removeLastTag' 14 | ], this); 15 | } 16 | 17 | getTags () { 18 | return this.props.multiSelected.map((val, i) => ( 19 | 25 | )); 26 | } 27 | 28 | getTagContainer () { 29 | const { multiSelect, multiSelected } = this.props; 30 | if (multiSelect && multiSelected && multiSelected.length) { 31 | return ( 32 |
33 | {this.getTags()} 34 |
35 | ); 36 | } 37 | return null; 38 | } 39 | 40 | handleQueryChange () { 41 | this.props.onQueryChange(this.refs.autocomplete.value); 42 | } 43 | 44 | removeLastTag (e) { 45 | const { onTagRemove, multiSelected } = this.props; 46 | const keyCode = e.which || e.keyCode; 47 | if (keyCode === 8 && multiSelected && multiSelected.length && !this.props.value.length) { 48 | e.preventDefault(); 49 | e.stopPropagation(); 50 | onTagRemove({ 51 | id: multiSelected.length - 1 52 | }); 53 | } 54 | } 55 | 56 | render () { 57 | const { 58 | onFocus, 59 | onBlur, 60 | disabled, 61 | placeholder, 62 | value, 63 | Reset, 64 | onReset, 65 | multiSelect 66 | } = this.props; 67 | 68 | const mainClass = classNames('ac-searchbox', { 69 | tags: multiSelect 70 | }); 71 | 72 | return ( 73 |
74 | {this.getTagContainer()} 75 | 76 | 88 | 89 | 93 | {value.length > 0 && } 94 | 95 | 96 | 97 |
98 | ); 99 | } 100 | } 101 | 102 | function noop () { 103 | 104 | } 105 | 106 | SearchBox.propTypes = { 107 | Reset: PropTypes.element.isRequired, 108 | disabled: PropTypes.bool, 109 | multiSelect: PropTypes.bool, 110 | multiSelected: PropTypes.array, 111 | onBlur: PropTypes.func, 112 | onFocus: PropTypes.func, 113 | onQueryChange: PropTypes.func.isRequired, 114 | onReset: PropTypes.func, 115 | onTagRemove: PropTypes.func, 116 | placeholder: PropTypes.string, 117 | showTagRemove: PropTypes.bool, 118 | value: PropTypes.string.isRequired, 119 | valueKey: PropTypes.string 120 | }; 121 | 122 | function ResetContent () { 123 | return ; 124 | } 125 | 126 | SearchBox.defaultProps = { 127 | Reset: ResetContent, 128 | onBlur: noop, 129 | onFocus: noop, 130 | placeholder: 'Search' 131 | }; 132 | -------------------------------------------------------------------------------- /components/Group/Group.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import autoBind from '../utils/autoBind'; 4 | import lastElement from '../utils/lastElement'; 5 | import getSelectedIds from '../utils/getSelectedIds'; 6 | 7 | import deepCopy from 'deep-copy'; 8 | 9 | import Toggle from '../Toggle/Toggle'; 10 | 11 | function handleSingleSelect (arr, index) { 12 | return arr.map((val, i) => { 13 | const obj = val; 14 | obj.value = (i === index) ? !val.value : false; 15 | return obj; 16 | }); 17 | } 18 | 19 | function formatValue (props) { 20 | const { value, selectedIds, id } = props; 21 | if (!selectedIds) return value; 22 | const value$ = deepCopy(value); 23 | return value$.map(val => { 24 | if ((typeof selectedIds === 'object' && selectedIds.indexOf(val[id]) >= 0) || 25 | (typeof selectedIds === 'number' && selectedIds === val[id])) val.value = true; 26 | return val; 27 | }); 28 | } 29 | 30 | export default class Group extends Component { 31 | constructor (props) { 32 | super(props); 33 | 34 | this.formattedValue = formatValue(props); 35 | 36 | autoBind([ 37 | 'handleChange' 38 | ], this); 39 | } 40 | 41 | componentDidMount () { 42 | this.formattedValue = formatValue(this.props); 43 | } 44 | 45 | componentWillReceiveProps (newProps) { 46 | this.formattedValue = formatValue(newProps); 47 | } 48 | 49 | getElements () { 50 | const { type, mode, disabled } = this.props; 51 | return this.formattedValue.map((val, i) => ( 52 | 62 | )); 63 | } 64 | 65 | handleChange (data) { 66 | const { name, type, id, value } = this.props; 67 | let newValue = deepCopy(this.formattedValue); 68 | 69 | const index = parseInt(lastElement(data.name.split('-')), 10); 70 | 71 | if (type === 'checkbox' || type === 'switch') { 72 | newValue[index].value = data.value; 73 | } else { 74 | newValue = handleSingleSelect(newValue, index); 75 | } 76 | 77 | this.props.onChange({ 78 | index, 79 | name, 80 | oldValue: value, 81 | value: newValue, 82 | selectedIds: getSelectedIds(newValue, id, type === 'radio') 83 | }); 84 | } 85 | 86 | render () { 87 | const { name, className, attributes, type } = this.props; 88 | const mainClass = classNames('rf-group', `${type}-group`, name, className); 89 | return ( 90 |
91 | {this.getElements()} 92 |
93 | ); 94 | } 95 | } 96 | 97 | Group.propTypes = { 98 | attributes: PropTypes.object, 99 | className: PropTypes.string, 100 | mode: PropTypes.oneOf(['normal', 'tag']), 101 | name: PropTypes.string.isRequired, 102 | onChange: PropTypes.func.isRequired, 103 | type: PropTypes.oneOf([ 104 | 'radio', 'checkbox', 'switch' 105 | ]), 106 | value: PropTypes.arrayOf( 107 | PropTypes.shape({ 108 | count: PropTypes.number, 109 | label: PropTypes.string, 110 | value: PropTypes.bool 111 | }) 112 | ), 113 | selectedIds: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), 114 | id: PropTypes.string, 115 | disabled: PropTypes.bool 116 | }; 117 | 118 | Group.defaultProps = { 119 | id: 'id', 120 | disabled: false 121 | }; 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-filters", 3 | "description": "A collection of filters made in React", 4 | "main": "dist/index.js", 5 | "jsnext:main": "components/index.js", 6 | "files": [ 7 | "components", 8 | "dist", 9 | "README", 10 | "docs" 11 | ], 12 | "scripts": { 13 | "lint": "eslint --format=node_modules/eslint-formatter-pretty components/**/*.js test/**/*.js stories/*.js", 14 | "lintfix": "eslint --fix components/**/*.js test/**/*.js stories/*.js", 15 | "prepublish": "npm run test && babel components --out-dir dist", 16 | "prebuild": "rm -rf dist/*", 17 | "storybook": "start-storybook -p 9002", 18 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 19 | "test": "mocha --require test/config/setup 'test/**/*.test.js'", 20 | "test:cover": "istanbul cover -x *.test.js _mocha -- -R spec --require test/config/setup 'test/**/*.test.js'", 21 | "test:report": "cat ./coverage/lcov.info | codecov && rm -rf ./coverage", 22 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 23 | "build": "build-storybook -o docs", 24 | "docs": "react-docgen ./components/**/*.jsx | node scripts/buildDocs.js" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/ritz078/react-filters.git" 29 | }, 30 | "keywords": [ 31 | "react-filters", 32 | "filters", 33 | "switch", 34 | "dropdown" 35 | ], 36 | "author": "Ritesh Kumar", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/ritz078/react-filters/issues" 40 | }, 41 | "homepage": "https://github.com/ritz078/react-filters", 42 | "devDependencies": { 43 | "@kadira/react-storybook-decorator-centered": "^1.0.0", 44 | "@kadira/storybook": "^2.3.0", 45 | "@kadira/storybook-deployer": "^1.0.0", 46 | "autoprefixer": "^6.3.6", 47 | "babel": "^6.5.2", 48 | "babel-cli": "^6.11.4", 49 | "babel-core": "^6.9.0", 50 | "babel-eslint": "^6.0.2", 51 | "babel-loader": "^6.2.4", 52 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 53 | "babel-plugin-transform-react-remove-prop-types": "^0.2.9", 54 | "babel-preset-es2015": "^6.9.0", 55 | "babel-preset-react": "^6.5.0", 56 | "babel-preset-stage-0": "^6.5.0", 57 | "babel-register": "^6.9.0", 58 | "chai": "^3.5.0", 59 | "codecov.io": "^0.1.6", 60 | "commitizen": "^2.8.1", 61 | "conventional-changelog-cli": "^1.2.0", 62 | "css-loader": "^0.23.1", 63 | "cz-conventional-changelog": "^1.1.6", 64 | "enzyme": "^2.3.0", 65 | "eslint": "^2.13.1", 66 | "eslint-config-airbnb": "^9.0.1", 67 | "eslint-formatter-pretty": "^0.2.2", 68 | "eslint-plugin-import": "^1.8.1", 69 | "eslint-plugin-jsx-a11y": "^1.5.5", 70 | "eslint-plugin-react": "^5.1.1", 71 | "eventsource-polyfill": "*", 72 | "file-loader": "^0.9.0", 73 | "ghooks": "^1.2.4", 74 | "isparta": "^4.0.0", 75 | "istanbul": "^1.1.0-alpha.1", 76 | "jsdom": "^9.0.0", 77 | "mocha": "^3.0.1", 78 | "node-sass": "^3.7.0", 79 | "raw-loader": "^0.5.1", 80 | "react-addons-test-utils": "*", 81 | "react-docgen": "^2.10.0", 82 | "redux": "^3.5.2", 83 | "rimraf": "^2.5.2", 84 | "sass-loader": "^3.2.0", 85 | "semantic-release": "^4.3.5", 86 | "sinon": "^1.17.4", 87 | "style-loader": "^0.13.1", 88 | "url-loader": "^0.5.7", 89 | "webpack-hot-middleware": "^2.10.0" 90 | }, 91 | "dependencies": { 92 | "classnames": "^2.2.5", 93 | "deep-copy": "^1.1.2", 94 | "fuse.js": "^2.2.0", 95 | "invariant": "^2.2.1", 96 | "react": "0.14.x || ^15.x.x", 97 | "react-dom": "0.14.x || ^15.x.x" 98 | }, 99 | "config": { 100 | "commitizen": { 101 | "path": "node_modules/cz-conventional-changelog" 102 | }, 103 | "ghooks": { 104 | "pre-commit": "npm run test" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/Group.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const { describe, it, before, __base } = global; 3 | import { shallow, mount } from 'enzyme'; 4 | import sinon from 'sinon'; 5 | import { expect } from 'chai'; 6 | 7 | const { Group } = require(`${__base}components`); 8 | 9 | describe('Group Component', () => { 10 | let value; 11 | 12 | before(() => ( 13 | value = [{ 14 | id:1, 15 | label: 'a', 16 | count: 6 17 | }, { 18 | id:2, 19 | label: 'b' 20 | }, { 21 | id:3, 22 | label: 'c' 23 | }] 24 | )); 25 | 26 | it('should render Toggle component', () => { 27 | const onChange = sinon.spy(); 28 | const wrapper = shallow( 29 | 35 | ); 36 | 37 | expect(wrapper.hasClass('switch-group')).to.equal(true); 38 | expect(wrapper.find('Toggle')).to.have.length(3); 39 | }); 40 | 41 | it('should call onChange on click with correct arguments', () => { 42 | const onChange = sinon.spy(); 43 | const wrapper = mount( 44 | 50 | ); 51 | 52 | wrapper.find('Toggle').at(1).simulate('click'); 53 | 54 | const args = { 55 | name: 'change', 56 | value: [{ 57 | id:1, 58 | label: 'a', 59 | count: 6 60 | }, { 61 | id:2, 62 | label: 'b', 63 | value: true 64 | }, { 65 | id:3, 66 | label: 'c' 67 | }], 68 | oldValue: value, 69 | index: 1, 70 | selectedIds: [2] 71 | }; 72 | 73 | expect(onChange.calledWith(args)).to.equal(true); 74 | 75 | wrapper.setProps({ value: args.value }); 76 | 77 | wrapper.find('Toggle').at(0).simulate('click'); 78 | 79 | const args2 = { 80 | name: 'change', 81 | value: [{ 82 | id:1, 83 | label: 'a', 84 | count: 6, 85 | value: true 86 | }, { 87 | id:2, 88 | label: 'b', 89 | value: true 90 | }, { 91 | id:3, 92 | label: 'c' 93 | }], 94 | oldValue: [{ 95 | id:1, 96 | label: 'a', 97 | count: 6 98 | }, { 99 | id:2, 100 | label: 'b', 101 | value: true 102 | }, { 103 | id:3, 104 | label: 'c' 105 | }], 106 | index: 0, 107 | selectedIds: [1, 2] 108 | }; 109 | 110 | expect(onChange.calledWith(args2)).to.equal(true); 111 | }); 112 | 113 | it('should behave like a radio button when type is radio', () => { 114 | const onChange = sinon.spy(); 115 | 116 | const wrapper = mount( 117 | 123 | ); 124 | 125 | wrapper.find('Toggle').at(1).simulate('click'); 126 | 127 | const args = { 128 | name: 'radio-group', 129 | value: [{ 130 | id:1, 131 | label: 'a', 132 | count: 6, 133 | value: false 134 | }, { 135 | id:2, 136 | label: 'b', 137 | value: true 138 | }, { 139 | id:3, 140 | label: 'c', 141 | value: false 142 | }], 143 | oldValue: value, 144 | index: 1, 145 | selectedIds: 2 146 | }; 147 | 148 | expect(onChange.calledWith(args)).to.equal(true); 149 | 150 | wrapper.setProps({ value: args.value }); 151 | 152 | wrapper.find('Toggle').at(0).simulate('click'); 153 | 154 | const args2 = { 155 | name: 'radio-group', 156 | value: [{ 157 | id:1, 158 | label: 'a', 159 | count: 6, 160 | value: true 161 | }, { 162 | id:2, 163 | label: 'b', 164 | value: false 165 | }, { 166 | id:3, 167 | label: 'c', 168 | value: false 169 | }], 170 | oldValue: args.value, 171 | index: 0, 172 | selectedIds: 1 173 | }; 174 | 175 | expect(onChange.calledWith(args2)).to.equal(true); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /components/Slider/Slider.scss: -------------------------------------------------------------------------------- 1 | @import "../theme"; 2 | @import "../mixins"; 3 | 4 | .rf-slider{ 5 | overflow: hidden; 6 | padding: 0 10px; 7 | &.slider-vertical{ 8 | overflow: visible; 9 | } 10 | } 11 | 12 | .slider-wrapper { 13 | width: 100%; 14 | overflow: visible; 15 | position: relative; 16 | display: inline-block; 17 | padding-top: 40px; 18 | 19 | .slider-vertical & { 20 | width: $slider-track-width; 21 | height: 400px; 22 | padding-top: 0; 23 | } 24 | } 25 | 26 | .slider-vertical { 27 | width: 20px; 28 | height: 400px; 29 | position: relative; 30 | 31 | .slider-track { 32 | position: absolute; 33 | } 34 | } 35 | 36 | .slider-track, .slider-rail { 37 | height: $slider-track-width; 38 | background-color: $secondary-color; 39 | border-radius: $slider-track-width/2; 40 | 41 | .slider-vertical & { 42 | width: $slider-track-width; 43 | height: 400px; 44 | } 45 | } 46 | 47 | .slider-slider-wrapper { 48 | will-change: transform, top; 49 | transition: transform .2s ease-out, top .2s ease-out; 50 | transform: translate3d(0, 0, 0); 51 | position: relative; 52 | } 53 | 54 | .slider-control { 55 | height: $slider-control-width; 56 | width: $slider-control-width; 57 | border-radius: $slider-control-width/2; 58 | background-color: darken($primary-color, 10%); 59 | position: absolute; 60 | display: inline-block; 61 | vertical-align: top; 62 | margin-top: -($slider-control-width + $slider-track-width)/2; 63 | margin-left: -$slider-control-width/2; 64 | cursor: pointer; 65 | transition: transform .2s ease-out; 66 | 67 | .slider-disabled & { 68 | background-color: grayscale(darken($primary-color, 10%)); 69 | } 70 | 71 | .slider-vertical & { 72 | margin-left: -($slider-control-width/2 - $slider-track-width/2); 73 | margin-top: -$slider-control-width/2; 74 | } 75 | } 76 | 77 | .slider-rail { 78 | position: relative; 79 | background-color: $primary-color; 80 | transition: all .2s ease-out; 81 | 82 | .slider-disabled & { 83 | background-color: grayscale($primary-color); 84 | } 85 | } 86 | 87 | .slider-active .slider-slider { 88 | transform: scale(1.4) translate3d(0, 0, 0); 89 | } 90 | 91 | // TODO : prevent text selection in disabled state 92 | 93 | .slider-value { 94 | position: absolute; 95 | width: 40px; 96 | margin-left: -20px; 97 | text-align: center; 98 | margin-top: - ($slider-track-width + $slider-control-width/2 + 20px); 99 | @include no-select(); 100 | 101 | .slider-disabled & { 102 | color: darken($secondary-color, 20%); 103 | } 104 | 105 | .slider-vertical & { 106 | margin-top: -($slider-control-width/2); 107 | margin-left: -40px; 108 | } 109 | } 110 | 111 | .slider-range { 112 | @include no-select(); 113 | 114 | div { 115 | display: inline-block; 116 | margin-top: 10px; 117 | 118 | &.slider-range-max { 119 | float: right; 120 | } 121 | } 122 | 123 | .slider-vertical & { 124 | margin-left: 20px; 125 | div{ 126 | margin-top: 0; 127 | } 128 | position: absolute; 129 | top: 0; 130 | height: 100%; 131 | 132 | .slider-range-min{ 133 | position: absolute; 134 | bottom: 0; 135 | margin-bottom: -10px; 136 | } 137 | 138 | .slider-range-max{ 139 | margin-top: -10px; 140 | } 141 | } 142 | } 143 | 144 | $stepWidth: 6px; 145 | 146 | .slider-steps-wrapper { 147 | width: 100%; 148 | height: $slider-track-width; 149 | margin-top: -$slider-track-width; 150 | position: absolute; 151 | 152 | .slider-vertical & { 153 | height: 100%; 154 | margin-top: -($slider-step-width + 4px)/2; 155 | } 156 | } 157 | 158 | .slider-step { 159 | width: $slider-step-width; 160 | height: $slider-step-width; 161 | background-color: white; 162 | display: inline-block; 163 | position: absolute; 164 | margin-top: -2px; 165 | border-radius: 50%; 166 | border: 2px solid $secondary-color; 167 | margin-left: -($slider-step-width+4px)/2; 168 | will-change: border-color; 169 | transition: border-color .2s ease-out; 170 | 171 | &.slider-step-active { 172 | border-color: $primary-color; 173 | } 174 | 175 | .slider-vertical & { 176 | margin-left: -2px; 177 | margin-top: 0; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /test/Toggle.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | const { describe, it, __base } = global; 4 | import { shallow } from 'enzyme'; 5 | import sinon from 'sinon'; 6 | import { expect } from 'chai'; 7 | 8 | const { Toggle } = require(`${__base}components`); 9 | 10 | function noop () { 11 | 12 | } 13 | 14 | describe('Toggle Component', () => { 15 | it('should call onChange function on click', () => { 16 | const onChange = sinon.spy(); 17 | 18 | const wrapper = shallow( 19 | 25 | ); 26 | 27 | wrapper.simulate('click'); 28 | 29 | expect(onChange.called).to.equal(true); 30 | }); 31 | 32 | it('should call onChange function with correct arguments', () => { 33 | const onChange = sinon.spy(); 34 | 35 | const wrapper = shallow( 36 | 42 | ); 43 | 44 | wrapper.simulate('click'); 45 | 46 | const args = { 47 | name: 'switch', 48 | value: true 49 | }; 50 | 51 | expect(onChange.calledWith(args)).to.equal(true); 52 | }); 53 | 54 | it('should change class to toggle-on on click', () => { 55 | const onChange = sinon.spy(); 56 | 57 | const wrapper = shallow( 58 | 64 | ); 65 | 66 | wrapper.simulate('click'); 67 | 68 | expect(wrapper.closest('.rf-active')).to.have.length(1); 69 | }); 70 | 71 | it('should render the count', () => { 72 | const onChange = sinon.spy(); 73 | 74 | const wrapper = shallow( 75 | 83 | ); 84 | 85 | expect(wrapper.find('span.toggle-count')).to.have.length(1); 86 | }); 87 | 88 | it('should render checkbox when checkbox option is passed', () => { 89 | const onChange = sinon.spy(); 90 | 91 | const wrapper = shallow( 92 | 98 | ); 99 | 100 | expect(wrapper.find('i.icon-check-box-outline-blank')).to.have.length(1); 101 | 102 | wrapper.simulate('click'); 103 | 104 | expect(onChange.calledOnce).to.equal(true); 105 | }); 106 | 107 | it('should render radio button when radio option is passed', () => { 108 | const onChange = sinon.spy(); 109 | 110 | const wrapper = shallow( 111 | 116 | ); 117 | 118 | expect(wrapper.find('i.icon-radio-button-unchecked')).to.have.length(1); 119 | 120 | wrapper.simulate('click'); 121 | 122 | expect(onChange.calledOnce).to.equal(true); 123 | }); 124 | 125 | it('should render label when passed', () => { 126 | const onChange = sinon.spy(); 127 | 128 | const wrapper = shallow( 129 | 135 | ); 136 | 137 | expect(wrapper.find('.toggle-icon-label').text()).to.equal('off'); 138 | 139 | wrapper.setProps({ value: true }); 140 | 141 | expect(wrapper.find('.toggle-icon-label').text()).to.equal('on'); 142 | }); 143 | 144 | it('should not call render if both updated value and count are same', () => { 145 | const onChange = function () { 146 | }; 147 | 148 | const render = sinon.spy(Toggle.prototype, 'render'); 149 | 150 | const wrapper = shallow( 151 | 158 | ); 159 | 160 | expect(render.calledOnce).to.equal(true); 161 | wrapper.setProps({ 162 | value: true, 163 | count: 5 164 | }); 165 | 166 | expect(render.calledTwice).to.equal(false); 167 | 168 | wrapper.setProps({ 169 | value: true, 170 | count: 6 171 | }); 172 | 173 | expect(render.calledTwice).to.equal(true); 174 | }); 175 | 176 | it('should render the icon element if passed', () => { 177 | const onChange = noop; 178 | 179 | const iconElement = function (p) { 180 | const iconClass = classNames({ 181 | 'icon-not-selected': !p.value, 182 | 'icon-selected': p.value 183 | }); 184 | return ; 185 | }; 186 | 187 | const wrapper = shallow( 188 | 194 | ); 195 | 196 | expect(wrapper.find('.icon-not-selected')).has.length(1); 197 | 198 | wrapper.setProps({ value: true }); 199 | 200 | expect(wrapper.find('.icon-selected')).has.length(1); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /components/Slider/Control.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | import { hasStepDifference, suppress, isWithinRange, removeClass, isVertical } from './utils'; 4 | import { getValueFromPosition, getRelativePosition, getPositionFromValue } from './helpers'; 5 | import autoBind from '../utils/autoBind'; 6 | 7 | import constants from './constants'; 8 | 9 | export default class Control extends Component { 10 | constructor (props, context) { 11 | super(props, context); 12 | 13 | autoBind([ 14 | 'handleMouseDown', 15 | 'handleDrag', 16 | 'handleMouseUp', 17 | 'handleTouchStart', 18 | 'handleTouchEnd', 19 | 'onChange' 20 | ], this); 21 | } 22 | 23 | componentDidMount () { 24 | this.setSliderPosition(this.props); 25 | this.controlWidth = this.getControlWidth(); 26 | } 27 | 28 | componentWillReceiveProps (newProps) { 29 | const propsChanged = (newProps.value !== this.props.value) || 30 | (newProps.trackOffset.width !== this.props.trackOffset.width); 31 | if (propsChanged) this.setSliderPosition(newProps); 32 | } 33 | 34 | shouldComponentUpdate (newProps) { 35 | const dimension = constants[newProps.orientation].dimension; 36 | 37 | return ( 38 | (hasStepDifference(newProps.value, this.props.value, newProps.step) && 39 | isWithinRange(newProps, newProps.value)) || 40 | newProps.trackOffset[dimension] !== this.props.trackOffset[dimension] 41 | ); 42 | } 43 | 44 | onChange (value, isRenderRequired = false) { 45 | this.props.onChange({ 46 | controlWidth: this.controlWidth, 47 | name: this.props.name, 48 | value 49 | }, isRenderRequired); 50 | } 51 | 52 | getControlWidth () { 53 | const control = this.refs.control; 54 | if (!control) return 0; 55 | return control.offsetWidth; 56 | } 57 | 58 | setSliderPosition (props) { 59 | const { value } = props; 60 | this.onChange(value, true); 61 | } 62 | 63 | handleMouseDown (e) { 64 | suppress(e); 65 | this.refs.controlWrapper.className += ' slider-active'; 66 | document.addEventListener('mouseup', this.handleMouseUp); 67 | if (this.props.readOnly) return; 68 | 69 | document.addEventListener('mousemove', this.handleDrag); 70 | this.props.onDragExtreme(this.props.name, this.props.value, 'start'); 71 | } 72 | 73 | handleMouseUp (e) { 74 | suppress(e); 75 | this.refs.controlWrapper.className = removeClass(this.refs.controlWrapper, 'slider-active'); 76 | document.removeEventListener('mouseup', this.handleMouseUp); 77 | 78 | if (this.props.readOnly) return; 79 | 80 | document.removeEventListener('mousemove', this.handleDrag); 81 | this.props.onDragExtreme(this.props.name, this.newValue, 'end'); 82 | } 83 | 84 | handleTouchStart () { 85 | document.addEventListener('touchmove', this.handleDrag); 86 | document.addEventListener('touchend', this.handleTouchEnd); 87 | this.props.onDragExtreme(this.props.name, this.props.value, 'start'); 88 | } 89 | 90 | handleTouchEnd () { 91 | document.removeEventListener('touchmove', this.handleDrag); 92 | document.removeEventListener('touchend', this.handleTouchEnd); 93 | this.props.onDragExtreme(this.props.name, this.newValue, 'end'); 94 | } 95 | 96 | handleDrag (e) { 97 | suppress(e); 98 | const position = getRelativePosition(e, this.props, this.controlWidth); 99 | 100 | this.newValue = getValueFromPosition(this.props, position); 101 | this.onChange(this.newValue); 102 | } 103 | 104 | render () { 105 | const { name, value, toolTipTemplate, disabled, orientation } = this.props; 106 | 107 | const className = classNames('slider-control', name); 108 | 109 | const sliderPosition = isVertical(orientation) ? 110 | (100 - getPositionFromValue(this.props)) : getPositionFromValue(this.props); 111 | 112 | let style; 113 | 114 | if (isVertical(orientation)) { 115 | style = { 116 | top: `${sliderPosition}%` 117 | }; 118 | } else { 119 | style = { 120 | transform: `translateX(${sliderPosition}%) translate3d(0,0,0)` 121 | }; 122 | } 123 | 124 | 125 | return ( 126 |
127 |
128 | {toolTipTemplate(value)} 129 |
130 |
137 |
138 | ); 139 | } 140 | } 141 | 142 | Control.propTypes = { 143 | disabled: PropTypes.bool.isRequired, 144 | max: PropTypes.number.isRequired, 145 | min: PropTypes.number.isRequired, 146 | name: PropTypes.string.isRequired, 147 | onChange: PropTypes.func.isRequired, 148 | onDragExtreme: PropTypes.func.isRequired, 149 | orientation: PropTypes.string.isRequired, 150 | readOnly: PropTypes.bool.isRequired, 151 | step: PropTypes.number.isRequired, 152 | toolTipTemplate: PropTypes.func.isRequired, 153 | trackOffset: PropTypes.object.isRequired, 154 | value: PropTypes.number.isRequired 155 | }; 156 | -------------------------------------------------------------------------------- /components/Toggle/Toggle.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | import autoBind from '../utils/autoBind'; 4 | import noop from '../utils/noop'; 5 | 6 | function radioElement (p) { 7 | const iconClass = classNames({ 8 | 'icon-radio-button-unchecked': !p.value, 9 | 'icon-radio-button-checked': p.value 10 | }); 11 | return ; 12 | } 13 | 14 | function checkBoxElement (p) { 15 | const iconClass = classNames({ 16 | 'icon-check-box-outline-blank': !p.value, 17 | 'icon-check-box': p.value 18 | }); 19 | return ; 20 | } 21 | 22 | function switchElement (prop) { 23 | const labelClass = classNames('toggle-icon-label', { 24 | 'toggle-il-left': prop.value, 25 | 'toggle-il-right': !prop.value 26 | }); 27 | 28 | let iconLabelText; 29 | 30 | if (prop.iconLabel && prop.iconLabel.length) { 31 | iconLabelText = prop.value ? prop.iconLabel[0] : prop.iconLabel[1]; 32 | } 33 | return ( 34 |
35 |
{iconLabelText}
36 |
37 |
38 | ); 39 | } 40 | 41 | /** 42 | * Hello world 43 | */ 44 | export default class Toggle extends React.Component { 45 | constructor (props) { 46 | super(props); 47 | autoBind([ 48 | 'handleClick' 49 | ], this); 50 | } 51 | 52 | shouldComponentUpdate (nextProps) { 53 | return ( 54 | (nextProps.value !== this.props.value) || 55 | (nextProps.count !== this.props.count) 56 | ); 57 | } 58 | 59 | getIconElement () { 60 | const { iconElement, type } = this.props; 61 | if (typeof iconElement === 'function') return iconElement(this.props); 62 | if (type === 'radio') return radioElement(this.props); 63 | else if (type === 'checkbox') return checkBoxElement(this.props); 64 | else return switchElement(this.props); 65 | } 66 | 67 | handleClick () { 68 | this.props.onChange({ 69 | name: this.props.name, 70 | value: !this.props.value 71 | }); 72 | } 73 | 74 | isNormal () { 75 | return this.props.mode === 'normal'; 76 | } 77 | 78 | render () { 79 | const { 80 | attributes, 81 | className, 82 | name, 83 | label, 84 | labelPosition, 85 | value, 86 | disabled, 87 | countElem, 88 | count, 89 | type 90 | } = this.props; 91 | 92 | const mainClass = classNames('rf-toggle', type, className, name, { 93 | 'toggle-disabled': disabled, 94 | 'toggle-active': value, 95 | 'toggle-tag': !this.isNormal() 96 | }); 97 | 98 | const labelClass = classNames('toggle-label', { 99 | 'toggle-before': labelPosition === 'before', 100 | 'toggle-after': labelPosition === 'after' 101 | }); 102 | 103 | return ( 104 |
109 | { 110 | label &&
111 | {label} 112 | {count !== undefined && countElem(this.props)} 113 |
114 | } 115 | {this.isNormal() && this.getIconElement()} 116 |
117 | ); 118 | } 119 | } 120 | 121 | Toggle.propTypes = { 122 | /** 123 | * Sometimes you may need to add some custom attributes to the root tag of the 124 | * component. attributes will accept an object where the key and values will 125 | * be those attributes and their value respectively. 126 | * 127 | * Eg : If you pass 128 | * ```js 129 | * attributes = { 130 | * 'data-attr1' : 'val1', 131 | * 'data-attr2' : 'val2' 132 | * } 133 | * ``` 134 | * the root tag will have the attributes `data-attr1` and `data-attr2` with the 135 | * corresponding values as `val1` and `val2` respectively 136 | */ 137 | attributes: PropTypes.object, 138 | 139 | /** 140 | * Optional className to be added to the root tag of the component 141 | */ 142 | className: PropTypes.string, 143 | 144 | /** 145 | * In case you want to show aggregation/count in front of label then pass the 146 | * number in this option. This is generally useful for showing the items present 147 | * corresponding to that filter option. 148 | */ 149 | count: PropTypes.number, 150 | countElem: PropTypes.oneOfType([ 151 | PropTypes.func, 152 | PropTypes.element 153 | ]), 154 | 155 | /** 156 | * Set to `true` if you want to disable the component interactions. 157 | */ 158 | disabled: PropTypes.bool, 159 | iconElement: PropTypes.func, 160 | iconLabel: PropTypes.array, 161 | 162 | /** 163 | * The label text present in the component. If this option is not set only the 164 | * icon element will render. 165 | */ 166 | label: PropTypes.string, 167 | labelPosition: PropTypes.oneOf([ 168 | 'before', 'after' 169 | ]), 170 | mode: PropTypes.oneOf(['normal', 'tag']), 171 | name: PropTypes.string.isRequired, 172 | onChange: PropTypes.func, 173 | type: PropTypes.oneOf([ 174 | 'switch', 'radio', 'checkbox' 175 | ]), 176 | value: PropTypes.bool.isRequired 177 | }; 178 | 179 | Toggle.defaultProps = { 180 | countElem (p) { 181 | return ({p.count}); 182 | }, 183 | disabled: false, 184 | labelPosition: 'before', 185 | mode: 'normal', 186 | onChange: noop, 187 | type: 'switch', 188 | value: false 189 | }; 190 | -------------------------------------------------------------------------------- /components/Slider/Slider.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import { isWithinRange, suppress, isEqual, isVertical, formatValue, capitalize } from './utils'; 4 | import { getNearestValue } from './helpers'; 5 | import Control from './Control'; 6 | import Steps from './Steps'; 7 | import Rail from './Rail'; 8 | import autoBind from '../utils/autoBind'; 9 | import noop from '../utils/noop'; 10 | 11 | export default class Slider extends Component { 12 | constructor (props) { 13 | super(props); 14 | 15 | this.state = { 16 | trackOffset: {} 17 | }; 18 | 19 | autoBind([ 20 | 'onChange', 21 | 'onControlChange', 22 | 'handleClick', 23 | 'updatePosition', 24 | 'onDragExtreme' 25 | ], this); 26 | } 27 | 28 | componentDidMount () { 29 | this.updatePosition(); 30 | window.addEventListener('resize', this.updatePosition); 31 | } 32 | 33 | shouldComponentUpdate (newProps, newState) { 34 | return ( 35 | isWithinRange(newProps, newProps.value) && 36 | (!isEqual(this.props.value, newProps.value) || !!this.isRerenderRequired || 37 | this.state.trackOffset.width !== newState.trackOffset.width) 38 | ); 39 | } 40 | 41 | componentDidUpdate () { 42 | this.isRerenderRequired = false; 43 | } 44 | 45 | componentWillUnmount () { 46 | window.removeEventListener('resize', this.updatePosition); 47 | } 48 | 49 | onChange (value, changed) { 50 | const args = { 51 | name: this.props.name, 52 | value 53 | }; 54 | 55 | if (changed) args.changed = changed; 56 | this.props.onChange(args); 57 | } 58 | 59 | onControlChange (data, isRenderRequired) { 60 | let value; 61 | if (this.isRangeType()) { 62 | value = formatValue(this.props.value, data.value, data.name, this.props.type); 63 | } else { 64 | value = data.value; 65 | } 66 | 67 | // only trigger on first onChange trigger 68 | this.isRerenderRequired = isRenderRequired; 69 | 70 | if (isWithinRange(this.props, value) && !isEqual(this.props.value, value)) { 71 | this.onChange(value, data.name); 72 | } 73 | } 74 | 75 | onDragExtreme (name, value, action) { 76 | const newValue = formatValue(this.props.value, value, name, this.props.type); 77 | this.props[`onDrag${capitalize(action)}`]({ 78 | changed: name, 79 | name: this.props.name, 80 | value: newValue 81 | }); 82 | } 83 | 84 | getControl (value, name) { 85 | const { 86 | step, 87 | orientation, 88 | min, 89 | max, 90 | readOnly, 91 | disabled, 92 | toolTipTemplate 93 | } = this.props; 94 | 95 | return ( 96 | 110 | ); 111 | } 112 | 113 | getTrackOffset () { 114 | return this.state.trackOffset; 115 | } 116 | 117 | updatePosition () { 118 | const track = this.refs.track; 119 | 120 | setTimeout(() => { 121 | window.requestAnimationFrame(() => { 122 | this.setState({ 123 | trackOffset: track ? track.getBoundingClientRect() : {} 124 | }); 125 | }); 126 | }, 0); 127 | } 128 | 129 | handleClick (e) { 130 | suppress(e); 131 | const newData = getNearestValue(e, this.props, this.getTrackOffset()); 132 | this.onChange(newData.value, newData.changed); 133 | this.onDragExtreme(newData.changed, newData.value, 'end'); 134 | } 135 | 136 | isRangeType () { 137 | return this.props.type === 'range'; 138 | } 139 | 140 | render () { 141 | const { 142 | name, 143 | disabled, 144 | step, 145 | min, 146 | max, 147 | value, 148 | rangeTemplate, 149 | showSteps, 150 | orientation, 151 | attributes 152 | } = this.props; 153 | 154 | const mainClass = classNames('react-filters', 'rf-slider', name, { 155 | 'slider-disabled': disabled, 156 | 'slider-vertical': isVertical(orientation) 157 | }); 158 | 159 | const lowerValue = this.isRangeType() ? value[0] : value; 160 | 161 | return ( 162 |
163 |
164 |
169 | 176 | 177 |
178 | { 179 | showSteps && 188 | } 189 | 190 | {this.getControl(lowerValue, 'lower')} 191 | {this.isRangeType() && this.getControl(value[1], 'upper')} 192 | 193 |
194 | {rangeTemplate(min, max)} 195 |
196 | ); 197 | } 198 | } 199 | 200 | Slider.propTypes = { 201 | attributes: PropTypes.object, 202 | disabled: PropTypes.bool, 203 | max: PropTypes.number, 204 | min: PropTypes.number, 205 | name: PropTypes.string.isRequired, 206 | onChange: PropTypes.func, 207 | onDragEnd: PropTypes.func, 208 | onDragStart: PropTypes.func, 209 | orientation: PropTypes.oneOf(['horizontal', 'vertical']), 210 | rangeTemplate: PropTypes.func, 211 | readOnly: PropTypes.bool, 212 | showSteps: PropTypes.bool, 213 | step: PropTypes.number, 214 | toolTipTemplate: PropTypes.func, 215 | type: PropTypes.oneOf(['value', 'range']), 216 | value: PropTypes.oneOfType([PropTypes.array, PropTypes.number]) 217 | }; 218 | 219 | Slider.defaultProps = { 220 | attributes: {}, 221 | disabled: false, 222 | max: 20, 223 | min: 0, 224 | onDragEnd: noop, 225 | onDragStart: noop, 226 | orientation: 'horizontal', 227 | rangeTemplate (min, max) { 228 | return ( 229 |
230 |
{min}
231 |
{max}
232 |
233 | ); 234 | }, 235 | readOnly: false, 236 | showSteps: false, 237 | step: 1, 238 | toolTipTemplate (value) { 239 | return value; 240 | }, 241 | type: 'value', 242 | value: [5, 10] 243 | }; 244 | -------------------------------------------------------------------------------- /components/AutoComplete/AutoComplete.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import Fuzzy from 'fuse.js'; 4 | import autoBind from '../utils/autoBind'; 5 | import debounce from '../utils/debounce'; 6 | import SearchBox from './SearchBox'; 7 | import Suggestions from './Suggestions'; 8 | import deepCopy from 'deep-copy'; 9 | 10 | export default class AutoComplete extends Component { 11 | constructor (props) { 12 | super(props); 13 | 14 | this.state = { 15 | multiSelected: [], 16 | query: '', 17 | results: props.showInitialResults ? props.list : [], 18 | selectedIndex: 0 19 | }; 20 | 21 | autoBind([ 22 | 'onSelect', 23 | 'onKeyDown', 24 | 'getOptions', 25 | 'onResetClick', 26 | 'handleQueryChange', 27 | 'removeTag' 28 | ], this); 29 | 30 | this.handleChange = debounce(this.handleChange, props.debounce); 31 | if (!props.async) this.fuse = new Fuzzy(props.list, this.getOptions()); 32 | } 33 | 34 | componentWillReceiveProps (newProps, newState) { 35 | if (newProps.async || (newProps.showInitialResults && !newState.query)) { 36 | this.setState({ results: newProps.list || [] }); 37 | } 38 | } 39 | 40 | onSelect () { 41 | const { name, onSelect } = this.props; 42 | onSelect(name); 43 | } 44 | 45 | onKeyDown (e) { 46 | const { selectedIndex, results } = this.state; 47 | const { name, valueKey, onSelect, multiSelect } = this.props; 48 | 49 | if (e.keyCode === 40 && (selectedIndex < results.length - 1)) { 50 | this.setState({ 51 | selectedIndex: selectedIndex + 1 52 | }); 53 | } else if (e.keyCode === 38 && (selectedIndex > 0)) { 54 | this.setState({ 55 | selectedIndex: selectedIndex - 1 56 | }); 57 | } else if (e.keyCode === 13) { 58 | if (multiSelect) { 59 | this.state.multiSelected.push(results[selectedIndex]); 60 | } 61 | 62 | if (multiSelect) { 63 | onSelect({ 64 | action: 'added', 65 | changed: results[selectedIndex], 66 | name, 67 | value: this.state.multiSelected 68 | }); 69 | } else if (results[selectedIndex]) { 70 | onSelect({ 71 | name, 72 | value: results[selectedIndex] 73 | }); 74 | } 75 | 76 | this.setState({ 77 | query: multiSelect ? '' : results[selectedIndex][valueKey], 78 | results: [], 79 | selectedIndex: 0 80 | }); 81 | } 82 | } 83 | 84 | onResetClick () { 85 | this.setState({ query: '' }); 86 | if (!this.props.showInitialResults) this.setState({ results: [] }); 87 | } 88 | 89 | getOptions () { 90 | return { ...this.props.fuzzyOptions, keys: this.props.keys }; 91 | } 92 | 93 | getSuggestions () { 94 | const { resultsTemplate } = this.props; 95 | const { results, selectedIndex } = this.state; 96 | if (results && results.length) { 97 | return ( 98 | 103 | ); 104 | } 105 | return null; 106 | } 107 | 108 | removeTag ({ id }) { 109 | const changed = this.state.multiSelected[id]; 110 | const multiSelected = deepCopy(this.state.multiSelected); 111 | multiSelected.splice(id, 1); 112 | this.setState({ multiSelected }, () => ( 113 | this.props.onSelect({ 114 | action: 'removed', 115 | changed, 116 | name: this.props.name, 117 | value: this.state.multiSelected 118 | }) 119 | )); 120 | } 121 | 122 | handleQueryChange (query) { 123 | if (!this.props.async) { 124 | this.setState({ 125 | query, 126 | results: this.props.showInitialResults && !query ? this.props.list : this.fuse.search(query) 127 | }); 128 | } 129 | 130 | if (typeof this.props.onChange === 'function') { 131 | this.setState({ 132 | query, 133 | results: this.props.onChange(query, this.props, this) 134 | }); 135 | } 136 | } 137 | 138 | render () { 139 | const { 140 | name, 141 | disabled, 142 | placeholder, 143 | onFocus, 144 | onBlur, 145 | Reset, 146 | multiSelect, 147 | showTagRemove, 148 | valueKey 149 | } = this.props; 150 | 151 | const mainClass = classNames('react-filters', 'rf-autocomplete', name, { 152 | disabled 153 | }); 154 | 155 | return ( 156 |
157 | 172 | 173 | {this.getSuggestions()} 174 |
175 | ); 176 | } 177 | } 178 | 179 | AutoComplete.propTypes = { 180 | Reset: PropTypes.func, 181 | async: PropTypes.bool, 182 | className: PropTypes.string, 183 | debounce: PropTypes.number, 184 | disabled: PropTypes.bool, 185 | list: PropTypes.array, 186 | multiSelect: PropTypes.bool, 187 | name: PropTypes.string.isRequired, 188 | onBlur: PropTypes.func, 189 | onChange: PropTypes.func, 190 | onFocus: PropTypes.func, 191 | onSelect: PropTypes.func.isRequired, 192 | placeholder: PropTypes.string, 193 | resultsTemplate: PropTypes.func, 194 | showInitialResults: PropTypes.bool, 195 | showTagRemove: PropTypes.bool, 196 | valueKey: PropTypes.string, 197 | width: PropTypes.number, 198 | fuzzyOptions: PropTypes.shape({ 199 | caseSensitive: PropTypes.bool, 200 | id: PropTypes.string, 201 | include: PropTypes.array, 202 | keys: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), 203 | shouldSort: PropTypes.bool, 204 | sortFn: PropTypes.func, 205 | tokenize: PropTypes.bool, 206 | verbose: PropTypes.bool, 207 | maxPatternLength: PropTypes.number, 208 | distance: PropTypes.number, 209 | threshold: PropTypes.number, 210 | location: PropTypes.number 211 | }) 212 | }; 213 | 214 | AutoComplete.defaultProps = { 215 | async: false, 216 | debounce: 250, 217 | disabled: false, 218 | multiSelect: false, 219 | placeholder: 'Search', 220 | resultsTemplate: Suggestions.defaultResultsTemplate, 221 | showInitialResults: false, 222 | showTagRemove: true, 223 | tags: false, 224 | valueKey: 'title', 225 | width: 430, 226 | fuzzyOptions: { 227 | caseSensitive: false, 228 | shouldSort: true, 229 | sortFn (a, b) { 230 | return a.score - b.score; 231 | }, 232 | threshold: 0.6, 233 | tokenize: false, 234 | verbose: false, 235 | distance: 100, 236 | include: [], 237 | location: 0 238 | } 239 | }; 240 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [](https://github.com/ritz078/react-filters/compare/v0.2.1...v) (2016-09-06) 3 | 4 | 5 | ### Performance Improvements 6 | 7 | * **build:** decreased filesize by removing webpack code from build files ([cb927ff](https://github.com/ritz078/react-filters/commit/cb927ff)) 8 | 9 | 10 | 11 | 12 | # [](https://github.com/ritz078/react-filters/compare/v0.1.6...v) (2016-08-11) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **group:** accept and return selectedIds as number if type is radio ([b5c67ee](https://github.com/ritz078/react-filters/commit/b5c67ee)) 18 | 19 | 20 | ### Features 21 | 22 | * **toggle:** added separated prop fro selected ids and added selectedIds in onChnage argument ([5f66bf0](https://github.com/ritz078/react-filters/commit/5f66bf0)) 23 | 24 | 25 | 26 | 27 | # [](https://github.com/ritz078/react-filters/compare/v0.1.6...v) (2016-08-11) 28 | 29 | 30 | ### Features 31 | 32 | * **toggle:** added separated prop fro selected ids and added selectedIds in onChnage argument ([5f66bf0](https://github.com/ritz078/react-filters/commit/5f66bf0)) 33 | 34 | 35 | 36 | 37 | # [](https://github.com/ritz078/react-filters/compare/v0.1.6...v) (2016-08-10) 38 | 39 | 40 | ### Features 41 | 42 | * **toggle:** added separated prop fro selected ids and added selectedIds in onChnage argument ([5f66bf0](https://github.com/ritz078/react-filters/commit/5f66bf0)) 43 | 44 | 45 | 46 | 47 | # [](https://github.com/ritz078/react-filters/compare/v0.1.6...v) (2016-08-10) 48 | 49 | 50 | ### Features 51 | 52 | * **toggle:** added separated prop fro selected ids and added selectedIds in onChnage argument ([5f66bf0](https://github.com/ritz078/react-filters/commit/5f66bf0)) 53 | 54 | 55 | 56 | 57 | ## [0.1.6](https://github.com/ritz078/react-filters/compare/v0.1.5...v0.1.6) (2016-08-10) 58 | 59 | 60 | 61 | 62 | ## [0.1.5](https://github.com/ritz078/react-filters/compare/v0.1.4...v0.1.5) (2016-08-09) 63 | 64 | 65 | 66 | 67 | ## [0.1.4](https://github.com/ritz078/react-filters/compare/v0.1.3...v0.1.4) (2016-08-09) 68 | 69 | 70 | 71 | 72 | ## [0.1.3](https://github.com/ritz078/react-filters/compare/v0.1.2...v0.1.3) (2016-08-09) 73 | 74 | 75 | 76 | 77 | ## [0.1.2](https://github.com/ritz078/react-filters/compare/v0.1.1...v0.1.2) (2016-08-09) 78 | 79 | 80 | 81 | 82 | ## [0.1.1](https://github.com/ritz078/react-filters/compare/v0.1.0...v0.1.1) (2016-08-09) 83 | 84 | 85 | 86 | 87 | # [0.1.0](https://github.com/ritz078/react-filters/compare/5102f55...v0.1.0) (2016-08-09) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **eslint:** added eslint loader ([7e39fe6](https://github.com/ritz078/react-filters/commit/7e39fe6)) 93 | * **eslint:** fixed the order of loader ([8cc1986](https://github.com/ritz078/react-filters/commit/8cc1986)) 94 | * **hmr:** css also live reloading in dev environment ([85ec692](https://github.com/ritz078/react-filters/commit/85ec692)) 95 | * **ignore:** added npm-debug-log ([dd456e9](https://github.com/ritz078/react-filters/commit/dd456e9)) 96 | * **mocha/document:** resolve eslint single-string error ([5102f55](https://github.com/ritz078/react-filters/commit/5102f55)) 97 | * **range:** initial rail render is now fixed ([2502cee](https://github.com/ritz078/react-filters/commit/2502cee)) 98 | * **range:** window resize event working as expected. ([8266296](https://github.com/ritz078/react-filters/commit/8266296)) 99 | * **slider:** now working fine for vertical range type sliders ([d366bbe](https://github.com/ritz078/react-filters/commit/d366bbe)) 100 | * **slider:** on first render track width wasn't calculated right ([531af63](https://github.com/ritz078/react-filters/commit/531af63)) 101 | * **slider:** value passed for range slider corrected ([60dffa4](https://github.com/ritz078/react-filters/commit/60dffa4)) 102 | * **switch:** no more sass, inline CSS implemented and switch is now working ([0176b28](https://github.com/ritz078/react-filters/commit/0176b28)) 103 | * **switch:** remove label div if label isn't passed in props ([130e5c4](https://github.com/ritz078/react-filters/commit/130e5c4)) 104 | * **switch:** separated styles into separate SCSS file ([8d3d78c](https://github.com/ritz078/react-filters/commit/8d3d78c)) 105 | * **test:** removed start script and fixed test files path ([36774e4](https://github.com/ritz078/react-filters/commit/36774e4)) 106 | * **tests:** and also removed cover task from ghook ([dfc45e5](https://github.com/ritz078/react-filters/commit/dfc45e5)) 107 | * **webpack:** added webpack middleware for storybook ([5a137fc](https://github.com/ritz078/react-filters/commit/5a137fc)) 108 | 109 | 110 | ### Features 111 | 112 | * **autocomplete:** added fuzzy search and async requests in autocomplete ([26d2216](https://github.com/ritz078/react-filters/commit/26d2216)) 113 | * **autocomplete:** show the suggesstions on load ([ccc52ba](https://github.com/ritz078/react-filters/commit/ccc52ba)) 114 | * **autocomplete:** trigger onSelect on removal of tags ([33abcc5](https://github.com/ritz078/react-filters/commit/33abcc5)) 115 | * **Count:** added count filter with its tests. ([79cb34c](https://github.com/ritz078/react-filters/commit/79cb34c)) 116 | * **css-modules:** added support for css modules loader with sass integration ([a5d389c](https://github.com/ritz078/react-filters/commit/a5d389c)) 117 | * **ghooks:** added ghooks and fixed linting errors ([0c1b6e8](https://github.com/ritz078/react-filters/commit/0c1b6e8)) 118 | * **group:** added group component and combined switch, radio and checkbox ([c38c022](https://github.com/ritz078/react-filters/commit/c38c022)) 119 | * **group:** added old value in the onChange ([23b62fa](https://github.com/ritz078/react-filters/commit/23b62fa)) 120 | * **hmr:** enable hmr and setup a demo page ([c0ba9eb](https://github.com/ritz078/react-filters/commit/c0ba9eb)) 121 | * **InputRange:** started input range and fixed linting errors ([ebf85dc](https://github.com/ritz078/react-filters/commit/ebf85dc)) 122 | * **radio:** started project, implemented react-storybook ([ee43572](https://github.com/ritz078/react-filters/commit/ee43572)) 123 | * **range:** added on click event handler on the track ([791f43f](https://github.com/ritz078/react-filters/commit/791f43f)) 124 | * **range:** added readOnly and disabled state ([2495823](https://github.com/ritz078/react-filters/commit/2495823)) 125 | * **range:** added value and range indicator ([ca17778](https://github.com/ritz078/react-filters/commit/ca17778)) 126 | * **range:** fixed steps increment issue and refactored code ([41fc6f8](https://github.com/ritz078/react-filters/commit/41fc6f8)) 127 | * **range:** zoom slider while dragging it ([89567d4](https://github.com/ritz078/react-filters/commit/89567d4)) 128 | * **range-slider:** completed the basic functionalities of a range slider ([bbf9b1b](https://github.com/ritz078/react-filters/commit/bbf9b1b)) 129 | * **slider:** added rails support is value slider ([d6465c9](https://github.com/ritz078/react-filters/commit/d6465c9)) 130 | * **slider:** added single control option in slider ([9ffe946](https://github.com/ritz078/react-filters/commit/9ffe946)) 131 | * **slider:** added support for onDragStart and onDragEnd methods ([c4941ce](https://github.com/ritz078/react-filters/commit/c4941ce)) 132 | * **slider:** added support for vertical slider ([3ae809b](https://github.com/ritz078/react-filters/commit/3ae809b)) 133 | * **styleProcessor:** added utility to process style based on different states ([c455c03](https://github.com/ritz078/react-filters/commit/c455c03)) 134 | * **switch:** created a container to preview switch ([8f35ec7](https://github.com/ritz078/react-filters/commit/8f35ec7)) 135 | * **tag:** added tag feature in autocomplete and refactored it ([2a35097](https://github.com/ritz078/react-filters/commit/2a35097)) 136 | * **toggle:** added tag mode to Toggle Component working for radio and checkbox type ([37fc772](https://github.com/ritz078/react-filters/commit/37fc772)) 137 | 138 | 139 | ### Performance Improvements 140 | 141 | * **range:** decreased the number of re renders of the elements ([a6369f2](https://github.com/ritz078/react-filters/commit/a6369f2)) 142 | * **range:** improved the responsiveness of Range Slider ([1857274](https://github.com/ritz078/react-filters/commit/1857274)) 143 | * **range:** optimized width calculations of slider and track ([78b8598](https://github.com/ritz078/react-filters/commit/78b8598)) 144 | * **range:** removed forced layout update ([c476203](https://github.com/ritz078/react-filters/commit/c476203)) 145 | 146 | 147 | 148 | --------------------------------------------------------------------------------