├── .babelrc
├── webpack.config.js
├── .gitignore
├── test
├── helpers
│ └── fakeDom.js
└── Switch-test.js
├── src
├── utils.js
├── Label.js
└── Switch.js
├── example
├── index.html
├── example.css
└── example.js
├── lib
├── utils.js
├── Label.js
└── Switch.js
├── CHANGELOG.md
├── package.json
└── README.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 |
3 | module.exports = {
4 | entry: {
5 | example: ['./example/example.js']
6 | },
7 | module: {
8 | loaders: [
9 | {
10 | test: /\.js/,
11 | loader: 'babel-loader'
12 | }
13 | ]
14 | },
15 | output: {
16 | path: path.resolve(__dirname, 'example'),
17 | publicPath: '/example',
18 | filename: 'bundle.js'
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Coverage tools
11 | lib-cov
12 | coverage
13 | coverage.html
14 | .cover*
15 |
16 | # Dependency directory
17 | node_modules
18 | package-lock.json
19 |
20 | # Example build directory
21 | example/dist
22 | .publish
23 |
24 | # Editor and other tmp files
25 | *.swp
26 | *.un~
27 | *.iml
28 | *.ipr
29 | *.iws
30 | *.sublime-*
31 | .idea/
32 | *.DS_Store
33 |
--------------------------------------------------------------------------------
/test/helpers/fakeDom.js:
--------------------------------------------------------------------------------
1 | const jsdom = require('jsdom');
2 | const doc = jsdom.jsdom('
');
3 | const win = doc.defaultView;
4 |
5 | global.document = doc;
6 | global.window = win;
7 | global.navigator = {userAgent: 'node.js'};
8 |
9 | propagateToGlobal(win);
10 |
11 | function propagateToGlobal (window) {
12 | for (let key in window) {
13 | if (!window.hasOwnProperty(key)) continue;
14 | if (key in global) continue;
15 |
16 | global[key] = window[key];
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export const events = {
2 | touch: {
3 | start: 'touchstart',
4 | stop: 'touchend',
5 | move: 'touchmove'
6 | },
7 | mouse: {
8 | start: 'mousedown',
9 | stop: 'mouseup'
10 | }
11 | };
12 |
13 | export function merge(...hashes) {
14 | return Object.assign({}, ...hashes);
15 | }
16 |
17 | export function disableScroll() {
18 | document.addEventListener(events.touch.move, preventScroll, false);
19 | }
20 |
21 | export function reEnableScroll() {
22 | document.removeEventListener(events.touch.move, preventScroll, false);
23 | }
24 |
25 | function preventScroll(e) {
26 | e.preventDefault();
27 | }
28 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | React Flexible Switch
4 |
5 |
6 |
7 |
8 |
9 |
React Flexible Switch
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Label.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import { merge } from './utils'
3 | import PropTypes from 'prop-types'
4 |
5 | export default class Label extends PureComponent {
6 | styles() {
7 | const offset = this.props.active ? { left: '20% ' } : { right: '20%' }
8 |
9 | return merge(
10 | {
11 | position: 'absolute',
12 | top: '50%',
13 | transform: 'translateY(-50%)',
14 | pointerEvents: 'none'
15 | },
16 | offset
17 | )
18 | }
19 |
20 | render() {
21 | return (
22 |
23 | {this.props.active ? this.props.labels.on : this.props.labels.off}
24 |
25 | )
26 | }
27 | }
28 |
29 | Label.propTypes = {
30 | active: PropTypes.bool,
31 | labels: PropTypes.shape({
32 | on: PropTypes.string.isRequired,
33 | off: PropTypes.string.isRequired
34 | }).isRequired
35 | }
36 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.merge = merge;
7 | exports.disableScroll = disableScroll;
8 | exports.reEnableScroll = reEnableScroll;
9 | var events = exports.events = {
10 | touch: {
11 | start: 'touchstart',
12 | stop: 'touchend',
13 | move: 'touchmove'
14 | },
15 | mouse: {
16 | start: 'mousedown',
17 | stop: 'mouseup'
18 | }
19 | };
20 |
21 | function merge() {
22 | for (var _len = arguments.length, hashes = Array(_len), _key = 0; _key < _len; _key++) {
23 | hashes[_key] = arguments[_key];
24 | }
25 |
26 | return Object.assign.apply(Object, [{}].concat(hashes));
27 | }
28 |
29 | function disableScroll() {
30 | document.addEventListener(events.touch.move, preventScroll, false);
31 | }
32 |
33 | function reEnableScroll() {
34 | document.removeEventListener(events.touch.move, preventScroll, false);
35 | }
36 |
37 | function preventScroll(e) {
38 | e.preventDefault();
39 | }
--------------------------------------------------------------------------------
/example/example.css:
--------------------------------------------------------------------------------
1 | /*
2 | // Examples Stylesheet
3 | // -------------------
4 | */
5 |
6 | body {
7 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
8 | font-size: 14px;
9 | color: #333;
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | a {
15 | color: #08c;
16 | text-decoration: none;
17 | }
18 |
19 | a:hover {
20 | text-decoration: underline;
21 | }
22 |
23 | .container {
24 | margin-left: auto;
25 | margin-right: auto;
26 | max-width: 720px;
27 | padding: 1em;
28 | }
29 |
30 | .footer {
31 | margin-top: 50px;
32 | border-top: 1px solid #eee;
33 | padding: 20px 0;
34 | font-size: 12px;
35 | color: #999;
36 | }
37 |
38 | h1,
39 | h2,
40 | h3,
41 | h4,
42 | h5,
43 | h6 {
44 | color: #222;
45 | font-weight: 100;
46 | margin: 0.5em 0;
47 | }
48 |
49 | label {
50 | color: #999;
51 | display: inline-block;
52 | font-size: 0.85em;
53 | font-weight: bold;
54 | margin: 1em 0;
55 | text-transform: uppercase;
56 | }
57 |
58 | .hint {
59 | margin: 15px 0;
60 | font-style: italic;
61 | color: #999;
62 | }
63 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 | ## 1.2.0 (Nov 21, 2017)
5 | ### Added
6 | - Add support for React 16
7 |
8 | ## 1.1.0 (Apr 5, 2017)
9 | ### Added
10 | - Add namespaced CSS class for the circle component
11 |
12 | ## 1.0.1 (Apr 5, 2017)
13 | ### Fixed
14 | - Updated CHANGELOG to mention 1.0.0
15 |
16 | ## 1.0.0 (Apr 5, 2017)
17 | ### Added
18 | - Namespace CSS classes in order to avoid colisions
19 |
20 | ## 0.6.0 (Jan 9, 2017)
21 | ### Added
22 | - Add keyboard access support
23 |
24 | ## 0.5.1 (Nov 26, 2016)
25 | ### Fixed
26 | - Fixed wrong reference to `value` in Readme
27 |
28 | ## 0.5.0 (Nov 26, 2016)
29 | ### Added
30 | - Make Switch a controlled component
31 |
32 | ### Breaking Changes
33 | - Remove `active` and `inactive` in favor of using `value` as the controlling property
34 | - Remove `onActive` and `onInactive` in favor of using `onChange`
35 |
36 | ## 0.4.1 (Sep 24, 2016)
37 | ### Added
38 | - Support for React 15 (peer dependency)
39 |
40 | ## 0.3.1 (Jun 30, 2016)
41 |
42 | ### Bug Fixes
43 | - Reset global events when `locked` props are changed
44 | - Only enable touch events when necessary
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-flexible-switch",
3 | "version": "1.2.1",
4 | "description": "Simple and flexible React Switch",
5 | "main": "lib/Switch.js",
6 | "author": "Netto Farah",
7 | "homepage": "https://github.com/nettofarah/react-flexible-switch",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/nettofarah/react-flexible-switch.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/nettofarah/react-flexible-switch/issues"
14 | },
15 | "dependencies": {
16 | "classnames": "^2.1.2",
17 | "prop-types": "^15.6.0"
18 | },
19 | "devDependencies": {
20 | "babel-cli": "^6.26.0",
21 | "babel-core": "^6.26.0",
22 | "babel-loader": "^7.1.2",
23 | "babel-preset-es2015": "^6.24.1",
24 | "babel-preset-react": "^6.24.1",
25 | "branchsite": "^4.0.2",
26 | "jsdom": "^8.4.0",
27 | "mocha": "^2.4.5",
28 | "np": "^2.17.0",
29 | "react": "^16.1.1",
30 | "react-dom": "^16.1.1",
31 | "webpack": "^3.8.1",
32 | "webpack-dev-server": "^2.9.4"
33 | },
34 | "peerDependencies": {
35 | "react": "^0.14 || ^15.0.0-rc || ^15.0 || ^16.0",
36 | "react-dom": "^0.14 || ^15.0.0-rc || ^15.0 || ^16.0"
37 | },
38 | "browserify-shim": {
39 | "react": "global:React"
40 | },
41 | "scripts": {
42 | "build": "babel src -d lib",
43 | "test":
44 | "NODE_ENV=test mocha --compilers js:babel-register --require ./test/helpers/fakeDom.js",
45 | "dev": "webpack-dev-server",
46 | "build-example": "webpack",
47 | "release": "np --yolo"
48 | },
49 | "keywords": [
50 | "react",
51 | "react-component",
52 | "switch",
53 | "toggle",
54 | "react-switch",
55 | "react-toggle"
56 | ],
57 | "files": ["lib", "src"]
58 | }
59 |
--------------------------------------------------------------------------------
/example/example.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import Switch from '../lib/Switch'
4 |
5 | class App extends React.Component {
6 | constructor() {
7 | super()
8 | this.state = { locked: true, externalValue: true }
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
On By default
15 |
16 |
17 | Off By default
18 |
19 |
20 | Custom Colors
21 |
22 |
23 | Custom Diameter
24 |
25 |
26 |
27 |
28 | Custom Switch Width
29 |
30 |
31 |
32 |
33 | Labels
34 |
35 |
36 | Locking the Switch
37 |
40 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | External Controls
50 |
57 |
58 |
59 |
60 | {
64 | this.setState({ externalValue: value })
65 | }}
66 | />
67 |
68 | {
72 | this.setState({ externalValue: !value })
73 | }}
74 | />
75 |
76 | )
77 | }
78 | }
79 |
80 | ReactDOM.render(, document.getElementById('app'))
81 |
--------------------------------------------------------------------------------
/lib/Label.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var _utils = require('./utils');
14 |
15 | var _propTypes = require('prop-types');
16 |
17 | var _propTypes2 = _interopRequireDefault(_propTypes);
18 |
19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20 |
21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
22 |
23 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
24 |
25 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
26 |
27 | var Label = function (_PureComponent) {
28 | _inherits(Label, _PureComponent);
29 |
30 | function Label() {
31 | _classCallCheck(this, Label);
32 |
33 | return _possibleConstructorReturn(this, (Label.__proto__ || Object.getPrototypeOf(Label)).apply(this, arguments));
34 | }
35 |
36 | _createClass(Label, [{
37 | key: 'styles',
38 | value: function styles() {
39 | var offset = this.props.active ? { left: '20% ' } : { right: '20%' };
40 |
41 | return (0, _utils.merge)({
42 | position: 'absolute',
43 | top: '50%',
44 | transform: 'translateY(-50%)',
45 | pointerEvents: 'none'
46 | }, offset);
47 | }
48 | }, {
49 | key: 'render',
50 | value: function render() {
51 | return _react2.default.createElement(
52 | 'span',
53 | { style: this.styles(), className: 'react-flexible-switch-label' },
54 | this.props.active ? this.props.labels.on : this.props.labels.off
55 | );
56 | }
57 | }]);
58 |
59 | return Label;
60 | }(_react.PureComponent);
61 |
62 | exports.default = Label;
63 |
64 |
65 | Label.propTypes = {
66 | active: _propTypes2.default.bool,
67 | labels: _propTypes2.default.shape({
68 | on: _propTypes2.default.string.isRequired,
69 | off: _propTypes2.default.string.isRequired
70 | }).isRequired
71 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Flexible Switch
2 | Easy and Flexible React switches with support for custom styles.
3 |
4 |
5 | ## Demo & Examples
6 |
7 | 
8 |
9 | Live demo: [http://nettofarah.github.io/react-flexible-switch](http://nettofarah.github.io/react-flexible-switch/)
10 | To build the examples locally, run:
11 |
12 | ```bash
13 | npm install
14 | npm start
15 | ```
16 |
17 | Then open [`localhost:8000`](http://localhost:8000) in a browser.
18 |
19 |
20 | ## Installation
21 |
22 | The easiest way to use react-switch is to install it from NPM and include it in your own React build process (using [Browserify](http://browserify.org), [Webpack](http://webpack.github.io/), etc).
23 |
24 | You can also use the standalone build by including `dist/react-switch.js` in your page. If you use this, make sure you have already included React, and it is available as a global variable.
25 |
26 | ```
27 | npm install react-flexible-switch --save
28 | ```
29 |
30 |
31 | ## Usage
32 |
33 | Just require 'react-flexible-switch' in your app and include it in your components.
34 |
35 | ```javascript
36 | const Switch = require('react-flexible-switch');
37 |
38 | ```
39 |
40 | ### Properties
41 |
42 | ```javascript
43 | Switch.propTypes = {
44 | value: React.PropTypes.bool,
45 |
46 | circleStyles: React.PropTypes.shape({
47 | onColor: React.PropTypes.string,
48 | offColor: React.PropTypes.string,
49 | diameter: React.PropTypes.number
50 | }),
51 |
52 | labels: React.PropTypes.shape({
53 | on: React.PropTypes.string,
54 | off: React.PropTypes.string
55 | }),
56 |
57 | locked: React.PropTypes.bool,
58 |
59 | onChange: React.PropTypes.func,
60 |
61 | switchStyles: React.PropTypes.shape({
62 | width: React.PropTypes.number
63 | })
64 | };
65 | ```
66 |
67 | #### value
68 | Allows you to start a switch either turned on or off.
69 |
70 | ```javascript
71 | //On by default
72 |
73 |
74 | //Off by default
75 |
76 | ```
77 |
78 | #### onChange
79 | Allows you to pass in callback for when state changes.
80 | This will allow you to make the switch a controlled component.
81 |
82 | ```javascript
83 | const onChange = (active) => {
84 | if (active) {
85 | console.log('active!')
86 | } else {
87 | console.log('inactive!')
88 | }
89 |
90 | // update your state here
91 | this.setState({ value: active })
92 | }
93 |
94 |
95 | ```
96 |
97 | #### Custom Styles
98 | You can style both the circle and switch styles with any css property, with
99 | the addition of `onColor`, `offColor` and `diameter`.
100 |
101 |
102 | ```javascript
103 | // Custom circle colors and size
104 |
105 |
106 |
107 |
108 | // Custom Switch width
109 |
110 |
111 | ```
112 |
113 | #### Customzing with CSS classes
114 | You can also style the components using the following css classes:
115 |
116 | - `react-flexible-switch`: the main component
117 | - `react-flexible-switch--active`: the main component, when active
118 | - `react-flexible-switch--inactive`: the main component, when inactive
119 | - `react-flexible-switch--sliding`: the main component, during the transition
120 | - `react-flexible-switch-label`: the label component
121 | - `react-flexible-switch-circle`: the circle component
122 |
123 | #### Labels
124 | Labels for the `on` and `off` states can be set by using the `labels` property.
125 |
126 | ```javascript
127 |
128 |
129 | ```
130 |
131 | #### Blocking User Interaction
132 | In case you need to lock the switch and block user interaction for some reason.
133 |
134 | ```javascript
135 |
136 |
137 | ```
138 |
139 | ## Development (`src`, `lib` and the build process)
140 |
141 | **NOTE:** The source code for the component is in `src`. A transpiled CommonJS version (generated with Babel) is available in `lib` for use with node.js, browserify and webpack. A UMD bundle is also built to `dist`, which can be included without the need for any build system.
142 |
143 | To build, watch and serve the examples (which will also watch the component source), run `npm start`. If you just want to watch changes to `src` and rebuild `lib`, run `npm run watch` (this is useful if you are working with `npm link`).
144 |
145 | ## License
146 | The module is available as open source under the terms of the MIT License.
147 | Copyright (c) 2016 Netto Farah.
148 |
--------------------------------------------------------------------------------
/src/Switch.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classNames from 'classnames'
3 | import Label from './Label'
4 | import { merge, events, disableScroll, reEnableScroll } from './utils'
5 | import PropTypes from 'prop-types'
6 |
7 | class Switch extends React.Component {
8 | constructor(props) {
9 | super(props)
10 |
11 | this.onActivateButton = this.onActivateButton.bind(this)
12 | this.onSlideEnd = this.onSlideEnd.bind(this)
13 | this.onSlideStart = this.onSlideStart.bind(this)
14 | this.onMouseLeave = this.onMouseLeave.bind(this)
15 |
16 | this.isTouchDevice = window['ontouchstart'] !== undefined
17 |
18 | this.state = { sliding: false, value: this.props.value }
19 | }
20 |
21 | componentDidMount() {
22 | this.addListener()
23 | }
24 |
25 | componentWillReceiveProps(nextProps) {
26 | if (nextProps.value === undefined) {
27 | return
28 | }
29 |
30 | if (nextProps.value !== this.state.value) {
31 | this.setState({ value: nextProps.value })
32 | }
33 | }
34 |
35 | componentDidUpdate(prevProps, prevState) {
36 | if (this.state.value != prevState.value) {
37 | this.props.onChange(this.state.value)
38 | }
39 | }
40 |
41 | componentWillUnmount() {
42 | this.removeListener()
43 | }
44 |
45 | addListener() {
46 | if (this.isTouchDevice) {
47 | document.addEventListener(events.touch.start, this.onSlideStart, false)
48 | document.addEventListener(events.touch.stop, this.onSlideEnd, false)
49 | } else {
50 | document.addEventListener(events.mouse.start, this.onSlideStart, false)
51 | document.addEventListener(events.mouse.stop, this.onSlideEnd, false)
52 | }
53 | }
54 |
55 | removeListener() {
56 | if (this.isTouchDevice) {
57 | document.removeEventListener(events.touch.start, this.onSlideStart, false)
58 | document.removeEventListener(events.touch.stop, this.onSlideEnd, false)
59 | } else {
60 | document.removeEventListener(events.mouse.start, this.onSlideStart, false)
61 | document.removeEventListener(events.mouse.stop, this.onSlideEnd, false)
62 | }
63 | }
64 |
65 | onActivateButton() {
66 | this.setState({ value: !this.state.value })
67 | }
68 |
69 | onSlideEnd() {
70 | if (this.props.locked) {
71 | return
72 | }
73 |
74 | if (this.state.sliding) {
75 | this.setState({ sliding: false, value: !this.state.value })
76 | reEnableScroll()
77 | }
78 | }
79 |
80 | onSlideStart(e) {
81 | if (this.props.locked) {
82 | return
83 | }
84 |
85 | if (e.target == this.refs.circle || e.target == this.refs.switch) {
86 | this.setState({ sliding: true })
87 | disableScroll()
88 | }
89 | }
90 |
91 | onMouseLeave(e) {
92 | this.onSlideEnd(e)
93 | }
94 |
95 | classes() {
96 | return classNames(
97 | 'react-flexible-switch',
98 | { 'react-flexible-switch--sliding': this.state.sliding },
99 | { 'react-flexible-switch--active': this.state.value },
100 | { 'react-flexible-switch--inactive': !this.state.value }
101 | )
102 | }
103 |
104 | switchStyles() {
105 | const switchStyles = this.switchStylesProps()
106 | return merge({ borderRadius: switchStyles.width / 2 }, switchStyles)
107 | }
108 |
109 | translationStyle() {
110 | const circleStyles = this.circleStylesProps()
111 | const switchStyles = this.switchStyles()
112 |
113 | const offset = switchStyles.width - circleStyles.diameter
114 | let translation = this.state.value ? offset : 0
115 |
116 | if (this.state.sliding && this.state.value) {
117 | translation -= circleStyles.diameter / 4 + switchStyles.padding / 4
118 | }
119 |
120 | return {
121 | transform: `translateX(${translation}px)`
122 | }
123 | }
124 |
125 | backgroundStyle() {
126 | const circleStyles = this.circleStylesProps()
127 | const backgroundColor = this.state.value
128 | ? circleStyles.onColor
129 | : circleStyles.offColor
130 | return { backgroundColor }
131 | }
132 |
133 | circleStylesProps() {
134 | return merge(defaultCircleStyles, this.props.circleStyles)
135 | }
136 |
137 | switchStylesProps() {
138 | return merge(defaultSwitchStyles, this.props.switchStyles)
139 | }
140 |
141 | circleDimensionsStyle() {
142 | const switchStyles = this.switchStyles()
143 | const circleStyles = this.circleStylesProps()
144 | const width = this.state.sliding
145 | ? circleStyles.diameter + circleStyles.diameter / 4
146 | : circleStyles.diameter
147 | return { width, height: circleStyles.diameter }
148 | }
149 |
150 | circleStyles() {
151 | return merge(
152 | this.circleDimensionsStyle(),
153 | this.backgroundStyle(),
154 | this.translationStyle(),
155 | this.circleStylesProps()
156 | )
157 | }
158 |
159 | render() {
160 | return (
161 |
167 |
172 |
177 |
178 |
185 |
186 | )
187 | }
188 | }
189 |
190 | const defaultSwitchStyles = {
191 | width: 80,
192 | padding: 4,
193 | border: '1px solid #CFCFCF',
194 | display: 'flex',
195 | position: 'relative',
196 | backgroundColor: 'white',
197 | boxSizing: 'content-box'
198 | }
199 |
200 | const defaultCircleStyles = {
201 | diameter: 35,
202 | borderRadius: 35,
203 | display: 'block',
204 | transition: 'transform 200ms, width 200ms, background-color 200ms',
205 | onColor: '#70D600',
206 | offColor: '#CFCFCF'
207 | }
208 |
209 | const hiddenButtonStyles = {
210 | backgroundColor: 'transparent',
211 | borderColor: 'transparent',
212 | color: 'transparent',
213 | height: '100%',
214 | left: 0,
215 | pointerEvents: 'none',
216 | position: 'absolute',
217 | top: 0,
218 | width: '100%'
219 | }
220 |
221 | Switch.propTypes = {
222 | value: PropTypes.bool,
223 |
224 | circleStyles: PropTypes.shape({
225 | onColor: PropTypes.string,
226 | offColor: PropTypes.string,
227 | diameter: PropTypes.number
228 | }),
229 |
230 | labels: PropTypes.shape({
231 | on: PropTypes.string,
232 | off: PropTypes.string
233 | }),
234 |
235 | locked: PropTypes.bool,
236 |
237 | onChange: PropTypes.func,
238 |
239 | switchStyles: PropTypes.shape({
240 | width: PropTypes.number
241 | })
242 | }
243 |
244 | Switch.defaultProps = {
245 | onChange: function() {},
246 | circleStyles: defaultCircleStyles,
247 | switchStyles: defaultSwitchStyles,
248 | labels: { on: '', off: '' },
249 | locked: false
250 | }
251 |
252 | export default Switch
253 |
--------------------------------------------------------------------------------
/test/Switch-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-dom/test-utils';
4 | import Switch from '../src/Switch';
5 | import Label from '../src/Label';
6 | import assert from 'assert';
7 |
8 | describe('props', () => {
9 | it('has default props', () => {
10 | const comp = renderComponent();
11 | const props = comp.props;
12 |
13 | assert(typeof props.onChange === 'function');
14 | assert(typeof props.circleStyles === 'object');
15 | assert(typeof props.switchStyles === 'object');
16 | });
17 |
18 | describe('active', () => {
19 |
20 | it('turns the switch on', () => {
21 | const comp = renderComponent({ value: true });
22 | assert(isOn(comp));
23 | });
24 |
25 | it('turns the switch off', () => {
26 | const comp = renderComponent({ value: false });
27 | assert(isOff(comp));
28 | });
29 | });
30 |
31 | describe('onChange', () => {
32 | it('gets called after the switch is turned on', () => {
33 | let called = false;
34 | const onChange = (switchValue) => {
35 | if (switchValue) {
36 | called = true
37 | }
38 | };
39 | const comp = renderComponent({ onChange });
40 |
41 | flip(comp);
42 |
43 | assert(isOn(comp));
44 | assert(called);
45 | });
46 |
47 | it('gets called when the switch is turned off', () => {
48 | let called = false;
49 | const onChange = (switchValue) => {
50 | if (!switchValue) {
51 | called = true;
52 | }
53 | }
54 | const comp = renderComponent({ value: true, onChange });
55 |
56 | flip(comp);
57 |
58 | assert(isOff(comp));
59 | assert(called);
60 | });
61 | });
62 |
63 | describe('locked', () => {
64 | it('turned on -> locks the switch, blocking user interaction', () => {
65 | const comp = renderComponent({ value: true, locked: true });
66 | assert(isOn(comp));
67 |
68 | flip(comp);
69 | assert(isOn(comp));
70 | });
71 |
72 | it('turned off -> locks the switch, blocking user interaction', () => {
73 | const comp = renderComponent({ value: false, locked: true });
74 | assert(isOff(comp));
75 |
76 | flip(comp);
77 | assert(isOff(comp));
78 | });
79 |
80 | it('disables keyboard control', () => {
81 | const comp = renderComponent({ value: false, locked: true });
82 | assert(isOff(comp));
83 |
84 | simulateEvent('click', comp.refs.button);
85 | assert(isOff(comp));
86 |
87 | assert(comp.refs.button.disabled);
88 | });
89 | });
90 |
91 | // TODO: add tests for styles
92 | });
93 |
94 |
95 | describe('mobile devices', () => {
96 | let switchComponent, node, circle;
97 |
98 | beforeEach(() => {
99 | window['ontouchstart'] = function() {};
100 | switchComponent = renderComponent();
101 | node = switchComponent.refs.switch;
102 | circle = switchComponent.refs.circle;
103 | });
104 |
105 | afterEach(() => {
106 | unmount();
107 | window['ontouchstart'] = undefined;
108 | });
109 |
110 | describe('can be turned on', () => {
111 | it('by touching the circle', () => {
112 | simulateEvent('touchstart', circle);
113 | simulateEvent('touchend', circle);
114 |
115 | assert(isOn(switchComponent));
116 | });
117 |
118 | it('by touching the switch', () => {
119 | simulateEvent('touchstart', node);
120 | simulateEvent('touchend', node);
121 |
122 | assert(isOn(switchComponent));
123 | });
124 | });
125 |
126 | describe('can be turned off', () => {
127 | it('by touching the circle', () => {
128 | simulateEvent('touchstart', circle);
129 | simulateEvent('touchend', circle);
130 |
131 | simulateEvent('touchstart', circle);
132 | simulateEvent('touchend', circle);
133 |
134 | assert(isOff(switchComponent));
135 | });
136 |
137 | it('by touching the switch', () => {
138 | simulateEvent('touchstart', node);
139 | simulateEvent('touchend', node);
140 |
141 | simulateEvent('touchstart', node);
142 | simulateEvent('touchend', node);
143 |
144 | assert(isOff(switchComponent));
145 | });
146 | });
147 | });
148 |
149 | describe('User interaction', () => {
150 | let switchComponent, node, circle, button;
151 |
152 | beforeEach(() => {
153 | switchComponent = renderComponent();
154 | node = switchComponent.refs.switch;
155 | circle = switchComponent.refs.circle;
156 | button = switchComponent.refs.button;
157 | });
158 |
159 |
160 | afterEach(unmount);
161 |
162 | describe('can be turned on', () => {
163 |
164 | it('is off by default', () => {
165 | assert(isOff(switchComponent));
166 | });
167 |
168 | it('by clicking the circle', () => {
169 | simulateEvent('mousedown', circle);
170 | simulateEvent('mouseup', circle);
171 |
172 | assert(isOn(switchComponent));
173 | });
174 |
175 | it('by clicking the switch', () => {
176 | simulateEvent('mousedown', node);
177 | simulateEvent('mouseup', node);
178 |
179 | assert(isOn(switchComponent));
180 | });
181 |
182 | it('using the keyboard', () => {
183 | simulateEvent('click', button);
184 |
185 | assert(isOn(switchComponent));
186 | });
187 | });
188 |
189 | describe('can be turned off', () => {
190 | it('by clicking the circle', () => {
191 | simulateEvent('mousedown', circle);
192 | simulateEvent('mouseup', circle);
193 |
194 | simulateEvent('mousedown', circle);
195 | simulateEvent('mouseup', circle);
196 |
197 | assert(isOff(switchComponent));
198 | });
199 |
200 | it('by clicking the switch', () => {
201 | simulateEvent('mousedown', node);
202 | simulateEvent('mouseup', node);
203 |
204 | simulateEvent('mousedown', node);
205 | simulateEvent('mouseup', node);
206 |
207 | assert(isOff(switchComponent));
208 | });
209 |
210 | it('using the keyboard', () => {
211 | simulateEvent('click', button);
212 | simulateEvent('click', button);
213 |
214 | assert(isOff(switchComponent));
215 | });
216 | });
217 | });
218 |
219 | describe('Labels', () => {
220 |
221 | it('renders labels as instances of