From cdda52d799ea9edeb135de44e02fecc9d6d8e94b Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 20 Jul 2018 10:25:45 -0700 Subject: Add ButtonGroup component --- .../button-group/button-group.component.js | 61 ++++++++++++++++++++++ .../button-group/button-group.stories.js | 49 +++++++++++++++++ ui/app/components/button-group/index.js | 1 + ui/app/components/button-group/index.scss | 38 ++++++++++++++ ui/app/components/index.scss | 2 + 5 files changed, 151 insertions(+) create mode 100644 ui/app/components/button-group/button-group.component.js create mode 100644 ui/app/components/button-group/button-group.stories.js create mode 100644 ui/app/components/button-group/index.js create mode 100644 ui/app/components/button-group/index.scss (limited to 'ui/app/components') diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js new file mode 100644 index 000000000..f99f710ce --- /dev/null +++ b/ui/app/components/button-group/button-group.component.js @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class ButtonGroup extends PureComponent { + static propTypes = { + defaultActiveButtonIndex: PropTypes.number, + disabled: PropTypes.bool, + children: PropTypes.array, + className: PropTypes.string, + style: PropTypes.object, + } + + static defaultProps = { + className: 'button-group', + } + + state = { + activeButtonIndex: this.props.defaultActiveButtonIndex || 0, + } + + handleButtonClick (activeButtonIndex) { + this.setState({ activeButtonIndex }) + } + + renderButtons () { + const { children, disabled } = this.props + + return React.Children.map(children, (child, index) => { + return child && ( + + ) + }) + } + + render () { + const { className, style } = this.props + + return ( +
+ { this.renderButtons() } +
+ ) + } +} diff --git a/ui/app/components/button-group/button-group.stories.js b/ui/app/components/button-group/button-group.stories.js new file mode 100644 index 000000000..14e1a7e49 --- /dev/null +++ b/ui/app/components/button-group/button-group.stories.js @@ -0,0 +1,49 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { action } from '@storybook/addon-actions' +import ButtonGroup from './' +import Button from '../button' +import { text, boolean } from '@storybook/addon-knobs/react' + +storiesOf('ButtonGroup', module) + .add('with Buttons', () => + + + + + + ) + .add('with a disabled Button', () => + + + + + ) diff --git a/ui/app/components/button-group/index.js b/ui/app/components/button-group/index.js new file mode 100644 index 000000000..df470bd57 --- /dev/null +++ b/ui/app/components/button-group/index.js @@ -0,0 +1 @@ +export { default } from './button-group.component' diff --git a/ui/app/components/button-group/index.scss b/ui/app/components/button-group/index.scss new file mode 100644 index 000000000..29713c75b --- /dev/null +++ b/ui/app/components/button-group/index.scss @@ -0,0 +1,38 @@ +.button-group { + display: flex; + justify-content: center; + align-items: center; + + &__button { + font-family: Roboto; + font-size: 1rem; + color: $tundora; + border-style: solid; + border-color: $alto; + border-width: 1px 1px 1px; + border-left: 0; + flex: 1; + padding: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:first-child { + border-left: 1px solid $alto; + border-radius: 4px 0 0 4px; + } + + &:last-child { + border-radius: 0 4px 4px 0; + } + + &--active { + background-color: $dodger-blue; + color: $white; + } + + &:disabled { + opacity: .5; + } + } +} \ No newline at end of file diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 32f0e90e4..b3e14ce23 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -1,3 +1,5 @@ +@import './button-group/index'; + @import './export-text-container/index'; @import './selected-account/index'; -- cgit From a146e5ebbcdf3ff334d2a5fb3b7aba1dcac9ab12 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 23 Jul 2018 16:49:28 -0230 Subject: Adds tests for button group component. --- .../tests/button-group-component.test.js | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 ui/app/components/button-group/tests/button-group-component.test.js (limited to 'ui/app/components') diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/button-group/tests/button-group-component.test.js new file mode 100644 index 000000000..f07bb97c8 --- /dev/null +++ b/ui/app/components/button-group/tests/button-group-component.test.js @@ -0,0 +1,97 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import ButtonGroup from '../button-group.component.js' + +const childButtonSpies = { + onClick: sinon.spy(), +} + +sinon.spy(ButtonGroup.prototype, 'handleButtonClick') +sinon.spy(ButtonGroup.prototype, 'renderButtons') + +const mockButtons = [ + , + , + , +] + +describe('ButtonGroup Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow({mockButtons}) + }) + + afterEach(() => { + childButtonSpies.onClick.resetHistory() + ButtonGroup.prototype.handleButtonClick.resetHistory() + ButtonGroup.prototype.renderButtons.resetHistory() + }) + + describe('handleButtonClick', () => { + it('should set the activeButtonIndex', () => { + assert.equal(wrapper.state('activeButtonIndex'), 1) + wrapper.instance().handleButtonClick(2) + assert.equal(wrapper.state('activeButtonIndex'), 2) + }) + }) + + describe('renderButtons', () => { + it('should render a button for each child', () => { + const childButtons = wrapper.find('.button-group__button') + assert.equal(childButtons.length, 3) + }) + + it('should render the correct button with an active state', () => { + const childButtons = wrapper.find('.button-group__button') + const activeChildButton = wrapper.find('.button-group__button--active') + assert.deepEqual(childButtons.get(1), activeChildButton.get(0)) + }) + + it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => { + assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0) + assert.equal(childButtonSpies.onClick.callCount, 0) + const childButtons = wrapper.find('.button-group__button') + childButtons.at(0).props().onClick() + childButtons.at(1).props().onClick() + childButtons.at(2).props().onClick() + assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3) + assert.equal(childButtonSpies.onClick.callCount, 3) + }) + + it('should render all child buttons as disabled if props.disabled is true', () => { + const childButtons = wrapper.find('.button-group__button') + childButtons.forEach(button => { + assert.equal(button.props().disabled, undefined) + }) + wrapper.setProps({ disabled: true }) + const disabledChildButtons = wrapper.find('[disabled=true]') + assert.equal(disabledChildButtons.length, 3) + }) + + it('should render the children of the button', () => { + const mockClass = wrapper.find('.mockClass') + assert.equal(mockClass.length, 1) + }) + }) + + describe('render', () => { + it('should render a div with the expected class and style', () => { + assert.equal(wrapper.find('div').at(0).props().className, 'someClassName') + assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' }) + }) + + it('should call renderButtons when rendering', () => { + assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1) + wrapper.instance().render() + assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2) + }) + }) +}) -- cgit