diff --git a/src/components/adslot-ui/Switch/index.jsx b/src/components/adslot-ui/Switch/index.jsx new file mode 100644 index 000000000..44f8b2c05 --- /dev/null +++ b/src/components/adslot-ui/Switch/index.jsx @@ -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 ( + + ); + } +} + +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; diff --git a/src/components/adslot-ui/Switch/index.spec.jsx b/src/components/adslot-ui/Switch/index.spec.jsx new file mode 100644 index 000000000..4892f7f96 --- /dev/null +++ b/src/components/adslot-ui/Switch/index.spec.jsx @@ -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(); + 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(); + 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(); + + 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(); + + 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(); + 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(); + + 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(); + + const inputElement = wrapper.find('input'); + expect(inputElement).to.have.lengthOf(1); + expect(inputElement.props().className).to.equal('some-class'); + }); +}); diff --git a/src/components/adslot-ui/Switch/style.scss b/src/components/adslot-ui/Switch/style.scss new file mode 100644 index 000000000..6054c43c2 --- /dev/null +++ b/src/components/adslot-ui/Switch/style.scss @@ -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%; +} + diff --git a/src/components/adslot-ui/index.js b/src/components/adslot-ui/index.js index d9a1d381a..2caef929d 100644 --- a/src/components/adslot-ui/index.js +++ b/src/components/adslot-ui/index.js @@ -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, @@ -63,6 +64,7 @@ export { Tabs, Textarea, TextEllipsis, + Switch, TreePickerGrid, TreePickerNav, TreePickerNode, diff --git a/src/index.js b/src/index.js index a548295f4..29e39111e 100644 --- a/src/index.js +++ b/src/index.js @@ -66,6 +66,7 @@ import { Search, SplitPane, StatusPill, + Switch, Tab, Tabs, Textarea, @@ -133,6 +134,7 @@ export { StatusPill, SvgSymbol, SvgSymbolCircle, + Switch, Tab, Tabs, Tag, diff --git a/src/styles/color.scss b/src/styles/color.scss index acbce2588..3c593a462 100644 --- a/src/styles/color.scss +++ b/src/styles/color.scss @@ -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; diff --git a/www/components/Layout/index.jsx b/www/components/Layout/index.jsx index db4a5be19..1999258d0 100644 --- a/www/components/Layout/index.jsx +++ b/www/components/Layout/index.jsx @@ -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'; @@ -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'], @@ -236,6 +238,7 @@ class PageLayout extends React.Component { + diff --git a/www/examples/SwitchExample.jsx b/www/examples/SwitchExample.jsx new file mode 100644 index 000000000..cee4b4eec --- /dev/null +++ b/www/examples/SwitchExample.jsx @@ -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 ( + +
+
Un-Controlled Switch without defaultChecked
+
+ +
+
+
+
Un-Controlled Switch with defaultChecked
+
+ +
+
+
+
Un-Controlled Switch with defaultChecked with onChange
+
+ +
+
+
+
Controlled Switch
+
+ +
+
+
+ ); + } +} + +const exampleProps = { + componentName: 'Switch', + exampleCodeSnippet: ` + + + func(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: ( +
+ This function is called when value is changed
+
const onChange = (nextState) => ...)
+
+ ), + }, + { + propType: 'className', + type: 'string', + defaultValue: null, + }, + { + propType: 'dts', + type: 'string', + defaultValue: 'switch-component', + }, + ], + }, + ], +}; + +export default () => ( + + + +); diff --git a/www/examples/styles.scss b/www/examples/styles.scss index f8956d0d5..52f3faf6f 100644 --- a/www/examples/styles.scss +++ b/www/examples/styles.scss @@ -56,6 +56,16 @@ } } + &.switch-example { + .component-heading { + padding-top: 20px; + } + + .component-container { + padding-top: 10px; + } + } + &.navigation-tabs-example { .adslot-ui-example { .dashboard-tab { @@ -66,7 +76,6 @@ &.popover-example { .adslot-ui-example { - .button-example-container { display: flex; flex-direction: column; @@ -98,7 +107,6 @@ left: 5px; top: -5px; } - } }