diff --git a/packages/patternfly-react/less/compound-label.less b/packages/patternfly-react/less/compound-label.less new file mode 100644 index 00000000000..3e6434a97b6 --- /dev/null +++ b/packages/patternfly-react/less/compound-label.less @@ -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; +} diff --git a/packages/patternfly-react/sass/patternfly-react.scss b/packages/patternfly-react/sass/patternfly-react.scss index 43c99bcef1f..bb7690fec9e 100644 --- a/packages/patternfly-react/sass/patternfly-react.scss +++ b/packages/patternfly-react/sass/patternfly-react.scss @@ -20,3 +20,4 @@ $icon-font-path: '~patternfly/dist/fonts/'; */ @import 'patternfly-react/patternfly-react'; +@import 'patternfly-react/compound-label'; diff --git a/packages/patternfly-react/sass/patternfly-react/_compound-label.scss b/packages/patternfly-react/sass/patternfly-react/_compound-label.scss new file mode 100644 index 00000000000..54d67f06fed --- /dev/null +++ b/packages/patternfly-react/sass/patternfly-react/_compound-label.scss @@ -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; +} diff --git a/packages/patternfly-react/src/components/Label/CompoundLabel.js b/packages/patternfly-react/src/components/Label/CompoundLabel.js new file mode 100644 index 00000000000..fa68266e107 --- /dev/null +++ b/packages/patternfly-react/src/components/Label/CompoundLabel.js @@ -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 => ( + + ); + + render() { + const values = [...this.props.values]; + if (values.length === 0) return null; + const categoryTooltip = {this.props.category.label}; + return ( + + + {this.props.categoryTruncate(this.props.category.label)} + + + + ); + } +} + +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; diff --git a/packages/patternfly-react/src/components/Label/CompoundLabel.test.js b/packages/patternfly-react/src/components/Label/CompoundLabel.test.js new file mode 100644 index 00000000000..c6659039437 --- /dev/null +++ b/packages/patternfly-react/src/components/Label/CompoundLabel.test.js @@ -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( + + ); + expect(view).toMatchSnapshot(); +}); diff --git a/packages/patternfly-react/src/components/Label/Label.stories.js b/packages/patternfly-react/src/components/Label/Label.stories.js index 5ebf5ff78cd..ac74926a340 100644 --- a/packages/patternfly-react/src/components/Label/Label.stories.js +++ b/packages/patternfly-react/src/components/Label/Label.stories.js @@ -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()(() => ( -
- {' '} - {' '} - -
- )) + withInfo()(() => { + const story = ( +
+ {' '} + {' '} + +
+ ); + 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()(() => , { + 'Label with Remove', + withInfo({ source: false, propTables: [DisposableLabel, RemoveButton], propTablesExclude: [MockLabelRemove], @@ -40,5 +41,42 @@ stories.add(
{mockLabelRemoveSource}
) + })(() => { + const story = ; + 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: ( +
+

Story Source

+
{mockCompoundLabelSource}
+
+ ) + })(() => { + const story = ; + return inlineTemplate({ + title: 'Compound Label', + documentationLink: `${DOCUMENTATION_URL.PATTERNFLY_ORG_WIDGETS}#labels`, + story, + reactBootstrapDocumentationLink: `${DOCUMENTATION_URL.REACT_BOOTSTRAP_COMPONENT}label/`, + description: ( +
+ 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. +
+ ) + }); }) ); diff --git a/packages/patternfly-react/src/components/Label/LabelWithTooltip.js b/packages/patternfly-react/src/components/Label/LabelWithTooltip.js new file mode 100644 index 00000000000..3b7f7939238 --- /dev/null +++ b/packages/patternfly-react/src/components/Label/LabelWithTooltip.js @@ -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 => {text}; + +const LabelWithTooltip = ({ onDeleteClick, category, value, truncate, bsStyle, className, overlayPlacement }) => ( +
  • + + + +
  • +); + +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; diff --git a/packages/patternfly-react/src/components/Label/LabelWithTooltip.test.js b/packages/patternfly-react/src/components/Label/LabelWithTooltip.test.js new file mode 100644 index 00000000000..d1bdf9bc6bf --- /dev/null +++ b/packages/patternfly-react/src/components/Label/LabelWithTooltip.test.js @@ -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( + + ); + expect(view).toMatchSnapshot(); +}); diff --git a/packages/patternfly-react/src/components/Label/__mocks__/mockCompoundLabel.js b/packages/patternfly-react/src/components/Label/__mocks__/mockCompoundLabel.js new file mode 100644 index 00000000000..bff957cb70c --- /dev/null +++ b/packages/patternfly-react/src/components/Label/__mocks__/mockCompoundLabel.js @@ -0,0 +1,77 @@ +import React from 'react'; +import { CompoundLabel } from '../index'; + +export class MockCompoundLabel extends React.Component { + constructor(props) { + super(props); + this.state = { + tag: { + id: 1, + label: 'Most delicious food you will ever eat', + values: [ + { id: 11, label: 'Strawberries harvested under full moon light' }, + { id: 12, label: 'Argentinian beef steak from hand massaged cow' }, + { id: 13, label: 'Enchanted cookies baked by insane chef' }, + { id: 14, label: 'Dumplings' } + ] + } + }; + } + + removeMe = (category, value) => { + const values = this.state.tag.values.filter(val => val.id !== value.id); + const state = { tag: { ...this.state.tag, values } }; + this.setState(state); + }; + + render() { + return ( + + ); + } +} + +export const mockCompoundLabelSource = ` +import React from 'react'; +import { CompoundLabel } from '../index'; + +export class MockCompoundLabel extends React.Component { + constructor(props) { + super(props); + this.state = { + tag: { + id: 1, + label: 'Most delicious food you will ever eat', + values: [ + { id: 11, label: 'Strawberries harvested under full moon light' }, + { id: 12, label: 'Argentinian beef steak from hand massaged cow' }, + { id: 13, label: 'Enchanted cookies baked by insane chef' }, + { id: 14, label: 'Dumplings' } + ] + } + }; + } + + removeMe = (category, value) => { + const values = this.state.tag.values.filter(val => val.id !== value.id); + const state = { tag: { ...this.state.tag, values } }; + this.setState(state); + }; + + render() { + return ( + + ); + } +} +`; diff --git a/packages/patternfly-react/src/components/Label/__mocks__/mockLabelExamples.js b/packages/patternfly-react/src/components/Label/__mocks__/mockLabelExamples.js index b1fd2fdd226..6252deb9397 100644 --- a/packages/patternfly-react/src/components/Label/__mocks__/mockLabelExamples.js +++ b/packages/patternfly-react/src/components/Label/__mocks__/mockLabelExamples.js @@ -39,7 +39,7 @@ export class MockLabelRemove extends React.Component { export const mockLabelRemoveSource = ` import React from 'react'; import { Label } from '../index'; - + export class MockLabelRemove extends React.Component { constructor(props) { super(props); @@ -56,7 +56,7 @@ export const mockLabelRemoveSource = ` removeMe = index => { this.setState(this.state.types.splice(index, 1)); }; - + render() { return (
    diff --git a/packages/patternfly-react/src/components/Label/__snapshots__/CompoundLabel.test.js.snap b/packages/patternfly-react/src/components/Label/__snapshots__/CompoundLabel.test.js.snap new file mode 100644 index 00000000000..7c7bf211e35 --- /dev/null +++ b/packages/patternfly-react/src/components/Label/__snapshots__/CompoundLabel.test.js.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test 1`] = ` + + + Food - category with very long description + + } + placement="bottom" + trigger={ + Array [ + "hover", + "focus", + ] + } + > + + Food - category wi... + + +
      + + + +
    +
    +`; diff --git a/packages/patternfly-react/src/components/Label/__snapshots__/LabelWithTooltip.test.js.snap b/packages/patternfly-react/src/components/Label/__snapshots__/LabelWithTooltip.test.js.snap new file mode 100644 index 00000000000..eecfe51e553 --- /dev/null +++ b/packages/patternfly-react/src/components/Label/__snapshots__/LabelWithTooltip.test.js.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test 1`] = ` +
  • + + Salad + + } + placement="bottom" + trigger={ + Array [ + "hover", + "focus", + ] + } + > + +
  • +`; diff --git a/packages/patternfly-react/src/components/Label/index.js b/packages/patternfly-react/src/components/Label/index.js index 56b20353647..17f0461218f 100644 --- a/packages/patternfly-react/src/components/Label/index.js +++ b/packages/patternfly-react/src/components/Label/index.js @@ -1,5 +1,6 @@ import DisposableLabel from './DisposableLabel'; import RemoveButton from './RemoveButton'; +import CompoundLabel from './CompoundLabel'; export { default as Label } from './Label'; -export { DisposableLabel, RemoveButton }; +export { DisposableLabel, RemoveButton, CompoundLabel };