diff --git a/CHANGELOG.md b/CHANGELOG.md
index b305d21b83e..093aff78ac4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
- Attach `noreferrer` also to links without `target="_blank"` ([#2008](https://github.com/elastic/eui/pull/2008))
- Convert observer utility components to TypeScript ([#2009](https://github.com/elastic/eui/pull/2009))
+- Convert tool tip components to TypeScript ([#2013](https://github.com/elastic/eui/pull/2013))
**Bug fixes**
diff --git a/src/components/badge/index.d.ts b/src/components/badge/index.d.ts
index bc8ef6ef073..e034e2ffaec 100644
--- a/src/components/badge/index.d.ts
+++ b/src/components/badge/index.d.ts
@@ -1,5 +1,5 @@
import { IconType } from '../icon';
-///
+import { ToolTipPositions } from '../tool_tip/tool_tip';
import {
HTMLAttributes,
diff --git a/src/components/index.d.ts b/src/components/index.d.ts
index e4ecf1ad4d9..fadcac60c2f 100644
--- a/src/components/index.d.ts
+++ b/src/components/index.d.ts
@@ -28,7 +28,6 @@
///
///
///
-///
declare module '@elastic/eui' {
// @ts-ignore
diff --git a/src/components/tool_tip/__snapshots__/icon_tip.test.js.snap b/src/components/tool_tip/__snapshots__/icon_tip.test.tsx.snap
similarity index 100%
rename from src/components/tool_tip/__snapshots__/icon_tip.test.js.snap
rename to src/components/tool_tip/__snapshots__/icon_tip.test.tsx.snap
diff --git a/src/components/tool_tip/__snapshots__/tool_tip.test.js.snap b/src/components/tool_tip/__snapshots__/tool_tip.test.tsx.snap
similarity index 100%
rename from src/components/tool_tip/__snapshots__/tool_tip.test.js.snap
rename to src/components/tool_tip/__snapshots__/tool_tip.test.tsx.snap
diff --git a/src/components/tool_tip/icon_tip.js b/src/components/tool_tip/icon_tip.js
deleted file mode 100644
index df88cdcb035..00000000000
--- a/src/components/tool_tip/icon_tip.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { EuiIcon } from '../icon';
-import { EuiToolTip } from './tool_tip';
-
-export const EuiIconTip = ({
- type,
- 'aria-label': ariaLabel,
- color,
- size,
- iconProps,
- ...rest
-}) => (
-
-
-
-);
-
-EuiIconTip.propTypes = {
- /**
- * The icon type.
- */
- type: PropTypes.string,
-
- /**
- * The icon color.
- */
- color: PropTypes.string,
-
- /**
- * The icon size.
- */
- size: PropTypes.string,
-
- /**
- * Explain what this icon means for screen readers.
- */
- 'aria-label': PropTypes.string,
-
- /**
- * Pass certain props down to `EuiIcon`
- */
- iconProps: PropTypes.object,
-};
-
-EuiIconTip.defaultProps = {
- type: 'questionInCircle',
- 'aria-label': 'Info',
-};
diff --git a/src/components/tool_tip/icon_tip.test.js b/src/components/tool_tip/icon_tip.test.tsx
similarity index 100%
rename from src/components/tool_tip/icon_tip.test.js
rename to src/components/tool_tip/icon_tip.test.tsx
diff --git a/src/components/tool_tip/icon_tip.tsx b/src/components/tool_tip/icon_tip.tsx
new file mode 100644
index 00000000000..ded7a9d181e
--- /dev/null
+++ b/src/components/tool_tip/icon_tip.tsx
@@ -0,0 +1,62 @@
+import React, { FunctionComponent } from 'react';
+
+import { Omit, PropsOf } from '../common';
+import { EuiIcon, IconSize, IconType } from '../icon';
+import { EuiToolTip, Props as EuiToolTipProps } from './tool_tip';
+
+export interface EuiIconTipProps {
+ /**
+ * The icon color.
+ */
+ color?: string;
+ /**
+ * The icon type.
+ */
+ type?: IconType;
+ /**
+ * The icon size.
+ */
+ size?: IconSize;
+ /**
+ * Explain what this icon means for screen readers.
+ */
+ 'aria-label'?: string;
+
+ /**
+ * Pass certain props down to `EuiIcon`
+ */
+ // EuiIconTip's `type` is passed to EuiIcon, so we want to exclude `type` from
+ // iconProps; however, due to TS's bivariant function arguments `type` could be
+ // passed without any error/feedback so we explicitly set it to `never` type
+ iconProps?: Omit, 'type'> & { type?: never };
+}
+
+type Props = Omit &
+ EuiIconTipProps & {
+ // This are copied from EuiToolTipProps, but made optional. Defaults
+ // are applied below.
+ delay?: EuiToolTipProps['delay'];
+ position?: EuiToolTipProps['position'];
+ };
+
+export const EuiIconTip: FunctionComponent = ({
+ type = 'questionInCircle',
+ 'aria-label': ariaLabel = 'Info',
+ color,
+ size,
+ iconProps,
+ position = 'top',
+ delay = 'regular',
+ ...rest
+}) => (
+
+
+
+);
diff --git a/src/components/tool_tip/index.d.ts b/src/components/tool_tip/index.d.ts
deleted file mode 100644
index 51773f81a36..00000000000
--- a/src/components/tool_tip/index.d.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-export { EuiToolTipPopover } from './tool_tip_popover';
-import { ReactElement, ReactNode, FunctionComponent } from 'react';
-import { EuiIcon, IconType } from '../icon';
-import { Omit, PropsOf } from '../common';
-
-declare module '@elastic/eui' {
- export type ToolTipPositions = 'top' | 'right' | 'bottom' | 'left';
-
- export type ToolTipDelay = 'regular' | 'long';
-
- export interface EuiToolTipProps {
- anchorClassName?: string;
- children: ReactElement;
- className?: string;
- content?: ReactNode;
- delay?: ToolTipDelay;
- title?: ReactNode;
- id?: string;
- position?: ToolTipPositions;
- }
- export const EuiToolTip: FunctionComponent;
-
- export interface EuiIconTipProps {
- color?: string;
- type?: IconType;
- size?: string;
- 'aria-label'?: string;
-
- // EuiIconTip's `type` is passed to EuiIcon, so we want to exclude `type` from
- // iconProps; however, due to TS's bivariant function arguments `type` could be
- // passed without any error/feedback so we explicitly set it to `never` type
- iconProps?: Omit, 'type'> & { type?: never };
- }
-
- export const EuiIconTip: FunctionComponent<
- Omit & EuiIconTipProps
- >;
-}
diff --git a/src/components/tool_tip/index.js b/src/components/tool_tip/index.ts
similarity index 100%
rename from src/components/tool_tip/index.js
rename to src/components/tool_tip/index.ts
diff --git a/src/components/tool_tip/tool_tip.test.js b/src/components/tool_tip/tool_tip.test.tsx
similarity index 100%
rename from src/components/tool_tip/tool_tip.test.js
rename to src/components/tool_tip/tool_tip.test.tsx
diff --git a/src/components/tool_tip/tool_tip.js b/src/components/tool_tip/tool_tip.tsx
similarity index 74%
rename from src/components/tool_tip/tool_tip.js
rename to src/components/tool_tip/tool_tip.tsx
index 9be8d74b7fc..e65f4775508 100644
--- a/src/components/tool_tip/tool_tip.js
+++ b/src/components/tool_tip/tool_tip.tsx
@@ -1,7 +1,14 @@
-import React, { Component, cloneElement, Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, {
+ Component,
+ cloneElement,
+ Fragment,
+ ReactElement,
+ ReactNode,
+ MouseEvent as ReactMouseEvent,
+} from 'react';
import classNames from 'classnames';
+import { keysOf } from '../common';
import { EuiPortal } from '../portal';
import { EuiToolTipPopover } from './tool_tip_popover';
import { findPopoverPosition } from '../../services';
@@ -9,23 +16,34 @@ import { findPopoverPosition } from '../../services';
import makeId from '../form/form_row/make_id';
import { EuiResizeObserver } from '../observer/resize_observer';
-const positionsToClassNameMap = {
+export type ToolTipPositions = 'top' | 'right' | 'bottom' | 'left';
+
+const positionsToClassNameMap: { [key in ToolTipPositions]: string } = {
top: 'euiToolTip--top',
right: 'euiToolTip--right',
bottom: 'euiToolTip--bottom',
left: 'euiToolTip--left',
};
-export const POSITIONS = Object.keys(positionsToClassNameMap);
+export const POSITIONS = keysOf(positionsToClassNameMap);
+
+export type ToolTipDelay = 'regular' | 'long';
-const delayToClassNameMap = {
+const delayToClassNameMap: { [key in ToolTipDelay]: string | null } = {
regular: null,
long: 'euiToolTip--delayLong',
};
-export const DELAY = Object.keys(delayToClassNameMap);
+export const DELAY = keysOf(delayToClassNameMap);
-const DEFAULT_TOOLTIP_STYLES = {
+interface ToolTipStyles {
+ top: number;
+ left: number | 'auto';
+ right?: number | 'auto';
+ opacity?: number;
+}
+
+const DEFAULT_TOOLTIP_STYLES: ToolTipStyles = {
// position the tooltip content near the top-left
// corner of the window so it can't create scrollbars
// 50,50 because who knows what negative margins, padding, etc
@@ -36,19 +54,74 @@ const DEFAULT_TOOLTIP_STYLES = {
opacity: 0,
};
-export class EuiToolTip extends Component {
- constructor(props) {
- super(props);
+export interface Props {
+ /**
+ * Passes onto the the trigger.
+ */
+ anchorClassName?: string;
+ /**
+ * The in-view trigger for your tooltip.
+ */
+ children: ReactElement;
+ /**
+ * Passes onto the tooltip itself, not the trigger.
+ */
+ className?: string;
+ /**
+ * The main content of your tooltip.
+ */
+ content?: ReactNode;
+ /**
+ * Delay before showing tooltip. Good for repeatable items.
+ */
+ delay: ToolTipDelay;
+ /**
+ * An optional title for your tooltip.
+ */
+ title?: ReactNode;
+ /**
+ * Unless you provide one, this will be randomly generated.
+ */
+ id?: string;
+ /**
+ * Suggested position. If there is not enough room for it this will be changed.
+ */
+ position: ToolTipPositions;
+
+ /**
+ * If supplied, called when mouse movement causes the tool tip to be
+ * hidden.
+ */
+ onMouseOut?: (event: ReactMouseEvent) => void;
+}
- this.state = {
- visible: false,
- hasFocus: false,
- calculatedPosition: this.props.position,
- toolTipStyles: DEFAULT_TOOLTIP_STYLES,
- arrowStyles: {},
- id: this.props.id || makeId(),
- };
- }
+interface State {
+ visible: boolean;
+ hasFocus: boolean;
+ calculatedPosition: ToolTipPositions;
+ toolTipStyles: ToolTipStyles;
+ arrowStyles: undefined | { left: number; top: number };
+ id: string;
+}
+
+export class EuiToolTip extends Component {
+ _isMounted = false;
+ anchor: null | HTMLElement = null;
+ popover: null | HTMLElement = null;
+
+ state: State = {
+ visible: false,
+ hasFocus: false,
+ calculatedPosition: this.props.position,
+ toolTipStyles: DEFAULT_TOOLTIP_STYLES,
+ arrowStyles: undefined,
+ id: this.props.id || makeId(),
+ };
+
+ static defaultProps: Partial = {
+ position: 'top',
+ delay: 'regular',
+ };
componentDidMount() {
this._isMounted = true;
@@ -58,7 +131,7 @@ export class EuiToolTip extends Component {
this._isMounted = false;
}
- componentDidUpdate(prevProps, prevState) {
+ componentDidUpdate(prevProps: Props, prevState: State) {
if (prevState.visible === false && this.state.visible === true) {
requestAnimationFrame(this.testAnchor);
}
@@ -79,7 +152,7 @@ export class EuiToolTip extends Component {
}
};
- setPopoverRef = ref => {
+ setPopoverRef = (ref: HTMLElement) => {
this.popover = ref;
// if the popover has been unmounted, clear
@@ -87,7 +160,7 @@ export class EuiToolTip extends Component {
if (ref == null) {
this.setState({
toolTipStyles: DEFAULT_TOOLTIP_STYLES,
- arrowStyles: {},
+ arrowStyles: undefined,
});
}
};
@@ -99,6 +172,10 @@ export class EuiToolTip extends Component {
positionToolTip = () => {
const requestedPosition = this.props.position;
+ if (!this.anchor || !this.popover) {
+ return;
+ }
+
const { position, left, top, arrow } = findPopoverPosition({
anchor: this.anchor,
popover: this.popover,
@@ -120,7 +197,7 @@ export class EuiToolTip extends Component {
document.documentElement.clientWidth || window.innerWidth;
const useRightValue = windowWidth / 2 < left;
- const toolTipStyles = {
+ const toolTipStyles: ToolTipStyles = {
top,
left: useRightValue ? 'auto' : left,
right: useRightValue
@@ -156,12 +233,12 @@ export class EuiToolTip extends Component {
this.hideToolTip();
};
- onMouseOut = e => {
+ onMouseOut = (e: ReactMouseEvent) => {
// Prevent mousing over children from hiding the tooltip by testing for whether the mouse has
// left the anchor for a non-child.
if (
this.anchor === e.relatedTarget ||
- !this.anchor.contains(e.relatedTarget)
+ (this.anchor != null && !this.anchor.contains(e.relatedTarget as Node))
) {
if (!this.state.hasFocus) {
this.hideToolTip();
@@ -169,7 +246,7 @@ export class EuiToolTip extends Component {
}
if (this.props.onMouseOut) {
- this.props.onMouseOut();
+ this.props.onMouseOut(e);
}
};
@@ -248,49 +325,3 @@ export class EuiToolTip extends Component {
);
}
}
-
-EuiToolTip.propTypes = {
- /**
- * The in-view trigger for your tooltip.
- */
- children: PropTypes.element.isRequired,
- /**
- * The main content of your tooltip.
- */
- content: PropTypes.node,
-
- /**
- * 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),
-
- /**
- * Delay before showing tooltip. Good for repeatable items.
- */
- delay: PropTypes.oneOf(DELAY),
-
- /**
- * Passes onto the tooltip itself, not the trigger.
- */
- className: PropTypes.string,
-
- /**
- * Passes onto the the trigger.
- */
- anchorClassName: PropTypes.string,
-
- /**
- * Unless you provide one, this will be randomly generated.
- */
- id: PropTypes.string,
-};
-
-EuiToolTip.defaultProps = {
- position: 'top',
- delay: 'regular',
-};
diff --git a/src/components/tool_tip/tool_tip_popover.tsx b/src/components/tool_tip/tool_tip_popover.tsx
index 069ba4f21e6..f2d29da157d 100644
--- a/src/components/tool_tip/tool_tip_popover.tsx
+++ b/src/components/tool_tip/tool_tip_popover.tsx
@@ -1,16 +1,16 @@
import React, { HTMLAttributes, Component, ReactNode } from 'react';
import classNames from 'classnames';
-import { CommonProps } from '../common';
+import { CommonProps, Omit } from '../common';
-export type EuiToolTipPopoverProps = CommonProps &
- HTMLAttributes & {
+type Props = CommonProps &
+ Omit, 'title'> & {
positionToolTip: (rect: ClientRect | DOMRect) => void;
children?: ReactNode;
title?: ReactNode;
popoverRef?: (ref: HTMLDivElement) => void;
};
-export class EuiToolTipPopover extends Component {
+export class EuiToolTipPopover extends Component {
private popover: HTMLDivElement | undefined;
updateDimensions = () => {
diff --git a/src/services/popover/popover_positioning.ts b/src/services/popover/popover_positioning.ts
index 0465b9cb140..c9b099974b2 100644
--- a/src/services/popover/popover_positioning.ts
+++ b/src/services/popover/popover_positioning.ts
@@ -70,6 +70,15 @@ interface FindPopoverPositionArgs {
returnBoundingBox?: boolean;
}
+interface FindPopoverPositionResult {
+ top: number;
+ left: number;
+ position: 'top' | 'right' | 'bottom' | 'left';
+ fit: number;
+ arrow?: { left: number; top: number };
+ anchorBoundingBox?: EuiClientRect;
+}
+
/**
* Calculates the absolute positioning (relative to document.body) to place a popover element
*
@@ -86,14 +95,9 @@ interface FindPopoverPositionArgs {
* present, describes the size & constraints for an arrow element, and the
* function return value will include an `arrow` param with position details
*
- * @returns {{
- * top: number,
- * left: number,
- * position: string,
- * fit: number,
- * arrow?: {left: number, top: number}
- * } | null} absolute page coordinates for the popover, and the
- * placements's relation to the anchor; if there's no room this returns null
+ * @returns {FindPopoverPositionResult} absolute page coordinates for the
+ * popover, and the placements's relation to the anchor or undefined
+ * there's no room.
*/
export function findPopoverPosition({
anchor,
@@ -107,7 +111,7 @@ export function findPopoverPosition({
container,
arrowConfig,
returnBoundingBox,
-}: FindPopoverPositionArgs) {
+}: FindPopoverPositionArgs): FindPopoverPositionResult {
// find the screen-relative bounding boxes of the anchor, popover, and container
const anchorBoundingBox = getElementBoundingBox(anchor);
const popoverBoundingBox = getElementBoundingBox(popover);
@@ -179,7 +183,7 @@ export function findPopoverPosition({
}
let bestFit = -Infinity;
- let bestPosition = null;
+ let bestPosition: FindPopoverPositionResult | null = null;
for (let idx = 0; idx < iterationPositions.length; idx++) {
const iterationPosition = iterationPositions[idx];
@@ -216,9 +220,15 @@ export function findPopoverPosition({
// If we haven't improved the fit, then continue on and try a new position.
}
- return returnBoundingBox
- ? { ...bestPosition, anchorBoundingBox: { ...anchorBoundingBox } }
- : bestPosition;
+ if (bestPosition == null) {
+ throw new Error('Failed to calculate bestPosition');
+ }
+
+ if (returnBoundingBox) {
+ bestPosition.anchorBoundingBox = anchorBoundingBox;
+ }
+
+ return bestPosition;
}
interface GetPopoverScreenCoordinatesArgs {
@@ -233,6 +243,13 @@ interface GetPopoverScreenCoordinatesArgs {
buffer?: number;
}
+interface GetPopoverScreenCoordinatesResult {
+ top: number;
+ left: number;
+ fit: number;
+ arrow: { top: number; left: number } | undefined;
+}
+
/**
* Given a target position and the popover's surrounding context, returns either an
* object with {top, left} screen coordinates or `null` if it's not possible to show
@@ -250,10 +267,9 @@ interface GetPopoverScreenCoordinatesArgs {
* @param [buffer=0] {number} Minimum distance between the popover's
* placement and the container edge
*
- * @returns {{top: number, left: number, relativePlacement: string, fit:
- * number, arrow?: {top: number, left: number}}|null}
+ * @returns {GetPopoverScreenCoordinatesResult}
* object with top/left coordinates, the popover's relative position to the anchor, and how well the
- * popover fits in the location (0.0 -> 1.0) oordinates and the popover's relative position, if
+ * popover fits in the location (0.0 -> 1.0) coordinates and the popover's relative position, if
* there is no room in this placement then null
*/
export function getPopoverScreenCoordinates({
@@ -266,7 +282,7 @@ export function getPopoverScreenCoordinates({
arrowConfig,
offset = 0,
buffer = 0,
-}: GetPopoverScreenCoordinatesArgs) {
+}: GetPopoverScreenCoordinatesArgs): GetPopoverScreenCoordinatesResult {
/**
* The goal is to find the best way to align the popover content
* on the given side of the anchor element. The popover prefers
@@ -377,7 +393,7 @@ export function getPopoverScreenCoordinates({
fit,
top: popoverPlacement.top,
left: popoverPlacement.left,
- arrow,
+ arrow: arrow ? { left: arrow.left!, top: arrow.top! } : undefined,
};
}