diff --git a/src/Button/CopyButton/CopyButton.example.md b/src/Button/CopyButton/CopyButton.example.md index a9ce5a55df..326949d8d8 100644 --- a/src/Button/CopyButton/CopyButton.example.md +++ b/src/Button/CopyButton/CopyButton.example.md @@ -3,7 +3,7 @@ This demonstrates the use of the CopyButton. ```jsx import CopyButton from '@terrestris/react-geo/dist/Button/CopyButton/CopyButton'; import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; -import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext' +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import {DigitizeUtil} from '@terrestris/react-util/dist/Util/DigitizeUtil'; import OlFormatGeoJSON from 'ol/format/GeoJSON'; import OlLayerTile from 'ol/layer/Tile'; @@ -22,6 +22,7 @@ const features = format.readFeatures(featuresJson); const CopyButtonExample = () => { const [map, setMap] = useState(); + const [pressed, setPressed] = useState(); useEffect(() => { const newMap = new OlMap({ @@ -57,7 +58,10 @@ const CopyButtonExample = () => { }} /> - + setPressed(!pressed)} + pressed={pressed} + > Copy feature diff --git a/src/Button/CopyButton/CopyButton.spec.tsx b/src/Button/CopyButton/CopyButton.spec.tsx index 8640b12abf..0fbb081331 100644 --- a/src/Button/CopyButton/CopyButton.spec.tsx +++ b/src/Button/CopyButton/CopyButton.spec.tsx @@ -74,6 +74,8 @@ describe('', () => { const button = screen.getByRole('button'); await userEvent.click(button); + renderInMapContext(map, ); + expect(layer.getSource()?.getFeatures()).toHaveLength(1); clickMap(map, 200, 200); diff --git a/src/Button/CopyButton/CopyButton.tsx b/src/Button/CopyButton/CopyButton.tsx index 24111f30c1..7cea91d3b7 100644 --- a/src/Button/CopyButton/CopyButton.tsx +++ b/src/Button/CopyButton/CopyButton.tsx @@ -88,13 +88,15 @@ const CopyButton: React.FC = ({ return null; } - return ; + return ( + + ); }; diff --git a/src/Button/DeleteButton/DeleteButton.example.md b/src/Button/DeleteButton/DeleteButton.example.md index bebb48ef8d..fafb0af1dc 100644 --- a/src/Button/DeleteButton/DeleteButton.example.md +++ b/src/Button/DeleteButton/DeleteButton.example.md @@ -20,6 +20,7 @@ const features = format.readFeatures(federalStates); const DeleteButtonExample = () => { const [map, setMap] = useState(); + const [pressed, setPressed] = useState(); useEffect(() => { const newMap = new OlMap({ @@ -54,7 +55,10 @@ const DeleteButtonExample = () => { height: '400px' }} /> - + setPressed(!pressed)} + pressed={pressed} + > Delete feature diff --git a/src/Button/DeleteButton/DeleteButton.spec.tsx b/src/Button/DeleteButton/DeleteButton.spec.tsx index 1017606e19..0c3215eece 100644 --- a/src/Button/DeleteButton/DeleteButton.spec.tsx +++ b/src/Button/DeleteButton/DeleteButton.spec.tsx @@ -60,6 +60,8 @@ describe('', () => { const button = screen.getByRole('button'); await userEvent.click(button); + renderInMapContext(map, ); + expect(layer.getSource()?.getFeatures()).toHaveLength(1); clickMap(map, 200, 200); diff --git a/src/Button/DrawButton/DrawButton.example.md b/src/Button/DrawButton/DrawButton.example.md index b88cab28cf..21b4f425a3 100644 --- a/src/Button/DrawButton/DrawButton.example.md +++ b/src/Button/DrawButton/DrawButton.example.md @@ -14,6 +14,7 @@ import { useEffect, useState } from 'react'; const DrawButtonExample = () => { + const [selected, setSelected] = useState(); const [map, setMap] = useState(); useEffect(() => { @@ -46,44 +47,49 @@ const DrawButtonExample = () => { />
Select a digitize type: - + { + setSelected(value) + }} + > Draw point Draw line Draw polygon Draw circle Draw rectangle Draw text label diff --git a/src/Button/DrawButton/DrawButton.spec.tsx b/src/Button/DrawButton/DrawButton.spec.tsx index 9387db3651..0bcc13d301 100644 --- a/src/Button/DrawButton/DrawButton.spec.tsx +++ b/src/Button/DrawButton/DrawButton.spec.tsx @@ -42,14 +42,10 @@ describe('', () => { describe('#Drawing', () => { xit('draws points', async () => { - renderInMapContext(map, ); - - const button = screen.getByRole('button'); + renderInMapContext(map, ); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); - await userEvent.click(button); - clickMap(map, 100, 100); expect(digitizeLayer.getSource()?.getFeatures()).toHaveLength(1); @@ -60,14 +56,10 @@ describe('', () => { }); xit('draws lines', async () => { - renderInMapContext(map, ); - - const button = screen.getByRole('button'); + renderInMapContext(map, ); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); - await userEvent.click(button); - clickMap(map, 100, 100); doubleClickMap(map, 120, 100); @@ -82,14 +74,10 @@ describe('', () => { }); xit('draws polygons', async () => { - renderInMapContext(map, ); - - const button = screen.getByRole('button'); + renderInMapContext(map, ); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); - await userEvent.click(button); - clickMap(map, 100, 100); clickMap(map, 120, 100); @@ -111,14 +99,10 @@ describe('', () => { }); xit('draws labels', async () => { - renderInMapContext(map, ); - - const button = screen.getByRole('button'); + renderInMapContext(map, ); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); - await userEvent.click(button); - clickMap(map, 100, 100); const dialog = screen.getByRole('dialog'); @@ -143,14 +127,10 @@ describe('', () => { }); xit('aborts drawing labels', async () => { - renderInMapContext(map, ); - - const button = screen.getByRole('button'); + renderInMapContext(map, ); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); - await userEvent.click(button); - clickMap(map, 100, 100); const dialog = screen.getByRole('dialog'); @@ -170,14 +150,10 @@ describe('', () => { }); xit('draws circles', async () => { - renderInMapContext(map, ); - - const button = screen.getByRole('button'); + renderInMapContext(map, ); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); - await userEvent.click(button); - clickMap(map, 100, 100); clickMap(map, 120, 120); @@ -190,14 +166,10 @@ describe('', () => { }); xit('draws rectangles', async () => { - renderInMapContext(map, ); - - const button = screen.getByRole('button'); + renderInMapContext(map, ); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); - await userEvent.click(button); - clickMap(map, 100, 100); clickMap(map, 120, 120); @@ -215,27 +187,25 @@ describe('', () => { }); xit('toggles off', async () => { - renderInMapContext(map, ); - - const button = screen.getByRole('button'); + const { rerenderInMapContext } = renderInMapContext(map, ); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); expect(digitizeLayer.getSource()?.getFeatures()).toHaveLength(0); - await userEvent.click(button); + rerenderInMapContext(); clickMap(map, 100, 100); expect(digitizeLayer.getSource()?.getFeatures()).toHaveLength(1); - await userEvent.click(button); + rerenderInMapContext(); clickMap(map, 120, 100); expect(digitizeLayer.getSource()?.getFeatures()).toHaveLength(1); - await userEvent.click(button); + rerenderInMapContext(); clickMap(map, 120, 100); @@ -246,11 +216,14 @@ describe('', () => { const startSpy = jest.fn(); const endSpy = jest.fn(); - renderInMapContext(map, ); - - const button = screen.getByRole('button'); - - await userEvent.click(button); + const { rerenderInMapContext } = renderInMapContext(map, ( + + )); expect(startSpy).not.toBeCalled(); expect(endSpy).not.toBeCalled(); @@ -277,22 +250,23 @@ describe('', () => { }); xit('multiple draw buttons use the same digitize layer', async () => { - renderInMapContext(map, <> - Point 1 - Point 2 - ); - - const button1 = screen.getByText('Point 1'); - const button2 = screen.getByText('Point 2'); + const { rerenderInMapContext } = renderInMapContext(map, ( + <> + Point 1 + Point 2 + + )); const digitizeLayer = DigitizeUtil.getDigitizeLayer(map); - await userEvent.click(button1); - clickMap(map, 100, 100); - await userEvent.click(button1); - await userEvent.click(button2); + rerenderInMapContext( + <> + Point 1 + Point 2 + + ); clickMap(map, 120, 120); @@ -306,11 +280,7 @@ describe('', () => { map.addLayer(layer); - renderInMapContext(map, ); - - const button = screen.getByRole('button'); - - await userEvent.click(button); + renderInMapContext(map, ); clickMap(map, 100, 100); @@ -322,11 +292,7 @@ describe('', () => { }); xit('can change the type', async () => { - const { rerenderInMapContext } = renderInMapContext(map, ); - - const button = screen.getByRole('button'); - - await userEvent.click(button); + const { rerenderInMapContext } = renderInMapContext(map, ); clickMap(map, 100, 100); @@ -335,7 +301,7 @@ describe('', () => { expect(digitizeLayer.getSource()?.getFeatures()).toHaveLength(1); expect(digitizeLayer.getSource()?.getFeatures()[0].getGeometry()?.getType()).toBe('Point'); - rerenderInMapContext(); + rerenderInMapContext(); clickMap(map, 120, 120); doubleClickMap(map, 140, 140); diff --git a/src/Button/DrawButton/DrawButton.tsx b/src/Button/DrawButton/DrawButton.tsx index 4d4b32a33e..7ed9c4b7d2 100644 --- a/src/Button/DrawButton/DrawButton.tsx +++ b/src/Button/DrawButton/DrawButton.tsx @@ -107,7 +107,7 @@ const DrawButton: React.FC = ({ onDrawStart, onModalLabelCancel, onModalLabelOk, - onToggle, + pressed, ...passThroughProps }) => { @@ -201,19 +201,18 @@ const DrawButton: React.FC = ({ }; }, [drawInteraction, onDrawStart, onDrawEnd]); + useEffect(() => { + if (!drawInteraction) { + return; + } + + drawInteraction.setActive(!!pressed); + }, [drawInteraction, pressed]); + if (!drawInteraction || !layer) { return null; } - /** - * Called when the draw button is toggled. If the button state is pressed, - * the draw interaction will be activated. - */ - const onToggleInternal = (pressed: boolean, lastClickEvent: any) => { - drawInteraction.setActive(pressed); - onToggle?.(pressed, lastClickEvent); - }; - const finalClassName = className ? `${defaultClassName} ${className}` : defaultClassName; @@ -247,8 +246,8 @@ const DrawButton: React.FC = ({ return ( {modal} diff --git a/src/Button/GeoLocationButton/GeoLocationButton.example.md b/src/Button/GeoLocationButton/GeoLocationButton.example.md index 492caf3f3e..d5feac115b 100644 --- a/src/Button/GeoLocationButton/GeoLocationButton.example.md +++ b/src/Button/GeoLocationButton/GeoLocationButton.example.md @@ -1,5 +1,4 @@ -This demonstrates the use of the geolocation button. - +This demonstrates the use of the GeoLocation button. ```jsx import GeoLocationButton from '@terrestris/react-geo/dist/Button/GeoLocationButton/GeoLocationButton'; @@ -10,11 +9,12 @@ import OlMap from 'ol/Map'; import { fromLonLat } from 'ol/proj'; import OlSourceOSM from 'ol/source/OSM'; import OlView from 'ol/View'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect,useState } from 'react'; const GeoLocationButtonExample = () => { const [map, setMap] = useState(); + const [pressed, setPressed] = useState(); useEffect(() => { setMap(new OlMap({ @@ -36,26 +36,25 @@ const GeoLocationButtonExample = () => { } return ( - - <> - - - Enable GeoLocation - - + + + setPressed(!pressed)} + > + Track location + ); -}; +} ``` diff --git a/src/Button/GeoLocationButton/GeoLocationButton.spec.tsx b/src/Button/GeoLocationButton/GeoLocationButton.spec.tsx index 04e741ce4d..49b861f463 100644 --- a/src/Button/GeoLocationButton/GeoLocationButton.spec.tsx +++ b/src/Button/GeoLocationButton/GeoLocationButton.spec.tsx @@ -3,9 +3,11 @@ import { enableGeolocationMock, fireGeolocationListeners } from '@terrestris/react-util/dist/Util/geolocationMock'; -import { render, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils'; +import { render } from '@testing-library/react'; +import { fromLonLat } from 'ol/proj'; import * as React from 'react'; +import { act } from 'react-dom/test-utils'; import GeoLocationButton from './GeoLocationButton'; @@ -33,40 +35,55 @@ describe('', () => { it('can be pressed', async () => { const callback = jest.fn(); - const { container } = render( + )); + + rerenderInMapContext(); - const button = within(container).getByRole('button'); - await userEvent.click(button); + act(() => { + fireGeolocationListeners(); + }); - fireGeolocationListeners(); expect(callback).toBeCalled(); }); it('can be pressed twice', async () => { const callback = jest.fn(); - const { container } = render(); - - fireGeolocationListeners(); + pressed={false} + />)); expect(callback).toBeCalledTimes(0); - const button = within(container).getByRole('button'); - await userEvent.click(button); + rerenderInMapContext(); - fireGeolocationListeners(); + act(() => { + fireGeolocationListeners(); + }); expect(callback).toBeCalledTimes(1); - await userEvent.click(button); - - fireGeolocationListeners(); + render(); expect(callback).toBeCalledTimes(1); }); @@ -74,30 +91,36 @@ describe('', () => { it('is called with the correct position', async () => { const callback = jest.fn(); - const { container } = render(); - const button = within(container).getByRole('button'); - await userEvent.click(button); + rerenderInMapContext(); const coordinates = [ 47.12, -64.99 ]; - fireGeolocationListeners({ - coords: { - longitude: coordinates[0], - latitude: coordinates[1], - accuracy: 7, - speed: 9, - heading: 0 - } + act(() => { + fireGeolocationListeners({ + coords: { + longitude: coordinates[0], + latitude: coordinates[1], + accuracy: 7, + speed: 9, + heading: 0 + } + }); }); expect(callback).toBeCalledWith({ accuracy: 7, heading: 0, - position: coordinates, + position: fromLonLat(coordinates), speed: 9 }); }); diff --git a/src/Button/GeoLocationButton/GeoLocationButton.tsx b/src/Button/GeoLocationButton/GeoLocationButton.tsx index 56a3bea9e5..1c274e663b 100644 --- a/src/Button/GeoLocationButton/GeoLocationButton.tsx +++ b/src/Button/GeoLocationButton/GeoLocationButton.tsx @@ -3,8 +3,7 @@ import { useGeoLocation } from '@terrestris/react-util/dist/Hooks/useGeoLocation/useGeoLocation'; import React, { - FC, - useState + FC } from 'react'; import { CSS_PREFIX } from '../../constants'; @@ -29,17 +28,13 @@ interface OwnProps { * Whether to follow the current position. */ follow?: boolean; - /** - * The className which should be added. - */ - className?: string; /** * Enable tracking of GeoLocations */ enableTracking?: boolean; } -export type GeoLocationButtonProps = OwnProps & Omit, 'onToggle' | 'className'>; +export type GeoLocationButtonProps = OwnProps & Partial; export const GeoLocationButton: FC = ({ className, @@ -49,14 +44,13 @@ export const GeoLocationButton: FC = ({ onError = () => undefined, showMarker = true, trackingOptions, + pressed, ...passThroughProps }) => { - const [isActive, setActive] = useState(false); - useGeoLocation({ - active: isActive, - enableTracking: isActive, + active: !!pressed, + enableTracking: pressed, follow, onError, onGeoLocationChange, @@ -68,12 +62,9 @@ export const GeoLocationButton: FC = ({ ? `${className} ${CSS_PREFIX}geolocationbutton` : `${CSS_PREFIX}geolocationbutton`; - const onToggle = (pressed: boolean) => setActive(pressed); - return ( diff --git a/src/Button/MeasureButton/MeasureButton.example.md b/src/Button/MeasureButton/MeasureButton.example.md index a1a067e2a8..e797cfc146 100644 --- a/src/Button/MeasureButton/MeasureButton.example.md +++ b/src/Button/MeasureButton/MeasureButton.example.md @@ -3,22 +3,22 @@ This demonstrates the use of MeasureButton with different measure types. ```jsx import MeasureButton from '@terrestris/react-geo/dist/Button/MeasureButton/MeasureButton'; import ToggleGroup from '@terrestris/react-geo/dist/Button/ToggleGroup/ToggleGroup'; +import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import OlLayerTile from 'ol/layer/Tile'; import OlMap from 'ol/Map'; import { fromLonLat } from 'ol/proj'; import OlSourceOSM from 'ol/source/OSM'; import OlView from 'ol/View'; -import * as React from 'react'; +import React, { useEffect,useState } from 'react'; -class MeasureButtonExample extends React.Component { +const MeasureButtonExample = () => { - constructor(props) { + const [map, setMap] = useState(); + const [selected, setSelected] = useState(); - super(props); - - this.mapDivId = `map-${Math.random()}`; - - this.map = new OlMap({ + useEffect(() => { + setMap(new OlMap({ layers: [ new OlLayerTile({ name: 'OSM', @@ -29,81 +29,76 @@ class MeasureButtonExample extends React.Component { center: fromLonLat([37.40570, 8.81566]), zoom: 4 }) - }); - } + })); + }, []); - componentDidMount() { - this.map.setTarget(this.mapDivId); + if (!map) { + return null; } - render() { - return ( -
-
+ return ( + + -
- Select a measure type: - - - Distance - + Select a measure type: - - Distance with step labels - + { + setSelected(value) + }} + > + + Distance + - - Distance with multiple drawing - + + Distance with step labels + - - Area (polygon) - + + Distance with multiple drawing + - - Area (circle) - + + Area (polygon) + - - Angle - - -
-
- ); - } + + Area (circle) + + + + Angle + + + + ); } diff --git a/src/Button/MeasureButton/MeasureButton.spec.tsx b/src/Button/MeasureButton/MeasureButton.spec.tsx index ce8f8689bb..2ce66b23e1 100644 --- a/src/Button/MeasureButton/MeasureButton.spec.tsx +++ b/src/Button/MeasureButton/MeasureButton.spec.tsx @@ -1,17 +1,12 @@ -import MeasureUtil from '@terrestris/ol-util/dist/MeasureUtil/MeasureUtil'; -import {EventTargetLike} from 'ol/events/Target'; -import OlFeature from 'ol/Feature'; -import OlGeomLineString from 'ol/geom/LineString'; -import OlGeomPoint from 'ol/geom/Point'; -import OlGeomPolygon from 'ol/geom/Polygon'; -import OlInteractionDraw, {DrawEvent} from 'ol/interaction/Draw'; +import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils'; +import { render } from '@testing-library/react'; +import OlInteractionDraw from 'ol/interaction/Draw'; import OlLayerVector from 'ol/layer/Vector'; import OlMap from 'ol/Map'; -import * as OlObservable from 'ol/Observable'; -import OlOverlay from 'ol/Overlay'; +import React from 'react'; -import TestUtil, {Wrapper} from '../../Util/TestUtil'; -import MeasureButton, {MeasureButtonProps} from './MeasureButton'; +import TestUtil from '../../Util/TestUtil'; +import MeasureButton from './MeasureButton'; describe('', () => { @@ -21,851 +16,138 @@ describe('', () => { map = TestUtil.createMap(); }); - describe('#Basics', () => { - - it('is defined', () => { - expect(MeasureButton).not.toBeUndefined(); - }); - - it('can be rendered', () => { - const wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - expect(wrapper).not.toBeUndefined(); - }); - - it('allows to set some props', () => { - const wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - - wrapper.setProps({ - measureLayerName: 'measureLayerName', - fillColor: '#ff0000', - strokeColor: '#0000ff', - showMeasureInfoOnClickedPoints: true, - clickToDrawText: 'Click to draw', - continuePolygonMsg: 'Continue draw polygon', - continueLineMsg: 'Continue draw line', - continueAngleMsg: 'Continue draw angle', - decimalPlacesInTooltips: 5, - measureTooltipCssClasses: { - tooltip: 'tooltip-cls', - tooltipDynamic: 'dynamic-tooltip-cls', - tooltipStatic: 'static-tooltip-cls' - }, - pressed: true - }); - - const props: MeasureButtonProps = wrapper.props() as MeasureButtonProps; - expect(props.measureLayerName).toBe('measureLayerName'); - expect(props.fillColor).toBe('#ff0000'); - expect(props.strokeColor).toBe('#0000ff'); - expect(props.showMeasureInfoOnClickedPoints).toBe(true); - expect(props.clickToDrawText).toBe('Click to draw'); - expect(props.continuePolygonMsg).toBe('Continue draw polygon'); - expect(props.continueLineMsg).toBe('Continue draw line'); - expect(props.continueAngleMsg).toBe('Continue draw angle'); - expect(props.decimalPlacesInTooltips).toBe(5); - expect(props.measureTooltipCssClasses).toEqual({ - tooltip: 'tooltip-cls', - tooltipDynamic: 'dynamic-tooltip-cls', - tooltipStatic: 'static-tooltip-cls' - }); - expect(props.pressed).toBe(true); - - expect(props.measureTooltipCssClasses).toBeInstanceOf(Object); - expect(wrapper.find('button').length).toBe(1); - }); - + it('is defined', () => { + expect(MeasureButton).not.toBeUndefined(); }); - describe('#Static methods', () => { - - describe('#onToggle', () => { - - it('calls a given toggle callback method if the pressed state changes', () => { - const onToggle = jest.fn(); + it('can be rendered', () => { + const { container } = render( + + ); + expect(container).toBeVisible(); + }); - const props = { - map: map, - measureType: 'line', - onToggle - }; + it('creates a draw interaction and toggles its active state', async () => { + const { rerenderInMapContext } = renderInMapContext( + map, + + ); - const wrapper = TestUtil.mountComponent(MeasureButton, props); + let drawInteractions = map.getInteractions().getArray() + .filter(interaction => interaction instanceof OlInteractionDraw); - wrapper.setProps({ - pressed: true - }); + expect(drawInteractions).toHaveLength(1); - expect(onToggle).toHaveBeenCalledTimes(1); - }); + expect(drawInteractions[0].getActive()).toBeFalsy(); - it('changes drawInteraction and event listener state if the button was toggled', () => { - const wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'angle' - }); + rerenderInMapContext( + + ); - wrapper.setProps({ - pressed: true - }); + drawInteractions = map.getInteractions().getArray() + .filter(interaction => interaction instanceof OlInteractionDraw); - const instance: MeasureButton = wrapper.instance() as MeasureButton; + expect(drawInteractions[0].getActive()).toBeTruthy(); - expect(instance._drawInteraction?.getActive()).toBe(true); - expect(instance._eventKeys?.drawstart).toBeDefined(); - expect(instance._eventKeys?.drawend).toBeDefined(); - expect(instance._eventKeys?.pointermove).toBeDefined(); + rerenderInMapContext( + + ); - }); - }); + drawInteractions = map.getInteractions().getArray() + .filter(interaction => interaction instanceof OlInteractionDraw); - describe('#createMeasureLayer', () => { + expect(drawInteractions[0].getActive()).toBeFalsy(); + }); - it('sets measure layer to state on method call', () => { - const wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); + it('removes the draw interaction on unmount', () => { + const { unmount } = renderInMapContext( + map, + + ); - const instance: MeasureButton = wrapper.instance() as MeasureButton; + const drawInteractions = map.getInteractions().getArray() + .filter(interaction => interaction instanceof OlInteractionDraw); - expect(instance._measureLayer).toBeDefined(); - expect(instance._measureLayer).toBeInstanceOf(OlLayerVector); - }); - }); - - describe('#createDrawInteraction', () => { - - it('sets drawInteraction to state on method call', () => { - const wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'polygon', - pressed: true - }); + expect(drawInteractions).toHaveLength(1); - const instance: MeasureButton = wrapper.instance() as MeasureButton; - - expect(instance._drawInteraction).toBeDefined(); - expect(instance._drawInteraction).toBeInstanceOf(OlInteractionDraw); - expect(instance._drawInteraction?.getActive()).toBeTruthy(); - }); - }); - - describe('#onDrawInteractionActiveChange', () => { - - it('calls create/remove tooltip functions depending on drawInteraction active state', () => { - const wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'polygon', - pressed: true - }); - - const instance: MeasureButton = wrapper.instance() as MeasureButton; - - const removeHelpTooltipSpy = jest.spyOn(instance, 'removeHelpTooltip'); - const removeMeasureTooltipSpy = jest.spyOn(instance, 'removeMeasureTooltip'); - const createHelpTooltipSpy = jest.spyOn(instance, 'createHelpTooltip'); - const createMeasureTooltipSpy = jest.spyOn(instance, 'createMeasureTooltip'); - - instance._drawInteraction?.setActive(false); - - expect(removeHelpTooltipSpy).toHaveBeenCalledTimes(1); - expect(removeMeasureTooltipSpy).toHaveBeenCalledTimes(1); - - instance._drawInteraction?.setActive(true); - - expect(createHelpTooltipSpy).toHaveBeenCalledTimes(1); - expect(createMeasureTooltipSpy).toHaveBeenCalledTimes(1); - - removeHelpTooltipSpy.mockRestore(); - removeMeasureTooltipSpy.mockRestore(); - createHelpTooltipSpy.mockRestore(); - createMeasureTooltipSpy.mockRestore(); - }); - }); - - describe('#drawStart', () => { - - let mockEvt: Partial; - let wrapper: Wrapper; - let instance: MeasureButton; - - beforeEach(() => { - mockEvt = { - feature: new OlFeature({ - geometry: new OlGeomPoint([0, 0]) - }) - }; - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line', - showMeasureInfoOnClickedPoints: true - }); - instance = wrapper.instance() as MeasureButton; - instance._measureLayer?.getSource()?.addFeature(mockEvt.feature!); - }); - - it('sets the feature', () => { - instance.onDrawStart(mockEvt as DrawEvent); - expect(instance._feature).toBe(mockEvt.feature); - }); - - it('sets event key for click', () => { - instance.onDrawStart(mockEvt as DrawEvent); - expect(instance._eventKeys.click).toBeDefined(); - }); - - it('calls cleanup methods', () => { - const cleanupTooltipsSpy = jest.spyOn(instance, 'cleanupTooltips'); - const createMeasureTooltipSpy = jest.spyOn(instance, 'createMeasureTooltip'); - const createHelpTooltipSpy = jest.spyOn(instance, 'createHelpTooltip'); - const clearSpy = jest.spyOn(instance._measureLayer?.getSource()!, 'clear'); - - instance.onDrawStart(mockEvt as DrawEvent); - - expect(cleanupTooltipsSpy).toHaveBeenCalledTimes(1); - expect(createMeasureTooltipSpy).toHaveBeenCalledTimes(1); - expect(createHelpTooltipSpy).toHaveBeenCalledTimes(1); - expect(clearSpy).toHaveBeenCalledTimes(1); - - cleanupTooltipsSpy.mockRestore(); - createMeasureTooltipSpy.mockRestore(); - createHelpTooltipSpy.mockRestore(); - clearSpy.mockRestore(); - }); - }); - - describe('#drawEnd', () => { - - let mockEvt: Partial; - let wrapper: Wrapper; - let instance: MeasureButton; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line', - showMeasureInfoOnClickedPoints: true - }); - instance = wrapper.instance() as MeasureButton; - mockEvt = { - feature: new OlFeature({ - geometry: new OlGeomPoint([0, 0]) - }) - }; - }); - - it ('unsets click event key', () => { - - instance._eventKeys.click = { - target: { - removeEventListener: jest.fn() - } as unknown as EventTargetLike, - listener: jest.fn(), - type: 'click' - }; - - const unByKeySpy = jest.spyOn(OlObservable, 'unByKey'); - - instance.onDrawEnd(mockEvt as DrawEvent); - - expect(unByKeySpy).toHaveBeenCalledTimes(1); - - unByKeySpy.mockRestore(); - }); - - it ('calls removeMeasureTooltip method', () => { - wrapper.setProps({ - showMeasureInfoOnClickedPoints: true - }); - const removeMeasureTooltipSpy = jest.spyOn(instance, 'removeMeasureTooltip'); - instance.onDrawEnd(mockEvt as DrawEvent); - expect(removeMeasureTooltipSpy).toHaveBeenCalledTimes(1); - removeMeasureTooltipSpy.mockRestore(); - }); - - it ('sets correct properties on measureTooltipElement', () => { - wrapper.setProps({ - showMeasureInfoOnClickedPoints: false - }); - - instance.createMeasureTooltip(); - instance.onDrawEnd(mockEvt as DrawEvent); - - const expectedClassName = 'react-geo-measure-tooltip react-geo-measure-tooltip-static'; - const expectedOffset = [0, -7]; - expect(instance._measureTooltipElement?.className).toContain(expectedClassName); - expect(instance._measureTooltip?.getOffset()).toEqual(expectedOffset); - }); - - it ('unsets the feature', () => { - instance.onDrawEnd(mockEvt as DrawEvent); - expect(instance._feature).toBeNull(); - }); - - it ('calls createMeasureTooltip method', () => { - wrapper.setProps({ - showMeasureInfoOnClickedPoints: true - }); - const createMeasureTooltipSpy = jest.spyOn(instance, 'createMeasureTooltip'); - instance.onDrawEnd(mockEvt as DrawEvent); - expect(createMeasureTooltipSpy).toHaveBeenCalledTimes(1); - createMeasureTooltipSpy.mockRestore(); - }); - }); - - describe('#addMeasureStopTooltip', () => { - - let wrapper: Wrapper; - let instance: MeasureButton; - let coordinate: number[]; - let mockLineFeat: OlFeature; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - coordinate = [100, 100]; - mockLineFeat = new OlFeature({ - geometry: new OlGeomLineString([[0, 0], [0, 100]]) - }); - }); - - it('becomes a lineString feature with valid geometry', () => { - - instance._feature = mockLineFeat; - instance.addMeasureStopTooltip(coordinate); - - expect(instance._feature).toBeDefined(); - expect(instance._feature.getGeometry()).toBeDefined(); - - const geometry = instance._feature?.getGeometry() as OlGeomLineString; - const value = MeasureUtil.formatLength(geometry, map, 2); - expect(value).toBe('99.89 m'); - }); - - it('becomes a polygon feature with valid geometry', () => { - - const polyCoords = [ - [0, 0], - [0, 10], - [10, 10], - [10, 0], - [0, 0] - ]; - const mockPolyFeat = new OlFeature({ - geometry: new OlGeomPolygon([polyCoords]) - }); - - wrapper.setProps({ - measureType: 'polygon' - }); - - instance._feature = mockPolyFeat; - instance.addMeasureStopTooltip(coordinate); - - expect(instance._feature).toBeDefined(); - expect(instance._feature.getGeometry()).toBeDefined(); - - const geometry = instance._feature?.getGeometry() as OlGeomPolygon; - const value = MeasureUtil.formatArea(geometry, map, 2); - expect(value).toBe('99.78 m2'); - }); - - it('adds a tooltip overlay with correct properties and position to the map', () => { - - wrapper.setProps({ - measureType: 'line' - }); - - instance._feature = mockLineFeat; - instance.addMeasureStopTooltip(coordinate); - - const geometry = instance._feature?.getGeometry() as OlGeomLineString; - const value = MeasureUtil.formatLength(geometry, map, 2); - expect(parseInt(value, 10)).toBeGreaterThan(10); - - const overlays = map.getOverlays(); - expect(overlays.getArray().length).toBe(1); - - const overlay = overlays.getArray()[0]; - const offset = overlay.getOffset(); - const positioning = overlay.getPositioning(); - const className = overlay.getElement()?.className; - - expect(offset).toEqual([0, -15]); - expect(positioning).toBe('bottom-center'); - expect(className).toBe('react-geo-measure-tooltip react-geo-measure-tooltip-static'); - expect(overlay.getPosition()).toEqual(coordinate); - - expect(instance._createdTooltipDivs.length).toBe(1); - expect(instance._createdTooltipOverlays.length).toBe(1); - }); - }); - - describe('#createMeasureTooltip', () => { - - let wrapper: Wrapper; - let instance: MeasureButton; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - }); - - it('returns undefined if measureTooltipElement already set', () => { - const div = document.createElement('div'); - div.innerHTML = 'some value'; - instance._measureTooltipElement = div; - const expectedOutput = instance.createMeasureTooltip(); - expect(expectedOutput).toBeUndefined(); - }); - - it('adds a tooltip overlay with correct properties', () => { - - instance.createMeasureTooltip(); - - const overlays = map.getOverlays(); - expect(overlays.getArray().length).toBe(1); - - const overlay = overlays.getArray()[0]; - const offset = overlay.getOffset(); - const positioning = overlay.getPositioning(); - const className = overlay.getElement()?.className; - - expect(offset).toEqual([0, -15]); - expect(positioning).toBe('bottom-center'); - expect(className).toBe('react-geo-measure-tooltip react-geo-measure-tooltip-dynamic'); - }); - }); - - describe('#createHelpTooltip', () => { - - let wrapper: Wrapper; - let instance: MeasureButton; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - }); - - it('returns undefined if _helpTooltipElement already set', () => { - const div = document.createElement('div'); - div.innerHTML = 'some value'; - instance._helpTooltipElement = div; - const expectedOutput = instance.createHelpTooltip(); - expect(expectedOutput).toBeUndefined(); - }); - - it('adds a tooltip overlay with correct properties', () => { - - instance.createHelpTooltip(); - - const overlays = map.getOverlays(); - expect(overlays.getArray().length).toBe(1); - - const overlay = overlays.getArray()[0]; - const offset = overlay.getOffset(); - const positioning = overlay.getPositioning(); - const className = overlay.getElement()?.className; - - expect(offset).toEqual([15, 0]); - expect(positioning).toBe('center-left'); - expect(className).toBe('react-geo-measure-tooltip'); - }); - }); - - describe('#removeHelpTooltip', () => { - - let wrapper: Wrapper; - let instance: MeasureButton; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - }); - - it ('removes help tooltip overlay from the map', () => { - instance._helpTooltipElement = document.createElement('div'); - instance._helpTooltip = new OlOverlay({ - element: instance._helpTooltipElement - }); - map.addOverlay(instance._helpTooltip); - - let overlayLength = map.getOverlays().getArray().length; - expect(overlayLength).toBe(1); - - instance.removeHelpTooltip(); - overlayLength = map.getOverlays().getArray().length; - expect(overlayLength).toBe(0); - - }); - - it ('resets help tooltips', () => { - instance.removeHelpTooltip(); - - expect(instance._helpTooltipElement).toBeNull(); - expect(instance._helpTooltip).toBeNull(); - }); - }); - - describe('#removeMeasureTooltip', () => { - - let wrapper: Wrapper; - let instance: MeasureButton; + unmount(); - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - }); + const drawInteractionsAfterUnmount = map.getInteractions().getArray() + .filter(interaction => interaction instanceof OlInteractionDraw); - it ('removes measure tooltip overlay from the map', () => { - instance._measureTooltipElement = document.createElement('div'); - instance._measureTooltip = new OlOverlay({ - element: instance._measureTooltipElement - }); - map.addOverlay(instance._measureTooltip); - - let overlayLength = map.getOverlays().getArray().length; - expect(overlayLength).toBe(1); - - instance.removeMeasureTooltip(); - overlayLength = map.getOverlays().getArray().length; - expect(overlayLength).toBe(0); - - }); - - it ('resets measure tooltips', () => { - instance.removeMeasureTooltip(); - - expect(instance._helpTooltipElement).toBeNull(); - expect(instance._helpTooltip).toBeNull(); - }); - }); - - describe('#cleanupTooltips', () => { - - let wrapper: Wrapper; - let instance: MeasureButton; - let tooltipDiv1: HTMLDivElement; - let tooltipDiv2: HTMLDivElement; - let tooltip1: OlOverlay; - let tooltip2: OlOverlay; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - - tooltipDiv1 = document.createElement('div'); - tooltipDiv2 = document.createElement('div'); - tooltip1 = new OlOverlay({ - element: tooltipDiv1 - }); - tooltip2 = new OlOverlay({ - element: tooltipDiv2 - }); - }); - - it ('removes tooltip overlays from the map', () => { - instance._createdTooltipOverlays.push(tooltip1, tooltip2); - - map.addOverlay(tooltip1); - map.addOverlay(tooltip2); - - expect(instance._createdTooltipOverlays.length).toBe(2); - expect(map.getOverlays().getArray().length).toBe(2); - - instance.cleanupTooltips(); + expect(drawInteractionsAfterUnmount).toHaveLength(0); + }); - expect(instance._createdTooltipOverlays.length).toBe(0); - }); + it('creates a vector layer and toggles its visibility', async () => { + const { rerenderInMapContext } = renderInMapContext( + map, + + ); - it ('removes tooltip divs', () => { - instance._createdTooltipDivs.push(tooltipDiv1, tooltipDiv2); + let vectorLayers = map.getLayers().getArray() + .filter(layer => layer instanceof OlLayerVector && layer.get('name') === 'react-geo_measure'); - expect(instance._createdTooltipDivs.length).toBe(2); + expect(vectorLayers).toHaveLength(1); - instance.cleanupTooltips(); + expect(vectorLayers[0].getVisible()).toBeFalsy(); - expect(instance._createdTooltipDivs.length).toBe(0); - }); - }); + rerenderInMapContext( + + ); - describe('#cleanup', () => { + vectorLayers = map.getLayers().getArray() + .filter(layer => layer instanceof OlLayerVector && layer.get('name') === 'react-geo_measure'); - let wrapper: Wrapper; - let instance: MeasureButton; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - }); - - it ('sets draw interaction state to false', () => { - instance.createDrawInteraction(); - instance._drawInteraction?.setActive(true); - - instance.cleanup(); - - expect(instance._drawInteraction?.getActive()).not.toBeTruthy(); - }); + expect(vectorLayers[0].getVisible()).toBeTruthy(); - it ('unbinds all event keys', () => { + rerenderInMapContext( + + ); - instance._eventKeys = { - drawstart: { - target: {} as EventTargetLike, - listener: jest.fn(), - type: 'click' - }, - drawend: { - target: {} as EventTargetLike, - listener: jest.fn(), - type: 'click' - }, - pointermove: { - target: {} as EventTargetLike, - listener: jest.fn(), - type: 'click' - }, - click: { - target: {} as EventTargetLike, - listener: jest.fn(), - type: 'click' - }, - change: { - target: {} as EventTargetLike, - listener: jest.fn(), - type: 'click' - } - }; - - // @ts-ignore - OlObservable.unByKey = jest.fn(); - - instance.cleanup(); - - expect(OlObservable.unByKey).toHaveBeenCalledTimes(5); - }); - - it ('calls cleanupTooltips method', () => { - - const cleanupSpy = jest.spyOn(instance, 'cleanupTooltips'); - - instance.cleanup(); - - expect(cleanupSpy).toHaveBeenCalledTimes(1); - - cleanupSpy.mockRestore(); - }); + vectorLayers = map.getLayers().getArray() + .filter(layer => layer instanceof OlLayerVector && layer.get('name') === 'react-geo_measure'); - it ('clears measureLayer source', () => { - instance.createMeasureLayer(); + expect(vectorLayers[0].getVisible()).toBeFalsy(); + }); - const mockFeat = new OlFeature(); + it('removes the vector layer on unmount', () => { + const { unmount } = renderInMapContext( + map, + + ); - instance._measureLayer?.getSource()?.addFeature(mockFeat); - expect(instance._measureLayer?.getSource()?.getFeatures().length).toBe(1); + const vectorLayers = map.getLayers().getArray() + .filter(layer => layer instanceof OlLayerVector && layer.get('name') === 'react-geo_measure'); - instance.cleanup(); + expect(vectorLayers).toHaveLength(1); - expect(instance._measureLayer?.getSource()?.getFeatures().length).toBe(0); - }); - }); - - describe('#updateMeasureTooltip', () => { - - let wrapper: Wrapper; - let instance: MeasureButton; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - }); - - it ('returns undefined if measure and tooltip elements are not set', () => { - const expectedOutput = instance.updateMeasureTooltip(); - expect(expectedOutput).toBeUndefined(); - }); - - it ('sets correct tooltip position for line measurements', () => { - instance._feature = new OlFeature({ - geometry: new OlGeomLineString([[0, 0], [0, 100]]) - }); - - instance._measureTooltipElement = document.createElement('div'); - instance._measureTooltip = new OlOverlay({ - element: instance._measureTooltipElement - }); - - instance.updateMeasureTooltip(); - - expect(instance._measureTooltipElement.innerHTML).toBe('99.89 m'); - expect(instance._measureTooltip.getPosition()).toEqual([0, 100]); - }); - - it ('sets correct tooltip position for area measurements', () => { - - const polyCoords = [ - [0, 0], - [0, 10], - [10, 10], - [10, 0], - [0, 0] - ]; - instance._feature = new OlFeature({ - geometry: new OlGeomPolygon([polyCoords]) - }); - - wrapper.setProps({ - measureType: 'polygon' - }); - - instance._measureTooltipElement = document.createElement('div'); - instance._measureTooltip = new OlOverlay({ - element: instance._measureTooltipElement - }); - - instance.updateMeasureTooltip(); - - expect(instance._measureTooltipElement.innerHTML).toBe('99.78 m2'); - // Interior point as XYM coordinate, where M is the length of the horizontal - // intersection that the point belongs to - expect(instance._measureTooltip.getPosition()).toEqual([5, 5, 10]); - }); + unmount(); - it ('sets correct tooltip position for angle measurements', () => { - instance._feature = new OlFeature({ - geometry: new OlGeomLineString([[0, 0], [0, 100]]) - }); + const vectorLayersAfterUnmount = map.getLayers().getArray() + .filter(layer => layer instanceof OlLayerVector && layer.get('name') === 'react-geo_measure'); - wrapper.setProps({ - measureType: 'angle' - }); - - instance._measureTooltipElement = document.createElement('div'); - instance._measureTooltip = new OlOverlay({ - element: instance._measureTooltipElement - }); - - instance.updateMeasureTooltip(); - - expect(instance._measureTooltipElement.innerHTML).toBe('0°'); - expect(instance._measureTooltip.getPosition()).toEqual([0, 100]); - }); - }); - - describe('#updateHelpTooltip', () => { - - let wrapper: Wrapper; - let instance: MeasureButton; - let geometry: OlGeomPoint; - - beforeEach(() => { - wrapper = TestUtil.mountComponent(MeasureButton, { - map: map, - measureType: 'line' - }); - instance = wrapper.instance() as MeasureButton; - geometry = new OlGeomPoint([100, 100]); - }); - - it ('returns undefined if measure and tooltip elements are not set', () => { - const expectedOutput = instance.updateHelpTooltip(geometry); - expect(expectedOutput).toBeUndefined(); - }); - - it ('sets correct help message and position for line measurements', () => { - instance._feature = new OlFeature({ - geometry: new OlGeomLineString([[0, 0], [0, 100]]) - }); - - instance._helpTooltipElement = document.createElement('div'); - instance._helpTooltip = new OlOverlay({ - element: instance._helpTooltipElement - }); - - instance.updateHelpTooltip(geometry.getLastCoordinate()); - - expect(instance._helpTooltipElement.innerHTML).toBe('Click to draw line'); - expect(instance._helpTooltip.getPosition()).toEqual(geometry.getCoordinates()); - }); - - it ('sets correct help message and position for area measurements', () => { - - const polyCoords = [ - [0, 0], - [0, 10], - [10, 10], - [10, 0], - [0, 0] - ]; - instance._feature = new OlFeature({ - geometry: new OlGeomPolygon([polyCoords]) - }); - - wrapper.setProps({ - measureType: 'polygon' - }); - - instance._helpTooltipElement = document.createElement('div'); - instance._helpTooltip = new OlOverlay({ - element: instance._helpTooltipElement - }); - - instance.updateHelpTooltip(geometry.getLastCoordinate()); - - expect(instance._helpTooltipElement.innerHTML).toBe('Click to draw area'); - expect(instance._helpTooltip.getPosition()).toEqual(geometry.getCoordinates()); - }); - - it ('sets correct help message and position for angle measurements', () => { - instance._feature = new OlFeature({ - geometry: new OlGeomLineString([[0, 0], [0, 100]]) - }); - - wrapper.setProps({ - measureType: 'angle' - }); - - instance._helpTooltipElement = document.createElement('div'); - instance._helpTooltip = new OlOverlay({ - element: instance._helpTooltipElement - }); - - instance.updateHelpTooltip(geometry.getLastCoordinate()); - - expect(instance._helpTooltipElement.innerHTML).toBe('Click to draw angle'); - expect(instance._helpTooltip.getPosition()).toEqual(geometry.getCoordinates()); - }); - }); + expect(vectorLayersAfterUnmount).toHaveLength(0); }); }); diff --git a/src/Button/MeasureButton/MeasureButton.tsx b/src/Button/MeasureButton/MeasureButton.tsx index 54f28cb920..6f19c3b65b 100644 --- a/src/Button/MeasureButton/MeasureButton.tsx +++ b/src/Button/MeasureButton/MeasureButton.tsx @@ -1,10 +1,16 @@ import './MeasureButton.less'; -import MapUtil from '@terrestris/ol-util/dist/MapUtil/MapUtil'; import MeasureUtil from '@terrestris/ol-util/dist/MeasureUtil/MeasureUtil'; +import { + useMap, + useOlInteraction, + useOlLayer +} from '@terrestris/react-util'; import _isNil from 'lodash/isNil'; import OlCollection from 'ol/Collection'; -import { EventsKey } from 'ol/events'; +import { + Coordinate as OlCoordinate +} from 'ol/coordinate'; import OlFeature from 'ol/Feature'; import OlGeomCircle from 'ol/geom/Circle'; import OlGeometry, { Type } from 'ol/geom/Geometry'; @@ -14,7 +20,6 @@ import OlMultiPolygon from 'ol/geom/MultiPolygon'; import OlGeomPolygon from 'ol/geom/Polygon'; import OlInteractionDraw, { DrawEvent } from 'ol/interaction/Draw'; import OlLayerVector from 'ol/layer/Vector'; -import OlMap from 'ol/Map'; import OlMapBrowserEvent from 'ol/MapBrowserEvent'; import { unByKey } from 'ol/Observable'; import OlOverlay from 'ol/Overlay'; @@ -23,7 +28,12 @@ import OlStyleCircle from 'ol/style/Circle'; import OlStyleFill from 'ol/style/Fill'; import OlStyleStroke from 'ol/style/Stroke'; import OlStyleStyle from 'ol/style/Style'; -import * as React from 'react'; +import React, { + FC, + useCallback, + useEffect, + useState +} from 'react'; import { CSS_PREFIX } from '../../constants'; import ToggleButton, { ToggleButtonProps } from '../ToggleButton/ToggleButton'; @@ -33,62 +43,62 @@ interface OwnProps { * Name of system vector layer which will be used to draw measurement * results. */ - measureLayerName: string; + measureLayerName?: string; /** * Fill color of the measurement feature. */ - fillColor: string; + fillColor?: string; /** * Stroke color of the measurement feature. */ - strokeColor: string; + strokeColor?: string; /** * Determines if a marker with current measurement should be shown every * time the user clicks while measuring a distance. Default is false. */ - showMeasureInfoOnClickedPoints: boolean; + showMeasureInfoOnClickedPoints?: boolean; /** * Determines if a tooltip with helpful information is shown next to the mouse * position. Default is true. */ - showHelpTooltip: boolean; + showHelpTooltip?: boolean; /** * How many decimal places will be allowed for the measure tooltips. * Default is 2. */ - decimalPlacesInTooltips: number; + decimalPlacesInTooltips?: number; /** * Used to allow / disallow multiple drawings at a time on the map. * Default is false. * TODO known issue: only label of the last drawn feature will be shown! */ - multipleDrawing: boolean; + multipleDrawing?: boolean; /** * Tooltip which will be shown on map mouserover after measurement button * was activated. */ - clickToDrawText: string; + clickToDrawText?: string; /** * Tooltip which will be shown after polygon measurement button was toggled * and at least one click in the map is occured. */ - continuePolygonMsg: string; + continuePolygonMsg?: string; /** * Tooltip which will be shown after line measurement button was toggled * and at least one click in the map is occured. */ - continueLineMsg: string; + continueLineMsg?: string; /** * Tooltip which will be shown after angle measurement button was toggled * and at least one click in the map is occured. */ - continueAngleMsg: string; + continueAngleMsg?: string; /** * CSS classes we'll assign to the popups and tooltips from measuring. * Overwrite this object to style the text of the popups / overlays, if you * don't want to use default classes. */ - measureTooltipCssClasses: { + measureTooltipCssClasses?: { tooltip: string; tooltipDynamic: string; tooltipStatic: string; @@ -96,19 +106,11 @@ interface OwnProps { /** * Whether the measure button is pressed. */ - pressed: boolean; - /** - * A custom onToogle function that will be called if button is toggled - */ - onToggle: (pressed: boolean) => void; + pressed?: boolean; /** * The className which should be added. */ className?: string; - /** - * Instance of OL map this component is bound to. - */ - map: OlMap; /** * Whether line, area, circle or angle will be measured. */ @@ -128,806 +130,445 @@ export type MeasureButtonProps = OwnProps & Partial; export type EventName = 'drawstart' | 'drawend' | 'pointermove' | 'click' | 'change'; export type EventsKeyType = {[K in EventName]: EventsKey | undefined}; -/** - * The MeasureButton. - * - * @class The MeasureButton - * @extends React.Component - */ -class MeasureButton extends React.Component { - - /** - * The default properties. - */ - static defaultProps = { - measureLayerName: 'react-geo_measure', - fillColor: 'rgba(255, 0, 0, 0.5)', - strokeColor: 'rgba(255, 0, 0, 0.8)', - showMeasureInfoOnClickedPoints: false, - showHelpTooltip: true, - decimalPlacesInTooltips: 2, - multipleDrawing: false, - continuePolygonMsg: 'Click to draw area', - continueLineMsg: 'Click to draw line', - continueAngleMsg: 'Click to draw angle', - clickToDrawText: 'Click to measure', - measureTooltipCssClasses: { - tooltip: `${CSS_PREFIX}measure-tooltip`, - tooltipDynamic: `${CSS_PREFIX}measure-tooltip-dynamic`, - tooltipStatic: `${CSS_PREFIX}measure-tooltip-static` +const defaulClassName = `${CSS_PREFIX}measurebutton`; + +export const MeasureButton: FC = ({ + measureType, + measureLayerName = 'react-geo_measure', + fillColor = 'rgba(255, 0, 0, 0.5)', + strokeColor = 'rgba(255, 0, 0, 0.8)', + showMeasureInfoOnClickedPoints = false, + showHelpTooltip = true, + decimalPlacesInTooltips = 2, + multipleDrawing = false, + continuePolygonMsg = 'Click to draw area', + continueLineMsg = 'Click to draw line', + continueAngleMsg = 'Click to draw angle', + clickToDrawText = 'Click to measure', + measureTooltipCssClasses = { + tooltip: `${CSS_PREFIX}measure-tooltip`, + tooltipDynamic: `${CSS_PREFIX}measure-tooltip-dynamic`, + tooltipStatic: `${CSS_PREFIX}measure-tooltip-static` + }, + pressed = false, + geodesic = true, + measureRadius = false, + className, + ...passThroughProps +}) => { + + const [feature, setFeature] = useState>(); + const [measureTooltip, setMeasureTooltip] = useState(); + const [helpTooltip, setHelpTooltip] = useState(); + const [stepMeasureTooltips, setStepMeasureTooltips] = useState([]); + + const map = useMap(); + + const measureLayer = useOlLayer(() => new OlLayerVector({ + properties: { + name: measureLayerName }, - pressed: false, - onToggle: () => undefined, - geodesic: true, - measureRadius: false - }; - - /** - * The className added to this component. - * - * @private - */ - className = `${CSS_PREFIX}measurebutton`; - - /** - * Currently drawn feature. - * - * @private - */ - _feature: OlFeature | null = null; - - /** - * Overlay to show the measurement. - * - * @private - */ - _measureTooltip: OlOverlay | null = null; - - /** - * Overlay to show the help messages. - * - * @private - */ - _helpTooltip: OlOverlay | null = null; - - /** - * The help tooltip element. - * - * @private - */ - _helpTooltipElement: HTMLDivElement | null = null; - - /** - * The measure tooltip element. - * - * @private - */ - _measureTooltipElement: HTMLDivElement | null = null; - - /** - * An array of created overlays we use for the tooltips. Used to eventually - * clean up everything we added. - * - * @private - */ - _createdTooltipOverlays: OlOverlay[] = []; - - /** - * An array of created divs we use for the tooltips. Used to eventually - * clean up everything we added. - * - * @private - */ - _createdTooltipDivs: HTMLDivElement[] = []; - - /** - * An object holding keyed `OlEventsKey` instances returned by the `on` - * method of `OlObservable`. These keys are used to unbind temporary - * listeners on events of the `OlInteractionDraw` or `OlMap`. The keys - * are the names of the events on the various objects. The `click` key is - * not always bound, but only for certain #measureType values. - * - * In #cleanup, we unbind all events we have bound so as to not leak - * memory, and to ensure we have no concurring listeners being active at a - * time (E.g. when multiple measure buttons are in an application). - * - * @private - */ - _eventKeys: EventsKeyType = { - drawstart: undefined, - drawend: undefined, - pointermove: undefined, - click: undefined, - change: undefined - }; - - /** - * The vector layer showing the geometries added by the draw interaction. - * - * @private - */ - _measureLayer: OlLayerVector | null = null; - - /** - * The draw interaction used to draw the geometries to measure. - * - * @private - */ - _drawInteraction: OlInteractionDraw | null = null; - - /** - * Creates the MeasureButton. - * - * @constructs MeasureButton - */ - constructor(props: MeasureButtonProps) { - - super(props); - - this.onDrawInteractionActiveChange = this.onDrawInteractionActiveChange.bind(this); - this.onToggle = this.onToggle.bind(this); - this.onDrawStart = this.onDrawStart.bind(this); - this.onDrawEnd = this.onDrawEnd.bind(this); - this.onDrawInteractionGeometryChange = this.onDrawInteractionGeometryChange.bind(this); - this.onMapPointerMove = this.onMapPointerMove.bind(this); - this.onMapClick = this.onMapClick.bind(this); - } - - /** - * `componentDidMount` method of the MeasureButton. - * - * @method - */ - componentDidMount() { - this.createMeasureLayer(); - this.createDrawInteraction(); - } - - /** - * Ensures that component is properly cleaned up on unmount. - */ - componentWillUnmount() { - if (this.props.pressed) { - this.onToggle(false); - } - } - - /** - * Called when the button is toggled, this method ensures that everything - * is cleaned up when unpressed, and that measuring can start when pressed. - * - * @method - */ - onToggle(pressed: boolean) { - const { - map, - onToggle - } = this.props; - - this.cleanup(); - - onToggle(pressed); - - if (pressed && this._drawInteraction) { - this._drawInteraction.setActive(pressed); - - this._eventKeys.drawstart = this._drawInteraction.on( - 'drawstart', e => this.onDrawStart(e) - ); - - this._eventKeys.drawend = this._drawInteraction.on( - 'drawend', e => this.onDrawEnd(e) - ); - - this._eventKeys.pointermove = map.on( - 'pointermove', e => this.onMapPointerMove(e) - ); - } - } + source: new OlSourceVector({ + features: new OlCollection>() + }), + style: new OlStyleStyle({ + fill: new OlStyleFill({ + color: fillColor + }), + stroke: new OlStyleStroke({ + color: strokeColor, + width: 2 + }), + image: new OlStyleCircle({ + radius: 7, + fill: new OlStyleFill({ + color: fillColor + }) + }) + }) + }), [ + measureLayerName, + fillColor, + strokeColor, + fillColor + ], pressed); + + const drawInteraction = useOlInteraction(() => { + const getDrawType = (input: MeasureType): Type => { + switch (input) { + case 'line': + case 'angle': + return 'MultiLineString'; + case 'polygon': + return 'MultiPolygon'; + case 'circle': + return 'Circle'; + default: + return 'MultiLineString'; + } + }; - /** - * Creates measure vector layer and add this to the map. - * - * @method - */ - createMeasureLayer() { - const { - measureLayerName, - fillColor, - strokeColor, - map - } = this.props; - - let measureLayer = MapUtil.getLayerByName(map, measureLayerName) as OlLayerVector; - - if (!measureLayer) { - measureLayer = new OlLayerVector({ - properties: { - name: measureLayerName, - }, - source: new OlSourceVector({ - features: new OlCollection() - }), + return ( + new OlInteractionDraw({ + source: measureLayer?.getSource() || undefined, + type: getDrawType(measureType), + maxPoints: measureType === 'angle' ? 2 : undefined, style: new OlStyleStyle({ fill: new OlStyleFill({ color: fillColor }), stroke: new OlStyleStroke({ color: strokeColor, + lineDash: [10, 10], width: 2 }), image: new OlStyleCircle({ - radius: 7, + radius: 5, + stroke: new OlStyleStroke({ + color: strokeColor + }), fill: new OlStyleFill({ color: fillColor }) }) - }) - }); + }), + freehandCondition: function() { + return false; + } + }) + ); + }, [measureType, measureLayer, fillColor, strokeColor, fillColor], pressed); + + const removeMeasureTooltip = useCallback(() => { + if (!map) { + return; + } - map.addLayer(measureLayer); + if (measureTooltip) { + map.removeOverlay(measureTooltip); } - this._measureLayer = measureLayer; - } + setMeasureTooltip(undefined); + }, [measureTooltip, map]); - /** - * Creates a correctly configured OL draw interaction depending on - * the configured measureType. - * - * @return {OlInteractionDraw} The created interaction, which is not yet - * added to the map. - * - * @method - */ - createDrawInteraction() { - const { - fillColor, - strokeColor, - measureType, - pressed, - map - } = this.props; - - if (!this._measureLayer) { + const removeStepMeasureTooltips = useCallback(() => { + if (!map) { + return; + } + + if (stepMeasureTooltips.length > 0) { + stepMeasureTooltips.forEach(overlay => { + map.removeOverlay(overlay); + }); + + setStepMeasureTooltips([]); + } + }, [stepMeasureTooltips, map]); + + const removeHelpTooltip = useCallback(() => { + if (!map) { return; } - const maxPoints = measureType === 'angle' ? 2 : undefined; + if (helpTooltip) { + map.removeOverlay(helpTooltip); + } - const getDrawType = (input: MeasureType): Type | undefined => { - switch (input) { - case 'line': - case 'angle': - return 'MultiLineString'; - case 'polygon': - return 'MultiPolygon'; - case 'circle': - return 'Circle'; - default: - return undefined; - } - }; + setHelpTooltip(undefined); + }, [map, helpTooltip]); + + const cleanupTooltips = useCallback(() => { + removeMeasureTooltip(); + + removeStepMeasureTooltips(); - const drawType = getDrawType(measureType); + removeHelpTooltip(); + }, [removeMeasureTooltip, removeStepMeasureTooltips, removeHelpTooltip]); - if (!drawType) { + const createHelpTooltip = useCallback(() => { + if (!map || helpTooltip) { return; } - const drawInteraction = new OlInteractionDraw({ - source: this._measureLayer.getSource() || undefined, - type: drawType, - maxPoints: maxPoints, - style: new OlStyleStyle({ - fill: new OlStyleFill({ - color: fillColor - }), - stroke: new OlStyleStroke({ - color: strokeColor, - lineDash: [10, 10], - width: 2 - }), - image: new OlStyleCircle({ - radius: 5, - stroke: new OlStyleStroke({ - color: strokeColor - }), - fill: new OlStyleFill({ - color: fillColor - }) - }) - }), - freehandCondition: function() { - return false; - } + const tooltip = document.createElement('div'); + tooltip.className = measureTooltipCssClasses?.tooltip ?? ''; + + const overlay = new OlOverlay({ + element: tooltip, + offset: [15, 0], + positioning: 'center-left' }); - map.addInteraction(drawInteraction); + setHelpTooltip(overlay); - drawInteraction.on('change:active', () => this.onDrawInteractionActiveChange()); + map.addOverlay(overlay); + }, [map, helpTooltip, measureTooltipCssClasses?.tooltip]); - this._drawInteraction = drawInteraction; + const createMeasureTooltip = useCallback(() => { + if (!map || measureTooltip?.getElement()) { + return; + } - if (pressed) { - this.onDrawInteractionActiveChange(); + const element = document.createElement('div'); + if (measureTooltipCssClasses) { + element.className = `${measureTooltipCssClasses.tooltip} ${measureTooltipCssClasses.tooltipDynamic}`; } - drawInteraction.setActive(pressed); - } + const overlay = new OlOverlay({ + element: element, + offset: [0, -15], + positioning: 'bottom-center' + }); + + setMeasureTooltip(overlay); - /** - * Adjusts visibility of measurement related tooltips depending on active - * status of draw interaction. - */ - onDrawInteractionActiveChange() { - const { - showHelpTooltip - } = this.props; + map.addOverlay(overlay); + }, [map, measureTooltip, measureTooltipCssClasses]); - if (!this._drawInteraction) { + const updateMeasureTooltip = useCallback(() => { + if (!measureTooltip || !feature || !map) { return; } - if (this._drawInteraction.getActive()) { - if (showHelpTooltip) { - this.createHelpTooltip(); - } - this.createMeasureTooltip(); - } else { - if (showHelpTooltip) { - this.removeHelpTooltip(); - } - this.removeMeasureTooltip(); + let output; + let geom = feature.getGeometry(); + + if (geom instanceof OlMultiPolygon) { + geom = geom.getPolygons()[0]; + } else if (geom instanceof OlMultiLineString) { + geom = geom.getLineStrings()[0]; } - } - /** - * Called if the current geometry of the draw interaction has changed. - */ - onDrawInteractionGeometryChange() { - this.updateMeasureTooltip(); - } + let measureTooltipCoord; - /** - * Called on map click. - * - * @param evt The pointer event. - */ - onMapClick(evt: OlMapBrowserEvent) { - const { - measureType, - showMeasureInfoOnClickedPoints - } = this.props; + if (geom instanceof OlGeomCircle) { + if (!measureRadius) { + output = MeasureUtil.formatArea(geom, map, decimalPlacesInTooltips, geodesic); + } else { + const area = MeasureUtil.getAreaOfCircle(geom, map); + const decimalHelper = Math.pow(10, decimalPlacesInTooltips); + const radius = Math.round(geom.getRadius() * decimalHelper) / decimalHelper; + output = `${radius.toString()} m`; + if (area > (Math.PI * 1000000)) { + output = (Math.round(geom.getRadius() / 1000 * decimalHelper) / + decimalHelper) + ' km'; + } + } + } else if (geom instanceof OlGeomPolygon) { + output = MeasureUtil.formatArea(geom, map, decimalPlacesInTooltips, geodesic); + // attach area at interior point + measureTooltipCoord = geom.getInteriorPoint().getCoordinates(); + } else if (geom instanceof OlGeomLineString) { + measureTooltipCoord = geom.getLastCoordinate(); + if (measureType === 'line') { + output = MeasureUtil.formatLength(geom, map, decimalPlacesInTooltips, geodesic); + } else if (measureType === 'angle') { + output = MeasureUtil.formatAngle(geom, 0); + } + } else { + return; + } - if (showMeasureInfoOnClickedPoints && measureType === 'line') { - this.addMeasureStopTooltip(evt.coordinate); + const el = measureTooltip.getElement(); + if (output && el) { + el.innerHTML = output; } - } - /** - * Sets up listeners whenever the drawing of a measurement sketch is - * started. - * - * @param evt The event which contains the - * feature we are drawing. - * - * @method - */ - onDrawStart(evt: DrawEvent) { - const { - showHelpTooltip, - multipleDrawing, - map - } = this.props; - - if (!this._measureLayer) { + measureTooltip.setPosition(measureTooltipCoord); + }, [decimalPlacesInTooltips, feature, geodesic, map, measureTooltip, measureType, measureRadius]); + + const onDrawStart = useCallback((evt: DrawEvent) => { + if (!measureLayer || !map) { return; } - const source = this._measureLayer.getSource(); - this._feature = evt.feature as OlFeature; - - this._eventKeys.change = this._feature.getGeometry()?.on('change', - this.onDrawInteractionGeometryChange); + const source = measureLayer.getSource(); - this._eventKeys.click = map.on('click', (e: OlMapBrowserEvent) => this.onMapClick(e)); + setFeature(evt.feature); const features = source?.getFeatures(); if (!multipleDrawing && features && features.length > 0) { - this.cleanupTooltips(); - this.createMeasureTooltip(); - - if (showHelpTooltip) { - this.createHelpTooltip(); - } + cleanupTooltips(); source?.clear(); } - } + }, [cleanupTooltips, map, measureLayer, multipleDrawing]); - /** - * Called whenever measuring stops, this method draws static tooltips with - * the result onto the map canvas and unregisters various listeners. - * - * @method - */ - onDrawEnd(evt: DrawEvent) { - const { - measureType, - multipleDrawing, - showMeasureInfoOnClickedPoints, - measureTooltipCssClasses - } = this.props; - - if (this._eventKeys.click) { - unByKey(this._eventKeys.click); + const addMeasureStopTooltip = useCallback((coordinate: OlCoordinate) => { + if (!feature || !map) { + return; + } + + let geom = feature.getGeometry(); + + if (geom instanceof OlMultiPolygon) { + geom = geom.getPolygons()[0]; + } + + if (geom instanceof OlMultiLineString) { + geom = geom.getLineStrings()[0]; } - if (this._eventKeys.change) { - unByKey(this._eventKeys.change); + const value = measureType === 'line' ? + MeasureUtil.formatLength(geom as OlGeomLineString, map, decimalPlacesInTooltips, geodesic) : + MeasureUtil.formatArea(geom as OlGeomPolygon, map, decimalPlacesInTooltips, geodesic); + + if (parseInt(value, 10) > 0) { + const div = document.createElement('div'); + if (measureTooltipCssClasses) { + div.className = `${measureTooltipCssClasses.tooltip} ${measureTooltipCssClasses.tooltipStatic}`; + } + div.innerHTML = value; + const tooltip = new OlOverlay({ + element: div, + offset: [0, -15], + positioning: 'bottom-center' + }); + map.addOverlay(tooltip); + + tooltip.setPosition(coordinate); + + setStepMeasureTooltips([...stepMeasureTooltips, tooltip]); } + }, [stepMeasureTooltips, decimalPlacesInTooltips, feature, + geodesic, map, measureTooltipCssClasses, measureType]); + const onDrawEnd = useCallback((evt: DrawEvent) => { if (multipleDrawing) { - this.addMeasureStopTooltip((evt.feature.getGeometry() as OlMultiPolygon|OlMultiLineString).getLastCoordinate()); + addMeasureStopTooltip((evt.feature.getGeometry() as OlMultiPolygon|OlMultiLineString).getLastCoordinate()); } + // TODO Recheck this // Fix doubled label for lastPoint of line if ( (multipleDrawing || showMeasureInfoOnClickedPoints) && (measureType === 'line' || measureType === 'polygon') ) { - this.removeMeasureTooltip(); + removeMeasureTooltip(); } else { - if (this._measureTooltipElement && measureTooltipCssClasses) { - this._measureTooltipElement.className = - `${measureTooltipCssClasses.tooltip} ${measureTooltipCssClasses.tooltipStatic}`; + const el = measureTooltip?.getElement(); + if (el && measureTooltipCssClasses) { + el.className = `${measureTooltipCssClasses.tooltip} ${measureTooltipCssClasses.tooltipStatic}`; } - this._measureTooltip?.setOffset([0, -7]); + measureTooltip?.setOffset([0, -7]); } - this.updateMeasureTooltip(); + updateMeasureTooltip(); // unset sketch - this._feature = null; + setFeature(undefined); // fix doubled label for last point of line if ( (multipleDrawing || showMeasureInfoOnClickedPoints) && (measureType === 'line' || measureType === 'polygon') ) { - this._measureTooltipElement = null; - this.createMeasureTooltip(); - } - } - - /** - * Adds a tooltip on click where a measuring stop occured. - * - * @param coordinate The coordinate for the tooltip. - */ - addMeasureStopTooltip(coordinate: Array) { - const { - measureType, - decimalPlacesInTooltips, - map, - measureTooltipCssClasses, - geodesic - } = this.props; - - if (!_isNil(this._feature)) { - let geom = this._feature.getGeometry(); - - if (geom instanceof OlMultiPolygon) { - geom = geom.getPolygons()[0]; - } - - if (geom instanceof OlMultiLineString) { - geom = geom.getLineStrings()[0]; - } - - const value = measureType === 'line' ? - MeasureUtil.formatLength(geom as OlGeomLineString, map, decimalPlacesInTooltips, geodesic) : - MeasureUtil.formatArea(geom as OlGeomPolygon, map, decimalPlacesInTooltips, geodesic); - - if (parseInt(value, 10) > 0) { - const div = document.createElement('div'); - if (measureTooltipCssClasses) { - div.className = `${measureTooltipCssClasses.tooltip} ${measureTooltipCssClasses.tooltipStatic}`; - } - div.innerHTML = value; - const tooltip = new OlOverlay({ - element: div, - offset: [0, -15], - positioning: 'bottom-center' - }); - map.addOverlay(tooltip); - - tooltip.setPosition(coordinate); - - this._createdTooltipDivs.push(div); - this._createdTooltipOverlays.push(tooltip); - } + measureTooltip?.setElement(undefined); + createMeasureTooltip(); } - } + }, [addMeasureStopTooltip, createMeasureTooltip, measureTooltip, measureTooltipCssClasses, + measureType, multipleDrawing, removeMeasureTooltip, showMeasureInfoOnClickedPoints, updateMeasureTooltip]); - /** - * Creates a new measure tooltip as `OlOverlay`. - */ - createMeasureTooltip() { - const { - map, - measureTooltipCssClasses - } = this.props; - - if (this._measureTooltipElement) { + const updateHelpTooltip = useCallback((coordinate: OlCoordinate) => { + if (!helpTooltip) { return; } - this._measureTooltipElement = document.createElement('div'); - if (measureTooltipCssClasses) { - this._measureTooltipElement.className = - `${measureTooltipCssClasses.tooltip} ${measureTooltipCssClasses.tooltipDynamic}`; - } - - this._measureTooltip = new OlOverlay({ - element: this._measureTooltipElement, - offset: [0, -15], - positioning: 'bottom-center' - }); - - map.addOverlay(this._measureTooltip); - } - - /** - * Creates a new help tooltip as `OlOverlay`. - */ - createHelpTooltip() { - const { - map, - measureTooltipCssClasses - } = this.props; + const helpTooltipElement = helpTooltip?.getElement(); - if (this._helpTooltipElement) { + if (!helpTooltipElement) { return; } - this._helpTooltipElement = document.createElement('div'); - this._helpTooltipElement.className = measureTooltipCssClasses?.tooltip ?? ''; + let msg = clickToDrawText; - this._helpTooltip = new OlOverlay({ - element: this._helpTooltipElement, - offset: [15, 0], - positioning: 'center-left' - }); + if (measureType === 'polygon') { + msg = continuePolygonMsg; + } else if (measureType === 'line') { + msg = continueLineMsg; + } else if (measureType === 'angle') { + msg = continueAngleMsg; + } - map.addOverlay(this._helpTooltip); - } + helpTooltipElement.innerHTML = msg ?? ''; + helpTooltip.setPosition(coordinate); + }, [clickToDrawText, continueAngleMsg, continueLineMsg, continuePolygonMsg, helpTooltip, measureType]); - /** - * Removes help tooltip from the map if measure button was untoggled. - */ - removeHelpTooltip() { - if (this._helpTooltip) { - this.props.map.removeOverlay(this._helpTooltip); + const onMapPointerMove = useCallback((evt: any) => { + if (!evt.dragging && pressed) { + updateHelpTooltip(evt.coordinate); } + }, [updateHelpTooltip, pressed]); - this._helpTooltipElement = null; - this._helpTooltip = null; - } - - /** - * Removes measure tooltip from the map if measure button was untoggled. - * - * @method - */ - removeMeasureTooltip() { - if (this._measureTooltip) { - this.props.map.removeOverlay(this._measureTooltip); + const onMapClick = useCallback((evt: OlMapBrowserEvent) => { + if (showMeasureInfoOnClickedPoints && measureType === 'line') { + addMeasureStopTooltip(evt.coordinate); } + }, [addMeasureStopTooltip, measureType, showMeasureInfoOnClickedPoints]); - this._measureTooltipElement = null; - this._measureTooltip = null; - } + useEffect(() => { + const onDrawStartKey = drawInteraction?.on('drawstart', onDrawStart); - /** - * Cleans up tooltips when the button is unpressed. - * - * @method - */ - cleanupTooltips() { - const { - map - } = this.props; + const onDrawEndKey = drawInteraction?.on('drawend', onDrawEnd); - this._createdTooltipOverlays.forEach((tooltipOverlay) => { - map.removeOverlay(tooltipOverlay); - }); + const onMapPointerMoveKey = map?.on('pointermove', onMapPointerMove); - this._createdTooltipOverlays = []; + const onMapClickKey = map?.on('click', onMapClick); - this._createdTooltipDivs.forEach((tooltipDiv) => { - const parent = tooltipDiv && tooltipDiv.parentNode; - if (parent) { - parent.removeChild(tooltipDiv); + return () => { + unByKey(onDrawStartKey); + unByKey(onDrawEndKey); + if (onMapPointerMoveKey) { + unByKey(onMapPointerMoveKey); } - }); - - this._createdTooltipDivs = []; - this.removeMeasureTooltip(); - } - - /** - * Cleans up artifacts from measuring when the button is unpressed. - * - * @method - */ - cleanup() { - if (this._drawInteraction) { - this._drawInteraction.setActive(false); - } - - Object.keys(this._eventKeys).forEach(key => { - const eventKey = this._eventKeys[key as EventName] as EventsKey; - if (eventKey) { - unByKey(eventKey); + if (onMapClickKey) { + unByKey(onMapClickKey); } - }); - - this.cleanupTooltips(); - - if (this._measureLayer) { - this._measureLayer.getSource()?.clear(); - } - } + }; + }, [drawInteraction, map, onDrawEnd, onDrawStart, onMapClick, onMapPointerMove]); - /** - * Called on map's pointermove event. - * - * @param evt The pointer event. - */ - onMapPointerMove(evt: any) { - if (!evt.dragging) { - this.updateHelpTooltip(evt.coordinate); - } - } + useEffect(() => { + createMeasureTooltip(); - /** - * Updates the position and the text of the help tooltip. - * - * @param coordinate The coordinate to center the tooltip to. - */ - updateHelpTooltip(coordinate: any) { - const { - measureType, - clickToDrawText, - continuePolygonMsg, - continueLineMsg, - continueAngleMsg - } = this.props; - - if (!this._helpTooltipElement) { - return; + if (showHelpTooltip) { + createHelpTooltip(); } + }, [createHelpTooltip, createMeasureTooltip, showHelpTooltip]); - let msg = clickToDrawText; - - if (this._helpTooltipElement) { - if (measureType === 'polygon') { - msg = continuePolygonMsg; - } else if (measureType === 'line') { - msg = continueLineMsg; - } else if (measureType === 'angle') { - msg = continueAngleMsg; - } + useEffect(() => { + if (!pressed) { + measureLayer?.getSource()?.clear(); - this._helpTooltipElement.innerHTML = msg ?? ''; - this._helpTooltip?.setPosition(coordinate); + cleanupTooltips(); } - } + }, [pressed, measureLayer, cleanupTooltips]); - /** - * Updates the text and position of the measture tooltip. - */ - updateMeasureTooltip() { - const { - measureType, - decimalPlacesInTooltips, - map, - geodesic, - measureRadius - } = this.props; - - if (!this._measureTooltipElement) { + useEffect(() => { + if (!feature) { return; } - if (this._feature) { - let output; - let geom = this._feature.getGeometry(); - - if (geom instanceof OlMultiPolygon) { - geom = geom.getPolygons()[0]; - } else if (geom instanceof OlMultiLineString) { - geom = geom.getLineStrings()[0]; - } - - let measureTooltipCoord; - - if (geom instanceof OlGeomCircle) { - measureTooltipCoord = geom.getLastCoordinate(); - if (!measureRadius) { - output = MeasureUtil.formatArea(geom, map, decimalPlacesInTooltips, geodesic); - } else { - const area = MeasureUtil.getAreaOfCircle(geom, map); - const decimalHelper = Math.pow(10, decimalPlacesInTooltips); - const radius = Math.round(geom.getRadius() * decimalHelper) / decimalHelper; - output = `${radius.toString()} m`; - if (area > (Math.PI * 1000000)) { - output = (Math.round(geom.getRadius() / 1000 * decimalHelper) / - decimalHelper) + ' km'; - } - } - } else if (geom instanceof OlGeomPolygon) { - output = MeasureUtil.formatArea(geom, map, decimalPlacesInTooltips, geodesic); - // attach area at interior point - measureTooltipCoord = geom.getInteriorPoint().getCoordinates(); - } else if (geom instanceof OlGeomLineString) { - measureTooltipCoord = geom.getLastCoordinate(); - if (measureType === 'line') { - output = MeasureUtil.formatLength(geom, map, decimalPlacesInTooltips, geodesic); - } else if (measureType === 'angle') { - output = MeasureUtil.formatAngle(geom, 0); - } - } else { - return; - } + const onFeatureChangeKey = feature.getGeometry()?.on('change', updateMeasureTooltip); - if (output) { - this._measureTooltipElement.innerHTML = output; + return () => { + if (onFeatureChangeKey) { + unByKey(onFeatureChangeKey); } - this._measureTooltip?.setPosition(measureTooltipCoord); - } - } - - /** - * The render function. - */ - render() { - const { - className, - geodesic, - map, - measureType, - measureLayerName, - fillColor, - strokeColor, - showMeasureInfoOnClickedPoints, - showHelpTooltip, - multipleDrawing, - clickToDrawText, - continuePolygonMsg, - continueLineMsg, - continueAngleMsg, - decimalPlacesInTooltips, - measureTooltipCssClasses, - onToggle, - ...passThroughProps - } = this.props; - - const finalClassName = className - ? `${className} ${this.className}` - : this.className; - - return ( - - ); - } -} + }; + }, [feature, updateMeasureTooltip]); + + const finalClassName = className + ? `${className} ${defaulClassName}` + : defaulClassName; + + return ( + + ); +}; export default MeasureButton; diff --git a/src/Button/ModifyButton/ModifyButton.example.md b/src/Button/ModifyButton/ModifyButton.example.md index 28697fe5db..04d9c98521 100644 --- a/src/Button/ModifyButton/ModifyButton.example.md +++ b/src/Button/ModifyButton/ModifyButton.example.md @@ -23,6 +23,7 @@ const features = format.readFeatures(featuresJson); const ModifyButtonExample = () => { const [map, setMap] = useState(); + const [pressed, setPressed] = useState(); useEffect(() => { const newMap = new OlMap({ @@ -58,7 +59,10 @@ const ModifyButtonExample = () => { }} /> - + setPressed(!pressed)} + pressed={pressed} + > Select feature diff --git a/src/Button/ModifyButton/ModifyButton.tsx b/src/Button/ModifyButton/ModifyButton.tsx index a8bcd1e544..ee6fb80502 100644 --- a/src/Button/ModifyButton/ModifyButton.tsx +++ b/src/Button/ModifyButton/ModifyButton.tsx @@ -60,7 +60,7 @@ interface OwnProps { */ onTranslateStart?: (event: TranslateEvent) => void; /** - * Listener function for the 'qtranslateend' event of an ol.interaction.Translate. + * Listener function for the 'translateend' event of an ol.interaction.Translate. * See https://openlayers.org/en/latest/apidoc/module-ol_interaction_Translate-TranslateEvent.html * for more information. */ @@ -124,12 +124,12 @@ export const ModifyButton: React.FC = ({ translateInteractionConfig, onModalLabelOk, onModalLabelCancel, - onToggle, maxLabelLineLength, modalPromptTitle, modalPromptOkButtonText, modalPromptCancelButtonText, editLabel = true, + pressed, ...passThroughProps }) => { const [layers, setLayers] = useState<[OlVectorLayer]|null>(null); diff --git a/src/Button/PrintButton/PrintButton.example.md b/src/Button/PrintButton/PrintButton.example.md index c7310595c3..d9747f39a8 100644 --- a/src/Button/PrintButton/PrintButton.example.md +++ b/src/Button/PrintButton/PrintButton.example.md @@ -75,7 +75,7 @@ const PrintButtonExample = () => { }, } ]} - )); + ), []); const image = new CircleStyle({ radius: 5, @@ -209,7 +209,7 @@ const PrintButtonExample = () => { }); setMap(newMap); - }, []); + }, [geojson]); if (!map) { return null; diff --git a/src/Button/PrintButton/PrintButton.spec.tsx b/src/Button/PrintButton/PrintButton.spec.tsx index 2dcbb45e4f..ca3de38744 100644 --- a/src/Button/PrintButton/PrintButton.spec.tsx +++ b/src/Button/PrintButton/PrintButton.spec.tsx @@ -35,7 +35,7 @@ describe('', () => { }); it('can be rendered', () => { - const { container } = renderInMapContext(map, ); + const { container } = renderInMapContext(map, ); expect(container).toBeVisible(); }); }); diff --git a/src/Button/SelectFeaturesButton/SelectFeaturesButton.example.md b/src/Button/SelectFeaturesButton/SelectFeaturesButton.example.md index 3292a8e99d..1b5fc88f62 100644 --- a/src/Button/SelectFeaturesButton/SelectFeaturesButton.example.md +++ b/src/Button/SelectFeaturesButton/SelectFeaturesButton.example.md @@ -24,6 +24,7 @@ const SelectFeaturesButtonExample = () => { const [map, setMap] = useState(); const [layers, setLayers] = useState(); const [feature, setFeature] = useState(); + const [pressed, setPressed] = useState(); useEffect(() => { const layer = new OlVectorLayer({ @@ -63,7 +64,12 @@ const SelectFeaturesButtonExample = () => { }} /> - setFeature(e.selected[0])}> + setFeature(e.selected[0])} + onChange={() => setPressed(!pressed)} + pressed={pressed} + > Select feature diff --git a/src/Button/SelectFeaturesButton/SelectFeaturesButton.spec.tsx b/src/Button/SelectFeaturesButton/SelectFeaturesButton.spec.tsx index 490ea60ce5..60eed9fe59 100644 --- a/src/Button/SelectFeaturesButton/SelectFeaturesButton.spec.tsx +++ b/src/Button/SelectFeaturesButton/SelectFeaturesButton.spec.tsx @@ -56,16 +56,29 @@ describe('', () => { }); describe('#Selection', () => { - xit('calls the listener', async () => { + xit('selects the clicked feature', async () => { const mock = mockForEachFeatureAtPixel(map, [200, 200], feature); const selectSpy = jest.fn(); - renderInMapContext(map, ); + renderInMapContext(map, ( + + )); const button = screen.getByRole('button'); await userEvent.click(button); + renderInMapContext(map, ( + + )); + clickMap(map, 200, 200); expect(selectSpy).toBeCalled(); diff --git a/src/Button/SelectFeaturesButton/SelectFeaturesButton.tsx b/src/Button/SelectFeaturesButton/SelectFeaturesButton.tsx index 2babdb95dc..d4739b6b0d 100644 --- a/src/Button/SelectFeaturesButton/SelectFeaturesButton.tsx +++ b/src/Button/SelectFeaturesButton/SelectFeaturesButton.tsx @@ -71,9 +71,9 @@ const SelectFeaturesButton: React.FC = ({ onFeatureSelect, hitTolerance = 5, layers, - onToggle, clearAfterSelect = false, featuresCollection, + pressed, ...passThroughProps }) => { const [selectInteraction, setSelectInteraction] = useState(); @@ -131,27 +131,33 @@ const SelectFeaturesButton: React.FC = ({ }; }, [selectInteraction, features, onFeatureSelect, clearAfterSelect]); - if (!selectInteraction) { - return null; - } + useEffect(() => { + if (!selectInteraction) { + return; + } + + selectInteraction.setActive(!!pressed); - const onToggleInternal = (pressed: boolean, lastClickEvt: any) => { - selectInteraction.setActive(pressed); - onToggle?.(pressed, lastClickEvt); if (!pressed) { selectInteraction.getFeatures().clear(); } - }; + }, [selectInteraction, pressed]); + + if (!selectInteraction) { + return null; + } const finalClassName = className ? `${defaultClassName} ${className}` : defaultClassName; - return ; + return ( + + ); }; export default SelectFeaturesButton; diff --git a/src/Button/ToggleButton/ToggleButton.example.md b/src/Button/ToggleButton/ToggleButton.example.md index 3c1bd80bce..7d5959bf3a 100644 --- a/src/Button/ToggleButton/ToggleButton.example.md +++ b/src/Button/ToggleButton/ToggleButton.example.md @@ -1,28 +1,47 @@ This demonstrates the use of ToggleButtons. -A ToggleButton without any configuration: +A ToggleButton without the basic configuration: ```jsx import ToggleButton from '@terrestris/react-geo/dist/Button/ToggleButton/ToggleButton'; +import * as React from 'react'; -{}} -> - Toggle me - +const StandaloneToggleButton = () => { + const [pressed, setPressed] = React.useState(); + + return ( + setPressed(!pressed)} + > + Toggle me + + ); +}; + + ``` A ToggleButton initially pressed: ```jsx import ToggleButton from '@terrestris/react-geo/dist/Button/ToggleButton/ToggleButton'; +import * as React from 'react'; + +const StandaloneToggleButton = () => { + const [pressed, setPressed] = React.useState(true); + + return ( + setPressed(!pressed)} + > + Toggle me + + ); +}; -{}} -> - Toggle me - + ``` A ToggleButton with an icon and a pressedIcon: @@ -31,18 +50,28 @@ A ToggleButton with an icon and a pressedIcon: import { faFaceFrown, faFaceSmile } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import ToggleButton from '@terrestris/react-geo/dist/Button/ToggleButton/ToggleButton'; +import * as React from 'react'; - - } - pressedIcon={ - { + const [pressed, setPressed] = React.useState(); + + return ( + setPressed(!pressed)} + icon={ + + } + pressedIcon={ + + } /> - } - onToggle={()=>{}} -/> + ); +}; + + ``` diff --git a/src/Button/ToggleButton/ToggleButton.less b/src/Button/ToggleButton/ToggleButton.less deleted file mode 100644 index 83a881e392..0000000000 --- a/src/Button/ToggleButton/ToggleButton.less +++ /dev/null @@ -1,11 +0,0 @@ -button.react-geo-togglebutton { - // overrides the ant-btn:empty style, that is setting the button to hidden if no text or icon are given - &:empty { - visibility: visible; - } - - &.btn-pressed { - background-color: var(--ant-primary-7); - border-color: var(--ant-primary-color-active); - } -} diff --git a/src/Button/ToggleButton/ToggleButton.spec.tsx b/src/Button/ToggleButton/ToggleButton.spec.tsx index 466f7c1517..922308d223 100644 --- a/src/Button/ToggleButton/ToggleButton.spec.tsx +++ b/src/Button/ToggleButton/ToggleButton.spec.tsx @@ -22,23 +22,23 @@ describe('', () => { }); it('sets the pressed class if pressed prop is set to true initially', () => { - render(); + render(); const button = screen.getByRole('button'); expect(button).toHaveClass('btn-pressed'); }); - it('ignores the onClick callback', async () => { + it('does not ignore the onClick callback', async () => { const onClick = jest.fn(); render(); const button = screen.getByRole('button'); await userEvent.click(button); - expect(onClick).not.toHaveBeenCalled(); + expect(onClick).toHaveBeenCalled(); }); it('toggles the pressed class if the pressed prop has changed', () => { - const { rerender } = render(); + const { rerender } = render(); const button = screen.getByRole('button'); expect(button).not.toHaveClass('btn-pressed'); @@ -50,117 +50,29 @@ describe('', () => { rerender(); expect(button).toHaveClass('btn-pressed'); - rerender(); + rerender(); expect(button).not.toHaveClass('btn-pressed'); }); - // eslint-disable-next-line max-len - it('calls the given toggle callback method if the pressed prop has changed initially to true', () => { - const onToggle = jest.fn(); - const { rerender } = render(); - - rerender(); - expect(onToggle).toHaveBeenCalledTimes(1); - // If the prop has been changed, no click evt is available. - expect(onToggle).toHaveBeenCalledWith(true, null); - - rerender(); - expect(onToggle).toHaveBeenCalledTimes(2); - expect(onToggle).toHaveBeenCalledWith(false, null); - - // Nothing should happen if the prop hasn't changed. - rerender(); - expect(onToggle).toHaveBeenCalledTimes(2); - - rerender(); - expect(onToggle).toHaveBeenCalledTimes(3); - expect(onToggle).toHaveBeenCalledWith(true, null); - }); - - // eslint-disable-next-line max-len - it('calls the given toggle callback method if the pressed prop has changed to false (from being false by default)', () => { - const onToggle = jest.fn(); - const { rerender } = render(); - - // Nothing should happen if the prop hasn't changed. - // (pressed property is false by default) - rerender(); - expect(onToggle).toHaveBeenCalledTimes(0); - - rerender(); - expect(onToggle).toHaveBeenCalledTimes(1); - // If the prop has been changed, no click evt is available. - expect(onToggle).toHaveBeenCalledWith(true, null); - - // Nothing should happen if the prop hasn't changed. - rerender(); - expect(onToggle).toHaveBeenCalledTimes(1); - - rerender(); - expect(onToggle).toHaveBeenCalledTimes(2); - expect(onToggle).toHaveBeenCalledWith(false, null); - }); - - it('cleans the last click event if not available', async () => { - const onToggle = jest.fn(); - const clickEvtMock = expect.objectContaining({ - type: 'click' - }); - const { rerender } = render(); - const button = screen.getByRole('button'); - - rerender(); - expect(onToggle).toHaveBeenCalledTimes(1); - // If the prop has been changed, no click evt is available. - expect(onToggle).toHaveBeenCalledWith(true, null); - - // Pressed will now become false. - await userEvent.click(button); - expect(onToggle).toHaveBeenCalledTimes(2); - expect(onToggle).toHaveBeenCalledWith(false, clickEvtMock); - - rerender(); - // If the prop has been changed, no click evt is available. - expect(onToggle).toHaveBeenCalledTimes(3); - expect(onToggle).toHaveBeenCalledWith(true, null); - - }); - - it('toggles the pressed class on click', async () => { - render(); - const button = screen.getByRole('button'); - - expect(button).not.toHaveClass('btn-pressed'); - - await userEvent.click(button); - expect(button).toHaveClass('btn-pressed'); - - await userEvent.click(button); - expect(button).not.toHaveClass('btn-pressed'); - - await userEvent.click(button); - expect(button).toHaveClass('btn-pressed'); - }); - it('calls the given toggle callback method on click', async () => { - const onToggle = jest.fn(); + const onChange = jest.fn(); const clickEvtMock = expect.objectContaining({ type: 'click' }); - render(); + render(); const button = screen.getByRole('button'); await userEvent.click(button); - expect(onToggle).toHaveBeenCalledTimes(1); - expect(onToggle).toHaveBeenCalledWith(true, clickEvtMock); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(clickEvtMock, 'test'); await userEvent.click(button); - expect(onToggle).toHaveBeenCalledTimes(2); - expect(onToggle).toHaveBeenCalledWith(false, clickEvtMock); + expect(onChange).toHaveBeenCalledTimes(2); + expect(onChange).toHaveBeenCalledWith(clickEvtMock, 'test'); await userEvent.click(button); - expect(onToggle).toHaveBeenCalledTimes(3); - expect(onToggle).toHaveBeenCalledWith(true, clickEvtMock); + expect(onChange).toHaveBeenCalledTimes(3); + expect(onChange).toHaveBeenCalledWith(clickEvtMock, 'test'); }); it('can be rendered if icon is set and no text or icon is set with the property pressed set to true', () => { diff --git a/src/Button/ToggleButton/ToggleButton.tsx b/src/Button/ToggleButton/ToggleButton.tsx index 91f31d0d43..62275f2021 100644 --- a/src/Button/ToggleButton/ToggleButton.tsx +++ b/src/Button/ToggleButton/ToggleButton.tsx @@ -1,286 +1,137 @@ -import './ToggleButton.less'; - import { Button, + theme, Tooltip } from 'antd'; -import { AbstractTooltipProps, TooltipPlacement } from 'antd/lib/tooltip'; +import { + AbstractTooltipProps, + TooltipPlacement +} from 'antd/lib/tooltip'; +import React, { + MouseEvent +} from 'react'; + +const { useToken } = theme; + import _isFunction from 'lodash/isFunction'; -import * as PropTypes from 'prop-types'; -import * as React from 'react'; import { CSS_PREFIX } from '../../constants'; import { SimpleButtonProps } from '../SimpleButton/SimpleButton'; interface OwnProps { - type: 'default' | 'primary' | 'ghost' | 'dashed' | 'danger' | 'link'; /** * Additional [antd tooltip](https://ant.design/components/tooltip/) * properties to pass to the tooltip component. Note: The props `title` * and `placement` will override the props `tooltip` and `tooltipPlacement` * of this component! */ - tooltipProps: AbstractTooltipProps; + tooltipProps?: AbstractTooltipProps; + /** * The initial pressed state of the ToggleButton * Note: If a ToggleButton is inside a ToggleGroup, the pressed state will be controlled by the selectedName property * of the ToggleGroup and this property will be ignored. */ - pressed: boolean; + pressed?: boolean; + /** - * The toggle handler + * The value associated with this button. */ - onToggle: (pressed: boolean, lastClickEvt: any) => void; + value?: string; - className?: string; /** - * The icon to render for the pressed state. See - * https://ant.design/components/icon/. + * The icon to render for the pressed state. */ pressedIcon?: React.ReactNode; + /** * The tooltip to be shown on hover. */ tooltip?: string; + /** * The position of the tooltip. */ tooltipPlacement?: TooltipPlacement; -} - -interface ToggleButtonState { - pressed: boolean; - lastClickEvt: any; - overallPressed: boolean; - isClicked: boolean; -} - -export type ToggleButtonProps = OwnProps & SimpleButtonProps; - -/** - * The ToggleButton. - * - * @class The ToggleButton - * @extends React.Component - */ -class ToggleButton extends React.Component { - - /** - * The default properties. - */ - static defaultProps = { - type: 'primary', - pressed: false, - tooltipProps: { - mouseEnterDelay: 1.5 - }, - onToggle: () => undefined - }; - - /** - * The context types. - */ - static contextTypes = { - toggleGroup: PropTypes.object - }; - - /** - * The className added to this component. - * @private - */ - _className = `${CSS_PREFIX}togglebutton`; - - /** - * The class to apply for a toggled/pressed button. - */ - pressedClass = 'btn-pressed'; - - /** - * Creates the ToggleButton. - * - * @constructs ToggleButton - */ - constructor(props: ToggleButtonProps) { - super(props); - - // Instantiate the state. - // components state - this.state = { - pressed: props.pressed, - lastClickEvt: null, - overallPressed: props.pressed, - isClicked: false - }; - } - - /** - * Invoked right before calling the render method, both on the initial mount - * and on subsequent updates. It should return an object to update the state, - * or null to update nothing. - * @param nextProps The next properties. - * @param prevState The previous state. - */ - static getDerivedStateFromProps(nextProps: ToggleButtonProps, prevState: ToggleButtonState) { - - // Checks to see if the pressed property has changed - if (prevState.pressed !== nextProps.pressed) { - return { - pressed: nextProps.pressed, - overallPressed: nextProps.pressed, - isClicked: false, - lastClickEvt: null - }; - } - return null; - } - - /** - * We will handle the initial state of the button here. - * If it is pressed, we will have to call its `onToggle` - * method, if it exists, in order to reflect the initial - * state correctly (e.g. activating ol.Controls) - */ - componentDidMount() { - if (this.props.onToggle && this.props.pressed) { - this.props.onToggle(true, null); - } - } /** - * Invoked immediately after updating occurs. This method is not called - * for the initial render. - * @method + * The onChange callback. */ - componentDidUpdate(prevProps: ToggleButtonProps, prevState: ToggleButtonState) { - const { - onToggle - } = this.props; - - const { - pressed, - lastClickEvt, - overallPressed, - isClicked - } = this.state; + onChange?: (evt: MouseEvent, value?: string) => void; +} - /** - * the following is performed here as a hack to keep track of the pressed changes. - * - * check if the button has been clicked - * |__ YES: ==> toggle the button - * | - * |__ NO: check if the prop has changed - * |__ YES: ==> Toggle the button - * |__ NO: check if previous update action was a click - * |__ YES: ==> run the Toggle function fo the prop value - */ - let shouldToggle: boolean = false; - if (isClicked || prevState.pressed !== pressed || prevState.isClicked) { - if (isClicked) { - // button is clicked - shouldToggle = true; - } else { - // check for prop change - if (pressed !== prevState.overallPressed) { - // pressed prop has changed - shouldToggle = true; - } else { - if (prevState.isClicked) { - // prop has not changed but the previous was click event - if (prevState.overallPressed !== overallPressed) { - shouldToggle = true; - } - } - } - } - if (shouldToggle) { - onToggle(overallPressed, lastClickEvt); +export type ToggleButtonProps = OwnProps & Omit; + +export const ToggleButton: React.FC = ({ + type = 'primary', + pressed = false, + tooltipProps = { + mouseEnterDelay: 1.5 + }, + className, + tooltip, + tooltipPlacement, + pressedIcon, + icon, + children, + value, + onClick, + onChange = () => {}, + ...passThroughProps +}) => { + + const token = useToken(); + + const handleChange = (evt: React.MouseEvent) => { + if (onClick) { + onClick(evt); + + if (evt.defaultPrevented) { + return; } } - } - - /** - * Called on click. - * - * @param evt The ClickEvent. - * @method - */ - onClick(evt: any) { - evt.persist(); - this.setState({ - overallPressed: !this.state.overallPressed, - lastClickEvt: evt, - isClicked: true - }, () => { - // This part can be removed in future if the ToggleGroup button is removed. - // @ts-ignore - if (this.context.toggleGroup && _isFunction(this.context.toggleGroup.onChange)) { - // @ts-ignore - this.context.toggleGroup.onChange(this.props); - // this allows for the allowDeselect property to be taken into account - // when used with ToggleGroup. Since the ToggleGroup changes the - // pressed prop for its child components the click event dose not need to - // change the pressed property. - this.setState({ overallPressed: !this.state.overallPressed }); - } - }); - } - /** - * The render function. - */ - render() { - const { - overallPressed - } = this.state; - - const { - className, - icon, - pressedIcon, - pressed, - onToggle, - tooltip, - tooltipPlacement, - tooltipProps, - ...antBtnProps - } = this.props; + onChange(evt, value); + }; - const { - onClick, - ...filteredAntBtnProps - } = antBtnProps; + const internalClassName = `${CSS_PREFIX}togglebutton`; - const finalClassName = className - ? `${className} ${this._className}` - : this._className; + const finalClassName = className + ? `${className} ${internalClassName}` + : internalClassName; - let pressedClass = ''; - if (overallPressed) { - pressedClass = ` ${this.pressedClass} `; - } + let pressedClass = ''; + if (pressed) { + pressedClass = ' btn-pressed'; + } - return ( - + - - ); - } -} + {children} + + + ); +}; export default ToggleButton; diff --git a/src/Button/ToggleGroup/ToggleGroup.example.md b/src/Button/ToggleGroup/ToggleGroup.example.md index 3b0d351adb..9e3bb9b082 100644 --- a/src/Button/ToggleGroup/ToggleGroup.example.md +++ b/src/Button/ToggleGroup/ToggleGroup.example.md @@ -5,52 +5,60 @@ import { faFaceFrown, faFaceSmile } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import ToggleButton from '@terrestris/react-geo/dist/Button/ToggleButton/ToggleButton'; import ToggleGroup from '@terrestris/react-geo/dist/Button/ToggleGroup/ToggleGroup'; +import * as React from 'react'; - - - } - pressedIcon={ - - } - onToggle={()=>{}} - /> - - } - pressedIcon={ - { + const [selected, setSelected] = React.useState(); + + return ( + { + setSelected(value) + }} + > + + } + pressedIcon={ + + } /> - } - onToggle={()=>{}} - /> - + } + pressedIcon={ + + } /> - } - pressedIcon={ - + } + pressedIcon={ + + } /> - } - onToggle={()=>{}} - /> - + + ); +}; + + ``` diff --git a/src/Button/ToggleGroup/ToggleGroup.spec.tsx b/src/Button/ToggleGroup/ToggleGroup.spec.tsx index 718b90bf06..5157aa6749 100644 --- a/src/Button/ToggleGroup/ToggleGroup.spec.tsx +++ b/src/Button/ToggleGroup/ToggleGroup.spec.tsx @@ -1,106 +1,110 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import TestUtil from '../../Util/TestUtil'; +// import TestUtil from '../../Util/TestUtil'; import ToggleButton from '../ToggleButton/ToggleButton'; -import ToggleGroup, { ToggleGroupState } from './ToggleGroup'; +import ToggleGroup from './ToggleGroup'; describe('', () => { it('is defined', () => { - expect(ToggleGroup).not.toBeUndefined(); + expect(ToggleGroup).toBeDefined(); }); it('can be rendered', () => { - const wrapper = TestUtil.mountComponent(ToggleGroup); - expect(wrapper).not.toBeUndefined(); - }); - - it('renders it\'s children horizontally or vertically', () => { - const wrapper = TestUtil.mountComponent(ToggleGroup, { - orientation: 'vertical' - }); - - expect(wrapper.find('div.vertical-toggle-group').length).toBe(1); - - wrapper.setProps({ - orientation: 'horizontal' - }); - - expect(wrapper.find('div.horizontal-toggle-group').length).toBe(1); + const { container } = render(); + expect(container).toBeVisible(); }); it('renders children when passed in', () => { - const props = { - children: [ - {}} />, - {}} />, - {}} /> - ] - }; - const wrapper = TestUtil.mountComponent(ToggleGroup, props); - - expect(wrapper.find(ToggleButton).length).toBe(3); + render( + + + + + + ); + + const buttons = screen.getAllByRole('button'); + + expect(buttons).toHaveLength(3); }); - it('calls the given onChange callback if a children is pressed', () => { - const changeSpy = jest.fn(); - const props = { - onChange: changeSpy, - children: [ - {}} /> - ] - }; - const wrapper = TestUtil.mountComponent(ToggleGroup, props); + it('calls the given onChange callback if a children is pressed', async () => { + const onChange = jest.fn(); + const clickEvtMock = expect.objectContaining({ + type: 'click' + }); - wrapper.find(ToggleButton).simulate('click'); + render( + + + + + + ); - expect(changeSpy).toHaveBeenCalled(); - }); + const buttons = screen.getAllByRole('button'); - it('sets the selected name on click', () => { - const changeSpy = jest.fn(); - const props = { - onChange: changeSpy, - children: [ - {}} />, - {}} />, - {}} /> - ] - }; - const wrapper = TestUtil.mountComponent(ToggleGroup, props); - - wrapper.find(ToggleButton).first().simulate('click'); - expect((wrapper.state() as ToggleGroupState).selectedName).toBe('Shinji'); - - wrapper.find(ToggleButton).at(2).simulate('click'); - expect((wrapper.state() as ToggleGroupState).selectedName).toBe('香川 真司'); - }); + await userEvent.click(buttons[0]); - it('allows to deselect an already pressed button', () => { - const changeSpy = jest.fn(); - const props = { - allowDeselect: false, - onChange: changeSpy, - children: [ - {}} />, - {}} />, - {}} /> - ] - }; - const wrapper = TestUtil.mountComponent(ToggleGroup, props); - - wrapper.find(ToggleButton).first().simulate('click'); - expect((wrapper.state() as ToggleGroupState).selectedName).toBe('Shinji'); - - wrapper.find(ToggleButton).first().simulate('click'); - expect((wrapper.state() as ToggleGroupState).selectedName).toBe('Shinji'); - - wrapper.setProps({ - allowDeselect: true - }); + expect(onChange).toHaveBeenCalled(); + expect(onChange).toHaveBeenCalledWith(clickEvtMock, 'Shinji'); + }); - wrapper.find(ToggleButton).first().simulate('click'); - expect((wrapper.state() as ToggleGroupState).selectedName).toBe(undefined); + it('allows to toggle between buttons', async () => { + const { rerender } = render( + + + + + + ); + + screen.getAllByRole('button').forEach(button => expect(button).not.toHaveClass('btn-pressed')); + + rerender( + + + + + + ); + + screen.getAllByRole('button').forEach(button => expect(button).not.toHaveClass('btn-pressed')); + expect(screen.getByRole('test')).toHaveClass('btn-pressed'); + + rerender( + + + + + + ); + + screen.getAllByRole('button').forEach(button => expect(button).not.toHaveClass('btn-pressed')); + expect(screen.getByRole('test')).not.toHaveClass('btn-pressed'); + expect(screen.getByRole('test-updated')).toHaveClass('btn-pressed'); + + rerender( + + + + + + ); + + screen.getAllByRole('button').forEach(button => expect(button).not.toHaveClass('btn-pressed')); }); }); diff --git a/src/Button/ToggleGroup/ToggleGroup.tsx b/src/Button/ToggleGroup/ToggleGroup.tsx index 39c7890625..8af543be35 100644 --- a/src/Button/ToggleGroup/ToggleGroup.tsx +++ b/src/Button/ToggleGroup/ToggleGroup.tsx @@ -1,173 +1,92 @@ import './ToggleGroup.less'; import _isFunction from 'lodash/isFunction'; -import * as PropTypes from 'prop-types'; -import * as React from 'react'; +import React, { + isValidElement, + MouseEvent, + ReactElement +} from 'react'; import { CSS_PREFIX } from '../../constants'; import { ToggleButtonProps } from '../ToggleButton/ToggleButton'; -export interface ToggleGroupProps { +export type ToggleGroupProps = { /** - * The orientation of the children. + * The orientation of the children. Default is to 'vertical'. */ - orientation: 'vertical' | 'horizontal'; - /** - * Whether it's allowed to deselect a children or not. - */ - allowDeselect: boolean; + orientation?: 'vertical' | 'horizontal'; + /** * The className which should be added. */ className?: string; + /** - * The name of this group. - */ - name?: string; - /** - * The value fo the `name` attribute of the children to select/press - * initially. + * The value fo the `value` attribute of the children to select/press + * initially. If not given, no child is set as selected. * Note: This prop will have full control over the pressed prop on its children. Setting select/pressed on the * children props directly will have no effect. */ - selectedName?: string; + selected?: string; + /** * Callback function for onChange. */ - onChange?: (childProps: any) => void; + onChange?: (evt: MouseEvent, value?: string) => void; + /** * The children of this group. Typically a set of `ToggleButton`s. */ - children?: React.ReactElement[]; -} - -export interface ToggleGroupState { - selectedName?: string; -} - -/** - * A group for toggle components (e.g. buttons) - * - * @class The ToggleGroup - * @extends React.Component - * - */ -class ToggleGroup extends React.Component { + children?: ReactElement[]; +} & React.ComponentProps<'div'>; - /** - * The default properties. - */ - static defaultProps = { - orientation: 'vertical', - allowDeselect: true - }; +export const ToggleGroupContext = React.createContext(false); - /** - * The child context types. - */ - static childContextTypes = { - toggleGroup: PropTypes.object - }; +export const ToggleGroup: React.FC = ({ + orientation = 'vertical', + className, + selected, + onChange = () => {}, + children, + ...passThroughProps +}) => { - /** - * The className added to this component. - * @private - */ - _className = `${CSS_PREFIX}togglegroup`; - - /** - * The constructor. - * - * @param props The properties. - */ - constructor(props: ToggleGroupProps) { - super(props); + const internalClassName = `${CSS_PREFIX}togglegroup`; - /** - * The initial state. - */ - this.state = { - selectedName: props.selectedName - }; - } - - /** - * Update selectedName in state if property was changed - * - * @param prevProps Previous props - */ - componentDidUpdate(prevProps: ToggleGroupProps) { - if (prevProps.selectedName !== this.props.selectedName) { - this.setState({ - selectedName: this.props.selectedName - }); - } - } + const finalClassName = className + ? `${className} ${internalClassName}` + : internalClassName; - /** - * Returns the context for the children. - * - * @return The child context. - */ - getChildContext() { - return { - toggleGroup: { - name: this.props.name, - selectedName: this.state.selectedName, - onChange: this.onChange - } - }; - } + const orientationClass = (orientation === 'vertical') + ? 'vertical-toggle-group' + : 'horizontal-toggle-group'; - /** - * The onChange handler. - * - * @param childProps The properties of the children. - */ - onChange = (childProps: ToggleButtonProps) => { - if (_isFunction(this.props.onChange)) { - this.props.onChange(childProps); - } - // Allow deselect. - if (this.props.allowDeselect && (childProps.name === this.state.selectedName)) { - this.setState({ selectedName: undefined }); - } else { - this.setState({ selectedName: childProps.name }); - } + const handleChange = (evt: MouseEvent, buttonValue?: string) => { + onChange(evt, selected === buttonValue ? undefined : buttonValue); }; - /** - * The render function. - */ - render() { - const { orientation, children } = this.props; - const className = this.props.className - ? `${this.props.className} ${this._className}` - : this._className; - const orientationClass = (orientation === 'vertical') - ? 'vertical-toggle-group' - : 'horizontal-toggle-group'; - - const childrenWithProps = React.Children.map(children, child => { - const item = child as React.ReactElement; - if (React.isValidElement(item)) { - // pass the press state through to child components - return React.cloneElement(item, { - pressed: this.state.selectedName === item.props.name - }); - } else { - return child; + return ( +
+ { + children + ?.map(child => { + if (!isValidElement(child)) { + return null; + } + + return React.cloneElement(child, { + key: child.props.value, + onChange: handleChange, + pressed: selected === child.props.value + }); + }) + .filter(child => child !== null) } - }); - - return ( -
- {childrenWithProps} -
- ); - } -} +
+ ); +}; export default ToggleGroup; diff --git a/src/Panel/TimeLayerSliderPanel/TimeLayerSliderPanel.tsx b/src/Panel/TimeLayerSliderPanel/TimeLayerSliderPanel.tsx index 08e28f9ce2..1c84b87bb6 100644 --- a/src/Panel/TimeLayerSliderPanel/TimeLayerSliderPanel.tsx +++ b/src/Panel/TimeLayerSliderPanel/TimeLayerSliderPanel.tsx @@ -284,9 +284,16 @@ export class TimeLayerSliderPanel extends React.Component { window.clearInterval(this._interval); + + if (!this.state.autoPlayActive) { + return; + } + this._interval = window.setInterval(() => { const { endDate @@ -311,17 +318,8 @@ export class TimeLayerSliderPanel extends React.Component { if (this.state.autoPlayActive) { - this.autoPlay(true); + this.autoPlay(); } }); }; @@ -491,7 +489,7 @@ export class TimeLayerSliderPanel extends React.Component