diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js
index 59b54c2434d17..2c6c60db9a012 100644
--- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js
+++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js
@@ -4,8 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import _ from 'lodash';
import turf from 'turf';
import turfBooleanContains from '@turf/boolean-contains';
+import uuid from 'uuid/v4';
import {
getLayerList,
getLayerListRaw,
@@ -14,7 +16,7 @@ import {
getMapReady,
getWaitingForMapReadyLayerListRaw,
getTransientLayerId,
- getTooltipState,
+ getOpenTooltips,
getQuery,
} from '../selectors/map_selectors';
import { FLYOUT_STATE } from '../reducers/ui';
@@ -63,7 +65,7 @@ export const CLEAR_GOTO = 'CLEAR_GOTO';
export const TRACK_CURRENT_LAYER_STATE = 'TRACK_CURRENT_LAYER_STATE';
export const ROLLBACK_TO_TRACKED_LAYER_STATE = 'ROLLBACK_TO_TRACKED_LAYER_STATE';
export const REMOVE_TRACKED_LAYER_STATE = 'REMOVE_TRACKED_LAYER_STATE';
-export const SET_TOOLTIP_STATE = 'SET_TOOLTIP_STATE';
+export const SET_OPEN_TOOLTIPS = 'SET_OPEN_TOOLTIPS';
export const UPDATE_DRAW_STATE = 'UPDATE_DRAW_STATE';
export const SET_SCROLL_ZOOM = 'SET_SCROLL_ZOOM';
export const SET_MAP_INIT_ERROR = 'SET_MAP_INIT_ERROR';
@@ -221,34 +223,36 @@ function setLayerDataLoadErrorStatus(layerId, errorMessage) {
export function cleanTooltipStateForLayer(layerId, layerFeatures = []) {
return (dispatch, getState) => {
- const tooltipState = getTooltipState(getState());
-
- if (!tooltipState) {
- return;
- }
-
- const nextTooltipFeatures = tooltipState.features.filter(tooltipFeature => {
- if (tooltipFeature.layerId !== layerId) {
- // feature from another layer, keep it
- return true;
- }
-
- // Keep feature if it is still in layer
- return layerFeatures.some(layerFeature => {
- return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id;
+ let featuresRemoved = false;
+ const openTooltips = getOpenTooltips(getState())
+ .map(tooltipState => {
+ const nextFeatures = tooltipState.features.filter(tooltipFeature => {
+ if (tooltipFeature.layerId !== layerId) {
+ // feature from another layer, keep it
+ return true;
+ }
+
+ // Keep feature if it is still in layer
+ return layerFeatures.some(layerFeature => {
+ return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id;
+ });
+ });
+
+ if (tooltipState.features.length !== nextFeatures.length) {
+ featuresRemoved = true;
+ }
+
+ return { ...tooltipState, features: nextFeatures };
+ })
+ .filter(tooltipState => {
+ return tooltipState.features.length > 0;
});
- });
-
- if (tooltipState.features.length === nextTooltipFeatures.length) {
- // no features got removed, nothing to update
- return;
- }
- if (nextTooltipFeatures.length === 0) {
- // all features removed from tooltip, close tooltip
- dispatch(setTooltipState(null));
- } else {
- dispatch(setTooltipState({ ...tooltipState, features: nextTooltipFeatures }));
+ if (featuresRemoved) {
+ dispatch({
+ type: SET_OPEN_TOOLTIPS,
+ openTooltips,
+ });
}
};
}
@@ -412,10 +416,61 @@ export function mapExtentChanged(newMapConstants) {
};
}
-export function setTooltipState(tooltipState) {
+export function closeOnClickTooltip(tooltipId) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: SET_OPEN_TOOLTIPS,
+ openTooltips: getOpenTooltips(getState()).filter(({ id }) => {
+ return tooltipId !== id;
+ }),
+ });
+ };
+}
+
+export function openOnClickTooltip(tooltipState) {
+ return (dispatch, getState) => {
+ const openTooltips = getOpenTooltips(getState()).filter(({ features, location, isLocked }) => {
+ return (
+ isLocked &&
+ !_.isEqual(location, tooltipState.location) &&
+ !_.isEqual(features, tooltipState.features)
+ );
+ });
+
+ openTooltips.push({
+ ...tooltipState,
+ isLocked: true,
+ id: uuid(),
+ });
+
+ dispatch({
+ type: SET_OPEN_TOOLTIPS,
+ openTooltips,
+ });
+ };
+}
+
+export function closeOnHoverTooltip() {
+ return (dispatch, getState) => {
+ if (getOpenTooltips(getState()).length) {
+ dispatch({
+ type: SET_OPEN_TOOLTIPS,
+ openTooltips: [],
+ });
+ }
+ };
+}
+
+export function openOnHoverTooltip(tooltipState) {
return {
- type: 'SET_TOOLTIP_STATE',
- tooltipState: tooltipState,
+ type: SET_OPEN_TOOLTIPS,
+ openTooltips: [
+ {
+ ...tooltipState,
+ isLocked: false,
+ id: uuid(),
+ },
+ ],
};
}
@@ -826,9 +881,9 @@ export function setJoinsForLayer(layer, joins) {
}
export function updateDrawState(drawState) {
- return async dispatch => {
+ return dispatch => {
if (drawState !== null) {
- await dispatch(setTooltipState(null)); //tooltips just get in the way
+ dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way
}
dispatch({
type: UPDATE_DRAW_STATE,
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js
index 0274f849daf3d..9148fbdfd2d1e 100644
--- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js
@@ -16,7 +16,6 @@ import {
setMapInitError,
} from '../../../actions/map_actions';
import {
- getTooltipState,
getLayerList,
getMapReady,
getGoto,
@@ -33,7 +32,6 @@ function mapStateToProps(state = {}) {
layerList: getLayerList(state),
goto: getGoto(state),
inspectorAdapters: getInspectorAdapters(state),
- tooltipState: getTooltipState(state),
scrollZoom: getScrollZoom(state),
disableInteractive: isInteractiveDisabled(state),
disableTooltipControl: isTooltipControlDisabled(state),
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap
index 7e8feeec01bbd..cffa441d04ff5 100644
--- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap
@@ -1,117 +1,97 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`TooltipControl render tooltipState is not provided should not render tooltip popover when tooltipState is not provided 1`] = `""`;
+exports[`TooltipControl render should not render tooltips when there are no open tooltips 1`] = `""`;
-exports[`TooltipControl render tooltipState is provided should render tooltip popover with custom tooltip content when renderTooltipContent provided 1`] = `
-
+exports[`TooltipControl render should render hover tooltip 1`] = `
+
-
- Custom tooltip content
-
-
+/>
`;
-exports[`TooltipControl render tooltipState is provided should render tooltip popover with features tooltip content 1`] = `
-
+exports[`TooltipControl render should render locked tooltip 1`] = `
+
-
-
-
-
+ }
+/>
`;
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap
new file mode 100644
index 0000000000000..d95a418988ae7
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap
@@ -0,0 +1,115 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TooltipPopover render should render tooltip popover 1`] = `
+
+ }
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ id="mapTooltip"
+ isOpen={true}
+ ownFocus={false}
+ panelPaddingSize="m"
+ style={
+ Object {
+ "pointerEvents": "none",
+ "transform": "translate(NaNpx, 2987px)",
+ }
+ }
+>
+
+
+
+
+`;
+
+exports[`TooltipPopover render should render tooltip popover with custom tooltip content when renderTooltipContent provided 1`] = `
+
+ }
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ id="mapTooltip"
+ isOpen={true}
+ ownFocus={false}
+ panelPaddingSize="m"
+ style={
+ Object {
+ "pointerEvents": "none",
+ "transform": "translate(NaNpx, 2987px)",
+ }
+ }
+>
+
+ Custom tooltip content
+
+
+`;
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js
index 6bc9511c6c580..d3cdbfeca3e57 100644
--- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js
@@ -6,28 +6,41 @@
import { connect } from 'react-redux';
import { TooltipControl } from './tooltip_control';
-import { setTooltipState } from '../../../../actions/map_actions';
+import {
+ closeOnClickTooltip,
+ openOnClickTooltip,
+ closeOnHoverTooltip,
+ openOnHoverTooltip,
+} from '../../../../actions/map_actions';
import {
getLayerList,
- getTooltipState,
+ getOpenTooltips,
+ getHasLockedTooltips,
isDrawingFilter,
} from '../../../../selectors/map_selectors';
function mapStateToProps(state = {}) {
return {
layerList: getLayerList(state),
- tooltipState: getTooltipState(state),
+ hasLockedTooltips: getHasLockedTooltips(state),
isDrawingFilter: isDrawingFilter(state),
+ openTooltips: getOpenTooltips(state),
};
}
function mapDispatchToProps(dispatch) {
return {
- setTooltipState(tooltipState) {
- dispatch(setTooltipState(tooltipState));
+ closeOnClickTooltip(tooltipId) {
+ dispatch(closeOnClickTooltip(tooltipId));
+ },
+ openOnClickTooltip(tooltipState) {
+ dispatch(openOnClickTooltip(tooltipState));
+ },
+ closeOnHoverTooltip() {
+ dispatch(closeOnHoverTooltip());
},
- clearTooltipState() {
- dispatch(setTooltipState(null));
+ openOnHoverTooltip(tooltipState) {
+ dispatch(openOnHoverTooltip(tooltipState));
},
};
}
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js
index cfb92a8677455..329d2b7fd2985 100644
--- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js
@@ -6,16 +6,8 @@
import _ from 'lodash';
import React from 'react';
-import { FEATURE_ID_PROPERTY_NAME, LAT_INDEX, LON_INDEX } from '../../../../../common/constants';
-import { FeaturesTooltip } from '../../features_tooltip/features_tooltip';
-import { EuiPopover, EuiText } from '@elastic/eui';
-
-export const TOOLTIP_TYPE = {
- HOVER: 'HOVER',
- LOCKED: 'LOCKED',
-};
-
-const noop = () => {};
+import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../../common/constants';
+import { TooltipPopover } from './tooltip_popover';
function justifyAnchorLocation(mbLngLat, targetFeature) {
let popupAnchorLocation = [mbLngLat.lng, mbLngLat.lat]; // default popup location to mouse location
@@ -35,80 +27,23 @@ function justifyAnchorLocation(mbLngLat, targetFeature) {
}
export class TooltipControl extends React.Component {
- state = {
- x: undefined,
- y: undefined,
- };
-
- constructor(props) {
- super(props);
- this._popoverRef = React.createRef();
- }
-
- static getDerivedStateFromProps(nextProps, prevState) {
- if (nextProps.tooltipState) {
- const nextPoint = nextProps.mbMap.project(nextProps.tooltipState.location);
- if (nextPoint.x !== prevState.x || nextPoint.y !== prevState.y) {
- return {
- x: nextPoint.x,
- y: nextPoint.y,
- };
- }
- }
-
- return null;
- }
-
componentDidMount() {
this.props.mbMap.on('mouseout', this._onMouseout);
this.props.mbMap.on('mousemove', this._updateHoverTooltipState);
- this.props.mbMap.on('move', this._updatePopoverPosition);
this.props.mbMap.on('click', this._lockTooltip);
}
- componentDidUpdate() {
- if (this.props.tooltipState && this._popoverRef.current) {
- this._popoverRef.current.positionPopoverFluid();
- }
- }
-
componentWillUnmount() {
this.props.mbMap.off('mouseout', this._onMouseout);
this.props.mbMap.off('mousemove', this._updateHoverTooltipState);
- this.props.mbMap.off('move', this._updatePopoverPosition);
this.props.mbMap.off('click', this._lockTooltip);
}
_onMouseout = () => {
this._updateHoverTooltipState.cancel();
- if (this.props.tooltipState && this.props.tooltipState.type !== TOOLTIP_TYPE.LOCKED) {
- this.props.clearTooltipState();
- }
- };
-
- _updatePopoverPosition = () => {
- if (!this.props.tooltipState) {
- return;
+ if (!this.props.hasLockedTooltips) {
+ this.props.closeOnHoverTooltip();
}
-
- const lat = this.props.tooltipState.location[LAT_INDEX];
- const lon = this.props.tooltipState.location[LON_INDEX];
- const bounds = this.props.mbMap.getBounds();
- if (
- lat > bounds.getNorth() ||
- lat < bounds.getSouth() ||
- lon < bounds.getWest() ||
- lon > bounds.getEast()
- ) {
- this.props.clearTooltipState();
- return;
- }
-
- const nextPoint = this.props.mbMap.project(this.props.tooltipState.location);
- this.setState({
- x: nextPoint.x,
- y: nextPoint.y,
- });
};
_getLayerByMbLayerId(mbLayerId) {
@@ -148,7 +83,7 @@ export class TooltipControl extends React.Component {
_lockTooltip = e => {
if (this.props.isDrawingFilter) {
- //ignore click events when in draw mode
+ // ignore click events when in draw mode
return;
}
@@ -156,7 +91,7 @@ export class TooltipControl extends React.Component {
const mbFeatures = this._getFeaturesUnderPointer(e.point);
if (!mbFeatures.length) {
- this.props.clearTooltipState();
+ // No features at click location so there is no tooltip to open
return;
}
@@ -164,42 +99,36 @@ export class TooltipControl extends React.Component {
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure);
const features = this._getIdsForFeatures(mbFeatures);
- this.props.setTooltipState({
- type: TOOLTIP_TYPE.LOCKED,
+ this.props.openOnClickTooltip({
features: features,
location: popupAnchorLocation,
});
};
_updateHoverTooltipState = _.debounce(e => {
- if (this.props.isDrawingFilter) {
- //ignore hover events when in draw mode
- return;
- }
-
- if (this.props.tooltipState && this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED) {
- //ignore hover events when tooltip is locked
+ if (this.props.isDrawingFilter || this.props.hasLockedTooltips) {
+ // ignore hover events when in draw mode or when there are locked tooltips
return;
}
const mbFeatures = this._getFeaturesUnderPointer(e.point);
if (!mbFeatures.length) {
- this.props.clearTooltipState();
+ this.props.closeOnHoverTooltip();
return;
}
const targetMbFeature = mbFeatures[0];
- if (this.props.tooltipState) {
- const firstFeature = this.props.tooltipState.features[0];
+ if (this.props.openTooltips[0]) {
+ const firstFeature = this.props.openTooltips[0].features[0];
if (targetMbFeature.properties[FEATURE_ID_PROPERTY_NAME] === firstFeature.id) {
+ // ignore hover events when hover tooltip is all ready opened for feature
return;
}
}
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature);
const features = this._getIdsForFeatures(mbFeatures);
- this.props.setTooltipState({
- type: TOOLTIP_TYPE.HOVER,
+ this.props.openOnHoverTooltip({
features: features,
location: popupAnchorLocation,
});
@@ -240,114 +169,32 @@ export class TooltipControl extends React.Component {
return this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbLayerIds });
}
- // Must load original geometry instead of using geometry from mapbox feature.
- // Mapbox feature geometry is from vector tile and is not the same as the original geometry.
- _loadFeatureGeometry = ({ layerId, featureId }) => {
- const tooltipLayer = this._findLayerById(layerId);
- if (!tooltipLayer) {
- return null;
- }
-
- const targetFeature = tooltipLayer.getFeatureById(featureId);
- if (!targetFeature) {
- return null;
- }
-
- return targetFeature.geometry;
- };
-
- _loadFeatureProperties = async ({ layerId, featureId }) => {
- const tooltipLayer = this._findLayerById(layerId);
- if (!tooltipLayer) {
- return [];
- }
-
- const targetFeature = tooltipLayer.getFeatureById(featureId);
- if (!targetFeature) {
- return [];
- }
- return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties);
- };
-
- _loadPreIndexedShape = async ({ layerId, featureId }) => {
- const tooltipLayer = this._findLayerById(layerId);
- if (!tooltipLayer) {
- return null;
- }
-
- const targetFeature = tooltipLayer.getFeatureById(featureId);
- if (!targetFeature) {
- return null;
- }
-
- return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties);
- };
-
- _findLayerById = layerId => {
- return this.props.layerList.find(layer => {
- return layer.getId() === layerId;
- });
- };
-
- _getLayerName = async layerId => {
- const layer = this._findLayerById(layerId);
- if (!layer) {
+ render() {
+ if (this.props.openTooltips.length === 0) {
return null;
}
- return layer.getDisplayName();
- };
-
- _renderTooltipContent = () => {
- const publicProps = {
- addFilters: this.props.addFilters,
- closeTooltip: this.props.clearTooltipState,
- features: this.props.tooltipState.features,
- isLocked: this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED,
- loadFeatureProperties: this._loadFeatureProperties,
- loadFeatureGeometry: this._loadFeatureGeometry,
- getLayerName: this._getLayerName,
- };
-
- if (this.props.renderTooltipContent) {
- return this.props.renderTooltipContent(publicProps);
- }
-
- return (
-
- {
+ const closeTooltip = isLocked
+ ? () => {
+ this.props.closeOnClickTooltip(id);
+ }
+ : this.props.closeOnHoverTooltip;
+ return (
+
-
- );
- };
-
- render() {
- if (!this.props.tooltipState) {
- return null;
- }
-
- const tooltipAnchor = (
-
- );
- return (
-
- {this._renderTooltipContent()}
-
- );
+ );
+ });
}
}
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js
index b9dc668cfb016..620d7cb9ff756 100644
--- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js
@@ -4,21 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-jest.mock('../../features_tooltip/features_tooltip', () => ({
- FeaturesTooltip: () => {
- return mockFeaturesTooltip
;
+jest.mock('./tooltip_popover', () => ({
+ TooltipPopover: () => {
+ return mockTooltipPopover
;
},
}));
import sinon from 'sinon';
import React from 'react';
import { mount, shallow } from 'enzyme';
-import { TooltipControl, TOOLTIP_TYPE } from './tooltip_control';
+import { TooltipControl } from './tooltip_control';
// mutable map state
let featuresAtLocation;
-let mapCenter;
-let mockMbMapBounds;
const layerId = 'tfi3f';
const mbLayerId = 'tfi3f_circle';
@@ -32,48 +30,16 @@ const mockLayer = {
canShowTooltip: () => {
return true;
},
- getFeatureById: () => {
- return {
- geometry: {
- type: 'Point',
- coordinates: [102.0, 0.5],
- },
- };
- },
};
const mockMbMapHandlers = {};
const mockMBMap = {
- project: lonLatArray => {
- const lonDistanceFromCenter = Math.abs(lonLatArray[0] - mapCenter[0]);
- const latDistanceFromCenter = Math.abs(lonLatArray[1] - mapCenter[1]);
- return {
- x: lonDistanceFromCenter * 100,
- y: latDistanceFromCenter * 100,
- };
- },
on: (eventName, callback) => {
mockMbMapHandlers[eventName] = callback;
},
off: eventName => {
delete mockMbMapHandlers[eventName];
},
- getBounds: () => {
- return {
- getNorth: () => {
- return mockMbMapBounds.north;
- },
- getSouth: () => {
- return mockMbMapBounds.south;
- },
- getWest: () => {
- return mockMbMapBounds.west;
- },
- getEast: () => {
- return mockMbMapBounds.east;
- },
- };
- },
getLayer: () => {},
queryRenderedFeatures: () => {
return featuresAtLocation;
@@ -82,16 +48,21 @@ const mockMBMap = {
const defaultProps = {
mbMap: mockMBMap,
- clearTooltipState: () => {},
- setTooltipState: () => {},
+ closeOnClickTooltip: () => {},
+ openOnClickTooltip: () => {},
+ closeOnHoverTooltip: () => {},
+ openOnHoverTooltip: () => {},
layerList: [mockLayer],
isDrawingFilter: false,
addFilters: () => {},
geoFields: [{}],
+ openTooltips: [],
+ hasLockedTooltips: false,
};
const hoverTooltipState = {
- type: TOOLTIP_TYPE.HOVER,
+ id: '1',
+ isLocked: false,
location: [-120, 30],
features: [
{
@@ -103,7 +74,8 @@ const hoverTooltipState = {
};
const lockedTooltipState = {
- type: TOOLTIP_TYPE.LOCKED,
+ id: '2',
+ isLocked: true,
location: [-120, 30],
features: [
{
@@ -117,82 +89,79 @@ const lockedTooltipState = {
describe('TooltipControl', () => {
beforeEach(() => {
featuresAtLocation = [];
- mapCenter = [0, 0];
- mockMbMapBounds = {
- west: -180,
- east: 180,
- north: 90,
- south: -90,
- };
});
describe('render', () => {
- describe('tooltipState is not provided', () => {
- test('should not render tooltip popover when tooltipState is not provided', () => {
- const component = shallow();
+ test('should not render tooltips when there are no open tooltips', () => {
+ const component = shallow();
- expect(component).toMatchSnapshot();
- });
+ expect(component).toMatchSnapshot();
});
- describe('tooltipState is provided', () => {
- test('should render tooltip popover with features tooltip content', () => {
- const component = shallow(
-
- );
+ test('should render hover tooltip', () => {
+ const component = shallow(
+
+ );
- expect(component).toMatchSnapshot();
- });
+ expect(component).toMatchSnapshot();
+ });
- test('should render tooltip popover with custom tooltip content when renderTooltipContent provided', () => {
- const component = shallow(
- {
- return Custom tooltip content
;
- }}
- />
- );
-
- expect(component).toMatchSnapshot();
- });
+ test('should render locked tooltip', () => {
+ const component = shallow(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('should un-register all map callbacks on unmount', () => {
+ const component = mount();
+
+ expect(Object.keys(mockMbMapHandlers).length).toBe(3);
+
+ component.unmount();
+ expect(Object.keys(mockMbMapHandlers).length).toBe(0);
});
});
describe('on mouse out', () => {
- const clearTooltipStateStub = sinon.stub();
+ const closeOnHoverTooltipStub = sinon.stub();
beforeEach(() => {
- clearTooltipStateStub.reset();
+ closeOnHoverTooltipStub.reset();
});
test('should clear hover tooltip state', () => {
mount(
);
mockMbMapHandlers.mouseout();
- sinon.assert.calledOnce(clearTooltipStateStub);
+ sinon.assert.calledOnce(closeOnHoverTooltipStub);
});
test('should not clear locked tooltip state', () => {
mount(
);
mockMbMapHandlers.mouseout();
- sinon.assert.notCalled(clearTooltipStateStub);
+ sinon.assert.notCalled(closeOnHoverTooltipStub);
});
});
@@ -201,44 +170,44 @@ describe('TooltipControl', () => {
point: { x: 0, y: 0 },
lngLat: { lng: 0, lat: 0 },
};
- const setTooltipStateStub = sinon.stub();
- const clearTooltipStateStub = sinon.stub();
+ const openOnClickTooltipStub = sinon.stub();
+ const closeOnClickTooltipStub = sinon.stub();
beforeEach(() => {
- setTooltipStateStub.reset();
- clearTooltipStateStub.reset();
+ openOnClickTooltipStub.reset();
+ closeOnClickTooltipStub.reset();
});
test('should ignore clicks when map is in drawing mode', () => {
mount(
);
mockMbMapHandlers.click(mockMapMouseEvent);
- sinon.assert.notCalled(clearTooltipStateStub);
- sinon.assert.notCalled(setTooltipStateStub);
+ sinon.assert.notCalled(closeOnClickTooltipStub);
+ sinon.assert.notCalled(openOnClickTooltipStub);
});
- test('should clear tooltip state when there are no features at clicked location', () => {
+ test('should not open tooltip when there are no features at clicked location', () => {
featuresAtLocation = [];
mount(
);
mockMbMapHandlers.click(mockMapMouseEvent);
- sinon.assert.calledOnce(clearTooltipStateStub);
- sinon.assert.notCalled(setTooltipStateStub);
+ sinon.assert.notCalled(closeOnClickTooltipStub);
+ sinon.assert.notCalled(openOnClickTooltipStub);
});
test('should set tooltip state when there are features at clicked location and remove duplicate features', () => {
@@ -258,93 +227,18 @@ describe('TooltipControl', () => {
mount(
);
mockMbMapHandlers.click(mockMapMouseEvent);
- sinon.assert.notCalled(clearTooltipStateStub);
- sinon.assert.calledWith(setTooltipStateStub, {
+ sinon.assert.notCalled(closeOnClickTooltipStub);
+ sinon.assert.calledWith(openOnClickTooltipStub, {
features: [{ id: 1, layerId: 'tfi3f' }],
location: [100, 30],
- type: 'LOCKED',
});
});
});
-
- describe('on map move', () => {
- const clearTooltipStateStub = sinon.stub();
-
- beforeEach(() => {
- clearTooltipStateStub.reset();
- });
-
- test('should safely handle map move when there is no tooltip location', () => {
- const component = mount(
-
- );
-
- mockMbMapHandlers.move();
- component.update();
-
- sinon.assert.notCalled(clearTooltipStateStub);
- });
-
- test('should update popover location', () => {
- const component = mount(
-
- );
-
- // ensure x and y set from original tooltipState.location
- expect(component.state('x')).toBe(12000);
- expect(component.state('y')).toBe(3000);
-
- mapCenter = [25, -15];
- mockMbMapHandlers.move();
- component.update();
-
- // ensure x and y updated from new map center with same tooltipState.location
- expect(component.state('x')).toBe(14500);
- expect(component.state('y')).toBe(4500);
-
- sinon.assert.notCalled(clearTooltipStateStub);
- });
-
- test('should clear tooltip state if tooltip location is outside map bounds', () => {
- const component = mount(
-
- );
-
- // move map bounds outside of hoverTooltipState.location, which is [-120, 30]
- mockMbMapBounds = {
- west: -180,
- east: -170,
- north: 90,
- south: 80,
- };
- mockMbMapHandlers.move();
- component.update();
-
- sinon.assert.calledOnce(clearTooltipStateStub);
- });
- });
-
- test('should un-register all map callbacks on unmount', () => {
- const component = mount();
-
- expect(Object.keys(mockMbMapHandlers).length).toBe(4);
-
- component.unmount();
- expect(Object.keys(mockMbMapHandlers).length).toBe(0);
- });
});
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js
new file mode 100644
index 0000000000000..867c779bc4dba
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js
@@ -0,0 +1,169 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Component } from 'react';
+import { LAT_INDEX, LON_INDEX } from '../../../../../common/constants';
+import { FeaturesTooltip } from '../../features_tooltip/features_tooltip';
+import { EuiPopover, EuiText } from '@elastic/eui';
+
+const noop = () => {};
+
+export class TooltipPopover extends Component {
+ state = {
+ x: undefined,
+ y: undefined,
+ isVisible: true,
+ };
+
+ constructor(props) {
+ super(props);
+ this._popoverRef = React.createRef();
+ }
+
+ componentDidMount() {
+ this._updatePopoverPosition();
+ this.props.mbMap.on('move', this._updatePopoverPosition);
+ }
+
+ componentDidUpdate() {
+ if (this._popoverRef.current) {
+ this._popoverRef.current.positionPopoverFluid();
+ }
+ }
+
+ componentWillUnmount() {
+ this.props.mbMap.off('move', this._updatePopoverPosition);
+ }
+
+ _updatePopoverPosition = () => {
+ const nextPoint = this.props.mbMap.project(this.props.location);
+ const lat = this.props.location[LAT_INDEX];
+ const lon = this.props.location[LON_INDEX];
+ const bounds = this.props.mbMap.getBounds();
+ this.setState({
+ x: nextPoint.x,
+ y: nextPoint.y,
+ isVisible:
+ lat < bounds.getNorth() &&
+ lat > bounds.getSouth() &&
+ lon > bounds.getWest() &&
+ lon < bounds.getEast(),
+ });
+ };
+
+ // Must load original geometry instead of using geometry from mapbox feature.
+ // Mapbox feature geometry is from vector tile and is not the same as the original geometry.
+ _loadFeatureGeometry = ({ layerId, featureId }) => {
+ const tooltipLayer = this._findLayerById(layerId);
+ if (!tooltipLayer) {
+ return null;
+ }
+
+ const targetFeature = tooltipLayer.getFeatureById(featureId);
+ if (!targetFeature) {
+ return null;
+ }
+
+ return targetFeature.geometry;
+ };
+
+ _loadFeatureProperties = async ({ layerId, featureId }) => {
+ const tooltipLayer = this._findLayerById(layerId);
+ if (!tooltipLayer) {
+ return [];
+ }
+
+ const targetFeature = tooltipLayer.getFeatureById(featureId);
+ if (!targetFeature) {
+ return [];
+ }
+ return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties);
+ };
+
+ _loadPreIndexedShape = async ({ layerId, featureId }) => {
+ const tooltipLayer = this._findLayerById(layerId);
+ if (!tooltipLayer) {
+ return null;
+ }
+
+ const targetFeature = tooltipLayer.getFeatureById(featureId);
+ if (!targetFeature) {
+ return null;
+ }
+
+ return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties);
+ };
+
+ _findLayerById = layerId => {
+ return this.props.layerList.find(layer => {
+ return layer.getId() === layerId;
+ });
+ };
+
+ _getLayerName = async layerId => {
+ const layer = this._findLayerById(layerId);
+ if (!layer) {
+ return null;
+ }
+
+ return layer.getDisplayName();
+ };
+
+ _renderTooltipContent = () => {
+ const publicProps = {
+ addFilters: this.props.addFilters,
+ closeTooltip: this.props.closeTooltip,
+ features: this.props.features,
+ isLocked: this.props.isLocked,
+ loadFeatureProperties: this._loadFeatureProperties,
+ loadFeatureGeometry: this._loadFeatureGeometry,
+ getLayerName: this._getLayerName,
+ };
+
+ if (this.props.renderTooltipContent) {
+ return this.props.renderTooltipContent(publicProps);
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ render() {
+ if (!this.state.isVisible) {
+ return null;
+ }
+
+ const tooltipAnchor = ;
+ // Although tooltip anchors are not visible, they take up horizontal space.
+ // This horizontal spacing needs to be accounted for in the translate function,
+ // otherwise the anchors get increasingly pushed to the right away from the actual location.
+ const offset = this.props.index * 26;
+ return (
+
+ {this._renderTooltipContent()}
+
+ );
+ }
+}
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js
new file mode 100644
index 0000000000000..bcef03c205b2b
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js
@@ -0,0 +1,142 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+jest.mock('../../features_tooltip/features_tooltip', () => ({
+ FeaturesTooltip: () => {
+ return mockFeaturesTooltip
;
+ },
+}));
+
+import sinon from 'sinon';
+import React from 'react';
+import { mount, shallow } from 'enzyme';
+import { TooltipPopover } from './tooltip_popover';
+
+// mutable map state
+let mapCenter;
+let mockMbMapBounds;
+
+const layerId = 'tfi3f';
+
+const mockMbMapHandlers = {};
+const mockMBMap = {
+ project: lonLatArray => {
+ const lonDistanceFromCenter = Math.abs(lonLatArray[0] - mapCenter[0]);
+ const latDistanceFromCenter = Math.abs(lonLatArray[1] - mapCenter[1]);
+ return {
+ x: lonDistanceFromCenter * 100,
+ y: latDistanceFromCenter * 100,
+ };
+ },
+ on: (eventName, callback) => {
+ mockMbMapHandlers[eventName] = callback;
+ },
+ off: eventName => {
+ delete mockMbMapHandlers[eventName];
+ },
+ getBounds: () => {
+ return {
+ getNorth: () => {
+ return mockMbMapBounds.north;
+ },
+ getSouth: () => {
+ return mockMbMapBounds.south;
+ },
+ getWest: () => {
+ return mockMbMapBounds.west;
+ },
+ getEast: () => {
+ return mockMbMapBounds.east;
+ },
+ };
+ },
+};
+
+const defaultProps = {
+ mbMap: mockMBMap,
+ closeTooltip: () => {},
+ layerList: [],
+ isDrawingFilter: false,
+ addFilters: () => {},
+ geoFields: [{}],
+ location: [-120, 30],
+ features: [
+ {
+ id: 1,
+ layerId: layerId,
+ geometry: {},
+ },
+ ],
+ isLocked: false,
+};
+
+describe('TooltipPopover', () => {
+ beforeEach(() => {
+ mapCenter = [0, 0];
+ mockMbMapBounds = {
+ west: -180,
+ east: 180,
+ north: 90,
+ south: -90,
+ };
+ });
+
+ describe('render', () => {
+ test('should render tooltip popover', () => {
+ const component = shallow();
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('should render tooltip popover with custom tooltip content when renderTooltipContent provided', () => {
+ const component = shallow(
+ {
+ return Custom tooltip content
;
+ }}
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('should un-register all map callbacks on unmount', () => {
+ const component = mount();
+
+ expect(Object.keys(mockMbMapHandlers).length).toBe(1);
+
+ component.unmount();
+ expect(Object.keys(mockMbMapHandlers).length).toBe(0);
+ });
+ });
+
+ describe('on map move', () => {
+ const closeTooltipStub = sinon.stub();
+
+ beforeEach(() => {
+ closeTooltipStub.reset();
+ });
+
+ test('should update popover location', () => {
+ const component = mount();
+
+ // ensure x and y set from original tooltipState.location
+ expect(component.state('x')).toBe(12000);
+ expect(component.state('y')).toBe(3000);
+
+ mapCenter = [25, -15];
+ mockMbMapHandlers.move();
+ component.update();
+
+ // ensure x and y updated from new map center with same tooltipState.location
+ expect(component.state('x')).toBe(14500);
+ expect(component.state('y')).toBe(4500);
+
+ sinon.assert.notCalled(closeTooltipStub);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/maps/public/reducers/map.js b/x-pack/legacy/plugins/maps/public/reducers/map.js
index 234584d08a311..7e81fb03dd85b 100644
--- a/x-pack/legacy/plugins/maps/public/reducers/map.js
+++ b/x-pack/legacy/plugins/maps/public/reducers/map.js
@@ -37,7 +37,7 @@ import {
ROLLBACK_TO_TRACKED_LAYER_STATE,
REMOVE_TRACKED_LAYER_STATE,
UPDATE_SOURCE_DATA_REQUEST,
- SET_TOOLTIP_STATE,
+ SET_OPEN_TOOLTIPS,
SET_SCROLL_ZOOM,
SET_MAP_INIT_ERROR,
UPDATE_DRAW_STATE,
@@ -97,7 +97,7 @@ const INITIAL_STATE = {
ready: false,
mapInitError: null,
goto: null,
- tooltipState: null,
+ openTooltips: [],
mapState: {
zoom: null, // setting this value does not adjust map zoom, read only value used to store current map zoom for persisting between sessions
center: null, // setting this value does not adjust map view, read only value used to store current map center for persisting between sessions
@@ -138,10 +138,10 @@ export function map(state = INITIAL_STATE, action) {
return trackCurrentLayerState(state, action.layerId);
case ROLLBACK_TO_TRACKED_LAYER_STATE:
return rollbackTrackedLayerState(state, action.layerId);
- case SET_TOOLTIP_STATE:
+ case SET_OPEN_TOOLTIPS:
return {
...state,
- tooltipState: action.tooltipState,
+ openTooltips: action.openTooltips,
};
case SET_MOUSE_COORDINATES:
return {
diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js
index 4b3d1355e4264..d1048a759beca 100644
--- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js
+++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js
@@ -42,8 +42,14 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) {
return new Source(sourceDescriptor, inspectorAdapters);
}
-export const getTooltipState = ({ map }) => {
- return map.tooltipState;
+export const getOpenTooltips = ({ map }) => {
+ return map && map.openTooltips ? map.openTooltips : [];
+};
+
+export const getHasLockedTooltips = state => {
+ return getOpenTooltips(state).some(({ isLocked }) => {
+ return isLocked;
+ });
};
export const getMapReady = ({ map }) => map && map.ready;