diff --git a/package.json b/package.json index 567e12b8..7ef7e175 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "react-spatial", "license": "MIT", "description": "Components to build React map apps.", - "version": "1.11.0", + "version": "1.11.1-beta.2", "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", diff --git a/src/components/BaseLayerSwitcher/BaseLayerSwitcher.js b/src/components/BaseLayerSwitcher/BaseLayerSwitcher.js index 40b12b27..983340e4 100644 --- a/src/components/BaseLayerSwitcher/BaseLayerSwitcher.js +++ b/src/components/BaseLayerSwitcher/BaseLayerSwitcher.js @@ -46,21 +46,24 @@ const propTypes = { * @param {function} Translation function returning the translated string. */ t: PropTypes.func, -}; -const defaultProps = { - className: "rs-base-layer-switcher", - altText: "Source not found", - titles: { - button: "Base layers", - openSwitcher: "Open Baselayer-Switcher", - closeSwitcher: "Close Baselayer-Switcher", - }, - closeButtonImage: , - layerImages: undefined, - t: (s) => { - return s; - }, + /** + * Callback function on close button click. + * @param {function} Callback function triggered when a switcher button is clicked. Takes the event as argument. + */ + onCloseButtonClick: PropTypes.func, + + /** + * Callback function on layer button click. + * @param {function} Callback function triggered when a switcher button is clicked. Takes the event and the layer as arguments. + */ + onLayerButtonClick: PropTypes.func, + + /** + * Callback function on main switcher button click. + * @param {function} Callback function triggered when a switcher button is clicked. Takes the event as argument. + */ + onSwitcherButtonClick: PropTypes.func, }; const getVisibleLayer = (layers) => { @@ -90,6 +93,31 @@ const getImageStyle = (url) => { : null; }; +function CloseButton({ onClick, tabIndex, title, children }) { + return ( +
{ + return e.which === 13 && onClick(); + }} + tabIndex={tabIndex} + aria-label={title} + title={title} + > + {children} +
+ ); +} + +CloseButton.propTypes = { + onClick: PropTypes.func.isRequired, + tabIndex: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + /** * The BaseLayerSwitcher component renders a button interface for switching the visible * [mobility-toolbox-js layer](https://mobility-toolbox-js.geops.io/api/identifiers%20html#ol-layers) @@ -98,12 +126,19 @@ const getImageStyle = (url) => { function BaseLayerSwitcher({ layers, - layerImages, - className, - altText, - titles, - closeButtonImage, - t, + layerImages = undefined, + className = "rs-base-layer-switcher", + altText = "Source not found", + titles = { + button: "Base layers", + openSwitcher: "Open Baselayer-Switcher", + closeSwitcher: "Close Baselayer-Switcher", + }, + closeButtonImage = , + onCloseButtonClick = null, + onLayerButtonClick = null, + onSwitcherButtonClick = null, + t = (s) => s, }) { const [switcherOpen, setSwitcherOpen] = useState(false); const [isClosed, setIsClosed] = useState(true); @@ -111,20 +146,6 @@ function BaseLayerSwitcher({ getVisibleLayer(layers) || layers[0], ); - useEffect(() => { - // Update the layer selected when a visibility changes. - const olKeys = (layers || []).map((layer) => { - return layer.on("change:visible", (evt) => { - if (evt.target.visible && currentLayer !== evt.target) { - setCurrentLayer(evt.target); - } - }); - }); - return () => { - unByKey(olKeys); - }; - }, [currentLayer, layers]); - /* Images are loaded from props if provided, fallback from layer */ const images = layerImages ? Object.keys(layerImages).map((layerImage) => { @@ -137,12 +158,17 @@ function BaseLayerSwitcher({ const openClass = switcherOpen ? " rs-open" : ""; const hiddenStyle = switcherOpen && !isClosed ? "visible" : "hidden"; - const handleSwitcherClick = () => { + const handleSwitcherClick = (evt) => { + const nextLayer = layers.find((layer) => { + return !layer.visible; + }); + const onButtonClick = + layers.length === 2 ? onLayerButtonClick : onSwitcherButtonClick; + if (onButtonClick) { + onButtonClick(evt, nextLayer); + } if (layers.length === 2) { /* On only two layer options the opener becomes a layer toggle button */ - const nextLayer = layers.find((layer) => { - return !layer.visible; - }); if (currentLayer.setVisible) { currentLayer.setVisible(false); } else { @@ -160,7 +186,10 @@ function BaseLayerSwitcher({ return setSwitcherOpen(true) && setIsClosed(false); }; - const onLayerSelect = (layer) => { + const onLayerSelect = (layer, evt) => { + if (onLayerButtonClick) { + onLayerButtonClick(evt, layer); + } if (!switcherOpen) { setSwitcherOpen(true); return; @@ -214,30 +243,24 @@ function BaseLayerSwitcher({ }; }, [switcherOpen]); + useEffect(() => { + // Update the layer selected when a visibility changes. + const olKeys = (layers || []).map((layer) => { + return layer.on("change:visible", (evt) => { + if (evt.target.visible && currentLayer !== evt.target) { + setCurrentLayer(evt.target); + } + }); + }); + return () => { + unByKey(olKeys); + }; + }, [currentLayer, layers]); + if (!layers || layers.length < 2 || !currentLayer) { return null; } - const toggleBtn = ( -
-
{ - return setSwitcherOpen(false); - }} - onKeyPress={(e) => { - return e.which === 13 && setSwitcherOpen(false); - }} - tabIndex={switcherOpen ? "0" : "-1"} - aria-label={titles.closeSwitcher} - title={titles.closeSwitcher} - > - {closeButtonImage} -
-
- ); - return (
{ - return onLayerSelect(layer); + onClick={(evt) => { + return onLayerSelect(layer, evt); }} - onKeyPress={(e) => { - if (e.which === 13) { - onLayerSelect(layer); + onKeyPress={(evt) => { + if (evt.which === 13) { + onLayerSelect(layer, evt); } }} style={imageStyle} @@ -311,12 +334,22 @@ function BaseLayerSwitcher({
); })} - {toggleBtn} + { + if (onCloseButtonClick) { + onCloseButtonClick(evt); + } + setSwitcherOpen(false); + }} + tabIndex={switcherOpen ? "0" : "-1"} + title={titles.closeSwitcher} + > + {closeButtonImage} +
); } BaseLayerSwitcher.propTypes = propTypes; -BaseLayerSwitcher.defaultProps = defaultProps; export default BaseLayerSwitcher; diff --git a/src/components/BaseLayerSwitcher/BaseLayerSwitcher.scss b/src/components/BaseLayerSwitcher/BaseLayerSwitcher.scss index 224b2b62..ad1a34aa 100644 --- a/src/components/BaseLayerSwitcher/BaseLayerSwitcher.scss +++ b/src/components/BaseLayerSwitcher/BaseLayerSwitcher.scss @@ -4,6 +4,7 @@ transition: 800ms width; overflow: hidden; display: flex; + align-items: center; padding: 2px; pointer-events: none; diff --git a/src/components/BaseLayerSwitcher/__snapshots__/BaseLayerSwitcher.test.js.snap b/src/components/BaseLayerSwitcher/__snapshots__/BaseLayerSwitcher.test.js.snap index 7a08b625..4997d301 100644 --- a/src/components/BaseLayerSwitcher/__snapshots__/BaseLayerSwitcher.test.js.snap +++ b/src/components/BaseLayerSwitcher/__snapshots__/BaseLayerSwitcher.test.js.snap @@ -64,25 +64,23 @@ exports[`BaseLayerSwitcher matches snapshots using default properties. 1`] = ` -
-
+ - - - - -
+ + +
`; diff --git a/src/components/Zoom/Zoom.js b/src/components/Zoom/Zoom.js index a3e393bb..322e5c98 100644 --- a/src/components/Zoom/Zoom.js +++ b/src/components/Zoom/Zoom.js @@ -45,17 +45,18 @@ const propTypes = { * Display a slider to zoom. */ zoomSlider: PropTypes.bool, -}; -const defaultProps = { - titles: { - zoomIn: "Zoom in", - zoomOut: "Zoom out", - }, - zoomInChildren: , - zoomOutChildren: , - zoomSlider: false, - delta: 1, + /** + * Callback function on zoom-in button click. + * @param {function} Callback function triggered when zoom-in button is clicked. Takes the event as argument. + */ + onZoomInButtonClick: PropTypes.func, + + /** + * Callback function on zoom-out button click. + * @param {function} Callback function triggered when the zoom-out button is clicked. Takes the event as argument. + */ + onZoomOutButtonClick: PropTypes.func, }; const updateZoom = (map, delta) => { @@ -79,11 +80,16 @@ const updateZoom = (map, delta) => { */ function Zoom({ map, - titles, - zoomInChildren, - zoomOutChildren, - zoomSlider, - delta, + titles = { + zoomIn: "Zoom in", + zoomOut: "Zoom out", + }, + zoomInChildren = , + zoomOutChildren = , + zoomSlider = false, + onZoomInButtonClick = null, + onZoomOutButtonClick = null, + delta = 1, ...other }) { const ref = useRef(); @@ -91,20 +97,26 @@ function Zoom({ const zoomIn = useCallback( (evt) => { + if (onZoomInButtonClick) { + onZoomInButtonClick(evt); + } if (!evt.which || evt.which === 13) { updateZoom(map, delta); } }, - [delta, map], + [delta, map, onZoomInButtonClick], ); const zoomOut = useCallback( (evt) => { + if (onZoomOutButtonClick) { + onZoomOutButtonClick(evt); + } if (!evt.which || evt.which === 13) { updateZoom(map, -delta); } }, - [delta, map], + [delta, map, onZoomOutButtonClick], ); const zoomInDisabled = useMemo(() => { @@ -175,6 +187,5 @@ function Zoom({ } Zoom.propTypes = propTypes; -Zoom.defaultProps = defaultProps; export default React.memo(Zoom); diff --git a/src/components/Zoom/Zoom.test.js b/src/components/Zoom/Zoom.test.js index c02a4404..228215dc 100644 --- a/src/components/Zoom/Zoom.test.js +++ b/src/components/Zoom/Zoom.test.js @@ -1,84 +1,61 @@ import React from "react"; -import renderer from "react-test-renderer"; -import { configure, shallow, mount } from "enzyme"; -import Adapter from "@cfaester/enzyme-adapter-react-18"; +import { fireEvent, render } from "@testing-library/react"; import { act } from "react-dom/test-utils"; import MapEvent from "ol/MapEvent"; import OLView from "ol/View"; import OLMap from "ol/Map"; import Zoom from "./Zoom"; -configure({ adapter: new Adapter() }); - describe("Zoom", () => { test("should match snapshot.", () => { const map = new OLMap({}); - const component = renderer.create(); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); + const { container } = render(); + expect(container.innerHTML).toMatchSnapshot(); }); test("should match snapshot with custom attributes", () => { const map = new OLMap({}); - const component = renderer.create( + const { container } = render( , ); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); + expect(container.innerHTML).toMatchSnapshot(); }); test("should match snapshot with zoom slider", () => { const map = new OLMap({}); - const component = renderer.create(); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); + const { container } = render(); + expect(container.innerHTML).toMatchSnapshot(); }); [ ["click", {}], ["keypress", { which: 13 }], ].forEach((evt) => { - test(`should zoom in on ${evt[0]}.`, () => { + test(`should zoom in on ${evt[0]}.`, async () => { const map = new OLMap({ view: new OLView({ zoom: 5 }) }); - const zooms = shallow(); - zooms - .find(".rs-zoom-in") - .first() - .simulate(...evt); - + const { container } = render(); + await fireEvent.click(container.querySelector(".rs-zoom-in")); expect(map.getView().getZoom()).toBe(6); }); - test(`should zoom in on ${evt[0]} (delta: 0.3).`, () => { + test(`should zoom in on ${evt[0]} (delta: 0.3).`, async () => { const map = new OLMap({ view: new OLView({ zoom: 5 }) }); - const zooms = shallow(); - zooms - .find(".rs-zoom-in") - .first() - .simulate(...evt); - + const { container } = render(); + await fireEvent.click(container.querySelector(".rs-zoom-in")); expect(map.getView().getZoom()).toBe(5.3); }); - test(`should zoom out on ${evt[0]}.`, () => { + test(`should zoom out on ${evt[0]}.`, async () => { const map = new OLMap({ view: new OLView({ zoom: 5 }) }); - const zooms = shallow(); - zooms - .find(".rs-zoom-out") - .first() - .simulate(...evt); - + const { container } = render(); + await fireEvent.click(container.querySelector(".rs-zoom-out")); expect(map.getView().getZoom()).toBe(4); }); - test(`should zoom out on ${evt[0]} (delta: 0.3).`, () => { + test(`should zoom out on ${evt[0]} (delta: 0.3).`, async () => { const map = new OLMap({ view: new OLView({ zoom: 5 }) }); - const zooms = shallow(); - zooms - .find(".rs-zoom-out") - .first() - .simulate(...evt); - + const { container } = render(); + await fireEvent.click(container.querySelector(".rs-zoom-out")); expect(map.getView().getZoom()).toBe(4.7); }); }); @@ -87,55 +64,75 @@ describe("Zoom", () => { const map = new OLMap({}); const spy = jest.spyOn(map, "removeControl"); const spy2 = jest.spyOn(map, "addControl"); - const wrapper = mount(); + const { unmount } = render(); expect(spy).toHaveBeenCalledTimes(0); - wrapper.unmount(); + unmount(); expect(spy).toHaveBeenCalledTimes(1); expect(spy.mock.calls[0][0]).toBe(spy2.mock.calls[0][0]); }); - test("should disable zoom-in button on mount with max zoom..", () => { + test("should disable zoom-in button on mount with max zoom..", async () => { const map = new OLMap({ view: new OLView({ maxZoom: 20, zoom: 20 }), }); const spy = jest.spyOn(map.getView(), "setZoom"); - const wrapper = mount(); + const { rerender, container } = render(); act(() => { map.dispatchEvent(new MapEvent("moveend", map)); }); - wrapper.update(); - expect(wrapper.find(".rs-zoom-in").prop("disabled")).toEqual(true); - wrapper.find(".rs-zoom-in").first().simulate("click"); + rerender(); + expect(container.querySelector(".rs-zoom-in").disabled).toEqual(true); + await fireEvent.click(container.querySelector(".rs-zoom-in")); expect(spy).toHaveBeenCalledTimes(0); }); - test("should disable zoom-out button on mount with min zoom.", () => { + test("should disable zoom-out button on mount with min zoom.", async () => { const map = new OLMap({ view: new OLView({ minZoom: 2, zoom: 2 }), }); const spy = jest.spyOn(map.getView(), "setZoom"); - const wrapper = mount(); + const { rerender, container } = render(); act(() => { map.dispatchEvent(new MapEvent("moveend", map)); }); - wrapper.update(); - expect(wrapper.find(".rs-zoom-out").prop("disabled")).toEqual(true); - wrapper.find(".rs-zoom-out").first().simulate("click"); + rerender(); + expect(container.querySelector(".rs-zoom-out").disabled).toEqual(true); + await fireEvent.click(container.querySelector(".rs-zoom-out")); expect(spy).toHaveBeenCalledTimes(0); }); }); -test("should disable zoom-out button when reaching min zoom.", () => { +test("should disable zoom-out button when reaching min zoom.", async () => { const map = new OLMap({ view: new OLView({ minZoom: 2, zoom: 3 }), }); const spy = jest.spyOn(map.getView(), "setZoom"); - const wrapper = mount(); - wrapper.find(".rs-zoom-out").first().simulate("click"); + const { rerender, container } = render(); + await fireEvent.click(container.querySelector(".rs-zoom-out")); expect(spy).toHaveBeenCalledTimes(1); act(() => { map.dispatchEvent(new MapEvent("moveend", map)); }); - wrapper.update(); - expect(wrapper.find(".rs-zoom-out").prop("disabled")).toEqual(true); + rerender(); + expect(container.querySelector(".rs-zoom-out").disabled).toEqual(true); +}); + +test("should trigger callback functions.", async () => { + const map = new OLMap({ + view: new OLView({ minZoom: 2, zoom: 3 }), + }); + const zoomIn = jest.fn(); + const zoomOut = jest.fn(); + const { container } = render( + , + ); + await fireEvent.click(container.querySelector(".rs-zoom-out")); + expect(zoomOut).toHaveBeenCalledTimes(1); + await fireEvent.click(container.querySelector(".rs-zoom-in")); + await fireEvent.click(container.querySelector(".rs-zoom-in")); + expect(zoomIn).toHaveBeenCalledTimes(2); }); diff --git a/src/components/Zoom/__snapshots__/Zoom.test.js.snap b/src/components/Zoom/__snapshots__/Zoom.test.js.snap index 4278c387..f01bc42b 100644 --- a/src/components/Zoom/__snapshots__/Zoom.test.js.snap +++ b/src/components/Zoom/__snapshots__/Zoom.test.js.snap @@ -1,200 +1,137 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Zoom should match snapshot with custom attributes 1`] = ` -
- -
`; exports[`Zoom should match snapshot with zoom slider 1`] = ` -
- -
- +
+
+ `; exports[`Zoom should match snapshot. 1`] = ` -
- -