Skip to content
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

feat(CompoundLabel) Add CompoundLabel component #464

Merged
merged 10 commits into from
Aug 15, 2018
41 changes: 41 additions & 0 deletions packages/patternfly-react/less/compound-label.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.compound-label-pf {
background-color: $color-pf-blue-500;
padding-left: 15px;
padding-right: 15px;
color: $color-pf-white;
margin-left: 5px;
margin-right: 10px;
font-size: 14px;
display: inline-block;
margin-bottom: .5em;
// for wrapping:
white-space: normal;
text-align: left;

.list-inline {
margin-bottom: 0;
}

.label {
display: inline-block;
font-size: 0.8em;
vertical-align: middle;
}

.list-inline {
display: inline-block;
&>li {
margin-top: .1em; // added for case when wrapping
margin-bottom: .1em; // added for case when wrapping
}
}
}

.category-label-pf {
padding-right: 15px;
vertical-align: text-bottom;
}

.compound-label-inner-color-pf {
background-color: $color-pf-blue-400;
}
1 change: 1 addition & 0 deletions packages/patternfly-react/sass/patternfly-react.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ $icon-font-path: '~patternfly/dist/fonts/';
*/

@import 'patternfly-react/patternfly-react';
@import 'patternfly-react/compound-label';
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.compound-label-pf {
background-color: $color-pf-blue-500;
padding-left: 15px;
padding-right: 15px;
color: $color-pf-white;
margin-left: 5px;
margin-right: 10px;
font-size: 14px;
display: inline-block;
margin-bottom: 0.5em;
// for wrapping:
white-space: normal;
text-align: left;

.list-inline {
margin-bottom: 0;
}

.label {
display: inline-block;
font-size: 0.8em;
vertical-align: middle;
}

.list-inline {
display: inline-block;

&>li {
margin-top: 0.1em; // added for case when wrapping
margin-bottom: 0.1em; // added for case when wrapping
}
}
}

.category-label-pf {
padding-right: 15px;
vertical-align: text-bottom;
}

.compound-label-inner-color-pf {
background-color: $color-pf-blue-400;
}
78 changes: 78 additions & 0 deletions packages/patternfly-react/src/components/Label/CompoundLabel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Tooltip } from '../Tooltip';
import { OverlayTrigger } from '../OverlayTrigger';
import LabelWithTooltip from './LabelWithTooltip';

class CompoundLabel extends React.Component {
generateTag = value => (
<LabelWithTooltip
key={value.id}
category={this.props.category}
value={value}
onDeleteClick={this.props.onDeleteClick}
truncate={this.props.valueTruncate}
bsStyle={this.props.bsStyle}
className={this.props.innerClassName}
overlayPlacement={this.props.overlayPlacement}
/>
);

render() {
const values = [...this.props.values];
if (values.length === 0) return null;
const categoryTooltip = <Tooltip id="tooltip">{this.props.category.label}</Tooltip>;
return (
<span className="label label-primary compound-label-pf">
<OverlayTrigger placement={this.props.overlayPlacement} overlay={categoryTooltip}>
<span className="category-label-pf">{this.props.categoryTruncate(this.props.category.label)}</span>
</OverlayTrigger>
<ul className={`list-inline ${this.props.className}`}>
{values.sort((a, b) => (a.label < b.label ? -1 : 1)).map(tagValue => this.generateTag(tagValue))}
</ul>
</span>
);
}
}

CompoundLabel.propTypes = {
/** Category in CATEGORY: value(s) pair */
/** Parent of label, it does not get displayed in this component */
category: PropTypes.shape({
id: PropTypes.any.isRequired,
label: PropTypes.string.isRequired
}).isRequired,
/** Array of Values in Category:VALUE(S) pair */
/** id uniquily identify value within its category, label is text which is displayed */
values: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.any.isRequired,
label: PropTypes.string.isRequired
}).isRequired
).isRequired,
/** Fuction callback called when X button is clicked */
onDeleteClick: PropTypes.func.isRequired,
/** Function used to truncate category label */
categoryTruncate: PropTypes.func,
/** Function used to truncate value label */
valueTruncate: PropTypes.func,
/** Name of CSS class(es) which are set to outer label */
className: PropTypes.string,
/** Bootstrap style which is set to label */
bsStyle: PropTypes.string,
/** Name of CSS class(es) which are set to inner label(s) */
innerClassName: PropTypes.string,
/** Placement of the overlay */
overlayPlacement: PropTypes.oneOf(['top', 'right', 'bottom', 'left'])
};

CompoundLabel.defaultProps = {
categoryTruncate: str => (str.length > 18 ? `${str.substring(0, 18)}...` : str),
valueTruncate: str => (str.length > 18 ? `${str.substring(0, 18)}...` : str),
className: '',
bsStyle: 'primary',
innerClassName: '',
overlayPlacement: 'bottom'
};

export default CompoundLabel;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { shallow } from 'enzyme';
import CompoundLabel from './CompoundLabel';
import { noop } from '../../common/helpers';

const tag = {
id: 1,
label: 'Food - category with very long description',
values: [
{ id: 11, label: 'Cake' },
{ id: 12, label: 'Bloody Steak from the famous Purple Cow' },
{ id: 13, label: 'Pineapple Pizza' }
]
};

test('snapshot test', () => {
const view = shallow(
<CompoundLabel key={tag.id} category={{ id: tag.id, label: tag.label }} values={tag.values} onDeleteClick={noop} />
);
expect(view).toMatchSnapshot();
});
76 changes: 57 additions & 19 deletions packages/patternfly-react/src/components/Label/Label.stories.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';
import { defaultTemplate } from 'storybook/decorators/storyTemplates';
import { inlineTemplate } from 'storybook/decorators/storyTemplates';
import { storybookPackageName, DOCUMENTATION_URL, STORYBOOK_CATEGORY } from 'storybook/constants/siteConstants';
import { Label, DisposableLabel, RemoveButton } from './index';
import { Label, DisposableLabel, RemoveButton, CompoundLabel } from './index';
import { MockCompoundLabel, mockCompoundLabelSource } from './__mocks__/mockCompoundLabel';
import { MockLabelRemove, mockLabelRemoveSource } from './__mocks__/mockLabelExamples';
import { name } from '../../../package.json';

const stories = storiesOf(`${storybookPackageName(name)}/${STORYBOOK_CATEGORY.WIDGETS}/Label`, module);

stories.addDecorator(
defaultTemplate({
title: 'Label',
documentationLink: `${DOCUMENTATION_URL.PATTERNFLY_ORG_WIDGETS}#labels`,
reactBootstrapDocumentationLink: `${DOCUMENTATION_URL.REACT_BOOTSTRAP_COMPONENT}label/`
})
);

stories.add(
'Label',
withInfo()(() => (
<div>
<Label bsStyle="default">Default</Label> <Label bsStyle="primary">Primary</Label>{' '}
<Label bsStyle="success">Success</Label> <Label bsStyle="info">Info</Label>{' '}
<Label bsStyle="warning">Warning</Label> <Label bsStyle="danger">Danger</Label>
</div>
))
withInfo()(() => {
const story = (
<div>
<Label bsStyle="default">Default</Label> <Label bsStyle="primary">Primary</Label>{' '}
<Label bsStyle="success">Success</Label> <Label bsStyle="info">Info</Label>{' '}
<Label bsStyle="warning">Warning</Label> <Label bsStyle="danger">Danger</Label>
</div>
);
return inlineTemplate({
title: 'Label',
documentationLink: `${DOCUMENTATION_URL.PATTERNFLY_ORG_WIDGETS}#labels`,
story,
reactBootstrapDocumentationLink: `${DOCUMENTATION_URL.REACT_BOOTSTRAP_COMPONENT}label/`
});
})
);

stories.add(
'Label with remove',
withInfo()(() => <MockLabelRemove />, {
'Label with Remove',
withInfo({
source: false,
propTables: [DisposableLabel, RemoveButton],
propTablesExclude: [MockLabelRemove],
Expand All @@ -40,5 +41,42 @@ stories.add(
<pre>{mockLabelRemoveSource}</pre>
</div>
)
})(() => {
const story = <MockLabelRemove />;
return inlineTemplate({
title: 'Label with Remove',
documentationLink: `${DOCUMENTATION_URL.PATTERNFLY_ORG_WIDGETS}#labels`,
story,
reactBootstrapDocumentationLink: `${DOCUMENTATION_URL.REACT_BOOTSTRAP_COMPONENT}label/`
});
})
);

stories.add(
'Compound Label',
withInfo({
source: false,
propTables: [CompoundLabel],
propTablesExclude: [MockCompoundLabel],
text: (
<div>
<h1>Story Source</h1>
<pre>{mockCompoundLabelSource}</pre>
</div>
)
})(() => {
const story = <MockCompoundLabel />;
return inlineTemplate({
title: 'Compound Label',
documentationLink: `${DOCUMENTATION_URL.PATTERNFLY_ORG_WIDGETS}#labels`,
story,
reactBootstrapDocumentationLink: `${DOCUMENTATION_URL.REACT_BOOTSTRAP_COMPONENT}label/`,
description: (
<div>
Compound label helps to visualize key/value or key/n:value component. Delete - Clicking on “X” deletes the
compound label. Tooltip - When a compound label is truncated, we use labels to show the text.
</div>
)
});
})
);
54 changes: 54 additions & 0 deletions packages/patternfly-react/src/components/Label/LabelWithTooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { Label } from '../Label';
import { OverlayTrigger } from '../OverlayTrigger';
import { Tooltip } from '../Tooltip';

const tooltip = text => <Tooltip id="tooltip">{text}</Tooltip>;

const LabelWithTooltip = ({ onDeleteClick, category, value, truncate, bsStyle, className, overlayPlacement }) => (
<li key={value.id}>
<OverlayTrigger placement={overlayPlacement} overlay={tooltip(value.label)}>
<Label
key={value.id}
onRemoveClick={() => onDeleteClick(category, value)}
bsStyle={bsStyle}
className={`compound-label-inner-color-pf ${className}`}
>
{truncate(value.label)}
</Label>
</OverlayTrigger>
</li>
);

LabelWithTooltip.propTypes = {
/** Fuction callback called when X button is clicked */
onDeleteClick: PropTypes.func.isRequired,
/** Category in CATEGORY: value(s) pair */
/** Parent of label, it does not get displayed in this component */
category: PropTypes.shape({
id: PropTypes.any.isRequired,
label: PropTypes.string.isRequired
}).isRequired,
/** Individual Value in Category:VALUE(s) pair */
/** id uniquily identify value within its category, label is text which is displayed */
value: PropTypes.PropTypes.shape({
id: PropTypes.any.isRequired,
label: PropTypes.string.isRequired
}).isRequired,
/** Function used to truncate value label */
truncate: PropTypes.func.isRequired,
/** Name of CSS class(es) which are set to label */
className: PropTypes.string,
/** Bootstrap style which is set to label */
bsStyle: PropTypes.string,
/** Placement of the overlay */
overlayPlacement: PropTypes.oneOf(['top', 'right', 'bottom', 'left'])
};

LabelWithTooltip.defaultProps = {
className: '',
bsStyle: 'primary',
overlayPlacement: 'bottom'
};
export default LabelWithTooltip;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import LabelWithTooltip from './LabelWithTooltip';
import { noop } from '../../common/helpers';

test('snapshot test', () => {
const view = shallow(
<LabelWithTooltip
key={11}
category={{ label: 'food', id: 1 }}
value={{ label: 'Salad', id: 11 }}
onDeleteClick={noop}
truncate={noop}
/>
);
expect(view).toMatchSnapshot();
});
Loading