-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #200 from appfolio/AddExpandableCardContainer
Add expandable card container [Deliver: #143170825]
- Loading branch information
Showing
3 changed files
with
224 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!'); | ||
}); | ||
|
||
}); | ||
}); |