MapScaleControlStyles in emotion (#221156)

Part of #207852

Before:


https://github.com/user-attachments/assets/5e69db7f-2437-4166-b52e-72c16fd950d8

After:


https://github.com/user-attachments/assets/86a7b6d8-b8fd-4d0b-9283-fe77fa0d8f98

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ola Pawlus 2025-05-24 10:47:47 +02:00 committed by GitHub
parent 43431d55e4
commit 5f85a5dee5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 266 additions and 119 deletions

View file

@ -4,4 +4,3 @@
@import 'toolbar_overlay/index';
@import 'mb_map/keydown_scroll_zoom/index';
@import 'mb_map/tooltip_control/features_tooltip/index';
@import 'mb_map/scale_control/index';

View file

@ -1,27 +1,206 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`isFullScreen 1`] = `
<div
className="mapScaleControl mapScaleControlFullScreen"
style={
<ScaleControl
isFullScreen={true}
mbMap={
Object {
"width": "62.7474245034397px",
"getBounds": [Function],
"getContainer": [Function],
"getZoom": [Function],
"off": [Function],
"on": [Function],
"unproject": [Function],
}
}
>
~50 km
</div>
<div
css="unknown styles"
style={
Object {
"width": "62.7474245034397px",
}
}
>
<Insertion
cache={
Object {
"insert": [Function],
"inserted": Object {
"1hok13p-mapScaleControlStyles-ScaleControl": true,
"5ojysv-mapScaleControlStyles-mapScaleControlFullScreenStyles-ScaleControl": true,
},
"key": "css",
"nonce": undefined,
"registered": Object {},
"sheet": StyleSheet {
"_alreadyInsertedOrderInsensitiveRule": true,
"_insertTag": [Function],
"before": null,
"container": <head>
<style
data-emotion="css"
data-s=""
>
.css-721pd1-floatTopRight{position:absolute;top:-8px;right:-8px;}
</style>
<style
data-emotion="css"
data-s=""
>
.css-1hok13p-mapScaleControlStyles-ScaleControl{position:absolute;z-index:1000;left:12px;bottom:12px;pointer-events:none;color:#1D2A3E;border-left:2px solid #1D2A3E99;border-bottom:2px solid #1D2A3E99;text-align:right;background-color:#F6F9FC;padding-left:4px;padding-right:4px;}
</style>
<style
data-emotion="css"
data-s=""
>
.css-5ojysv-mapScaleControlStyles-mapScaleControlFullScreenStyles-ScaleControl{position:absolute;z-index:1000;left:12px;bottom:12px;pointer-events:none;color:#1D2A3E;border-left:2px solid #1D2A3E99;border-bottom:2px solid #1D2A3E99;text-align:right;background-color:#F6F9FC;padding-left:4px;padding-right:4px;bottom:calc(24px * 2);}
</style>
</head>,
"ctr": 2,
"insertionPoint": undefined,
"isSpeedy": false,
"key": "css",
"nonce": undefined,
"prepend": undefined,
"tags": Array [
<style
data-emotion="css"
data-s=""
>
.css-1hok13p-mapScaleControlStyles-ScaleControl{position:absolute;z-index:1000;left:12px;bottom:12px;pointer-events:none;color:#1D2A3E;border-left:2px solid #1D2A3E99;border-bottom:2px solid #1D2A3E99;text-align:right;background-color:#F6F9FC;padding-left:4px;padding-right:4px;}
</style>,
<style
data-emotion="css"
data-s=""
>
.css-5ojysv-mapScaleControlStyles-mapScaleControlFullScreenStyles-ScaleControl{position:absolute;z-index:1000;left:12px;bottom:12px;pointer-events:none;color:#1D2A3E;border-left:2px solid #1D2A3E99;border-bottom:2px solid #1D2A3E99;text-align:right;background-color:#F6F9FC;padding-left:4px;padding-right:4px;bottom:calc(24px * 2);}
</style>,
],
},
}
}
isStringTag={true}
serialized={
Object {
"map": undefined,
"name": "5ojysv-mapScaleControlStyles-mapScaleControlFullScreenStyles-ScaleControl",
"next": undefined,
"styles": "position:absolute;z-index:1000;left:12px;bottom:12px;pointer-events:none;color:#1D2A3E;border-left:2px solid #1D2A3E99;border-bottom:2px solid #1D2A3E99;text-align:right;background-color:#F6F9FC;padding-left:4px;padding-right:4px;;label:mapScaleControlStyles;;;bottom:calc(24px * 2);;label:mapScaleControlFullScreenStyles;;;;label:ScaleControl;;",
"toString": [Function],
}
}
/>
<div
className="css-5ojysv-mapScaleControlStyles-mapScaleControlFullScreenStyles-ScaleControl"
style={
Object {
"width": "62.7474245034397px",
}
}
>
~50 km
</div>
</div>
</ScaleControl>
`;
exports[`render 1`] = `
<div
className="mapScaleControl"
style={
<ScaleControl
isFullScreen={false}
mbMap={
Object {
"width": "62.7474245034397px",
"getBounds": [Function],
"getContainer": [Function],
"getZoom": [Function],
"off": [Function],
"on": [Function],
"unproject": [Function],
}
}
>
~50 km
</div>
<div
css="unknown styles"
style={
Object {
"width": "62.7474245034397px",
}
}
>
<Insertion
cache={
Object {
"insert": [Function],
"inserted": Object {
"1hok13p-mapScaleControlStyles-ScaleControl": true,
},
"key": "css",
"nonce": undefined,
"registered": Object {},
"sheet": StyleSheet {
"_alreadyInsertedOrderInsensitiveRule": true,
"_insertTag": [Function],
"before": null,
"container": <head>
<style
data-emotion="css"
data-s=""
>
.css-721pd1-floatTopRight{position:absolute;top:-8px;right:-8px;}
</style>
<style
data-emotion="css"
data-s=""
>
.css-1hok13p-mapScaleControlStyles-ScaleControl{position:absolute;z-index:1000;left:12px;bottom:12px;pointer-events:none;color:#1D2A3E;border-left:2px solid #1D2A3E99;border-bottom:2px solid #1D2A3E99;text-align:right;background-color:#F6F9FC;padding-left:4px;padding-right:4px;}
</style>
</head>,
"ctr": 1,
"insertionPoint": undefined,
"isSpeedy": false,
"key": "css",
"nonce": undefined,
"prepend": undefined,
"tags": Array [
<style
data-emotion="css"
data-s=""
>
.css-1hok13p-mapScaleControlStyles-ScaleControl{position:absolute;z-index:1000;left:12px;bottom:12px;pointer-events:none;color:#1D2A3E;border-left:2px solid #1D2A3E99;border-bottom:2px solid #1D2A3E99;text-align:right;background-color:#F6F9FC;padding-left:4px;padding-right:4px;}
</style>,
],
},
}
}
isStringTag={true}
serialized={
Object {
"map": undefined,
"name": "1hok13p-mapScaleControlStyles-ScaleControl",
"next": undefined,
"styles": "position:absolute;z-index:1000;left:12px;bottom:12px;pointer-events:none;color:#1D2A3E;border-left:2px solid #1D2A3E99;border-bottom:2px solid #1D2A3E99;text-align:right;background-color:#F6F9FC;padding-left:4px;padding-right:4px;;label:mapScaleControlStyles;;;;;label:ScaleControl;;",
"toString": [Function],
}
}
/>
<div
className="css-1hok13p-mapScaleControlStyles-ScaleControl"
style={
Object {
"width": "62.7474245034397px",
}
}
>
~50 km
</div>
</div>
</ScaleControl>
`;

View file

@ -1,16 +0,0 @@
.mapScaleControl {
position: absolute;
z-index: $euiZLevel1;
left: $euiSizeM;
bottom: $euiSizeM;
pointer-events: none;
color: $euiTextColor;
border-left: 2px solid rgba($euiTextColor, .6);
border-bottom: 2px solid rgba($euiTextColor, .6);
text-align: right;
@include mapOverlayIsTextOnly;
}
.mapScaleControlFullScreen {
bottom: $euiSizeL * 2;
}

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { mount, shallow } from 'enzyme';
import { mount } from 'enzyme';
import { ScaleControl } from './scale_control';
import type { LngLat, LngLatBounds, Map as MapboxMap, PointLike } from '@kbn/mapbox-gl';
@ -49,12 +49,12 @@ const mockMBMap = {
} as unknown as MapboxMap;
test('render', () => {
const component = shallow(<ScaleControl mbMap={mockMBMap} isFullScreen={false} />);
const component = mount(<ScaleControl mbMap={mockMBMap} isFullScreen={false} />);
expect(component).toMatchSnapshot();
});
test('isFullScreen', () => {
const component = shallow(<ScaleControl mbMap={mockMBMap} isFullScreen={true} />);
const component = mount(<ScaleControl mbMap={mockMBMap} isFullScreen={true} />);
expect(component).toMatchSnapshot();
});

View file

@ -6,9 +6,12 @@
*/
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
import React, { Component } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { UseEuiTheme } from '@elastic/eui';
import type { Map as MapboxMap } from '@kbn/mapbox-gl';
import { css } from '@emotion/react';
import { useMemoizedStyles } from '@kbn/core/public';
const MAX_WIDTH = 110;
interface Props {
@ -16,107 +19,89 @@ interface Props {
mbMap: MapboxMap;
}
interface State {
label: string;
width: number;
}
function getScaleDistance(value: number) {
const getScaleDistance = (value: number) => {
const orderOfMagnitude = Math.floor(Math.log10(value));
const pow10 = Math.pow(10, orderOfMagnitude);
// reduce value to single order of magnitude to making rounding simple regardless of order of magnitude
const distance = value / pow10;
if (distance < 1) {
return pow10 * (Math.round(distance * 10) / 10);
}
if (distance < 1) return pow10 * (Math.round(distance * 10) / 10);
// provide easy to multiple round numbers for scale distance so its easy to measure distances longer then the scale
if (distance >= 10) {
return 10 * pow10;
}
if (distance >= 5) {
return 5 * pow10;
}
if (distance >= 3) {
return 3 * pow10;
}
if (distance >= 10) return 10 * pow10;
if (distance >= 5) return 5 * pow10;
if (distance >= 3) return 3 * pow10;
return Math.floor(distance) * pow10;
}
};
export class ScaleControl extends Component<Props, State> {
private _isMounted: boolean = false;
const componentStyles = {
mapScaleControlStyles: ({ euiTheme }: UseEuiTheme) =>
css({
position: 'absolute',
zIndex: euiTheme.levels.header,
left: euiTheme.size.m,
bottom: euiTheme.size.m,
pointerEvents: 'none',
color: euiTheme.colors.textParagraph,
borderLeft: `2px solid ${euiTheme.colors.textParagraph}99`,
borderBottom: `2px solid ${euiTheme.colors.textParagraph}99`,
textAlign: 'right',
backgroundColor: euiTheme.colors.backgroundBaseSubdued,
paddingLeft: euiTheme.size.xs,
paddingRight: euiTheme.size.xs,
}),
mapScaleControlFullScreenStyles: ({ euiTheme }: UseEuiTheme) =>
css({
bottom: `calc(${euiTheme.size.l} * 2)`,
}),
};
state: State = { label: '', width: 0 };
componentDidMount() {
this._isMounted = true;
this.props.mbMap.on('move', this._onUpdate);
this._onUpdate();
}
componentWillUnmount() {
this._isMounted = false;
this.props.mbMap.off('move', this._onUpdate);
}
_onUpdate = () => {
if (!this._isMounted) {
return;
}
const centerHeight = this.props.mbMap.getContainer().clientHeight / 2;
const leftLatLon = this.props.mbMap.unproject([0, centerHeight]);
const rightLatLon = this.props.mbMap.unproject([MAX_WIDTH, centerHeight]);
export const ScaleControl: React.FC<Props> = ({ isFullScreen, mbMap }) => {
const [label, setLabel] = useState('');
const [width, setWidth] = useState(0);
const styles = useMemoizedStyles(componentStyles);
const onUpdate = useCallback(() => {
const container = mbMap.getContainer();
const centerHeight = container.clientHeight / 2;
const leftLatLon = mbMap.unproject([0, centerHeight]);
const rightLatLon = mbMap.unproject([MAX_WIDTH, centerHeight]);
const maxDistanceMeters = leftLatLon.distanceTo(rightLatLon);
if (maxDistanceMeters >= 1000) {
this._setScale(
maxDistanceMeters / 1000,
i18n.translate('xpack.maps.kilometersAbbr', {
defaultMessage: 'km',
})
);
} else {
this._setScale(
maxDistanceMeters,
i18n.translate('xpack.maps.metersAbbr', {
defaultMessage: 'm',
})
);
}
};
_setScale(maxDistance: number, unit: string) {
const isKm = maxDistanceMeters >= 1000;
const unit = isKm
? i18n.translate('xpack.maps.kilometersAbbr', { defaultMessage: 'km' })
: i18n.translate('xpack.maps.metersAbbr', { defaultMessage: 'm' });
const maxDistance = isKm ? maxDistanceMeters / 1000 : maxDistanceMeters;
const scaleDistance = getScaleDistance(maxDistance);
const zoom = this.props.mbMap.getZoom();
const bounds = this.props.mbMap.getBounds();
let label = `${scaleDistance} ${unit}`;
const zoom = mbMap.getZoom();
const bounds = mbMap.getBounds();
let nextLabel = `${scaleDistance} ${unit}`;
if (
zoom <= 4 ||
(zoom <= 6 && (bounds.getNorth() > 23.5 || bounds.getSouth() < -23.5)) ||
(zoom <= 8 && (bounds.getNorth() > 45 || bounds.getSouth() < -45))
) {
label = '~' + label;
nextLabel = `~${nextLabel}`;
}
this.setState({
width: MAX_WIDTH * (scaleDistance / maxDistance),
label,
});
}
render() {
return (
<div
className={classNames('mapScaleControl', {
mapScaleControlFullScreen: this.props.isFullScreen,
})}
style={{ width: `${this.state.width}px` }}
>
{this.state.label}
</div>
);
}
}
setLabel(nextLabel);
setWidth(MAX_WIDTH * (scaleDistance / maxDistance));
}, [mbMap]);
useEffect(() => {
mbMap.on('move', onUpdate);
onUpdate();
return () => {
mbMap.off('move', onUpdate);
};
}, [mbMap, onUpdate]);
return (
<div
css={[styles.mapScaleControlStyles, isFullScreen && styles.mapScaleControlFullScreenStyles]}
style={{ width: `${width}px` }}
>
{label}
</div>
);
};