From 7eb49ed55eb548c342f83bcdbf9dc655655bafe7 Mon Sep 17 00:00:00 2001 From: Martin Schuhfuss Date: Mon, 6 Nov 2023 09:42:03 +0100 Subject: [PATCH] feat: new MapControl component (#51) The MapControl component allows users to add custom react components to the controls of a Map instance. --- docs/api-reference/components/map-control.md | 49 +++++++++++++++ docs/table-of-contents.json | 1 + src/components/__tests__/map-control.test.tsx | 48 +++++++++++++++ src/components/map-control.tsx | 60 +++++++++++++++++++ src/index.ts | 1 + 5 files changed, 159 insertions(+) create mode 100644 docs/api-reference/components/map-control.md create mode 100644 src/components/__tests__/map-control.test.tsx create mode 100644 src/components/map-control.tsx diff --git a/docs/api-reference/components/map-control.md b/docs/api-reference/components/map-control.md new file mode 100644 index 00000000..87a6fcb3 --- /dev/null +++ b/docs/api-reference/components/map-control.md @@ -0,0 +1,49 @@ +# `` Component + +The `MapControl` component can be used to render components into the +control-containers of a map instance. + +The Maps JavaScript API uses a custom layout algorithm for map controls. +While you can add your buttons or whatever controls you need on top of +the map canvas, that isn't much of an option when you need to mix built-in +controls with your own controls. In this case adding your controls to +the map is the best option. + +See [the official documentation on this topic][gmp-custom-ctrl]. + +## Usage + +You can add as many `MapControl` components as you like to any `Map`, multiple +controls for the same position are possible as well. + +```tsx +import { + APIProvider, + ControlPosition, + Map, + MapControl +} from '@vis.gl/react-google-maps'; + +const App = () => ( + + + + .. any component here will be added to the control-containers of the + google map instance .. + + + +); +``` + +## Props + +### Required + +#### `position`: ControlPosition + +The position is specified as one of the values of the `ControlPosition` enum, which +is an exact copy of the [`google.maps.ControlPosition`][gmp-ctrl-pos] type. + +[gmp-custom-ctrl]: https://developers.google.com/maps/documentation/javascript/controls#CustomControls +[gmp-ctrl-pos]: https://developers.google.com/maps/documentation/javascript/controls#ControlPositioning diff --git a/docs/table-of-contents.json b/docs/table-of-contents.json index 290f43ed..6fe42bdd 100644 --- a/docs/table-of-contents.json +++ b/docs/table-of-contents.json @@ -26,6 +26,7 @@ "items": [ "api-reference/components/api-provider", "api-reference/components/map", + "api-reference/components/map-control", "api-reference/components/info-window", "api-reference/components/marker", "api-reference/components/advanced-marker", diff --git a/src/components/__tests__/map-control.test.tsx b/src/components/__tests__/map-control.test.tsx new file mode 100644 index 00000000..74067af1 --- /dev/null +++ b/src/components/__tests__/map-control.test.tsx @@ -0,0 +1,48 @@ +import '@testing-library/jest-dom'; + +import React, {ReactElement} from 'react'; +import {initialize} from '@googlemaps/jest-mocks'; +import {cleanup, render} from '@testing-library/react'; + +import {APIProvider} from '../api-provider'; +import {Map} from '../map'; +import {ControlPosition, MapControl} from '../map-control'; +import {waitForMockInstance} from './__utils__/wait-for-mock-instance'; + +jest.mock('../../libraries/google-maps-api-loader'); + +let wrapper: ({children}: {children: React.ReactNode}) => ReactElement | null; + +beforeEach(() => { + initialize(); + + wrapper = ({children}: {children: React.ReactNode}) => ( + + + {children} + + + ); +}); + +afterEach(() => { + cleanup(); + jest.restoreAllMocks(); +}); + +test('control is added to the map', async () => { + render( + + + , + {wrapper} + ); + + const map = await waitForMockInstance(google.maps.Map); + const controlsArray = map.controls[ControlPosition.BOTTOM_CENTER]; + + expect(controlsArray.push).toHaveBeenCalled(); + + const [controlEl] = (controlsArray.push as jest.Mock).mock.calls[0]; + expect(controlEl).toHaveTextContent('control button'); +}); diff --git a/src/components/map-control.tsx b/src/components/map-control.tsx new file mode 100644 index 00000000..90eba9ee --- /dev/null +++ b/src/components/map-control.tsx @@ -0,0 +1,60 @@ +import {useEffect, useMemo} from 'react'; +import {createPortal} from 'react-dom'; +import {useMap} from '../hooks/use-map'; + +import type {PropsWithChildren} from 'react'; + +type MapControlProps = PropsWithChildren<{ + position: ControlPosition; +}>; + +/** + * Copy of the `google.maps.ControlPosition` constants. + * They have to be duplicated here since we can't wait for the maps API to load to be able to use them. + */ +export enum ControlPosition { + BLOCK_END_INLINE_CENTER = 0, + BLOCK_END_INLINE_END = 1, + BLOCK_END_INLINE_START = 2, + BLOCK_START_INLINE_CENTER = 3, + BLOCK_START_INLINE_END = 4, + BLOCK_START_INLINE_START = 5, + BOTTOM_CENTER = 6, + BOTTOM_LEFT = 7, + BOTTOM_RIGHT = 8, + INLINE_END_BLOCK_CENTER = 9, + INLINE_END_BLOCK_END = 10, + INLINE_END_BLOCK_START = 11, + INLINE_START_BLOCK_CENTER = 12, + INLINE_START_BLOCK_END = 13, + INLINE_START_BLOCK_START = 14, + LEFT_BOTTOM = 15, + LEFT_CENTER = 16, + LEFT_TOP = 17, + RIGHT_BOTTOM = 18, + RIGHT_CENTER = 19, + RIGHT_TOP = 20, + TOP_CENTER = 21, + TOP_LEFT = 22, + TOP_RIGHT = 23 +} + +export const MapControl = ({children, position}: MapControlProps) => { + const controlContainer = useMemo(() => document.createElement('div'), []); + const map = useMap(); + + useEffect(() => { + if (!map) return; + + const controls = map.controls[position]; + + controls.push(controlContainer); + + return () => { + const index = controls.getArray().indexOf(controlContainer); + controls.removeAt(index); + }; + }, [map, position]); + + return createPortal(children, controlContainer); +}; diff --git a/src/index.ts b/src/index.ts index 9377bb00..a07a3899 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from './components/advanced-marker'; export * from './components/api-provider'; export * from './components/info-window'; export * from './components/map'; +export * from './components/map-control'; export * from './components/marker'; export * from './components/pin'; export * from './hooks/use-api-loading-status';