diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba1b869048d..49986e0f8cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
- Added `initialPageSize` option to `EuiInMemoryTable` ([#477](https://github.com/elastic/eui/pull/477))
- Added design guidelines for button and toast usage ([#371](https://github.com/elastic/eui/pull/371))
+**Breaking changes**
+
+- Complete refactor of `EuiToolTip`. They now work. Only a breaking change if you were using them. ([#484](https://github.com/elastic/eui/pull/484))
+
# [`0.0.24`](https://github.com/elastic/eui/tree/v0.0.24)
- Removed hover and focus states from non-selectable `EuiSideNavItem`s ([#434](https://github.com/elastic/eui/pull/434))
diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js
index 62837d86aa4..74d2cce28dd 100644
--- a/src-docs/src/routes.js
+++ b/src-docs/src/routes.js
@@ -143,6 +143,9 @@ import { PanelExample }
import { PopoverExample }
from './views/popover/popover_example';
+import { PortalExample }
+ from './views/portal/portal_example';
+
import { ProgressExample }
from './views/progress/progress_example';
@@ -176,8 +179,8 @@ import { TitleExample }
import { ToastExample }
from './views/toast/toast_example';
-import { TooltipExample }
- from './views/tooltip/tooltip_example';
+import { ToolTipExample }
+ from './views/tool_tip/tool_tip_example';
/**
* Lowercases input and replaces spaces with hyphens:
@@ -283,7 +286,7 @@ const navigation = [{
TextExample,
TitleExample,
ToastExample,
- TooltipExample,
+ ToolTipExample,
].map(example => createExample(example)),
}, {
name: 'Forms',
@@ -304,6 +307,7 @@ const navigation = [{
ErrorBoundaryExample,
IsColorDarkExample,
OutsideClickDetectorExample,
+ PortalExample,
].map(example => createExample(example)),
}].map(({ name, items, ...rest }) => ({
name,
diff --git a/src-docs/src/views/portal/portal.js b/src-docs/src/views/portal/portal.js
new file mode 100644
index 00000000000..0889c98d6b2
--- /dev/null
+++ b/src-docs/src/views/portal/portal.js
@@ -0,0 +1,49 @@
+import React, {
+ Component,
+} from 'react';
+
+import {
+ EuiPortal,
+ EuiButton,
+ EuiBottomBar,
+} from '../../../../src/components';
+
+export class Portal extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isPortalVisible: false,
+ };
+
+ this.togglePortal = this.togglePortal.bind(this);
+ }
+
+ togglePortal() {
+ this.setState(prevState => ({ isPortalVisible: !prevState.isPortalVisible }));
+ }
+
+ render() {
+
+ let portal;
+
+ if (this.state.isPortalVisible) {
+ portal = (
+
+
+ This element is appended to the body in the DOM if you inspect
+
+
+ );
+ }
+ return (
+
+
+ Toggle portal
+
+
+ {portal}
+
+ );
+ }
+}
diff --git a/src-docs/src/views/portal/portal_example.js b/src-docs/src/views/portal/portal_example.js
new file mode 100644
index 00000000000..d56ce50d40e
--- /dev/null
+++ b/src-docs/src/views/portal/portal_example.js
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import { renderToHtml } from '../../services';
+
+import {
+ GuideSectionTypes,
+} from '../../components';
+
+import {
+ EuiCode,
+ EuiPortal,
+} from '../../../../src/components';
+
+import { Portal } from './portal';
+const portalSource = require('!!raw-loader!./portal');
+const portalHtml = renderToHtml(Portal);
+
+export const PortalExample = {
+ title: 'Portal',
+ sections: [{
+ title: 'Portal',
+ source: [{
+ type: GuideSectionTypes.JS,
+ code: portalSource,
+ }, {
+ type: GuideSectionTypes.HTML,
+ code: portalHtml,
+ }],
+ text: (
+
+ EuiPortal allows you to append its contained children
+ onto the document body. It is useful for moving fixed elements like modals,
+ tooltips or toasts when you are worried about a z-index or overflow conflict.
+
+ ),
+ components: { EuiPortal },
+ demo: ,
+ }],
+};
diff --git a/src-docs/src/views/tool_tip/tool_tip.js b/src-docs/src/views/tool_tip/tool_tip.js
new file mode 100644
index 00000000000..ea7d5ca0fc7
--- /dev/null
+++ b/src-docs/src/views/tool_tip/tool_tip.js
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import {
+ EuiIcon,
+ EuiToolTip,
+ EuiLink,
+ EuiText,
+ EuiFieldText,
+ EuiSpacer,
+ EuiButton,
+} from '../../../../src/components';
+
+export default () => (
+
+
+
+ This tooltip appears on the{' '}
+
+ top
+
+
+
+
+ This tooltip appears on the{' '}
+
+ left
+
+ {' '} and includes the optional title.
+
+
+
+ This tooltip appears on the{' '}
+
+ right
+
+
+
+
+ This tooltip appears on the bottom of this icon:{' '}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ alert('Buttons are still clickable within tooltips.')}>Hover over me
+
+
+);
diff --git a/src-docs/src/views/tool_tip/tool_tip_example.js b/src-docs/src/views/tool_tip/tool_tip_example.js
new file mode 100644
index 00000000000..ba6aa79ffd6
--- /dev/null
+++ b/src-docs/src/views/tool_tip/tool_tip_example.js
@@ -0,0 +1,41 @@
+import React from 'react';
+
+import { renderToHtml } from '../../services';
+
+import {
+ GuideSectionTypes,
+} from '../../components';
+
+import {
+ EuiCode,
+ EuiToolTip,
+} from '../../../../src/components';
+
+import ToolTip from './tool_tip';
+const toolTipSource = require('!!raw-loader!./tool_tip');
+const toolTipHtml = renderToHtml(ToolTip);
+
+export const ToolTipExample = {
+ title: 'ToolTip',
+ sections: [{
+ title: 'ToolTip',
+ source: [{
+ type: GuideSectionTypes.JS,
+ code: toolTipSource,
+ }, {
+ type: GuideSectionTypes.HTML,
+ code: toolTipHtml,
+ }],
+ text: (
+
+ Wrap EuiToolTip around any item that you need a tooltip for.
+ The position prop will take a suggested position, but will
+ change it if the tool tip gets too close to the edge of the screen. You can use
+ the clickOnly prop to tell the too tip to only appear on click
+ wrather than on hover.
+
+ ),
+ props: { EuiToolTip },
+ demo: ,
+ }],
+};
diff --git a/src-docs/src/views/tooltip/examples.js b/src-docs/src/views/tooltip/examples.js
deleted file mode 100644
index 21efbbe69ca..00000000000
--- a/src-docs/src/views/tooltip/examples.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-
-import {
- EuiLink,
- 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 {(
-
- medium tooltip
-
- )} with title .
-
-
-
-
- Check out this {(
-
- tooltip on the top .
-
- )}
-
-
-
-
- Check out this {(
-
- tooltip on click .
-
- )}
-
-
-
-
- Check out this {(
-
- large 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
deleted file mode 100644
index 11158eec1dd..00000000000
--- a/src-docs/src/views/tooltip/tooltip_example.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react';
-
-import { renderToHtml } from '../../services';
-
-import {
- GuideSectionTypes,
-} from '../../components';
-
-import {
- EuiCallOut,
- EuiSpacer,
- TooltipTrigger,
-} 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: (
-
- ),
- props: { TooltipTrigger },
- demo: ,
- }],
-};
diff --git a/src/components/index.js b/src/components/index.js
index b99f97ab8a3..6d33e33c80c 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -204,6 +204,10 @@ export {
EuiPopoverTitle,
} from './popover';
+export {
+ EuiPortal,
+} from './portal';
+
export {
EuiProgress,
} from './progress';
@@ -260,9 +264,8 @@ export {
} from './toast';
export {
- Tooltip,
- TooltipTrigger
-} from './tooltip';
+ EuiToolTip,
+} from './tool_tip';
export {
EuiTitle,
diff --git a/src/components/index.scss b/src/components/index.scss
index d03ad7b27ad..305a8a731f0 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -44,5 +44,5 @@
@import 'tabs/index';
@import 'title/index';
@import 'toast/index';
-@import 'tooltip/index';
+@import 'tool_tip/index';
@import 'text/index';
diff --git a/src/components/portal/__snapshots__/portal.test.js.snap b/src/components/portal/__snapshots__/portal.test.js.snap
new file mode 100644
index 00000000000..092d405cb69
--- /dev/null
+++ b/src/components/portal/__snapshots__/portal.test.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiPortal is rendered 1`] = `
+
+ Content
+
+`;
diff --git a/src/components/portal/index.js b/src/components/portal/index.js
new file mode 100644
index 00000000000..f55db5dad03
--- /dev/null
+++ b/src/components/portal/index.js
@@ -0,0 +1,3 @@
+export {
+ EuiPortal,
+} from './portal';
diff --git a/src/components/portal/portal.js b/src/components/portal/portal.js
new file mode 100644
index 00000000000..3e44ece314d
--- /dev/null
+++ b/src/components/portal/portal.js
@@ -0,0 +1,40 @@
+/**
+ * NOTE: We can't test this component because Enzyme doesn't support rendering
+ * into portals.
+ */
+
+import { Component } from 'react';
+import PropTypes from 'prop-types';
+import { createPortal } from 'react-dom';
+
+export class EuiPortal extends Component {
+ constructor(props) {
+ super(props);
+
+ const {
+ children, // eslint-disable-line no-unused-vars
+ } = this.props;
+
+ this.portalNode = document.createElement('div');
+ }
+
+ componentDidMount() {
+ document.body.appendChild(this.portalNode);
+ }
+
+ componentWillUnmount() {
+ document.body.removeChild(this.portalNode);
+ this.portalNode = null;
+ }
+
+ render() {
+ return createPortal(
+ this.props.children,
+ this.portalNode
+ );
+ }
+}
+
+EuiPortal.propTypes = {
+ children: PropTypes.node,
+};
diff --git a/src/components/portal/portal.test.js b/src/components/portal/portal.test.js
new file mode 100644
index 00000000000..d89df757e0a
--- /dev/null
+++ b/src/components/portal/portal.test.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { render } from 'enzyme';
+import ReactDOM from 'react-dom';
+import { requiredProps } from '../../test';
+import { EuiPortal } from './portal';
+
+// TODO: Temporary hack which we can remove once react-test-renderer supports portals.
+// More info at https://github.com/facebook/react/issues/11565.
+ReactDOM.createPortal = node => node;
+
+describe('EuiPortal', () => {
+ test('is rendered', () => {
+ const component = render(
+
+
+ Content
+
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/src/components/tool_tip/__snapshots__/tool_tip.test.js.snap b/src/components/tool_tip/__snapshots__/tool_tip.test.js.snap
new file mode 100644
index 00000000000..389c48a626a
--- /dev/null
+++ b/src/components/tool_tip/__snapshots__/tool_tip.test.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiToolTip is rendered 1`] = `
+
+
+ Trigger
+
+
+`;
diff --git a/src/components/tool_tip/__snapshots__/tool_tip_popover.test.js.snap b/src/components/tool_tip/__snapshots__/tool_tip_popover.test.js.snap
new file mode 100644
index 00000000000..5016391cd67
--- /dev/null
+++ b/src/components/tool_tip/__snapshots__/tool_tip_popover.test.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiToolTipPopover is rendered 1`] = `
+
+`;
diff --git a/src/components/tool_tip/_index.scss b/src/components/tool_tip/_index.scss
new file mode 100644
index 00000000000..3e998fcbf09
--- /dev/null
+++ b/src/components/tool_tip/_index.scss
@@ -0,0 +1 @@
+@import 'tool_tip';
diff --git a/src/components/tool_tip/_tool_tip.scss b/src/components/tool_tip/_tool_tip.scss
new file mode 100644
index 00000000000..99037652415
--- /dev/null
+++ b/src/components/tool_tip/_tool_tip.scss
@@ -0,0 +1,117 @@
+/**
+ * 1. Relative / absolute positioning so they still work during scrolling.
+ */
+.euiBody-hasToolTip {
+ position: relative; /* 1 */
+}
+
+.euiToolTip {
+ @include euiBottomShadow;
+ @include euiFontSizeS();
+
+ position: absolute; /* 1 */
+ border-radius: $euiBorderRadius;
+ padding: $euiSizeM;
+ background-color: tintOrShade($euiColorFullShade, 25%, 90%);
+ color: $euiColorGhost;
+ max-width: 256px;
+ opacity: 0;
+ animation: euiToolTipTop $euiAnimSpeedSlow ease-out $euiAnimSpeedNormal forwards;
+ z-index: $euiZLevel9;
+
+ &::before {
+ content: "";
+ position: absolute;
+ bottom: -$euiSize/2;
+ left: 50%;
+ transform: translateX(-50%) rotateZ(45deg);
+ transform-origin: center;
+ background-color: tintOrShade($euiColorFullShade, 25%, 90%);
+ width: $euiSize;
+ height: $euiSize;
+ }
+
+ // Positions the arrow and animates in from the same side.
+ &.euiToolTip--right {
+ animation-name: euiToolTipRight;
+
+ &:before {
+ bottom: 50%;
+ left: -$euiSize/2;
+ transform: translateY(50%) rotateZ(45deg);
+ }
+ }
+
+ &.euiToolTip--bottom {
+ animation-name: euiToolTipBottom;
+
+ &:before {
+ bottom: auto;
+ top: -$euiSize/2;
+ }
+ }
+
+ &.euiToolTip--left {
+ animation-name: euiToolTipLeft;
+
+ &:before {
+ bottom: 50%;
+ left: auto;
+ right: -$euiSize/2;
+ transform: translateY(50%) rotateZ(45deg);
+ }
+ }
+
+ .euiToolTip__title {
+ font-weight: $euiFontWeightBold;
+ border-bottom: solid 1px tintOrShade($euiColorFullShade, 35%, 80%);
+ padding-bottom: $euiSizeXS;
+ margin-bottom: $euiSizeXS;
+ }
+}
+
+
+// Keyframes to animate in the tooltip.
+@keyframes euiToolTipTop {
+ 0% {
+ opacity: 0;
+ transform: translateY(-$euiSize);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes euiToolTipBottom {
+ 0% {
+ opacity: 0;
+ transform: translateY($euiSize);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes euiToolTipLeft {
+ 0% {
+ opacity: 0;
+ transform: translateX(-$euiSize);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes euiToolTipRight {
+ 0% {
+ opacity: 0;
+ transform: translateX($euiSize);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
diff --git a/src/components/tool_tip/index.js b/src/components/tool_tip/index.js
new file mode 100644
index 00000000000..cadac9c8b64
--- /dev/null
+++ b/src/components/tool_tip/index.js
@@ -0,0 +1,3 @@
+export {
+ EuiToolTip,
+} from './tool_tip';
diff --git a/src/components/tool_tip/tool_tip.js b/src/components/tool_tip/tool_tip.js
new file mode 100644
index 00000000000..b56bc3da12a
--- /dev/null
+++ b/src/components/tool_tip/tool_tip.js
@@ -0,0 +1,167 @@
+import React, {
+ Component,
+ cloneElement,
+ Fragment,
+} from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import { EuiPortal } from '../portal';
+import { EuiToolTipPopover } from './tool_tip_popover';
+import { calculatePopoverPosition, calculatePopoverStyles } from '../../services';
+
+import makeId from '../form/form_row/make_id';
+
+const positionsToClassNameMap = {
+ top: 'euiToolTip--top',
+ right: 'euiToolTip--right',
+ bottom: 'euiToolTip--bottom',
+ left: 'euiToolTip--left',
+};
+
+export const POSITIONS = Object.keys(positionsToClassNameMap);
+
+export class EuiToolTip extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ visible: false,
+ hasFocus: false,
+ calculatedPosition: this.props.position,
+ toolTipStyles: {},
+ id: this.props.id || makeId(),
+ };
+ }
+
+ showToolTip = () => {
+ this.setState({ visible: true });
+ };
+
+ positionToolTip = (toolTipRect) => {
+ const wrapperRect = this.wrapper.getBoundingClientRect();
+ const userPosition = this.props.position;
+
+ const calculatedPosition = calculatePopoverPosition(wrapperRect, toolTipRect, userPosition);
+ const toolTipStyles = calculatePopoverStyles(wrapperRect, toolTipRect, calculatedPosition);
+
+ this.setState({
+ visible: true,
+ calculatedPosition,
+ toolTipStyles,
+ });
+ };
+
+ hideToolTip = () => {
+ this.setState({ visible: false });
+ };
+
+ onFocus = () => {
+ this.setState({
+ hasFocus: true,
+ });
+ this.showToolTip();
+ };
+
+ onBlur = () => {
+ this.setState({
+ hasFocus: false,
+ });
+ this.hideToolTip();
+ };
+
+ onMouseOut = () => {
+ if (!this.state.hasFocus) {
+ this.hideToolTip();
+ }
+ };
+
+ render() {
+ const {
+ children,
+ className,
+ content,
+ title,
+ ...rest
+ } = this.props;
+
+ const classes = classNames(
+ 'euiToolTip',
+ positionsToClassNameMap[this.state.calculatedPosition],
+ className
+ );
+
+ let tooltip;
+ if (this.state.visible) {
+ tooltip = (
+
+
+ {content}
+
+
+ );
+ }
+
+ const trigger = (
+ this.wrapper = wrapper}>
+ {cloneElement(children, {
+ onFocus: this.showToolTip,
+ onBlur: this.hideToolTip,
+ 'aria-describedby': this.state.id,
+ onMouseOver: this.showToolTip,
+ onMouseOut: this.onMouseOut
+ })}
+
+ );
+
+ return (
+
+ {trigger}
+ {tooltip}
+
+ );
+ }
+}
+
+EuiToolTip.propTypes = {
+ /**
+ * The in-view trigger for your tooltip.
+ */
+ children: PropTypes.element.isRequired,
+ /**
+ * The main content of your tooltip.
+ */
+ content: PropTypes.node.isRequired,
+
+ /**
+ * An optional title for your tooltip.
+ */
+ title: PropTypes.node,
+
+ /**
+ * Suggested position. If there is not enough room for it this will be changed.
+ */
+ position: PropTypes.oneOf(POSITIONS),
+
+ /**
+ * Passes onto the tooltip itself, not the trigger.
+ */
+ className: PropTypes.string,
+
+ /**
+ * Unless you provide one, this will be randomly generated.
+ */
+ id: PropTypes.string,
+};
+
+EuiToolTip.defaultProps = {
+ position: 'top',
+};
diff --git a/src/components/tool_tip/tool_tip.test.js b/src/components/tool_tip/tool_tip.test.js
new file mode 100644
index 00000000000..ecf2908c50c
--- /dev/null
+++ b/src/components/tool_tip/tool_tip.test.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test';
+
+import { EuiToolTip } from './tool_tip';
+
+describe('EuiToolTip', () => {
+ test('is rendered', () => {
+ const component = render(
+
+ Trigger
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/src/components/tool_tip/tool_tip_popover.js b/src/components/tool_tip/tool_tip_popover.js
new file mode 100644
index 00000000000..41063bff46b
--- /dev/null
+++ b/src/components/tool_tip/tool_tip_popover.js
@@ -0,0 +1,74 @@
+import React, {
+ Component,
+} from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+export class EuiToolTipPopover extends Component {
+ static propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ title: PropTypes.node,
+ positionToolTip: PropTypes.func.isRequired,
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.updateDimensions = this.updateDimensions.bind(this);
+ }
+
+ componentDidMount() {
+ document.body.classList.add('euiBody-hasToolTip');
+
+ this.updateDimensions();
+ window.addEventListener('resize', this.updateDimensions);
+ }
+
+ updateDimensions() {
+ requestAnimationFrame(() => {
+ // Because of this delay, sometimes `positionToolTip` becomes unavailable.
+ if (this.popover) {
+ this.props.positionToolTip(this.popover.getBoundingClientRect());
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ document.body.classList.remove('euiBody-hasToolTip');
+ window.removeEventListener('resize', this.updateDimensions);
+ }
+
+ render() {
+ const {
+ children,
+ title,
+ className,
+ positionToolTip, // eslint-disable-line no-unused-vars
+ ...rest
+ } = this.props;
+
+ const classes = classNames(
+ 'euiToolTipPopover',
+ className
+ );
+
+ let optionalTitle;
+ if (title) {
+ optionalTitle = (
+ {title}
+ );
+ }
+
+ return (
+ this.popover = popover}
+ {...rest}
+ >
+ {optionalTitle}
+ {children}
+
+ );
+ }
+}
diff --git a/src/components/tool_tip/tool_tip_popover.test.js b/src/components/tool_tip/tool_tip_popover.test.js
new file mode 100644
index 00000000000..44eb63ef5d5
--- /dev/null
+++ b/src/components/tool_tip/tool_tip_popover.test.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test';
+
+import { EuiToolTipPopover } from './tool_tip_popover';
+
+describe('EuiToolTipPopover', () => {
+ test('is rendered', () => {
+ const component = render(
+ {}} {...requiredProps} />
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/src/components/tooltip/_index.scss b/src/components/tooltip/_index.scss
deleted file mode 100644
index 91752d2fa28..00000000000
--- a/src/components/tooltip/_index.scss
+++ /dev/null
@@ -1,2 +0,0 @@
-@import "mixins";
-@import "tooltip";
diff --git a/src/components/tooltip/_mixins.scss b/src/components/tooltip/_mixins.scss
deleted file mode 100644
index 4fd5d809a92..00000000000
--- a/src/components/tooltip/_mixins.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@mixin euiTooltipSize($multiplier){
- max-width: $euiSize * $multiplier;
-
- .euiTooltip__content {
- white-space: inherit; // allow text wrapping of content
- }
-}
\ No newline at end of file
diff --git a/src/components/tooltip/_tooltip.scss b/src/components/tooltip/_tooltip.scss
deleted file mode 100644
index a82ef408b0a..00000000000
--- a/src/components/tooltip/_tooltip.scss
+++ /dev/null
@@ -1,120 +0,0 @@
-// Outer most element (contains both the triggering element and tooltip itself)
-.euiTooltip {
- display: inline-block;
-}
-
-// The tooltip itself (for positioning, sizing, animating)
-.euiTooltip__container {
- position: absolute; // TODO: Change to fixed position?
-
- // SIZING
- &.euiTooltip--small {
- @include euiTooltipSize(10);
- }
-
- &.euiTooltip--medium {
- @include euiTooltipSize(15);
- }
-
- &.euiTooltip--large {
- @include euiTooltipSize(30);
- }
-
- // ANIMATING -- with slight delay
- transition:
- opacity $euiAnimSpeedNormal ease-out $euiAnimSpeedSlow,
- visibility $euiAnimSpeedNormal ease-out $euiAnimSpeedSlow,
- transform $euiAnimSpeedNormal ease-out $euiAnimSpeedSlow;
- opacity: 0;
- visibility: hidden;
- transform: translateX(0) translateY($euiSize * -1) translateY(0); // default starting position of top
-
- // Don't delay the animation if the tooltip is on click
- .euiTooltip--click & {
- transition:
- opacity $euiAnimSpeedNormal ease-out,
- visibility $euiAnimSpeedNormal ease-out,
- transform $euiAnimSpeedNormal ease-out;
- }
-
- &.euiTooltip-isVisible {
- opacity: 1;
- visibility: visible;
- transform: translateX(0) translateY(0) translateZ(0) !important;
- // Using important here so we're always at (0,0,0) when visible, no matter the position.
- // May need to revisit if we find override issues.
- }
-
- // Starting positions
- .euiTooltip--right & {
- transform: translateX($euiSize) translateY(0) translateZ(0);
- }
-
- .euiTooltip--bottom & {
- transform: translateX(0) translateY($euiSize) translateZ(0);
- }
-
- .euiTooltip--left & {
- transform: translateX($euiSize * -1) translateY(0) translateZ(0);
- }
-}
-
- // The tooltip content (for styling)
-
- .euiTooltip__content {
-
- // Scoped variables for component-only re-use
- $background-color: $euiColorDarkestShade;
- $text-color: $euiColorEmptyShade;
- $arrow-size: $euiSize;
-
- // STYLING
- @include euiBottomShadow;
- @include euiFontSizeS();
- background-color: $background-color;
- border-radius: $euiBorderRadius;
- padding: $euiSizeM;
- color: $text-color;
- white-space: nowrap;
-
- // ARROW
- position: relative;
- &::before {
- content: "";
- position: absolute;
- bottom: -$arrow-size/2;
- left: 50%;
- transform: translateX(-50%) rotateZ(45deg);
- transform-origin: center;
- background-color: $background-color;
- width: $arrow-size;
- height: $arrow-size;
-
- // Positions
- .euiTooltip--right & {
- bottom: 50%;
- left: -$arrow-size/2;
- transform: translateY(50%) rotateZ(45deg);
- }
-
- .euiTooltip--bottom & {
- bottom: auto;
- top: -$arrow-size/2;
- }
-
- .euiTooltip--left & {
- bottom: 50%;
- left: auto;
- right: -$arrow-size/2;
- transform: translateY(50%) rotateZ(45deg);
- }
- }
- }
-
- // The tooltip title if it exists
- .euiTooltip__title {
- @include euiFontSize;
- font-weight: $euiFontWeightMedium;
- margin-bottom: $euiSizeS;
- border-bottom: $euiBorderWidthThin solid $euiColorDarkShade;
- }
diff --git a/src/components/tooltip/index.js b/src/components/tooltip/index.js
deleted file mode 100644
index 215305339ab..00000000000
--- a/src/components/tooltip/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { Tooltip } from './tooltip';
-export { TooltipTrigger } from './tooltip_trigger';
diff --git a/src/components/tooltip/tooltip.js b/src/components/tooltip/tooltip.js
deleted file mode 100644
index 89f02cae1f1..00000000000
--- a/src/components/tooltip/tooltip.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-
-const sizeToClassNameMap = {
- auto: null,
- s: 'euiTooltip--small',
- m: 'euiTooltip--medium',
- l: 'euiTooltip--large',
-};
-
-export const SIZES = Object.keys(sizeToClassNameMap);
-
-export const Tooltip = ({
- children,
- className,
- size,
- isVisible,
- isSticky,
- title,
- ...rest
-}) => {
-
- const classes = classNames(
- 'euiTooltip__container',
- sizeToClassNameMap[size],
- {
- 'euiTooltip-isVisible': isVisible,
- 'euiTooltip-isHidden': !isVisible,
- 'euiTooltip-isSticky': isSticky,
- },
- className
- );
-
- let tooltipTitle;
- if (title) {
- tooltipTitle = (
- {title}
- );
- }
-
- return (
-
-
- {tooltipTitle}
- {children}
-
-
- );
-
-};
-
-Tooltip.propTypes = {
- children: PropTypes.node,
- className: PropTypes.string,
- size: PropTypes.oneOf(SIZES),
- isVisible: PropTypes.bool,
- isSticky: PropTypes.bool,
- title: PropTypes.string
-};
-
-Tooltip.defaultProps = {
- size: 'auto',
- isVisible: true,
- isSticky: false
-};
diff --git a/src/components/tooltip/tooltip_constants.js b/src/components/tooltip/tooltip_constants.js
deleted file mode 100644
index 6b9e7b5861f..00000000000
--- a/src/components/tooltip/tooltip_constants.js
+++ /dev/null
@@ -1,6 +0,0 @@
-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
deleted file mode 100644
index 509e3b3e96c..00000000000
--- a/src/components/tooltip/tooltip_trigger.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import React, { Component } 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 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('euiTooltip__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('euiTooltip', className, {
- 'tooltip-light': theme === 'light',
- [`euiTooltip--${this.state.noOverflowPlacement}`]: this.state.noOverflowPlacement !== 'top',
- [`euiTooltip--${trigger}`]: trigger !== 'hover',
- });
- 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 2226e139dc0..c3c1fea6743 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -59,5 +59,6 @@ export {
} from './sort';
export {
- noOverflowPlacement,
-} from './overflow';
+ calculatePopoverPosition,
+ calculatePopoverStyles,
+} from './popover';
diff --git a/src/services/overflow/index.js b/src/services/overflow/index.js
deleted file mode 100644
index 9fabb253d76..00000000000
--- a/src/services/overflow/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { noOverflowPlacement } from './no_overflow_placement';
diff --git a/src/services/overflow/no_overflow_placement.js b/src/services/overflow/no_overflow_placement.js
deleted file mode 100644
index 5e17d68da4b..00000000000
--- a/src/services/overflow/no_overflow_placement.js
+++ /dev/null
@@ -1,62 +0,0 @@
-
-/**
- * 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;
-}
diff --git a/src/services/popover/index.js b/src/services/popover/index.js
new file mode 100644
index 00000000000..28cdd4279cc
--- /dev/null
+++ b/src/services/popover/index.js
@@ -0,0 +1,2 @@
+export { calculatePopoverPosition } from './popover_calculate_position';
+export { calculatePopoverStyles } from './popover_calculate_styles';
diff --git a/src/services/popover/popover_calculate_position.js b/src/services/popover/popover_calculate_position.js
new file mode 100644
index 00000000000..afa6e9f3ab5
--- /dev/null
+++ b/src/services/popover/popover_calculate_position.js
@@ -0,0 +1,60 @@
+
+/**
+ * Determine the best position for a popup that avoids clipping by the window view port.
+ *
+ * @param {native DOM Element} wrapperRect - getBoundingClientRect() of wrapping node around the popover.
+ * @param {native DOM Element} popupRect - getBoundingClientRect() of the popup node.
+ * @param {string} requestedPosition - Position the user wants. One of ["top", "right", "bottom", "left"]
+ * @param {number} buffer - The space between the wrapper and the popup. Also the minimum space between the popup and the window.
+ *
+ * @returns {string} One of ["top", "right", "bottom", "left"] that ensures no window overflow.
+ */
+export function calculatePopoverPosition(wrapperRect, popupRect, requestedPosition, buffer = 16) {
+
+ // determine popup overflow in each direction
+ // negative values signal window overflow, large values signal lots of free space
+ const popupOverflow = {
+ top: wrapperRect.top - (popupRect.height + (2 * buffer)),
+ right: window.innerWidth - wrapperRect.right - (popupRect.width + (2 * buffer)),
+ left: wrapperRect.left - (popupRect.width + (2 * buffer)),
+ bottom: window.innerHeight - wrapperRect.bottom - (popupRect.height + (2 * buffer)),
+ };
+
+ function hasCrossDimensionOverflow(key) {
+ if (key === 'left' || key === 'right') {
+ const domNodeCenterY = wrapperRect.top + (wrapperRect.height / 2);
+ const tooltipTop = domNodeCenterY - ((popupRect.height / 2) + buffer);
+ if (tooltipTop <= 0) {
+ return true;
+ }
+ const tooltipBottom = domNodeCenterY + (popupRect.height / 2) + buffer;
+ if (tooltipBottom >= window.innerHeight) {
+ return true;
+ }
+ } else {
+ const domNodeCenterX = wrapperRect.left + (wrapperRect.width / 2);
+ const tooltipLeft = domNodeCenterX - ((popupRect.width / 2) + buffer);
+ if (tooltipLeft <= 0) {
+ return true;
+ }
+ const tooltipRight = domNodeCenterX + (popupRect.width / 2) + buffer;
+ if (tooltipRight >= window.innerWidth) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ let calculatedPopoverPosition = requestedPosition;
+ if (popupOverflow[requestedPosition] <= 0 || hasCrossDimensionOverflow(requestedPosition)) {
+ // requested position overflows window bounds
+ // select direction what has the most free space
+ Object.keys(popupOverflow).forEach((key) => {
+ if (popupOverflow[key] > popupOverflow[calculatedPopoverPosition] && !hasCrossDimensionOverflow(key)) {
+ calculatedPopoverPosition = key;
+ }
+ });
+ }
+
+ return calculatedPopoverPosition;
+}
diff --git a/src/services/popover/popover_calculate_styles.js b/src/services/popover/popover_calculate_styles.js
new file mode 100644
index 00000000000..741d3e2c422
--- /dev/null
+++ b/src/services/popover/popover_calculate_styles.js
@@ -0,0 +1,19 @@
+export function calculatePopoverStyles(wrapperNodeRect, popupNodeRect, position, buffer = 16) {
+ const styles = {};
+
+ if (position === 'top') {
+ styles.top = wrapperNodeRect.top + window.scrollY - (popupNodeRect.height + buffer);
+ styles.left = wrapperNodeRect.left + (wrapperNodeRect.width / 2) - (popupNodeRect.width / 2);
+ } else if (position === 'bottom') {
+ styles.top = wrapperNodeRect.top + window.scrollY + wrapperNodeRect.height + buffer;
+ styles.left = wrapperNodeRect.left + (wrapperNodeRect.width / 2) - (popupNodeRect.width / 2);
+ } else if (position === 'right') {
+ styles.top = wrapperNodeRect.top + window.scrollY - ((popupNodeRect.height - wrapperNodeRect.height) / 2);
+ styles.left = wrapperNodeRect.left + wrapperNodeRect.width + buffer;
+ } else if (position === 'left') {
+ styles.top = wrapperNodeRect.top + window.scrollY - ((popupNodeRect.height - wrapperNodeRect.height) / 2);
+ styles.left = wrapperNodeRect.left - popupNodeRect.width - buffer;
+ }
+
+ return styles;
+}