diff --git a/CHANGELOG.md b/CHANGELOG.md index 003e11429f1..d6ba64bfcb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -- Added utility CSS classes for text and alignment concerns. ([#774](https://github.com/elastic/eui/pull/774)) -- Added `compressed` versions of `EuiFormRow` and all form controls. ([#800](https://github.com/elastic/eui/pull/800)) -- Removed pointer cursor on `EuiFormLabel` when a `for` property is not set. ([#825](https://github.com/elastic/eui/pull/825)) -- Added the ability to add tooltips to `EuiContextMenuItem`s. ([#817](https://github.com/elastic/eui/pull/817)) +- Added utility CSS classes for text and alignment concerns ([#774](https://github.com/elastic/eui/pull/774)) +- Added `compressed` versions of `EuiFormRow` and all form controls ([#800](https://github.com/elastic/eui/pull/800)) +- Removed pointer cursor on `EuiFormLabel` when a `for` property is not set ([#825](https://github.com/elastic/eui/pull/825)) +- Added the ability to add tooltips to `EuiContextMenuItem`s ([#817](https://github.com/elastic/eui/pull/817)) +- Added `EuiBreadcrumbs` ([#815](https://github.com/elastic/eui/pull/815)) **Bug fixes** @@ -35,7 +36,7 @@ - Added `direction` prop to EuiFlexGroup ([#711](https://github.com/elastic/eui/pull/711)) - Added `EuiEmptyPrompt` which can be used as a placeholder over empty tables and lists ([#711](https://github.com/elastic/eui/pull/711)) - Added `EuiTabbedContent` ([#737](https://github.com/elastic/eui/pull/737)) -- `EuiComboBox` added buttons for clearing and opening/closing the combo box. ([#698](https://github.com/elastic/eui/pull/698)) +- `EuiComboBox` added buttons for clearing and opening/closing the combo box ([#698](https://github.com/elastic/eui/pull/698)) **Bug fixes** @@ -63,11 +64,11 @@ - `EuiForm` and `EuiFormRow` now accept nodes for `errors` prop ([#685](https://github.com/elastic/eui/pull/685)) - Removed the default `max-width` from `EuiText`. This can still be applied by setting `grow={false}` ([#683](https://github.com/elastic/eui/pull/683)) - Added support for text alignment with `EuiTextAlign` ([#683](https://github.com/elastic/eui/pull/683)) -- `EuiBasicTable` added the `compressed` prop to allow for tables with smaller fonts and padding. ([#687](https://github.com/elastic/eui/pull/687)) +- `EuiBasicTable` added the `compressed` prop to allow for tables with smaller fonts and padding ([#687](https://github.com/elastic/eui/pull/687)) **Breaking changes** -- Added responsive support for tables. This isn't technically a breaking change, but you will need to apply some new props (`hasActions`, `isSelectable`) for certain tables to make them look their best in mobile. **Responsive table views are on by default.** ([#584](https://github.com/elastic/eui/pull/584)) +- Added responsive support for tables. This isn't technically a breaking change, but you will need to apply some new props (`hasActions`, `isSelectable`) for certain tables to make them look their best in mobile. **Responsive table views are on by default.** ([#584](https://github.com/elastic/eui/pull/584)) **Bug fixes** @@ -75,7 +76,7 @@ - Fixed `EuiCard` `icon` prop to include user provided className ([#684](https://github.com/elastic/eui/pull/684)) - `EuiInMemoryTable` pagination state is now reset automatically when a search is executed ([#686](https://github.com/elastic/eui/pull/686)) - Fixed slow performance of `EuiComboBox` when there are hundreds or thousands of options by virtualizing `EuiComboBoxOptionsList` ([#670](https://github.com/elastic/eui/pull/670)) -- Fixed some text styles ([#683](https://github.com/elastic/eui/pull/683)) +- Fixed some text styles ([#683](https://github.com/elastic/eui/pull/683)) - Fixed font-family of input, textarea, select, and buttons - Fixed style of code, pre, and dl’s inside `EuiText` - Fixed ghost text color which was being set to a dark gray @@ -186,7 +187,7 @@ - Fixed `EuiToolTip` bug which caused the tooltip to hide when moving the mouse around inside of the trigger element ([#557](https://github.com/elastic/eui/pull/557), [#564](https://github.com/elastic/eui/pull/564)) - Fixed a bug where `EuiButtonEmpty` would offer a white background on hover when it was disabled, even when there was no such background transition on hover when the buttons are not disabled ([#561](https://github.com/elastic/eui/pull/561)) - Fixed table cell bugs ([#565](https://github.com/elastic/eui/pull/565)) - - `EuiBasicTable` now supports explicitly setting `truncateText` and `textOnly` on column definitions, and supports passing through unrecognized props to the cell (e.g. `data-test-subj`). + - `EuiBasicTable` now supports explicitly setting `truncateText` and `textOnly` on column definitions, and supports passing through unrecognized props to the cell (e.g. `data-test-subj`). - Updated table cell CSS so that long single-word cell content will break and wrap mid-word. ## [`0.0.33`](https://github.com/elastic/eui/tree/v0.0.33) @@ -265,7 +266,7 @@ instead of just string ([#516](https://github.com/elastic/eui/pull/516)) - `EuiSelect` do not set `defaultValue` property when `value` property is provided ([#504](https://github.com/elastic/eui/pull/504)). - `EuiBottomBar` now uses `EuiPortal` to avoid zindex conflicts ([#487](https://github.com/elastic/eui/pull/487)) -- Upped dark theme contrast on disabled buttons ([#487](https://github.com/elastic/eui/pull/487)) +- Upped dark theme contrast on disabled buttons ([#487](https://github.com/elastic/eui/pull/487)) **Breaking changes** diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index bde014ec8a9..2c6b734d8be 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -60,6 +60,9 @@ import { BadgeExample } import { BottomBarExample } from './views/bottom_bar/bottom_bar_example'; +import { BreadcrumbsExample } + from './views/breadcrumbs/breadcrumbs_example'; + import { ButtonExample } from './views/button/button_example'; @@ -286,6 +289,7 @@ const navigation = [{ }, { name: 'Navigation', items: [ + BreadcrumbsExample, ButtonExample, ContextMenuExample, KeyPadMenuExample, diff --git a/src-docs/src/views/breadcrumbs/breadcrumbs.js b/src-docs/src/views/breadcrumbs/breadcrumbs.js new file mode 100644 index 00000000000..789ed26a1fe --- /dev/null +++ b/src-docs/src/views/breadcrumbs/breadcrumbs.js @@ -0,0 +1,49 @@ +import React, { Fragment } from 'react'; + +import { + EuiBreadcrumbs, + EuiButton, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, + EuiSpacer, +} from '../../../../src/components'; + +export default () => { + const breadcrumbs = [{ + text: 'Animals', + href: '#', + onClick: (e) => { e.preventDefault(); console.log('You clicked Animals'); }, + 'data-test-subj': 'breadcrumbsAnimals', + className: 'customClass', + }, { + text: 'Reptiles', + href: '#', + onClick: (e) => { e.preventDefault(); console.log('You clicked Reptiles'); }, + }, { + text: 'Boa constrictor', + href: '#', + onClick: (e) => { e.preventDefault(); console.log('You clicked Boa constrictor'); }, + }, { + text: 'Edit', + }]; + + return ( + + + + + + + +

Boa constrictor

+
+
+ + + Cancel + +
+
+ ); +}; diff --git a/src-docs/src/views/breadcrumbs/breadcrumbs_example.js b/src-docs/src/views/breadcrumbs/breadcrumbs_example.js new file mode 100644 index 00000000000..5917e2c1dba --- /dev/null +++ b/src-docs/src/views/breadcrumbs/breadcrumbs_example.js @@ -0,0 +1,99 @@ +import React from 'react'; + +import { renderToHtml } from '../../services'; + +import { + GuideSectionTypes, +} from '../../components'; + +import { + EuiCode, + EuiBreadcrumbs, +} from '../../../../src/components'; + +import Breadcrumbs from './breadcrumbs'; +const breadcrumbsSource = require('!!raw-loader!./breadcrumbs'); +const breadcrumbsHtml = renderToHtml(Breadcrumbs); + +import Responsive from './responsive'; +const responsiveSource = require('!!raw-loader!./responsive'); +const responsiveHtml = renderToHtml(Responsive); + +import Truncate from './truncate'; +const truncateSource = require('!!raw-loader!./truncate'); +const truncateHtml = renderToHtml(Truncate); + +import Max from './max'; +const maxSource = require('!!raw-loader!./max'); +const maxHtml = renderToHtml(Max); + +export const BreadcrumbsExample = { + title: 'Breadcrumbs', + sections: [{ + source: [{ + type: GuideSectionTypes.JS, + code: breadcrumbsSource, + }, { + type: GuideSectionTypes.HTML, + code: breadcrumbsHtml, + }], + text: ( +

+ EuiBreadcrumbs let the user track their progress within and back out of + a UX flow. They work well within EuiPageContentHeader but be careful + not to be use them within an app that also uses EuiHeaderBreadcrumbs. +

+ ), + props: { EuiBreadcrumbs }, + demo: , + }, { + title: 'Responsive', + source: [{ + type: GuideSectionTypes.JS, + code: responsiveSource, + }, { + type: GuideSectionTypes.HTML, + code: responsiveHtml, + }], + text: ( +

+ The responsive prop will hide breadcrumbs on narrower screens. +

+ ), + props: { EuiBreadcrumbs }, + demo: , + }, { + title: 'Truncate each breadcrumb', + source: [{ + type: GuideSectionTypes.JS, + code: truncateSource, + }, { + type: GuideSectionTypes.HTML, + code: truncateHtml, + }], + text: ( +

+ The truncate prop will truncate breadcrumbs which are too long. +

+ ), + props: { EuiBreadcrumbs }, + demo: , + }, { + title: 'Limit the number of breadcrumbs', + source: [{ + type: GuideSectionTypes.JS, + code: maxSource, + }, { + type: GuideSectionTypes.HTML, + code: maxHtml, + }], + text: ( +

+ Use the max prop to cull breadcrumbs beyond a certain number. By default, + this number is 5. +

+ ), + props: { EuiBreadcrumbs }, + demo: , + }], +}; diff --git a/src-docs/src/views/breadcrumbs/max.js b/src-docs/src/views/breadcrumbs/max.js new file mode 100644 index 00000000000..24527a4d3f7 --- /dev/null +++ b/src-docs/src/views/breadcrumbs/max.js @@ -0,0 +1,34 @@ +import React from 'react'; + +import { + EuiBreadcrumbs, +} from '../../../../src/components'; + +export default () => { + const breadcrumbs = [{ + text: 'Animals', + href: '#', + }, { + text: 'Metazoans', + href: '#', + }, { + text: 'Chordates', + href: '#', + }, { + text: 'Vertebrates', + href: '#', + }, { + text: 'Tetrapods', + href: '#', + }, { + text: 'Reptiles', + href: '#', + }, { + text: 'Boa constrictor', + href: '#', + }, { + text: 'Nebulosa subspecies', + }]; + + return ; +}; diff --git a/src-docs/src/views/breadcrumbs/responsive.js b/src-docs/src/views/breadcrumbs/responsive.js new file mode 100644 index 00000000000..76f3b44edca --- /dev/null +++ b/src-docs/src/views/breadcrumbs/responsive.js @@ -0,0 +1,34 @@ +import React from 'react'; + +import { + EuiBreadcrumbs, +} from '../../../../src/components'; + +export default () => { + const breadcrumbs = [{ + text: 'Animals', + href: '#', + }, { + text: 'Metazoans', + href: '#', + }, { + text: 'Chordates', + href: '#', + }, { + text: 'Vertebrates', + href: '#', + }, { + text: 'Tetrapods', + href: '#', + }, { + text: 'Reptiles', + href: '#', + }, { + text: 'Boa constrictor', + href: '#', + }, { + text: 'Nebulosa subspecies', + }]; + + return ; +}; diff --git a/src-docs/src/views/breadcrumbs/truncate.js b/src-docs/src/views/breadcrumbs/truncate.js new file mode 100644 index 00000000000..353f6956dc7 --- /dev/null +++ b/src-docs/src/views/breadcrumbs/truncate.js @@ -0,0 +1,19 @@ +import React from 'react'; + +import { + EuiBreadcrumbs, +} from '../../../../src/components'; + +export default () => { + const breadcrumbs = [{ + text: 'Animals', + href: '#', + }, { + text: 'Metazoans is a real mouthful, especially for creatures without mouths', + href: '#', + }, { + text: 'Nebulosa subspecies', + }]; + + return ; +}; diff --git a/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.js.snap b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.js.snap new file mode 100644 index 00000000000..6582a70375a --- /dev/null +++ b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.js.snap @@ -0,0 +1,271 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiBreadcrumbs is rendered 1`] = ` +
+ + + Animals + + +
+ +
+ + Boa constrictor + +
+ + Edit + +
+`; + +exports[`EuiBreadcrumbs props max doesn't break when max exceeds the number of breadcrumbs 1`] = ` +
+ +
+ +
+ +
+ + Edit + +
+`; + +exports[`EuiBreadcrumbs props max renders 1 item 1`] = ` +
+
+ … +
+
+ + Edit + +
+`; + +exports[`EuiBreadcrumbs props max renders 2 items 1`] = ` +
+ +
+
+ … +
+
+ + Edit + +
+`; + +exports[`EuiBreadcrumbs props max renders 3 items 1`] = ` +
+ +
+
+ … +
+
+ +
+ + Edit + +
+`; + +exports[`EuiBreadcrumbs props responsive is rendered 1`] = ` +
+ +
+ +
+ +
+ + Edit + +
+`; + +exports[`EuiBreadcrumbs props truncate is rendered 1`] = ` +
+ +
+ +
+ +
+ + Edit + +
+`; diff --git a/src/components/breadcrumbs/_breadcrumbs.scss b/src/components/breadcrumbs/_breadcrumbs.scss new file mode 100644 index 00000000000..15b8dc39a17 --- /dev/null +++ b/src/components/breadcrumbs/_breadcrumbs.scss @@ -0,0 +1,74 @@ +.euiBreadcrumbs { + font-size: $euiFontSizeS; +} + +.euiBreadcrumb { + display: inline-block; + + &:not(.euiBreadcrumb--last) { + margin-right: $euiBreadcrumbSpacing; + } +} + +.euiBreadcrumb--last { + font-weight: $euiFontWeightMedium; +} + +.euiBreadcrumb--collapsed { + color: $euiColorLightShade; +} + +.euiBreadcrumbSeparator { + pointer-events: none; + display: inline-block; + margin-right: $euiBreadcrumbSpacing; + width: 1px; + height: $euiSize; + transform: translateY(0.2em) rotate(15deg); + background: $euiColorLightShade; +} + +.euiBreadcrumbs--responsive { + // Laptop + @include screenMedium { + .euiBreadcrumbSeparator, + .euiBreadcrumb { + display: none; + + // Only show last 4 breadcrumbs + &:nth-last-of-type(-n+3) { + display: inline-block; + } + } + } + + // Tablets + @include screenSmall { + .euiBreadcrumbSeparator, + .euiBreadcrumb { + display: none; + + // Only show last 2 breadcrumbs + &:nth-last-of-type(-n+1) { + display: inline-block; + } + } + } + + // Mobile + @include screenXSmall { + .euiBreadcrumbSeparator, + .euiBreadcrumb { + display: none; + } + } +} + +.euiBreadcrumbs--truncate { + .euiBreadcrumb { + white-space: nowrap; + max-width: 150px; + text-overflow: ellipsis; + overflow: hidden; + } +} diff --git a/src/components/breadcrumbs/_index.scss b/src/components/breadcrumbs/_index.scss new file mode 100644 index 00000000000..aef674aad77 --- /dev/null +++ b/src/components/breadcrumbs/_index.scss @@ -0,0 +1,3 @@ +$euiBreadcrumbSpacing: $euiSizeS; + +@import 'breadcrumbs'; diff --git a/src/components/breadcrumbs/breadcrumbs.js b/src/components/breadcrumbs/breadcrumbs.js new file mode 100644 index 00000000000..fb1b886aa8c --- /dev/null +++ b/src/components/breadcrumbs/breadcrumbs.js @@ -0,0 +1,144 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { EuiLink } from '../link'; + +const limitBreadcrumbs = (breadcrumbs, max) => { + const breadcrumbsAtStart = []; + const breadcrumbsAtEnd = []; + const limit = Math.min(max, breadcrumbs.length); + + for (let i = 0; i < limit; i++) { + // We'll alternate with displaying breadcrumbs at the end and at the start, but be biased + // towards breadcrumbs the end so that if max is an odd number, we'll have one more + // breadcrumb visible at the end than at the beginning. + const isEven = i % 2 === 0; + + // We're picking breadcrumbs from the front AND the back, so we treat each iteration as a + // half-iteration. + const normalizedIndex = Math.floor(i * 0.5); + const indexOfBreadcrumb = isEven ? breadcrumbs.length - 1 - normalizedIndex : normalizedIndex; + const breadcrumb = breadcrumbs[indexOfBreadcrumb]; + + if (isEven) { + breadcrumbsAtEnd.unshift(breadcrumb); + } else { + breadcrumbsAtStart.push(breadcrumb); + } + } + + if (max < breadcrumbs.length) { + breadcrumbsAtStart.push(); + } + + return [ + ...breadcrumbsAtStart, + ...breadcrumbsAtEnd, + ]; +} + +const EuiBreadcrumbCollapsed = () => ( + +
+ +
+); + +const EuiBreadcrumbSeparator = () =>
; + +export const EuiBreadcrumbs = ({ + breadcrumbs, + className, + responsive, + truncate, + max, + ...rest, +}) => { + const breadcrumbElements = breadcrumbs.map((breadcrumb, index) => { + const { + text, + href, + onClick, + className: breadcrumbClassName, + ...breadcrumbRest + } = breadcrumb; + + const isLastBreadcrumb = index === breadcrumbs.length - 1; + + const breadcrumbClasses = classNames('euiBreadcrumb', breadcrumbClassName, { + 'euiBreadcrumb--last': isLastBreadcrumb, + }); + + let link; + + if (isLastBreadcrumb) { + link = ( + + { text } + + ); + } else { + link = ( + + {text} + + ); + } + + let separator; + + if (!isLastBreadcrumb) { + separator = ; + } + + return ( + + {link} + {separator} + + ); + }) + + const limitedBreadcrumbs = max ? limitBreadcrumbs(breadcrumbElements, max) : breadcrumbElements; + + const classes = classNames('euiBreadcrumbs', className, { + 'euiBreadcrumbs--truncate': truncate, + 'euiBreadcrumbs--responsive': responsive, + }); + + return ( +
+ {limitedBreadcrumbs} +
+ ); +}; + +EuiBreadcrumbs.propTypes = { + className: PropTypes.string, + responsive: PropTypes.bool, + truncate: PropTypes.bool, + max: PropTypes.number, + breadcrumbs: PropTypes.arrayOf(PropTypes.shape({ + text: PropTypes.node.isRequired, + href: PropTypes.string, + onClick: PropTypes.func, + })).isRequired, +}; + +EuiBreadcrumbs.defaultProps = { + responsive: true, + truncate: true, + max: 5, +}; diff --git a/src/components/breadcrumbs/breadcrumbs.test.js b/src/components/breadcrumbs/breadcrumbs.test.js new file mode 100644 index 00000000000..309c9edd223 --- /dev/null +++ b/src/components/breadcrumbs/breadcrumbs.test.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../test'; + +import { EuiBreadcrumbs } from './breadcrumbs'; + +describe('EuiBreadcrumbs', () => { + test('is rendered', () => { + const breadcrumbs = [{ + text: Animals, + href: '#', + onClick: (e) => { e.preventDefault(); console.log('You clicked Animals'); }, + 'data-test-subj': 'breadcrumbsAnimals', + className: 'customClass', + }, { + text: 'Reptiles', + onClick: (e) => { e.preventDefault(); console.log('You clicked Reptiles'); }, + }, { + text: 'Boa constrictor', + href: '#', + }, { + text: 'Edit', + }]; + + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + const breadcrumbs = [{ + text: 'Animals', + }, { + text: 'Reptiles', + }, { + text: 'Boa constrictor', + }, { + text: 'Edit', + }]; + + describe('responsive', () => { + test('is rendered', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('truncate', () => { + test('is rendered', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('max', () => { + test('renders 1 item', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + + test('renders 2 items', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + + test('renders 3 items', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + + test(`doesn't break when max exceeds the number of breadcrumbs`, () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/src/components/breadcrumbs/index.js b/src/components/breadcrumbs/index.js new file mode 100644 index 00000000000..35a6bb68c4c --- /dev/null +++ b/src/components/breadcrumbs/index.js @@ -0,0 +1,3 @@ +export { + EuiBreadcrumbs, +} from './breadcrumbs'; diff --git a/src/components/breadcrumbs/max.js b/src/components/breadcrumbs/max.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/components/index.js b/src/components/index.js index cb9863e38fa..9734eba0ace 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -11,10 +11,19 @@ export { EuiScreenReaderOnly, } from './accessibility'; +export { + EuiBadge, + EuiBetaBadge, +} from './badge'; + export { EuiBottomBar, } from './bottom_bar'; +export { + EuiBreadcrumbs, +} from './breadcrumbs'; + export { EuiButton, EuiButtonEmpty, @@ -22,18 +31,13 @@ export { } from './button'; export { - EuiBadge, - EuiBetaBadge, -} from './badge'; + EuiCallOut, +} from './call_out'; export { EuiCard, } from './card'; -export { - EuiCallOut, -} from './call_out'; - export { EuiCode, EuiCodeBlock, diff --git a/src/components/index.scss b/src/components/index.scss index e736d0d2340..cb290518e8e 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -8,6 +8,7 @@ @import 'basic_table/index'; @import 'bottom_bar/index'; @import 'button/index'; +@import 'breadcrumbs/index'; @import 'call_out/index'; @import 'card/index'; @import 'code/index'; diff --git a/src/components/link/link.js b/src/components/link/link.js index cee3b79cd5f..573041a4962 100644 --- a/src/components/link/link.js +++ b/src/components/link/link.js @@ -24,6 +24,7 @@ export const EuiLink = ({ target, rel, type, + onClick, ...rest }) => { const classes = classNames('euiLink', colorsToClassNameMap[color], className); @@ -33,6 +34,7 @@ export const EuiLink = ({