Skip to content

Commit

Permalink
feat: new MapControl component (#51)
Browse files Browse the repository at this point in the history
The MapControl component allows users to add custom react components to the controls of a Map instance.
  • Loading branch information
usefulthink authored Nov 6, 2023
1 parent b01fc8b commit 7eb49ed
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 0 deletions.
49 changes: 49 additions & 0 deletions docs/api-reference/components/map-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# `<MapControl>` 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 = () => (
<APIProvider apiKey={'...'}>
<Map {...mapProps}>
<MapControl position={ControlPosition.TOP_LEFT}>
.. any component here will be added to the control-containers of the
google map instance ..
</MapControl>
</Map>
</APIProvider>
);
```

## 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
1 change: 1 addition & 0 deletions docs/table-of-contents.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
48 changes: 48 additions & 0 deletions src/components/__tests__/map-control.test.tsx
Original file line number Diff line number Diff line change
@@ -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}) => (
<APIProvider apiKey={'apikey'}>
<Map zoom={10} center={{lat: 0, lng: 0}}>
{children}
</Map>
</APIProvider>
);
});

afterEach(() => {
cleanup();
jest.restoreAllMocks();
});

test('control is added to the map', async () => {
render(
<MapControl position={ControlPosition.BOTTOM_CENTER}>
<button>control button</button>
</MapControl>,
{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');
});
60 changes: 60 additions & 0 deletions src/components/map-control.tsx
Original file line number Diff line number Diff line change
@@ -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);
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 7eb49ed

Please sign in to comment.