-
Notifications
You must be signed in to change notification settings - Fork 370
feat(listView): add ListView components #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import cx from 'classnames' | ||
import React from 'react' | ||
import { storiesOf } from '@storybook/react' | ||
import { Row, Col } from 'react-bootstrap' | ||
import { withKnobs, boolean } from '@storybook/addon-knobs' | ||
import { defaultTemplate } from '../../storybook/decorators/storyTemplates' | ||
|
||
import { | ||
Button, | ||
DropdownKebab, | ||
ListView, | ||
ListViewInfoItem, | ||
ListViewItem, | ||
ListViewIcon, | ||
MenuItem | ||
} from '../index' | ||
import { mockListItems } from './__mocks__/mockListItems' | ||
|
||
const stories = storiesOf('ListView', module) | ||
stories.addDecorator(withKnobs) | ||
stories.addDecorator( | ||
defaultTemplate({ | ||
title: 'ListView', | ||
documentationLink: | ||
'http://www.patternfly.org/pattern-library/content-views/list-view/' | ||
}) | ||
) | ||
|
||
const renderActions = () => | ||
<div> | ||
<Button>Details</Button> | ||
</div> | ||
|
||
const renderAdditionalInfoItems = itemProperties => { | ||
return ( | ||
itemProperties && | ||
Object.keys(itemProperties).map(prop => { | ||
const classNames = cx('pficon', { | ||
'pficon-flavor': prop === 'hosts', | ||
'pficon-cluster': prop === 'clusters', | ||
'pficon-container-node': prop === 'nodes', | ||
'pficon-image': prop === 'images' | ||
}) | ||
return ( | ||
<ListViewInfoItem key={prop}> | ||
<span className={classNames} /> | ||
<strong>{itemProperties[prop]}</strong> {prop} | ||
</ListViewInfoItem> | ||
) | ||
}) | ||
) | ||
} | ||
|
||
stories.addWithInfo( | ||
'List of expandable items', | ||
`ListView usage example.`, | ||
() => { | ||
return ( | ||
<ListView> | ||
{mockListItems.map((item, index) => | ||
<ListViewItem | ||
key={index} | ||
actions={renderActions(item.actions)} | ||
checkboxInput={<input type="checkbox" />} | ||
leftContent={<ListViewIcon icon="fa fa-plane" />} | ||
additionalInfo={renderAdditionalInfoItems(item.properties)} | ||
heading={item.title} | ||
description={item.description} | ||
stacked={boolean('Stacked', false)} | ||
> | ||
<Row> | ||
<Col sm={11}> | ||
{item.expandedContentText} | ||
</Col> | ||
</Row> | ||
</ListViewItem> | ||
)} | ||
</ListView> | ||
) | ||
} | ||
) | ||
|
||
stories.addWithInfo('ListItem variants', `ListView usage example.`, () => | ||
<ListView | ||
id="listView--listItemVariants" | ||
className="listView--listItemVariants" | ||
> | ||
<ListViewItem | ||
id="item1" | ||
className="listViewItem--listItemVariants" | ||
key="item1" | ||
description="Expandable item with description, additional items and actions" | ||
heading="Event One" | ||
checkboxInput={<input type="checkbox" />} | ||
leftContent={<ListViewIcon icon="fa fa-plane" />} | ||
additionalInfo={[ | ||
<ListViewInfoItem key="1"> | ||
<span className="pficon pficon-flavor" /> Item 1 | ||
</ListViewInfoItem>, | ||
<ListViewInfoItem key="2"> | ||
<span className="fa fa-bug" /> Item 2 | ||
</ListViewInfoItem> | ||
]} | ||
actions={ | ||
<div> | ||
<Button>Action 1</Button> | ||
<DropdownKebab id="action2kebab" pullRight> | ||
<MenuItem>Action 2</MenuItem> | ||
</DropdownKebab> | ||
</div> | ||
} | ||
stacked={boolean('Stacked', false)} | ||
> | ||
Expanded Content | ||
</ListViewItem> | ||
<ListViewItem | ||
key="item2" | ||
leftContent={<ListViewIcon size="lg" icon="fa fa-plane" />} | ||
heading={ | ||
<span> | ||
This is EVENT One that is with very LONG and should not overflow and | ||
push other elements out of the bounding box. | ||
<small>Feb 23, 2015 12:32 am</small> | ||
</span> | ||
} | ||
description={ | ||
<span> | ||
The following snippet of text is rendered as <a href="">link text</a>. | ||
</span> | ||
} | ||
stacked={boolean('Stacked', false)} | ||
/> | ||
<ListViewItem | ||
key="item3" | ||
checkboxInput={<input type="checkbox" />} | ||
heading="Stacked Additional Info items" | ||
description={ | ||
<span> | ||
The following snippet of text is rendered as <a href="">link text</a>. | ||
</span> | ||
} | ||
additionalInfo={[ | ||
<ListViewInfoItem key="1" stacked> | ||
<strong>113,735</strong> | ||
<span>Service One</span> | ||
</ListViewInfoItem>, | ||
<ListViewInfoItem key="2" stacked> | ||
<strong>35%</strong> | ||
<span>Service Two</span> | ||
</ListViewInfoItem> | ||
]} | ||
stacked={boolean('Stacked', false)} | ||
/> | ||
<ListViewItem | ||
key="item4" | ||
additionalInfo={[ | ||
<ListViewInfoItem key="1"> | ||
<span className="pficon pficon-screen" /> Only Additional | ||
</ListViewInfoItem>, | ||
<ListViewInfoItem key="2"> | ||
<span className="pficon pficon-cluster" /> Info Items | ||
</ListViewInfoItem> | ||
]} | ||
stacked={boolean('Stacked', false)} | ||
/> | ||
<ListViewItem | ||
key="item5" | ||
heading="Custom Event Icon" | ||
leftContent={ | ||
<ListViewIcon | ||
size="md" | ||
icon="pficon pficon-ok list-view-pf-icon-success" | ||
/> | ||
} | ||
description={ | ||
<span> | ||
The following snippet of text is rendered as <a href="">link text</a>. | ||
</span> | ||
} | ||
additionalInfo={[ | ||
<ListViewInfoItem key="1"> | ||
<span className="pficon pficon-screen" /> | ||
<strong>108</strong> Hosts | ||
</ListViewInfoItem>, | ||
<ListViewInfoItem key="2"> | ||
<span className="pficon pficon-cluster" /> | ||
<strong>28</strong> Clusters | ||
</ListViewInfoItem> | ||
]} | ||
stacked={boolean('Stacked', false)} | ||
/> | ||
</ListView> | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React from 'react' | ||
import renderer from 'react-test-renderer' | ||
import { Row, Col } from 'react-bootstrap' | ||
|
||
import { ListView, ListViewItem, ListViewIcon } from '../index' | ||
import { | ||
mockListItems, | ||
renderActions, | ||
renderAdditionalInfoItems | ||
} from './__mocks__/mockListItems' | ||
|
||
test('ListView renders properly', () => { | ||
const component = renderer.create( | ||
<ListView> | ||
{mockListItems.map((item, index) => | ||
<ListViewItem | ||
key={index} | ||
actions={renderActions(item.actions)} | ||
checkboxInput={<input type="checkbox" />} | ||
leftContent={<ListViewIcon icon="fa fa-plane" />} | ||
additionalInfo={renderAdditionalInfoItems(item.properties)} | ||
heading={item.title} | ||
description={item.description} | ||
stacked | ||
> | ||
<Row> | ||
<Col sm={11}> | ||
{item.expandedContentText} | ||
</Col> | ||
</Row> | ||
</ListViewItem> | ||
)} | ||
</ListView> | ||
) | ||
|
||
const tree = component.toJSON() | ||
expect(tree).toMatchSnapshot() | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import React from 'react' | ||
import PropTypes from 'prop-types' | ||
|
||
import { | ||
ListViewExpand, | ||
ListGroupItem, | ||
ListGroupItemHeader, | ||
ListGroupItemContainer | ||
} from './index' | ||
import ListViewRow from './ListViewRow' | ||
|
||
/** | ||
* ListViewItem - main ListViewItem component which handles the expansion logic. | ||
* ListViewItem is considered expandable if it has child props. In that case it | ||
* renders ListGroupItemHeader and ListGroupItemContainer | ||
*/ | ||
export default class ListViewItem extends React.Component { | ||
constructor() { | ||
super() | ||
this.state = { expanded: false } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question - do we prefer state object defined inside the constructor or outside within the class? We don't have a linter checking this right now, but it may be good to introduce it at some point for consistency... maybe we can consider more lint settings in a future PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All examples in ReactJS doc initialize the state in constructor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jtomasek I'm good with this pattern (just wanted to note it though). Will update my other PRs accordingly and follow this standard. Thanks! |
||
} | ||
|
||
toggleExpanded(index) { | ||
const { onExpand, onExpandClose } = this.props | ||
if (this.state.expanded) { | ||
onExpandClose && onExpandClose() | ||
} else { | ||
onExpand && onExpand() | ||
} | ||
this.setState(prevState => ({ expanded: !prevState.expanded })) | ||
} | ||
|
||
render() { | ||
const { | ||
children, | ||
stacked, | ||
actions, | ||
additionalInfo, | ||
description, | ||
heading, | ||
leftContent, | ||
checkboxInput, | ||
...other | ||
} = this.props | ||
const { expanded } = this.state | ||
|
||
if (children) { | ||
return ( | ||
<ListGroupItem expanded={expanded} stacked={stacked} {...other}> | ||
<ListGroupItemHeader toggleExpanded={() => this.toggleExpanded()}> | ||
<ListViewExpand | ||
expanded={expanded} | ||
toggleExpanded={() => this.toggleExpanded()} | ||
/> | ||
<ListViewRow | ||
checkboxInput={checkboxInput} | ||
leftContent={leftContent} | ||
heading={heading} | ||
description={description} | ||
additionalInfo={additionalInfo} | ||
actions={actions} | ||
/> | ||
</ListGroupItemHeader> | ||
<ListGroupItemContainer | ||
expanded={expanded} | ||
onClose={() => this.toggleExpanded()} | ||
> | ||
{children} | ||
</ListGroupItemContainer> | ||
</ListGroupItem> | ||
) | ||
} else { | ||
return ( | ||
<ListGroupItem stacked={stacked} {...other}> | ||
<ListViewRow | ||
checkboxInput={checkboxInput} | ||
leftContent={leftContent} | ||
heading={heading} | ||
description={description} | ||
additionalInfo={additionalInfo} | ||
actions={actions} | ||
/> | ||
</ListGroupItem> | ||
) | ||
} | ||
} | ||
} | ||
ListViewItem.propTypes = { | ||
/** Child node rendered as expanded content of the ListViewItem */ | ||
children: PropTypes.node, | ||
/** Display the ListViewItem stacked or not */ | ||
stacked: PropTypes.bool.isRequired, | ||
/** Function triggered when expandable content is expanded */ | ||
onExpand: PropTypes.func, | ||
/** Function triggered when expandable content is closed */ | ||
onExpandClose: PropTypes.func, | ||
/** Node which renders right-positioned actions (e.g. Buttons, DropdownKebab...) */ | ||
actions: PropTypes.node, | ||
/** An array of ListViewInfoItem instances to render additional info items */ | ||
additionalInfo: PropTypes.arrayOf(PropTypes.node), | ||
/** Contents of ListViewItem description section */ | ||
description: PropTypes.node, | ||
/** Contents of ListViewItem heading */ | ||
heading: PropTypes.node, | ||
/** Contents for left section of ListViewItem (usually ListViewIcon) */ | ||
leftContent: PropTypes.node, | ||
/** Checkbox form input component */ | ||
checkboxInput: PropTypes.node | ||
} | ||
ListViewItem.defaultProps = { | ||
stacked: false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like how the
ListViewItem
knows there is an expansion based solely on if there is a child node rather than requiring an explicit property.