diff --git a/src-docs/src/services/routes/routes.js b/src-docs/src/services/routes/routes.js
index 2ecbdaac45c..eb68ba38b8f 100644
--- a/src-docs/src/services/routes/routes.js
+++ b/src-docs/src/services/routes/routes.js
@@ -118,6 +118,10 @@ import { TitleExample }
import { ToastExample }
from '../../views/toast/toast_example';
+import { TooltipExample }
+ from '../../views/tooltip/tooltip_example';
+
+
// Sandboxes
import AdvancedSettingsSandbox
@@ -193,6 +197,7 @@ const components = [
TextExample,
TitleExample,
ToastExample,
+ TooltipExample,
].map(example => createExample(example));
const sandboxes = [{
diff --git a/src-docs/src/views/tooltip/examples.js b/src-docs/src/views/tooltip/examples.js
new file mode 100644
index 00000000000..da312e2ff28
--- /dev/null
+++ b/src-docs/src/views/tooltip/examples.js
@@ -0,0 +1,59 @@
+import React from 'react';
+
+import {
+ TooltipTrigger,
+} from '../../../../src/components';
+
+const autoPlacementTooltip = `I should be on the top but may get placed in another location
+if I overflow the browser window. This will come in handy when tooltips get placed near the top
+of pages. Its very hard to read a tooltip when part of it gets cut off and if you can't read it
+then what is the point?`;
+
+export default () => (
+
+
+ Check out this {(
+
+ tooltip with title.
+
+ )}
+
+
+ Check out this {(
+
+ tooltip on the top.
+
+ )}
+
+
+ Check out this {(
+
+ tooltip on click.
+
+ )}
+
+
+ Check out this {(
+
+ tooltip on the left.
+
+ )}
+
+
+
+ Check out this {(
+
+ tooltip on the right.
+
+ )}
+
+
+
+ Check out this {(
+
+ tooltip on the bottom.
+
+ )}
+
+
+);
diff --git a/src-docs/src/views/tooltip/tooltip_example.js b/src-docs/src/views/tooltip/tooltip_example.js
new file mode 100644
index 00000000000..4c81fa51ff8
--- /dev/null
+++ b/src-docs/src/views/tooltip/tooltip_example.js
@@ -0,0 +1,49 @@
+import React from 'react';
+
+import { renderToHtml } from '../../services';
+
+import {
+ GuideSectionTypes,
+} from '../../components';
+
+import {
+ EuiCallOut,
+ EuiSpacer,
+} from '../../../../src/components';
+
+import TooltipExamples from './examples';
+const examplesSource = require('!!raw-loader!./examples');
+const examplesHtml = renderToHtml(TooltipExamples);
+
+export const TooltipExample = {
+ title: 'Tooltip',
+ intro: (
+
+
+
+ This component is still undergoing active development, and its interface and implementation
+ are both subject to change.
+
+
+
+
+
+ ),
+ sections: [{
+ title: 'Tooltip',
+ source: [{
+ type: GuideSectionTypes.JS,
+ code: examplesSource,
+ }, {
+ type: GuideSectionTypes.HTML,
+ code: examplesHtml,
+ }],
+ text: (
+
+ ),
+ demo: ,
+ }],
+};
diff --git a/src/components/index.js b/src/components/index.js
index 24fd4181be1..08edc51e236 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -197,6 +197,11 @@ export {
EuiToast,
} from './toast';
+export {
+ Tooltip,
+ TooltipTrigger
+} from './tooltip';
+
export {
EuiTitle,
} from './title';
diff --git a/src/components/index.scss b/src/components/index.scss
index 55f3d87c8b2..ef2b75b6ae0 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -33,4 +33,5 @@
@import 'tabs/index';
@import 'title/index';
@import 'toast/index';
+@import 'tooltip/index';
@import 'text/index';
diff --git a/src/components/tooltip/_index.scss b/src/components/tooltip/_index.scss
new file mode 100644
index 00000000000..467d91f2207
--- /dev/null
+++ b/src/components/tooltip/_index.scss
@@ -0,0 +1 @@
+@import "tooltip";
diff --git a/src/components/tooltip/_tooltip.scss b/src/components/tooltip/_tooltip.scss
new file mode 100644
index 00000000000..e399c6643a6
--- /dev/null
+++ b/src/components/tooltip/_tooltip.scss
@@ -0,0 +1,311 @@
+@mixin transition-pui($type:all, $speed:300ms, $ease: ease-out){
+ -webkit-transition: $type $speed $ease;
+ -moz-transition: $type $speed $ease;
+ transition: $type $speed $ease;
+ @if($type == all) {
+ -webkit-transition-property: background-color, color, opacity;
+ -moz-transition-property: background-color, color, opacity;
+ transition-property: background-color, color, opacity;
+ }
+}
+
+$font-size-extra-small: 12px;
+$base-unit: 8px;
+
+$blue-3: #1B78B3;
+
+$navy-2: #243641;
+$navy-9: #DFE5E8;
+
+$dark-2: $navy-2;
+$dark-9: $navy-9;
+
+$accent-3: $blue-3;
+
+$link-color: $accent-3;
+
+$box-shadow-key-1: 0px 2px 2px 0px rgba(36,54,65,0.1);
+
+$box-shadow-amb-1: 0px 0px 2px 0px rgba(36,54,65,0.1);
+
+$tooltip-border-radius: 2px;
+$tooltip-font-size: $font-size-extra-small;
+$tooltip-padding: ($base-unit / 2) $base-unit;
+$tooltip-margin: $base-unit;
+$tooltip-container-z: 10;
+$tooltip-arrow-pull: -4px;
+$tooltip-arrow-z-bottom: -1;
+$tooltip-arrow-z-top: 1;
+$tooltip-opacity-transition: ease-out .2s;
+
+
+$tooltip-bg: $dark-2;
+$tooltip-color: #fff;
+$tooltip-border-color: $dark-2;
+
+$tooltip-light-bg: #fff;
+$tooltip-light-color: $dark-2;
+$tooltip-light-border-color: $dark-9;
+
+$tooltip-sm-width: $base-unit * 15;
+$tooltip-md-width: $base-unit * 30;
+$tooltip-lg-width: $base-unit * 45;
+
+//Hover Effect for HTML & CSS ONLY
+.tooltip:hover .tooltip-container:not(.tooltip-container-hidden) {
+ visibility: visible;
+ opacity: 1;
+}
+
+.tooltip-container {
+ //Hover Effect for HTML & CSS ONLY
+ visibility: hidden;
+ transition: opacity $tooltip-opacity-transition;
+ z-index: $tooltip-container-z;
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin: 0 0 $tooltip-margin 0; //space for the triangle indicator
+ text-align: left;
+
+ &.tooltip-container-visible {
+ visibility: visible;
+ }
+
+ &.tooltip-hoverable {
+ &:after {
+ content:"";
+ position: absolute;
+ width:calc(100% + 16px);
+ height:calc(100% + 16px);
+ top:50%;
+ left:50%;
+ transform: translateX(-50%) translateY(-50%);
+ }
+ }
+
+ .tooltip-content {
+ white-space: nowrap;
+ padding: $tooltip-padding;
+ font-size: $tooltip-font-size;
+ line-height: 16px;
+ font-weight: 400;
+ letter-spacing: 0;
+ text-transform: none;
+ background-color: $tooltip-bg;
+ color: $tooltip-color;
+ border-radius: $tooltip-border-radius;
+ border: 1px solid $tooltip-border-color;
+ box-shadow: $box-shadow-key-1, $box-shadow-amb-1;
+
+ &:before {
+ content: "";
+ z-index: $tooltip-arrow-z-top;
+ position: absolute;
+ bottom: $tooltip-arrow-pull;
+ left: 50%;
+ transform: translateX(-50%) rotateZ(45deg);
+ background-color: $tooltip-bg;
+ border-bottom: 1px solid $tooltip-border-color;
+ border-right: 1px solid $tooltip-border-color;
+ width: $base-unit;
+ height: $base-unit;
+ }
+
+ &:after {
+ content: "";
+ box-sizing: content-box;
+ z-index: $tooltip-arrow-z-bottom;
+ position: absolute;
+ bottom: $tooltip-arrow-pull;
+ left: 50%;
+ transform: translateX(-50%) rotateZ(45deg);
+ background-color: $tooltip-bg;
+ box-shadow: $box-shadow-key-1, $box-shadow-amb-1;
+ width: $base-unit;
+ height: $base-unit;
+ }
+ }
+}
+
+.tooltip {
+ position: relative;
+ display: inline-block;
+
+ a {
+ svg {
+ fill: currentColor;
+ }
+ }
+
+ &.tooltip-light {
+ .tooltip-content {
+ background-color: $tooltip-light-bg;
+ color: $tooltip-light-color;
+ border: 1px solid $tooltip-light-border-color;
+
+ &:before {
+ background-color: $tooltip-light-bg;
+ border-bottom: 1px solid $tooltip-light-border-color;
+ border-right: 1px solid $tooltip-light-border-color;
+ }
+
+ &:after {
+ background-color: $tooltip-light-bg;
+ }
+ }
+ }
+
+ &.tooltip-bottom {
+ .tooltip-container {
+ top: 100%;
+ bottom: auto;
+ left: 50%;
+ transform: translateX(-50%);
+ margin: $tooltip-margin 0 0 0;
+
+ .tooltip-content {
+ &:before {
+ bottom: auto;
+ top: $tooltip-arrow-pull;
+ border-top: 1px solid $tooltip-border-color;
+ border-right: none;
+ border-bottom: none;
+ border-left: 1px solid $tooltip-border-color;
+ }
+
+ &:after {
+ bottom: auto;
+ top: $tooltip-arrow-pull;
+ }
+ }
+ }
+
+ &.tooltip-light {
+ .tooltip-content:before {
+ border-top: 1px solid $tooltip-light-border-color;
+ border-left: 1px solid $tooltip-light-border-color;
+ }
+ }
+ }
+
+ &.tooltip-right {
+ .tooltip-container {
+ top: 50%;
+ bottom: auto;
+ left: 100%;
+ transform: translatey(-50%);
+ margin: 0 0 0 $tooltip-margin;
+
+ .tooltip-content {
+ &:before {
+ bottom: auto;
+ left: $tooltip-arrow-pull;
+ top: 50%;
+ transform: translatey(-50%) rotateZ(45deg);
+ border-top: none;
+ border-right: none;
+ border-bottom: 1px solid $tooltip-border-color;
+ border-left: 1px solid $tooltip-border-color;
+ }
+
+ &:after {
+ bottom: auto;
+ left: $tooltip-arrow-pull;
+ top: 50%;
+ transform: translatey(-50%) rotateZ(45deg);
+ }
+ }
+ }
+
+ &.tooltip-light {
+ .tooltip-content:before {
+ border-bottom: 1px solid $tooltip-light-border-color;
+ border-left: 1px solid $tooltip-light-border-color;
+ }
+ }
+ }
+ &.tooltip-left {
+ .tooltip-container {
+ top: 50%;
+ bottom: auto;
+ right: 100%;
+ left: auto;
+ transform: translatey(-50%);
+ margin: 0 $tooltip-margin 0 0;
+
+ .tooltip-content {
+ &:before {
+ bottom: auto;
+ right: $tooltip-arrow-pull;
+ left: auto;
+ top: 50%;
+ transform: translatey(-50%) rotateZ(45deg);
+ border-top: 1px solid $tooltip-border-color;
+ border-right: 1px solid $tooltip-border-color;
+ border-bottom: none;
+ border-left: none;
+ }
+
+ &:after {
+ bottom: auto;
+ right: $tooltip-arrow-pull;
+ left: auto;
+ top: 50%;
+ transform: translatey(-50%) rotateZ(45deg);
+ }
+ }
+ }
+
+ &.tooltip-light {
+ .tooltip-content:before {
+ border-top: 1px solid $tooltip-light-border-color;
+ border-right: 1px solid $tooltip-light-border-color;
+ }
+ }
+ }
+}
+
+//Tooltip Sizes
+.tooltip-s.tooltip-container {
+ width: $tooltip-sm-width;
+ .tooltip-content {
+ white-space: normal;
+ }
+}
+
+.tooltip-m.tooltip-container {
+ width: $tooltip-md-width;
+ .tooltip-content {
+ white-space: normal;
+ }
+}
+
+.tooltip-l.tooltip-container {
+ width: $tooltip-lg-width;
+ .tooltip-content {
+ white-space: normal;
+ }
+}
+
+//Overlay
+.tether-element {
+ z-index: 99;
+}
+
+.overlay-trigger {
+ color: $link-color;
+ @include transition-pui();
+
+ &:hover, &:focus {
+ color: lighten($link-color, 6%);
+ cursor: pointer;
+ outline:none;
+ text-decoration: none;
+ }
+
+ &:active, &.active{
+ color: darken($link-color, 6%);
+ }
+}
diff --git a/src/components/tooltip/index.js b/src/components/tooltip/index.js
new file mode 100644
index 00000000000..215305339ab
--- /dev/null
+++ b/src/components/tooltip/index.js
@@ -0,0 +1,2 @@
+export { Tooltip } from './tooltip';
+export { TooltipTrigger } from './tooltip_trigger';
diff --git a/src/components/tooltip/tooltip.js b/src/components/tooltip/tooltip.js
new file mode 100644
index 00000000000..a66fd7a9603
--- /dev/null
+++ b/src/components/tooltip/tooltip.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import { SIZE } from './tooltip_constants';
+
+export class Tooltip extends React.PureComponent {
+ static propTypes = {
+ isVisible: PropTypes.bool,
+ size: PropTypes.oneOf([SIZE.AUTO, SIZE.SMALL, SIZE.MEDIUM, SIZE.LARGE]),
+ isSticky: PropTypes.bool,
+ title: PropTypes.string
+ };
+
+ static defaultProps = {
+ isVisible: true,
+ size: SIZE.AUTO,
+ isSticky: false
+ };
+
+ render() {
+ const {
+ isSticky,
+ isVisible,
+ size,
+ title,
+ className,
+ children,
+ ...others
+ } = this.props;
+
+ const newClasses = classnames('tooltip-container', {
+ 'tooltip-container-visible': isVisible,
+ 'tooltip-container-hidden': !isVisible,
+ 'tooltip-hoverable': isSticky,
+ [`tooltip-${size}`]: size !== 'auto'
+ }, className);
+
+ let tooltipTitle;
+ if (title) {
+ tooltipTitle = (
+ {title}
+ );
+ }
+
+ return (
+
+
+ {tooltipTitle}
+ {children}
+
+
+ );
+ }
+}
diff --git a/src/components/tooltip/tooltip_constants.js b/src/components/tooltip/tooltip_constants.js
new file mode 100644
index 00000000000..6b9e7b5861f
--- /dev/null
+++ b/src/components/tooltip/tooltip_constants.js
@@ -0,0 +1,6 @@
+export const SIZE = {
+ SMALL: 's',
+ MEDIUM: 'm',
+ LARGE: 'l',
+ AUTO: 'auto',
+};
diff --git a/src/components/tooltip/tooltip_trigger.js b/src/components/tooltip/tooltip_trigger.js
new file mode 100644
index 00000000000..5a8275c4557
--- /dev/null
+++ b/src/components/tooltip/tooltip_trigger.js
@@ -0,0 +1,148 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import { Tooltip } from './tooltip';
+import { SIZE } from './tooltip_constants';
+import { noOverflowPlacement } from '../../services';
+
+export class TooltipTrigger extends React.Component {
+ static propTypes = {
+ display: PropTypes.bool,
+ title: PropTypes.string,
+ tooltip: PropTypes.oneOfType([PropTypes.node, PropTypes.object]).isRequired,
+ placement: PropTypes.oneOf(['left', 'right', 'bottom', 'top']),
+ trigger: PropTypes.oneOf(['manual', 'hover', 'click']),
+ clickHideDelay: PropTypes.number,
+ onClick: PropTypes.func,
+ onEntered: PropTypes.func,
+ onExited: PropTypes.func,
+ theme: PropTypes.oneOf(['dark', 'light']),
+ size: PropTypes.oneOf([SIZE.AUTO, SIZE.SMALL, SIZE.MEDIUM, SIZE.LARGE]),
+ isSticky: PropTypes.bool
+ };
+
+ static defaultProps = {
+ display: false,
+ placement: 'top',
+ trigger: 'hover',
+ clickHideDelay: 1000,
+ onClick: () => {},
+ onEntered: () => {},
+ onExited: () => {},
+ theme: 'dark',
+ size: SIZE.AUTO,
+ isSticky: false
+ };
+
+ constructor(props) {
+ super(props);
+ const openOnLoad = props.trigger === 'manual' ? props.display : false;
+ this.state = {
+ isVisible: openOnLoad,
+ noOverflowPlacement: props.placement
+ };
+ this.clickHandler = this.clickHandler.bind(this);
+ }
+
+ getPlacement() {
+ const domNode = ReactDOM.findDOMNode(this);
+ const tooltipContainer = domNode.getElementsByClassName('tooltip-container')[0];
+ const userPlacement = this.props.placement;
+ const WINDOW_BUFFER = 8;
+ return noOverflowPlacement(domNode, tooltipContainer, userPlacement, WINDOW_BUFFER);
+ }
+
+ hoverHandler(e) {
+ this.setState({
+ isVisible: e.type === 'mouseenter',
+ noOverflowPlacement: this.getPlacement()
+ });
+ }
+
+ clickHandler(e, onClick) {
+ this.setState({
+ isVisible: true,
+ noOverflowPlacement: this.getPlacement()
+ });
+ onClick(e);
+ setTimeout(() => {
+ this.setState({ isVisible: false });
+ }, this.props.clickHideDelay);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const triggerChanged = this.props.trigger !== nextProps.trigger;
+ const displayChanged = this.props.display !== nextProps.display;
+
+ if (triggerChanged && nextProps.trigger === 'manual') {
+ this.setState({ isVisible: nextProps.display });
+ } else if (triggerChanged) {
+ this.setState({ isVisible: false });
+ } else if (displayChanged) {
+ this.setState({ isVisible: nextProps.display });
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if(prevState.isVisible && !this.state.isVisible) {
+ this.props.onExited();
+ } else if(!prevState.isVisible && this.state.isVisible) {
+ this.props.onEntered();
+ }
+ }
+
+ getTriggerHandler(trigger, onClick) {
+ switch(trigger) {
+ case 'click':
+ return { onClick: e => this.clickHandler(e, onClick) };
+ case 'manual':
+ return {};
+ default:
+ return {
+ onClick,
+ onMouseEnter: this.hoverHandler.bind(this),
+ onMouseLeave: this.hoverHandler.bind(this)
+ };
+ }
+ }
+
+ render() {
+ const {
+ isSticky,
+ title,
+ tooltip,
+ trigger,
+ className,
+ clickHideDelay, // eslint-disable-line no-unused-vars
+ onEntered, // eslint-disable-line no-unused-vars
+ onExited, // eslint-disable-line no-unused-vars
+ theme,
+ size,
+ onClick,
+ display, // eslint-disable-line no-unused-vars
+ ...rest
+ } = this.props;
+ const { isVisible } = this.state;
+
+ const triggerHandler = this.getTriggerHandler(trigger, onClick);
+
+ const newClasses = classnames('tooltip', className, {
+ 'tooltip-light': theme === 'light',
+ [`tooltip-${this.state.noOverflowPlacement}`]: this.state.noOverflowPlacement !== 'top'
+ });
+ const newProps = {
+ className: newClasses,
+ ...triggerHandler,
+ ...rest
+ };
+ const tooltipProps = { isSticky, size, isVisible, title };
+
+ return (
+
+ {this.props.children}
+ {tooltip}
+
+ );
+ }
+}
diff --git a/src/services/index.js b/src/services/index.js
index 6d339006ccf..a77970a9a65 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -21,3 +21,7 @@ export {
export {
SortableProperties,
} from './sort';
+
+export {
+ noOverflowPlacement,
+} from './overflow';
diff --git a/src/services/overflow/index.js b/src/services/overflow/index.js
new file mode 100644
index 00000000000..9fabb253d76
--- /dev/null
+++ b/src/services/overflow/index.js
@@ -0,0 +1 @@
+export { noOverflowPlacement } from './no_overflow_placement';
diff --git a/src/services/overflow/no_overflow_placement.js b/src/services/overflow/no_overflow_placement.js
new file mode 100644
index 00000000000..5e17d68da4b
--- /dev/null
+++ b/src/services/overflow/no_overflow_placement.js
@@ -0,0 +1,62 @@
+
+/**
+ * Determine the best placement for a popup that avoids clipping by the window view port.
+ *
+ * @param {native DOM Element} staticNode - DOM node that popup placement is referenced too.
+ * @param {native DOM Element} popupNode - DOM node containing popup content.
+ * @param {string} requestedPlacement - Preferred placement. One of ["top", "right", "bottom", "left"]
+ * @param {number} buffer - avoid popup from getting too close to window edge
+ *
+ * @returns {string} One of ["top", "right", "bottom", "left"] that ensures no window overflow.
+ */
+export function noOverflowPlacement(staticNode, popupNode, requestedPlacement, buffer = 0) {
+ const staticNodeRect = staticNode.getBoundingClientRect();
+ const popupNodeRect = popupNode.getBoundingClientRect();
+
+ // determine popup overflow in each direction
+ // negative values signal window overflow, large values signal lots of free space
+ const popupOverflow = {
+ top: staticNodeRect.top - (popupNodeRect.height + buffer),
+ right: window.innerWidth - (staticNodeRect.right + popupNodeRect.width + buffer),
+ bottom: window.innerHeight - (staticNodeRect.bottom + popupNodeRect.height + buffer),
+ left: staticNodeRect.left - (staticNodeRect.width + buffer)
+ };
+
+ function hasCrossDimensionOverflow(key) {
+ if (key === 'left' || key === 'right') {
+ const domNodeCenterY = staticNodeRect.top + (staticNodeRect.height / 2);
+ const tooltipTop = domNodeCenterY - ((popupNodeRect.height / 2) + buffer);
+ if (tooltipTop <= 0) {
+ return true;
+ }
+ const tooltipBottom = domNodeCenterY + (popupNodeRect.height / 2) + buffer;
+ if (tooltipBottom >= window.innerHeight) {
+ return true;
+ }
+ } else {
+ const domNodeCenterX = staticNodeRect.left + (staticNodeRect.width / 2);
+ const tooltipLeft = domNodeCenterX - ((popupNodeRect.width / 2) + buffer);
+ if (tooltipLeft <= 0) {
+ return true;
+ }
+ const tooltipRight = domNodeCenterX + (popupNodeRect.width / 2) + buffer;
+ if (tooltipRight >= window.innerWidth) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ let noOverflowPlacement = requestedPlacement;
+ if (popupOverflow[requestedPlacement] <= 0 || hasCrossDimensionOverflow(requestedPlacement)) {
+ // requested placement overflows window bounds
+ // select direction what has the most free space
+ Object.keys(popupOverflow).forEach((key) => {
+ if (popupOverflow[key] > popupOverflow[noOverflowPlacement] && !hasCrossDimensionOverflow(key)) {
+ noOverflowPlacement = key;
+ }
+ });
+ }
+
+ return noOverflowPlacement;
+}