Skip to content

Commit

Permalink
Merge pull request #900 from Adslot/toggle-switch-component
Browse files Browse the repository at this point in the history
feat: Switch component
  • Loading branch information
lteacher authored Aug 15, 2019
2 parents 75aa079 + 7d6632b commit 497f6ed
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 2 deletions.
66 changes: 66 additions & 0 deletions src/components/adslot-ui/Switch/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import './style.scss';

class Switch extends React.Component {
state = { checked: this.props.defaultChecked || false };

handleChange = event => {
const { onChange, checked } = this.props;
const targetCheckedValue = _.get(event, 'target.checked');

if (_.isNil(checked)) {
this.setState({ checked: targetCheckedValue });
}

if (onChange) {
onChange(targetCheckedValue);
}
};

render() {
const { defaultChecked, checked, value, onChange, className, dts } = this.props;

if (!_.isNil(checked) && !_.isNil(defaultChecked))
console.warn(
'Failed prop type: Contains an input of type checkbox with both `checked` and `defaultChecked` props. Input elements must be either controlled or uncontrolled'
);

if (!_.isNil(checked) && _.isNil(onChange))
console.warn(
'Failed prop type: You have provided a `checked` prop to Switch Component without an `onChange` handler. This will render a read-only field.'
);

const toggleInputChecked = !_.isNil(checked) ? checked : this.state.checked;
return (
<label className="aui--switch-label">
<input
type="checkbox"
checked={toggleInputChecked}
value={value}
onChange={this.handleChange}
className={className}
dts={dts}
/>
<span className="aui--switch-slider round"></span>
</label>
);
}
}

Switch.defaultProps = {
value: '',
dts: 'switch-component',
};

Switch.propTypes = {
defaultChecked: PropTypes.bool,
checked: PropTypes.bool,
value: PropTypes.string,
onChange: PropTypes.func,
dts: PropTypes.string,
className: PropTypes.string,
};

export default Switch;
92 changes: 92 additions & 0 deletions src/components/adslot-ui/Switch/index.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import _ from 'lodash';
import sinon from 'sinon';
import { mount } from 'enzyme';
import Switch from 'adslot-ui/Switch';

describe('Switch', () => {
let sandbox = null;

before(() => {
sandbox = sinon.createSandbox();
});

afterEach(() => {
sandbox.restore();
});

it('should correctly render defaults', () => {
const wrapper = mount(<Switch />);
const inputElement = wrapper.find('input');

expect(inputElement).to.have.lengthOf(1);

const inputElementProps = inputElement.props();
expect(inputElementProps.checked).to.equal(false);
expect(inputElementProps.dts).to.equal('switch-component');
});

it('should correctly render controlled Switch', () => {
const wrapper = mount(<Switch checked onChange={_.noop} />);
const inputElement = wrapper.find('input');

expect(inputElement).to.have.lengthOf(1);
expect(inputElement.props().checked).to.equal(true);
});

it('should throw warning if checked is provided without onChange', () => {
sandbox.stub(console, 'warn');
mount(<Switch checked />);

expect(
console.warn.calledWith(
'Failed prop type: You have provided a `checked` prop to Switch Component without an `onChange` handler. This will render a read-only field.'
)
).to.equal(true);
});

it('should throw warning if both defaultChecked and checked are provided', () => {
sandbox.stub(console, 'warn');
mount(<Switch defaultChecked checked onChange={_.noop} />);

expect(
console.warn.calledWith(
'Failed prop type: Contains an input of type checkbox with both `checked` and `defaultChecked` props. Input elements must be either controlled or uncontrolled'
)
).to.equal(true);
});

it('should correctly call onChange for controlled Switch', () => {
const onChangeSpy = sinon.spy();

const wrapper = mount(<Switch checked onChange={onChangeSpy} />);
const inputElement = wrapper.find('input');

expect(inputElement).to.have.lengthOf(1);
expect(inputElement.props().checked).to.equal(true);

inputElement.simulate('change');

expect(onChangeSpy.calledOnce).to.equal(true);
});

it('should correctly change switch checked for uncontrolled Switch', () => {
const wrapper = mount(<Switch defaultChecked={false} />);

const inputElement = wrapper.find('input');
expect(inputElement).to.have.lengthOf(1);
expect(inputElement.props().checked).to.equal(false);

inputElement.simulate('change', { target: { checked: true } });
wrapper.update();
expect(wrapper.find('input').props().checked).to.equal(true);
});

it('should correctly apply className', () => {
const wrapper = mount(<Switch className="some-class" />);

const inputElement = wrapper.find('input');
expect(inputElement).to.have.lengthOf(1);
expect(inputElement.props().className).to.equal('some-class');
});
});
60 changes: 60 additions & 0 deletions src/components/adslot-ui/Switch/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@import '~styles/color';
@import '~styles/variable';

$color-switch-background-off: $color-light-red;
$color-switch-background-on: $color-light-green;
$color-switch-circle-off: $color-dark-red;
$color-switch-circle-on: $color-dark-green;

.aui--switch-label {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}

.aui--switch-label input {
opacity: 0;
width: 0;
height: 0;
}

.aui--switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: $color-switch-background-off;
transition: .4s;
}

.aui--switch-slider::before {
position: absolute;
content: '';
height: 12px;
width: 12px;
margin-left: 4px;
margin-top: 4px;
background-color: $color-switch-circle-off;
transition: .4s;
}

input:checked + .aui--switch-slider {
background-color: $color-light-green;
}

input:checked + .aui--switch-slider::before {
background-color: $color-dark-green;
transform: translateX(20px);
}

.aui--switch-slider.round {
border-radius: 34px;
}

.aui--switch-slider.round::before {
border-radius: 50%;
}

2 changes: 2 additions & 0 deletions src/components/adslot-ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import Nav from 'adslot-ui/Navigation';
import VerticalNav from 'adslot-ui/VerticalNavigation';
import OverlayLoader from 'adslot-ui/OverlayLoader';
import ActionPanel from 'adslot-ui/ActionPanel';
import Switch from 'adslot-ui/Switch';

export {
Accordion,
Expand Down Expand Up @@ -63,6 +64,7 @@ export {
Tabs,
Textarea,
TextEllipsis,
Switch,
TreePickerGrid,
TreePickerNav,
TreePickerNode,
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
Search,
SplitPane,
StatusPill,
Switch,
Tab,
Tabs,
Textarea,
Expand Down Expand Up @@ -133,6 +134,7 @@ export {
StatusPill,
SvgSymbol,
SvgSymbolCircle,
Switch,
Tab,
Tabs,
Tag,
Expand Down
6 changes: 6 additions & 0 deletions src/styles/color.scss
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,9 @@ $color-carousel-button: rgba($color-gray-darkest, .5);

// Help
$color-help: $color-gray;

// Switch
$color-light-green: #def1de;
$color-dark-green: #5bb75b;
$color-light-red: #f8dcdb;
$color-dark-red: #da4f49;
3 changes: 3 additions & 0 deletions www/components/Layout/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import VerticalNavigationExample from '../../examples/VerticalNavigationExample'
import OverlayLoaderExample from '../../examples/OverlayLoaderExample';
import SearchExample from '../../examples/SearchExample';
import ActionPanelExample from '../../examples/ActionPanelExample';
import SwitchExample from '../../examples/SwitchExample';

import './styles.scss';
import '../../examples/styles.scss';
Expand Down Expand Up @@ -90,6 +91,7 @@ const componentsBySection = {
'radio-group',
'select',
'date-picker',
'switch',
],
'typography-and-text-layout': ['text-ellipsis'],
'stats-and-data': ['count-badge', 'statistic', 'totals', 'slicey'],
Expand Down Expand Up @@ -236,6 +238,7 @@ class PageLayout extends React.Component {
<SearchExample />
<TagExample />

<SwitchExample />
<PageTitle title="Grouping" />
<PageTitleExample />
<CardExample />
Expand Down
105 changes: 105 additions & 0 deletions www/examples/SwitchExample.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import _ from 'lodash';
import React from 'react';
import Example from '../components/Example';
import { Switch } from '../../src';

class SwitchExample extends React.PureComponent {
state = {
isToggleOn: true,
};

onChange = newValue => {
this.setState({ isToggleOn: newValue });
};

render() {
return (
<React.Fragment>
<div>
<div className="component-heading">Un-Controlled Switch without defaultChecked</div>
<div className="component-container">
<Switch />
</div>
</div>
<div>
<div className="component-heading">Un-Controlled Switch with defaultChecked</div>
<div className="component-container">
<Switch defaultChecked={true} />
</div>
</div>
<div>
<div className="component-heading">Un-Controlled Switch with defaultChecked with onChange</div>
<div className="component-container">
<Switch defaultChecked={false} onChange={_.noop} />
</div>
</div>
<div>
<div className="component-heading">Controlled Switch</div>
<div className="component-container">
<Switch checked={this.state.isToggleOn} onChange={this.onChange} />
</div>
</div>
</React.Fragment>
);
}
}

const exampleProps = {
componentName: 'Switch',
exampleCodeSnippet: `
<Switch />
<Switch defaultValue={true} />
<Switch defaultChecked={true} onChange={(nextState) => func(nextState)} />
<Switch checked={true} onChange={(nextState) => func(nextState)} />
`,
propTypeSectionArray: [
{
propTypes: [
{
propType: 'defaultChecked',
type: 'boolean',
defaultValue: null,
note: 'switch value, if the value is un-controlled',
},
{
propType: 'checked',
type: 'boolean',
defaultValue: null,
note: 'switch value, if the value is controlled',
},
{
propType: 'value',
type: 'string',
defaultValue: '',
},
{
propType: 'onChange',
type: 'func',
defaultValue: null,
note: (
<div>
This function is called when value is changed <br />
<pre>const onChange = (nextState) => ...)</pre>
</div>
),
},
{
propType: 'className',
type: 'string',
defaultValue: null,
},
{
propType: 'dts',
type: 'string',
defaultValue: 'switch-component',
},
],
},
],
};

export default () => (
<Example {...exampleProps}>
<SwitchExample />
</Example>
);
Loading

0 comments on commit 497f6ed

Please sign in to comment.