[Maps] use EuiPopover instead of mapbox tooltip (#45938)

* [Maps] use EuiPopover instead of mapbox tooltip

* update draw tooltip to use EuiPopover, remove mapbox popup css hacks

* use anchor size of 26px to fix positioning calculations

* close tooltip when position is no longer visible

* remove reevaluateTooltipPosition, its no longer needed

* fix problem with map move and hover tooltip location not also moving
This commit is contained in:
Nathan Reese 2019-09-18 18:22:17 -06:00 committed by GitHub
parent 921e356a4b
commit 1ef6373f77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 132 additions and 137 deletions

View file

@ -3,22 +3,6 @@
.mapContainer {
flex-grow: 1;
.mapboxgl-popup {
z-index: $euiZLevel2;
border-color: $euiColorEmptyShade;
}
.mapboxgl-popup-content {
@include euiBottomShadow($color: #000);
background-color: $euiColorEmptyShade;
padding: $euiSizeS;
overflow: hidden;
}
.mapboxgl-popup-tip {
border-top-color: $euiColorEmptyShade !important;
}
.mapboxgl-ctrl-top-left .mapboxgl-ctrl {
margin-left: $euiSizeM;
margin-top: $euiSizeM;

View file

@ -16,14 +16,6 @@ import { GeometryFilterForm } from '../../components/geometry_filter_form';
export class FeatureGeometryFilterForm extends Component {
componentDidMount() {
this.props.reevaluateTooltipPosition();
}
componentDidUpdate() {
this.props.reevaluateTooltipPosition();
}
_createFilter = ({ geometryLabel, indexPatternId, geoFieldName, geoFieldType, relation }) => {
const filter = createSpatialFilterWithGeometry({
geometry: this.props.feature.geometry,

View file

@ -30,7 +30,6 @@ export class FeatureProperties extends React.Component {
componentDidUpdate() {
this._loadProperties();
this.props.reevaluateTooltipPosition();
}
componentWillUnmount() {

View file

@ -34,7 +34,6 @@ const defaultProps = {
layerId: `layer`,
onCloseTooltip: () => {},
showFilterButtons: false,
reevaluateTooltipPosition: () => {},
};
const mockTooltipProperties = [

View file

@ -154,7 +154,6 @@ export class FeatureTooltip extends React.Component {
showFilterButtons={this.props.showFilterButtons}
onCloseTooltip={this._onCloseTooltip}
addFilters={this.props.addFilters}
reevaluateTooltipPosition={this.props.reevaluateTooltipPosition}
/>
);
}
@ -352,7 +351,6 @@ export class FeatureTooltip extends React.Component {
feature={currentFeature}
geoFields={filteredGeoFields}
addFilters={this.props.addFilters}
reevaluateTooltipPosition={this.props.reevaluateTooltipPosition}
/>
);
}

View file

@ -6,44 +6,43 @@
import _ from 'lodash';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { EuiText } from '@elastic/eui';
import { EuiPopover, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DRAW_TYPE } from '../../../../../common/constants';
import mapboxgl from 'mapbox-gl';
import { I18nProvider } from '@kbn/i18n/react';
const noop = () => {};
export class DrawTooltip extends Component {
state = {
x: undefined,
y: undefined,
isOpen: false,
};
constructor(props) {
super(props);
this._tooltipContainer = document.createElement('div');
this._mbPopup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
offset: 12,
anchor: 'left',
maxWidth: '300px',
});
this._popoverRef = React.createRef();
}
componentDidMount() {
this._showTooltip();
this.props.mbMap.on('mousemove', this._updateTooltipLocation);
this.props.mbMap.on('mouseout', this._hideTooltip);
}
componentDidUpdate() {
if (this._popoverRef.current) {
this._popoverRef.current.positionPopoverFluid();
}
}
componentWillUnmount() {
this.props.mbMap.off('mousemove', this._updateTooltipLocation);
this.props.mbMap.off('mouseout', this._hideTooltip);
this._hideTooltip();
this._updateTooltipLocation.cancel();
}
render() {
return null;
}
_showTooltip = () => {
const instructions = this.props.drawState.drawType === DRAW_TYPE.BOUNDS
? i18n.translate('xpack.maps.drawTooltip.boundsInstructions', {
defaultMessage: 'Click to start rectangle. Click again to finish.'
@ -51,31 +50,40 @@ export class DrawTooltip extends Component {
: i18n.translate('xpack.maps.drawTooltip.polygonInstructions', {
defaultMessage: 'Click to add vertex. Double click to finish.'
});
ReactDOM.render((
<I18nProvider>
<div>
<EuiText color="subdued" size="xs">
{instructions}
</EuiText>
</div>
</I18nProvider>
), this._tooltipContainer);
this._mbPopup.setDOMContent(this._tooltipContainer)
.addTo(this.props.mbMap);
const tooltipAnchor = <div style={{ height: '26px', width: '26px', background: 'transparent' }}/>;
return (
<EuiPopover
id="drawInstructionsTooltip"
button={tooltipAnchor}
anchorPosition="rightCenter"
isOpen={this.state.isOpen}
closePopover={noop}
ref={this._popoverRef}
style={{
pointerEvents: 'none',
transform: `translate(${this.state.x - 13}px, ${this.state.y - 13}px)`
}}
>
<EuiText color="subdued" size="xs">
{instructions}
</EuiText>
</EuiPopover>
);
}
_hideTooltip = () => {
if (this._mbPopup.isOpen()) {
this._mbPopup.remove();
ReactDOM.unmountComponentAtNode(this._tooltipContainer);
}
this._updateTooltipLocation.cancel();
this.setState({ isOpen: false });
}
_updateTooltipLocation = _.throttle(({ lngLat }) => {
if (!this._mbPopup.isOpen()) {
this._showTooltip();
}
this._mbPopup.setLngLat(lngLat);
const mouseLocation = this.props.mbMap.project(lngLat);
this.setState({
isOpen: true,
x: mouseLocation.x,
y: mouseLocation.y,
});
}, 100)
}

View file

@ -6,20 +6,21 @@
import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import {
FEATURE_ID_PROPERTY_NAME,
LAT_INDEX,
LON_INDEX,
} from '../../../../../common/constants';
import mapboxgl from 'mapbox-gl';
import { FeatureTooltip } from '../../feature_tooltip';
import { EuiPopover, EuiText } from '@elastic/eui';
const TOOLTIP_TYPE = {
HOVER: 'HOVER',
LOCKED: 'LOCKED'
};
const noop = () => {};
function justifyAnchorLocation(mbLngLat, targetFeature) {
let popupAnchorLocation = [mbLngLat.lng, mbLngLat.lat]; // default popup location to mouse location
if (targetFeature.geometry.type === 'Point') {
@ -39,34 +40,48 @@ function justifyAnchorLocation(mbLngLat, targetFeature) {
export class TooltipControl extends React.Component {
state = {
x: undefined,
y: undefined,
};
constructor(props) {
super(props);
this._tooltipContainer = document.createElement('div');
this._mbPopup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
maxWidth: '260px', // width of table columns max-widths plus all padding
});
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._isMounted = true;
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() {
this._syncTooltipState();
if (this.props.tooltipState && this._popoverRef.current) {
this._popoverRef.current.positionPopoverFluid();
}
}
componentWillUnmount() {
this._isMounted = false;
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);
this._hideTooltip();
}
_onMouseout = () => {
@ -76,6 +91,29 @@ export class TooltipControl extends React.Component {
}
}
_updatePopoverPosition = () => {
if (!this.props.tooltipState) {
return;
}
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) {
return this.props.layerList.find((layer) => {
const mbLayerIds = layer.getMbLayerIds();
@ -113,7 +151,6 @@ export class TooltipControl extends React.Component {
}
_lockTooltip = (e) => {
if (this.props.isDrawingFilter) {
//ignore click events when in draw mode
return;
@ -139,7 +176,6 @@ export class TooltipControl extends React.Component {
};
_updateHoverTooltipState = _.debounce((e) => {
if (this.props.isDrawingFilter) {
//ignore hover events when in draw mode
return;
@ -175,7 +211,6 @@ export class TooltipControl extends React.Component {
}, 100);
_getMbLayerIdsForTooltips() {
const mbLayerIds = this.props.layerList.reduce((mbLayerIds, layer) => {
return layer.canShowTooltip() ? mbLayerIds.concat(layer.getMbLayerIds()) : mbLayerIds;
}, []);
@ -191,7 +226,6 @@ export class TooltipControl extends React.Component {
}
_getFeaturesUnderPointer(mbLngLatPoint) {
if (!this.props.mbMap) {
return [];
}
@ -211,49 +245,6 @@ export class TooltipControl extends React.Component {
return this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbLayerIds });
}
_reevaluateTooltipPosition = () => {
// Force mapbox to ensure tooltip does not clip map boundary and move anchor when clipping occurs
requestAnimationFrame(() => {
if (this._isMounted && this.props.tooltipState && this.props.tooltipState.location) {
this._mbPopup.setLngLat(this.props.tooltipState.location);
}
});
}
_hideTooltip() {
if (this._mbPopup.isOpen()) {
this._mbPopup.remove();
ReactDOM.unmountComponentAtNode(this._tooltipContainer);
}
}
_showTooltip() {
if (!this._isMounted) {
return;
}
const isLocked = this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED;
ReactDOM.render((
<I18nProvider>
<FeatureTooltip
features={this.props.tooltipState.features}
anchorLocation={this.props.tooltipState.location}
loadFeatureProperties={this._loadFeatureProperties}
findLayerById={this._findLayerById}
closeTooltip={this.props.clearTooltipState}
showFilterButtons={!!this.props.addFilters && isLocked}
isLocked={isLocked}
addFilters={this.props.addFilters}
geoFields={this.props.geoFields}
reevaluateTooltipPosition={this._reevaluateTooltipPosition}
/>
</I18nProvider>
), this._tooltipContainer);
this._mbPopup.setLngLat(this.props.tooltipState.location)
.setDOMContent(this._tooltipContainer)
.addTo(this.props.mbMap);
}
_loadFeatureProperties = async ({ layerId, featureId }) => {
const tooltipLayer = this._findLayerById(layerId);
if (!tooltipLayer) {
@ -272,17 +263,41 @@ export class TooltipControl extends React.Component {
});
};
_syncTooltipState() {
if (this.props.tooltipState) {
this.props.mbMap.getCanvas().style.cursor = 'pointer';
this._showTooltip();
} else {
this.props.mbMap.getCanvas().style.cursor = '';
this._hideTooltip();
}
}
render() {
return null;
if (!this.props.tooltipState) {
return null;
}
const tooltipAnchor = <div style={{ height: '26px', width: '26px', background: 'transparent' }}/>;
const isLocked = this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED;
return (
<EuiPopover
id="mapTooltip"
button={tooltipAnchor}
anchorPosition="upCenter"
isOpen
closePopover={noop}
ref={this._popoverRef}
style={{
pointerEvents: 'none',
transform: `translate(${this.state.x - 13}px, ${this.state.y - 13}px)`
}}
>
<EuiText size="xs">
<FeatureTooltip
features={this.props.tooltipState.features}
anchorLocation={this.props.tooltipState.location}
loadFeatureProperties={this._loadFeatureProperties}
findLayerById={this._findLayerById}
closeTooltip={this.props.clearTooltipState}
showFilterButtons={!!this.props.addFilters && isLocked}
isLocked={isLocked}
addFilters={this.props.addFilters}
geoFields={this.props.geoFields}
/>
</EuiText>
</EuiPopover>
);
}
}