diff --git a/CHANGELOG.md b/CHANGELOG.md
index 222fb0791f8..d6dbd5e95e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,20 +1,29 @@
# [`master`](https://github.com/elastic/eui/tree/master)
-No public interface changes since `0.0.36`.
+- Added `EuiComboBox` for selecting many options from a list of options ([567](https://github.com/elastic/eui/pull/567))
+- Added `EuiHighlight` for highlighting a substring within text ([567](https://github.com/elastic/eui/pull/567))
+- `calculatePopoverPosition` service now accepts a `positions` argument so you can specify which positions are acceptable ([567](https://github.com/elastic/eui/pull/567))
+- Added `closeButtonProps` prop to `EuiBadge`, `hollow` badge type, and support for arbitrary hex color ([567](https://github.com/elastic/eui/pull/567))
+- Added support for arbitrary hex color to `EuiIcon` ([567](https://github.com/elastic/eui/pull/567))
+
+**Breaking changes**
+
+- Renamed `euiBody-hasToolTip` class to `euiBody-hasPortalContent` ([567](https://github.com/elastic/eui/pull/567))
# [`0.0.36`](https://github.com/elastic/eui/tree/v0.0.36)
-- Relaxed query syntax of `EuiSearchBar` to allow usage of hyphens without escaping ([#581](https://github.com/elastic/eui/pull/581))
- Added support for range queries in `EuiSearchBar` (works for numeric and date values) ([#485](https://github.com/elastic/eui/pull/485))
- Added support for emitting a `EuiSearchBar` query to an Elasticsearch query string ([#598](https://github.com/elastic/eui/pull/598))
-- Add support for expandable rows to `EuiBasicTable` ([#585](https://github.com/elastic/eui/pull/585))
+- Added support for expandable rows to `EuiBasicTable` ([#585](https://github.com/elastic/eui/pull/585))
**Bug fixes**
-- Fix font-weight issue in K6 theme ([#596](https://github.com/elastic/eui/pull/596))
+
+- Relaxed query syntax of `EuiSearchBar` to allow usage of hyphens without escaping ([#581](https://github.com/elastic/eui/pull/581))
+- Fixed font-weight issue in K6 theme ([#596](https://github.com/elastic/eui/pull/596))
# [`0.0.35`](https://github.com/elastic/eui/tree/v0.0.35)
-- Modified `link` and all buttons to support both href and onClick ([#554](https://github.com/elastic/eui/pull/554))
+- Modified `EuiLink` and all buttons to support both href and onClick ([#554](https://github.com/elastic/eui/pull/554))
- Added `color` prop to `EuiIconTip` ([#580](https://github.com/elastic/eui/pull/580))
# [`0.0.34`](https://github.com/elastic/eui/tree/v0.0.34)
diff --git a/generator-eui/documentation/templates/documentation_page.js b/generator-eui/documentation/templates/documentation_page.js
index 7c19c54f1b7..32758cffcc6 100644
--- a/generator-eui/documentation/templates/documentation_page.js
+++ b/generator-eui/documentation/templates/documentation_page.js
@@ -31,7 +31,7 @@ export const <%= componentExampleName %>Example = {
Description needed: how to use the Eui<%= componentExampleName %> component.
+ Use a EuiComboBox when the input has so many options that the user
+ needs to be able to search them, the user needs to be able to select multiple options,
+ and/or the user should have the ability to specify
+ a custom value in addition to selecting from a predetermined list.
+
+
+
+
+
+
+
+ The combo box will have errors if any of the options you pass to it share the same label
+ property. It’s OK if options have duplicate values, though. This is because the label
+ is the only thing the combo box is concerned about, since this is what the user sees
+ and what is matched against when the user searches.
+
+ This example demonstrates how the combo box works within containers. Because this component
+ uses portals, it’s important that it works within other portal-using components.
+
+ Useful for visualization or tagging systems. You can also pass a color in
+ your option list. The color can be a hex value
+ (like #000) or any other named color value accepted by
+ the Badge component.
+
+ You can provide a renderOption prop which will accept option
+ and searchValue arguments. Use the value prop of the
+ option object to store metadata about the option for use in this callback.
+
- Use the color prop to assign a color for your icons.
+ Use the color prop to assign a color for your icons. It
+ can accept named colors from our pallete or a three or six color hex code.
The default behavior is to inherit the text color as the SVG
color fill property via currentColor in CSS.
diff --git a/src/components/accordion/__snapshots__/accordion.test.js.snap b/src/components/accordion/__snapshots__/accordion.test.js.snap
index a9fc7b00761..813c6c91220 100644
--- a/src/components/accordion/__snapshots__/accordion.test.js.snap
+++ b/src/components/accordion/__snapshots__/accordion.test.js.snap
@@ -57,6 +57,11 @@ exports[`EuiAccordion behavior closes when clicked twice 1`] = `
-
+
Content
@@ -23,7 +25,9 @@ exports[`EuiBadge props color accent is rendered 1`] = `
-
+
Content
@@ -37,7 +41,9 @@ exports[`EuiBadge props color danger is rendered 1`] = `
-
+
Content
@@ -51,7 +57,25 @@ exports[`EuiBadge props color default is rendered 1`] = `
-
+
+ Content
+
+
+
+`;
+
+exports[`EuiBadge props color hollow is rendered 1`] = `
+
+
+
Content
@@ -65,7 +89,9 @@ exports[`EuiBadge props color primary is rendered 1`] = `
-
+
Content
@@ -79,7 +105,9 @@ exports[`EuiBadge props color secondary is rendered 1`] = `
-
+
Content
@@ -93,7 +121,9 @@ exports[`EuiBadge props color warning is rendered 1`] = `
-
+
Content
@@ -108,7 +138,7 @@ exports[`EuiBadge props iconSide left is rendered 1`] = `
class="euiBadge__content"
>
-
+
Content
@@ -140,7 +172,7 @@ exports[`EuiBadge props iconSide right is rendered 1`] = `
class="euiBadge__content"
>
-
+
Content
@@ -172,7 +206,7 @@ exports[`EuiBadge props iconType is rendered 1`] = `
class="euiBadge__content"
>
-
+
Content
diff --git a/src/components/badge/_badge.scss b/src/components/badge/_badge.scss
index 207d31a9836..b635706cb6f 100644
--- a/src/components/badge/_badge.scss
+++ b/src/components/badge/_badge.scss
@@ -1,16 +1,22 @@
+/**
+ * 1. Accounts for the border
+ */
.euiBadge {
font-size: $euiSizeM;
font-weight: $euiFontWeightMedium;
- line-height: $euiSize + $euiSizeXS;
+ line-height: $euiSize + 2px; /* 1 */
display: inline-block;
text-decoration: none;
box-sizing: content-box;
border-radius: $euiBorderRadius / 2;
- padding: 0 $euiSizeXS;
+ border: solid 1px transparent;
+ padding: 0 $euiSizeS;
background-color: transparent;
white-space: nowrap;
vertical-align: middle;
text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ .euiBadge {
margin-left: $euiSizeXS;
@@ -19,6 +25,13 @@
.euiBadge__content {
display: flex;
align-items: center;
+ overflow: hidden;
+ }
+
+ .euiBadge__text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1 1 auto;
}
&:focus {
@@ -26,6 +39,7 @@
}
.euiBadge__icon {
+ flex: 0 0 auto;
margin-right: $euiSizeXS;
&:focus {
@@ -66,3 +80,10 @@ $badgeTypes: (
}
}
}
+
+// Hollow has a border and is mostly used for autocompleters.
+.euiBadge--hollow {
+ background-color: $euiColorEmptyShade;
+ border-color: $euiBorderColor;
+ color: $euiTextColor;
+}
diff --git a/src/components/badge/badge.js b/src/components/badge/badge.js
index c7c910d08ac..52844b3165f 100644
--- a/src/components/badge/badge.js
+++ b/src/components/badge/badge.js
@@ -17,6 +17,7 @@ const colorToClassNameMap = {
accent: 'euiBadge--accent',
warning: 'euiBadge--warning',
danger: 'euiBadge--danger',
+ hollow: 'euiBadge--hollow',
};
export const COLORS = Object.keys(colorToClassNameMap);
@@ -36,6 +37,7 @@ export const EuiBadge = ({
className,
onClick,
iconOnClick,
+ closeButtonProps,
...rest
}) => {
@@ -69,7 +71,7 @@ export const EuiBadge = ({
if (iconOnClick) {
optionalIcon = (
-
+
);
@@ -105,7 +107,7 @@ export const EuiBadge = ({
>
{optionalIcon}
-
+
{children}
@@ -114,6 +116,16 @@ export const EuiBadge = ({
}
};
+function checkValidColor(props, propName, componentName) {
+ const validHex = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(props.color);
+ if (props.color && !validHex && !COLORS.includes(props.color)) {
+ throw new Error(
+ `${componentName} needs to pass a valid color. This can either be a three ` +
+ `or six character hex value or one of the following: ${COLORS}`
+ );
+ }
+}
+
EuiBadge.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
@@ -140,7 +152,12 @@ EuiBadge.propTypes = {
/**
* Accepts either our palette colors (primary, secondary ..etc) or a hex value `#FFFFFF`, `#000`.
*/
- color: PropTypes.string,
+ color: checkValidColor,
+
+ /**
+ * Props passed to the close button.
+ */
+ closeButtonProps: PropTypes.object,
};
EuiBadge.defaultProps = {
diff --git a/src/components/button/__snapshots__/button.test.js.snap b/src/components/button/__snapshots__/button.test.js.snap
index b010733d217..ee3804c9c2b 100644
--- a/src/components/button/__snapshots__/button.test.js.snap
+++ b/src/components/button/__snapshots__/button.test.js.snap
@@ -136,7 +136,7 @@ exports[`EuiButton props iconSide left is rendered 1`] = `
>
+`;
+
+exports[`EuiHighlight behavior matching only applies to first match 1`] = `
+
+
+ match
+
+ match match
+
+`;
+
+exports[`EuiHighlight behavior strict matching doesn't match strings with different casing 1`] = `
+
+ different case match
+
+`;
+
+exports[`EuiHighlight is rendered 1`] = `
+
+ value
+
+`;
diff --git a/src/components/highlight/highlight.js b/src/components/highlight/highlight.js
new file mode 100644
index 00000000000..8311b9ef13a
--- /dev/null
+++ b/src/components/highlight/highlight.js
@@ -0,0 +1,50 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+
+const highlight = (searchSubject, searchValue, isStrict = false) => {
+ if (!searchValue) {
+ return searchSubject;
+ }
+
+ const normalizedSearchSubject = isStrict ? searchSubject : searchSubject.toLowerCase();
+ const normalizedSearchValue = isStrict ? searchValue : searchValue.toLowerCase();
+
+ const indexOfMatch = normalizedSearchSubject.indexOf(normalizedSearchValue);
+ if (indexOfMatch === -1) {
+ return searchSubject;
+ }
+
+ const preMatch = searchSubject.substr(0, indexOfMatch);
+ const match = searchSubject.substr(indexOfMatch, searchValue.length)
+ const postMatch = searchSubject.substr(indexOfMatch + searchValue.length);
+
+ return (
+
+ {preMatch}{match}{postMatch}
+
+ );
+}
+
+export const EuiHighlight = ({
+ children,
+ className,
+ search,
+ strict,
+ ...rest,
+}) => {
+ return (
+
+ {highlight(children, search, strict)}
+
+ );
+};
+
+EuiHighlight.propTypes = {
+ children: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ search: PropTypes.string.isRequired,
+ strict: PropTypes.bool,
+};
diff --git a/src/components/highlight/highlight.test.js b/src/components/highlight/highlight.test.js
new file mode 100644
index 00000000000..4fb7c33b7e0
--- /dev/null
+++ b/src/components/highlight/highlight.test.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test';
+
+import { EuiHighlight } from './highlight';
+
+describe('EuiHighlight', () => {
+ test('is rendered', () => {
+ const component = render(
+ value
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('behavior', () => {
+ describe('matching', () => {
+ test('only applies to first match', () => {
+ const component = render(
+ match match match
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+
+ describe('loose matching', () => {
+ test('matches strings with different casing', () => {
+ const component = render(
+ different case match
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+
+ describe('strict matching', () => {
+ test(`doesn't match strings with different casing`, () => {
+ const component = render(
+ different case match
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/src/components/highlight/index.js b/src/components/highlight/index.js
new file mode 100644
index 00000000000..4b994d1599d
--- /dev/null
+++ b/src/components/highlight/index.js
@@ -0,0 +1,3 @@
+export {
+ EuiHighlight,
+} from './highlight';
diff --git a/src/components/icon/__snapshots__/icon.test.js.snap b/src/components/icon/__snapshots__/icon.test.js.snap
index 96c72f2ab7b..89075fd81f9 100644
--- a/src/components/icon/__snapshots__/icon.test.js.snap
+++ b/src/components/icon/__snapshots__/icon.test.js.snap
@@ -3,7 +3,7 @@
exports[`EuiIcon is rendered 1`] = `
{
- const classes = classNames('euiIcon', className, sizeToClassNameMap[size], colorToClassMap[color]);
+ let optionalColorClass = null;
+ let optionalCustomStyles = null;
+
+ if (COLORS.indexOf(color) > -1) {
+ optionalColorClass = colorToClassMap[color];
+ } else {
+ optionalCustomStyles = { fill: color };
+ }
+
+ const classes = classNames(
+ 'euiIcon',
+ sizeToClassNameMap[size],
+ optionalColorClass,
+ className,
+ );
const Svg = typeToIconMap[type] || empty;
- return ;
+ return ;
};
+function checkValidColor(props, propName, componentName) {
+ const validHex = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(props.color);
+ if (props.color && !validHex && !COLORS.includes(props.color)) {
+ throw new Error(
+ `${componentName} needs to pass a valid color. This can either be a three ` +
+ `or six character hex value or one of the following: ${COLORS}`
+ );
+ }
+}
+
EuiIcon.propTypes = {
type: PropTypes.oneOf(TYPES),
- color: PropTypes.oneOf(COLORS),
+ color: checkValidColor,
size: PropTypes.oneOf(SIZES)
};
diff --git a/src/components/index.js b/src/components/index.js
index bfb1de5524d..0777c546f5c 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -47,6 +47,10 @@ export {
EuiColorPicker,
} from './color_picker';
+export {
+ EuiComboBox,
+} from './combo_box';
+
export {
EuiContextMenu,
EuiContextMenuPanel,
@@ -131,6 +135,10 @@ export {
EuiHealth,
} from './health';
+export {
+ EuiHighlight,
+} from './highlight';
+
export {
EuiHorizontalRule,
} from './horizontal_rule';
diff --git a/src/components/index.scss b/src/components/index.scss
index 305a8a731f0..75d2c6c3779 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -13,6 +13,7 @@
@import 'code/index';
@import 'code_editor/index';
@import 'color_picker/index';
+@import 'combo_box/index';
@import 'context_menu/index';
@import 'description_list/index';
@import 'error_boundary/index';
@@ -35,6 +36,7 @@
@import 'pagination/index';
@import 'panel/index';
@import 'popover/index';
+@import 'portal/index';
@import 'progress/index';
@import 'search_bar/index';
@import 'side_nav/index';
diff --git a/src/components/modal/__snapshots__/confirm_modal.test.js.snap b/src/components/modal/__snapshots__/confirm_modal.test.js.snap
index cf7035a7635..78549a05b2c 100644
--- a/src/components/modal/__snapshots__/confirm_modal.test.js.snap
+++ b/src/components/modal/__snapshots__/confirm_modal.test.js.snap
@@ -15,7 +15,7 @@ exports[`renders EuiConfirmModal 1`] = `
>
{
return { left, top, width, height };
};
+const positionToPositionerMap = {
+ top: positionAtTop,
+ right: positionAtRight,
+ bottom: positionAtBottom,
+ left: positionAtLeft,
+};
+
/**
* Determine the best position for a popover that avoids clipping by the window view port.
*
@@ -41,27 +48,28 @@ const positionAtLeft = (anchorBounds, width, height, buffer) => {
* @param {Object} popoverBounds - getBoundingClientRect() of the popover node (e.g. the tooltip).
* @param {string} requestedPosition - Position the user wants. One of ["top", "right", "bottom", "left"]
* @param {number} buffer - The space between the wrapper and the popover. Also the minimum space between the popover and the window.
+ * @param {Array} positions - List of acceptable positions. Defaults to ["top", "right", "bottom", "left"].
*
* @returns {Object} With properties position (one of ["top", "right", "bottom", "left"]), left, top, width, and height.
*/
-export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedPosition, buffer = 16) {
+export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedPosition, buffer = 16, positions = ['top', 'right', 'bottom', 'left']) {
+ if (typeof buffer !== 'number') {
+ throw new Error(`calculatePopoverPosition received a buffer argument of ${buffer}' but expected a number`);
+ }
+
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const { width: popoverWidth, height: popoverHeight } = popoverBounds;
- const positionToBoundsMap = {
- top: positionAtTop(anchorBounds, popoverWidth, popoverHeight, buffer),
- right: positionAtRight(anchorBounds, popoverWidth, popoverHeight, buffer),
- bottom: positionAtBottom(anchorBounds, popoverWidth, popoverHeight, buffer),
- left: positionAtLeft(anchorBounds, popoverWidth, popoverHeight, buffer),
- };
+ const positionToBoundsMap = {};
+ const positionToVisibleAreaMap = {};
- const positions = Object.keys(positionToBoundsMap);
+ positions.forEach(position => {
+ const bounds = positionToPositionerMap[position](anchorBounds, popoverWidth, popoverHeight, buffer);
+ positionToBoundsMap[position] = bounds;
- // Calculate how much area of the popover is visible at each position.
- const positionToVisibleAreaMap = {};
- positions.forEach((position) => {
- positionToVisibleAreaMap[position] = getVisibleArea(positionToBoundsMap[position], windowWidth, windowHeight);
+ // Calculate how much area of the popover is visible at each position.
+ positionToVisibleAreaMap[position] = getVisibleArea(bounds, windowWidth, windowHeight);
});
// If the requested position clips the popover, find the position which clips the popover the least.
diff --git a/yarn.lock b/yarn.lock
index 22f86566a9e..af087564f13 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7163,6 +7163,12 @@ react-dom@^16.2.0:
object-assign "^4.1.1"
prop-types "^15.6.0"
+react-input-autosize@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"
+ dependencies:
+ prop-types "^15.5.8"
+
react-reconciler@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d"