Skip to content

Commit

Permalink
Merge pull request #200 from appfolio/AddExpandableCardContainer
Browse files Browse the repository at this point in the history
Add expandable card container [Deliver: #143170825]
  • Loading branch information
gthomas-appfolio authored May 8, 2017
2 parents cf10521 + cd2ea79 commit 51f732c
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 27 deletions.
88 changes: 70 additions & 18 deletions src/components/BlockPanel.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,72 @@
import React from 'react';
import { Button, Card, CardBlock } from 'reactstrap';

const BlockPanel = (props) => (
<Card className="bg-faded">
<CardBlock>
{props.onEdit ? <Button color="link" className="float-right p-0" onClick={props.onEdit}>edit</Button> : null}
{props.title ? <h3>{props.title}</h3> : null}
{props.children || props.value}
</CardBlock>
</Card>
);

BlockPanel.propTypes = {
onEdit: React.PropTypes.func,
title: React.PropTypes.string,
value: React.PropTypes.string
};
import React, { Component } from 'react';
import { Button, Card, CardBlock, CardHeader, CardTitle, Icon } from '../';

class BlockPanel extends Component {

static propTypes = {
children: React.PropTypes.node,
controls: React.PropTypes.node,
className: React.PropTypes.string,
expandable: React.PropTypes.bool,
onEdit: React.PropTypes.func,
title: React.PropTypes.string.isRequired
};

static defaultProps = {
className: '',
open: true,
expandable: false
};

constructor(props) {
super(props);

this.state = {
open: props.open
};
}

toggle = () => this.setState({ open: !this.state.open });

render() {
const { children, className, controls, expandable, title, onEdit, ...props } = this.props;
const { open } = this.state;

return (
<Card className={`rounded-0 border-0 shadow-1 ${className}`} {...props}>
<CardHeader
className={`border-0 d-flex align-items-center justify-content-end py-2 ${expandable ? 'pl-2' : ''}`}
style={{ borderRadius: 0 }}
>
{expandable ?
<Icon
className="text-muted mr-1"
name="caret-right"
rotate={open ? 90 : undefined}
fixedWidth
style={{ transition: 'transform 200ms ease-in-out' }}
onClick={this.toggle}
ref="icon"
style={{ cursor: expandable ? 'pointer' : 'default' }}
/> : null}
<CardTitle
className="m-0 my-1 mr-auto"
onClick={this.toggle}
ref="title"
style={{ cursor: expandable ? 'pointer' : 'default' }}
>
{title}
</CardTitle>
{onEdit ? <Button color="link" className="p-0" ref="edit" onClick={onEdit}>edit</Button> : controls}
</CardHeader>
{!expandable || open ?
<CardBlock>
{children}
</CardBlock>
: null}
</Card>
);
}
}

export default BlockPanel;
34 changes: 25 additions & 9 deletions stories/BlockPanel.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
import React from 'react';
import { storiesOf } from '@kadira/storybook';

import { BlockPanel } from '../src';
import { text } from '@kadira/storybook-addon-knobs';
import { BlockPanel, Button, Icon, HelpBubble } from '../src';
import { boolean, text } from '@kadira/storybook-addon-knobs';

storiesOf('BlockPanel', module)
.addWithInfo('with props', () => (
.addWithInfo('Live example', () => (
<BlockPanel
title={text('title', 'Some simple content would go here')}
onEdit={() => alert('Edit clicked!')}
expandable={boolean('expandable', true)}
>
Hello.
Now you see me.
</BlockPanel>
))
.addWithInfo('with title', () => (
<BlockPanel title={text('title', 'Some simple content would go here')}>
Hello.
.addWithInfo('Initially closed', () => (
<BlockPanel
title={text('title', 'Some simple content would go here')}
onEdit={() => alert('Edit clicked!')}
expandable={boolean('expandable', true)}
open={false}
>
Now you don't.
</BlockPanel>
))
.addWithInfo('with onEdit', () => (
<BlockPanel onEdit={() => alert('Edit clicked!')}>
.addWithInfo('components for title and controls', () => (
<BlockPanel
title={
<span className="text-uppercase">
{text('title', 'Invoices')} <HelpBubble className="text-primary" title="What does this mean?">It means nothing.</HelpBubble>
</span>
}
controls={[
<Button size="sm" color="link" onClick={() => alert('Cool I passed this in.')}><Icon name="upload" /> Upload</Button>,
<Button size="sm" color="link" onClick={() => alert('This one too.')}><Icon name="plus-circle" /> Add Activity</Button>
]}
>
Hello.
</BlockPanel>
));
Expand Down
129 changes: 129 additions & 0 deletions test/components/BlockPanel.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* eslint-env mocha */

import React from 'react';
import assert from 'assert';
import sinon from 'sinon';
import { Button, CardTitle, Icon } from '../../src';
import { mount, shallow } from 'enzyme';


import BlockPanel from '../../src/components/BlockPanel.js';

describe('<BlockPanel />', () => {
context('is expandable', () => {
it('should be open by default', () => {
const component = mount(
<BlockPanel title="Open">
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);

assert.equal(component.find('#hi').length, 1);
});

it('should be open by default', () => {
const component = shallow(
<BlockPanel title="Open" expandable>
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);

assert.equal(component.find('#hi').length, 1);
});

it('should be closed when false passed as prop', () => {
const component = shallow(
<BlockPanel title="Open" open={false} expandable>
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);

assert.equal(component.find('#hi').length, 0);
});

it('should be open when true passed as prop', () => {
const component = shallow(
<BlockPanel title="Open" open expandable>
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);

assert.equal(component.find('#hi').length, 1);
});

it('should be open and close when clicked', () => {
const component = shallow(
<BlockPanel title="Open" expandable>
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);

assert.equal(component.find('#hi').length, 1, 'inner block should be visible');
component.find(CardTitle).simulate('click');
assert.equal(component.find('#hi').length, 0, 'inner block should not be visible');
component.find(Icon).simulate('click');
assert.equal(component.find('#hi').length, 1, 'inner block should be visible');
});
});

context('contains headerComponent', () => {
it('should render headerComponent', () => {
const component = shallow(
<BlockPanel title="Open" controls={<p id="edit">Edit</p>}>
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);

assert.equal(component.find('#hi').length, 1);
assert.equal(component.find('#edit').length, 1);
});
});

context('header components', () => {
it('should not render edit link by default', () => {
const component = mount(
<BlockPanel title="Open">
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);
assert.equal(component.ref('edit').exists(), false);
});

it('should render edit link when passed onEdit', () => {
const component = mount(
<BlockPanel title="Open" onEdit={() => {}}>
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);
assert.equal(component.ref('edit').exists(), true);
});

it('should call onEdit when clicked', () => {
const onEdit = sinon.spy();

const component = mount(
<BlockPanel title="Open" onEdit={onEdit}>
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);
component.ref('edit').simulate('click');
assert.equal(onEdit.calledOnce, true);
});

it('should render title components when passed', () => {
const component = mount(
<BlockPanel
title={<h1 id="title">WE ARE THE CHAMPIONS</h1>}
controls={<Button id="action">Go!</Button>}
>
<h1 id="hi">Hello World!</h1>
</BlockPanel>
);
assert.equal(component.find('#title').exists(), true);
assert.equal(component.find('#title').text(), 'WE ARE THE CHAMPIONS');
assert.equal(component.find('#action').exists(), true);
assert.equal(component.find('#action').text(), 'Go!');
});

});
});

0 comments on commit 51f732c

Please sign in to comment.