├── .gitignore ├── lib ├── Control.d.ts ├── Control.js.map └── Control.js ├── tsconfig.json ├── LICENSE.md ├── package.json ├── src └── Control.tsx └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /lib/Control.d.ts: -------------------------------------------------------------------------------- 1 | import L from 'leaflet'; 2 | import React from 'react'; 3 | interface Props { 4 | position: L.ControlPosition; 5 | children?: React.ReactNode; 6 | container?: React.HTMLAttributes; 7 | prepend?: boolean; 8 | } 9 | declare const Control: (props: Props) => JSX.Element; 10 | export default Control; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "baseUrl": "./src", 11 | "declaration": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "outDir": "./lib", 19 | "module": "esnext", 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "sourceMap": true, 24 | "jsx": "react" 25 | }, 26 | "include": [ 27 | "./src/*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christopher McBride 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-leaflet-custom-control", 3 | "version": "1.5.0", 4 | "description": "Creates a control wrapper around a React element", 5 | "main": "lib/Control.js", 6 | "author": "chris-m92", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/chris-m92/react-leaflet-custom-control.git" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "leaflet", 15 | "react-leaflet", 16 | "react-leaflet-v3", 17 | "react-leaflet-control", 18 | "react-leaflet-custom-control" 19 | ], 20 | "peerDependencies": { 21 | "leaflet": "^1.9.0", 22 | "react": "^17.0.2 || ^18.0.0 || ^19.0.0", 23 | "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" 24 | }, 25 | "devDependencies": { 26 | "@testing-library/jest-dom": "^5.16.4", 27 | "@testing-library/react": "^13.1.1", 28 | "@testing-library/user-event": "^14.1.1", 29 | "@types/jest": "^27.4.1", 30 | "@types/leaflet": "^1.7.9", 31 | "@types/node": "^17.0.25", 32 | "@types/react": "^18.0.6", 33 | "@types/react-dom": "^18.0.2", 34 | "react-scripts": "^5.0.1", 35 | "typescript": "^5.0.4" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/chris-m92/react-leaflet-custom-control/issues" 39 | }, 40 | "homepage": "https://github.com/chris-m92/react-leaflet-custom-control#readme", 41 | "type": "module", 42 | "dependencies": { 43 | "react-leaflet": "^5.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/Control.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Control.js","sourceRoot":"","sources":["../src/Control.tsx"],"names":[],"mappings":";;;;;;;;;;;AAAA,OAAO,CAAC,MAAM,SAAS,CAAA;AACvB,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAStC,IAAM,gBAAgB,GAAG;IACvB,UAAU,EAAE,6BAA6B;IACzC,WAAW,EAAE,8BAA8B;IAC3C,OAAO,EAAE,0BAA0B;IACnC,QAAQ,EAAE,2BAA2B;CACtC,CAAA;AAED,IAAM,OAAO,GAAG,UAAC,KAAY;;IACrB,IAAA,KAA8B,KAAK,CAAC,QAAQ,CAAM,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,EAA/E,UAAU,QAAA,EAAE,aAAa,QAAsD,CAAA;IACtF,IAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,IAAI,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAA;IACzG,IAAM,mBAAmB,GAAG,KAAK,CAAC,MAAM,CAAwB,IAAI,CAAC,CAAA;IACrE,IAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IAEpB;;;;OAIG;IACH,KAAK,CAAC,SAAS,CAAC;QACd,IAAI,mBAAmB,CAAC,OAAO,KAAK,IAAI,EAAE;YACxC,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAA;YAC/D,CAAC,CAAC,QAAQ,CAAC,wBAAwB,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAA;SACjE;IACH,CAAC,EAAE,CAAC,mBAAmB,CAAC,CAAC,CAAA;IAEzB;;;;OAIG;IACH,KAAK,CAAC,SAAS,CAAC;QACd,IAAM,YAAY,GAAG,GAAG,CAAC,YAAY,EAAE,CAAA;QACvC,IAAM,SAAS,GAAG,YAAY,CAAC,sBAAsB,CAAC,aAAa,CAAC,CAAA;QACpE,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7B,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAA;IAEnB;;;OAGG;IACH,KAAK,CAAC,SAAS,CAAC;QACd,IAAI,UAAU,KAAK,IAAI,EAAE;YACvB,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,EAAE;gBACzD,UAAU,CAAC,OAAO,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAA;aAChD;iBAAM;gBACL,UAAU,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAA;aAC/C;SACF;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAA;IAEpD;;OAEG;IACH,IAAM,SAAS,GAAG,CAAC,CAAA,MAAA,MAAA,KAAK,CAAC,SAAS,0CAAE,SAAS,0CAAE,MAAM,CAAC,GAAG,CAAC,KAAI,EAAE,CAAC,GAAG,iBAAiB,CAAA;IAErF;;OAEG;IACH,OAAO,CACL,wCACM,KAAK,CAAC,SAAS,IACnB,GAAG,EAAE,mBAAmB,EACxB,SAAS,EAAE,SAAS,KAEnB,KAAK,CAAC,QAAQ,CACX,CACP,CAAA;AACH,CAAC,CAAA;AAED,eAAe,OAAO,CAAA"} -------------------------------------------------------------------------------- /src/Control.tsx: -------------------------------------------------------------------------------- 1 | import L from 'leaflet' 2 | import React from 'react' 3 | import { useMap } from 'react-leaflet' 4 | 5 | interface Props { 6 | position: L.ControlPosition 7 | children?: React.ReactNode 8 | container?: React.HTMLAttributes 9 | prepend?: boolean 10 | } 11 | 12 | const POSITION_CLASSES = { 13 | bottomleft: 'leaflet-bottom leaflet-left', 14 | bottomright: 'leaflet-bottom leaflet-right', 15 | topleft: 'leaflet-top leaflet-left', 16 | topright: 'leaflet-top leaflet-right', 17 | } 18 | 19 | const Control = (props: Props): JSX.Element => { 20 | const [portalRoot, setPortalRoot] = React.useState(document.createElement('div')) 21 | const positionClass = ((props.position && POSITION_CLASSES[props.position]) || POSITION_CLASSES.topright) 22 | const controlContainerRef = React.useRef(null) 23 | const map = useMap() 24 | 25 | /** 26 | * Whenever the control container ref is created, 27 | * Ensure the click / scroll propagation is removed 28 | * This way click/scroll events do not bubble down to the map 29 | */ 30 | React.useEffect(() => { 31 | if (controlContainerRef.current !== null) { 32 | L.DomEvent.disableClickPropagation(controlContainerRef.current) 33 | L.DomEvent.disableScrollPropagation(controlContainerRef.current) 34 | } 35 | }, [controlContainerRef]) 36 | 37 | /** 38 | * Whenever the position is changed, go ahead and get the container of the map and the first 39 | * instance of the position class in that map container 40 | * Fixes #17 41 | */ 42 | React.useEffect(() => { 43 | const mapContainer = map.getContainer() 44 | const targetDiv = mapContainer.getElementsByClassName(positionClass) 45 | setPortalRoot(targetDiv[0]) 46 | }, [positionClass]) 47 | 48 | /** 49 | * Whenever the portal root is complete, 50 | * append or prepend the control container to the portal root 51 | */ 52 | React.useEffect(() => { 53 | if (portalRoot !== null) { 54 | if (props.prepend !== undefined && props.prepend === true) { 55 | portalRoot.prepend(controlContainerRef.current) 56 | } else { 57 | portalRoot.append(controlContainerRef.current) 58 | } 59 | } 60 | }, [portalRoot, props.prepend, controlContainerRef]) 61 | 62 | /** 63 | * Concatenate the props.container className to the class of the control div 64 | */ 65 | const className = (props.container?.className?.concat(' ') || '') + 'leaflet-control' 66 | 67 | /** 68 | * Render 69 | */ 70 | return ( 71 |
76 | {props.children} 77 |
78 | ) 79 | } 80 | 81 | export default Control 82 | -------------------------------------------------------------------------------- /lib/Control.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || function () { 2 | __assign = Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | return __assign.apply(this, arguments); 11 | }; 12 | import L from 'leaflet'; 13 | import React from 'react'; 14 | import { useMap } from 'react-leaflet'; 15 | var POSITION_CLASSES = { 16 | bottomleft: 'leaflet-bottom leaflet-left', 17 | bottomright: 'leaflet-bottom leaflet-right', 18 | topleft: 'leaflet-top leaflet-left', 19 | topright: 'leaflet-top leaflet-right', 20 | }; 21 | var Control = function (props) { 22 | var _a, _b; 23 | var _c = React.useState(document.createElement('div')), portalRoot = _c[0], setPortalRoot = _c[1]; 24 | var positionClass = ((props.position && POSITION_CLASSES[props.position]) || POSITION_CLASSES.topright); 25 | var controlContainerRef = React.useRef(null); 26 | var map = useMap(); 27 | /** 28 | * Whenever the control container ref is created, 29 | * Ensure the click / scroll propagation is removed 30 | * This way click/scroll events do not bubble down to the map 31 | */ 32 | React.useEffect(function () { 33 | if (controlContainerRef.current !== null) { 34 | L.DomEvent.disableClickPropagation(controlContainerRef.current); 35 | L.DomEvent.disableScrollPropagation(controlContainerRef.current); 36 | } 37 | }, [controlContainerRef]); 38 | /** 39 | * Whenever the position is changed, go ahead and get the container of the map and the first 40 | * instance of the position class in that map container 41 | * Fixes #17 42 | */ 43 | React.useEffect(function () { 44 | var mapContainer = map.getContainer(); 45 | var targetDiv = mapContainer.getElementsByClassName(positionClass); 46 | setPortalRoot(targetDiv[0]); 47 | }, [positionClass]); 48 | /** 49 | * Whenever the portal root is complete, 50 | * append or prepend the control container to the portal root 51 | */ 52 | React.useEffect(function () { 53 | if (portalRoot !== null) { 54 | if (props.prepend !== undefined && props.prepend === true) { 55 | portalRoot.prepend(controlContainerRef.current); 56 | } 57 | else { 58 | portalRoot.append(controlContainerRef.current); 59 | } 60 | } 61 | }, [portalRoot, props.prepend, controlContainerRef]); 62 | /** 63 | * Concatenate the props.container className to the class of the control div 64 | */ 65 | var className = (((_b = (_a = props.container) === null || _a === void 0 ? void 0 : _a.className) === null || _b === void 0 ? void 0 : _b.concat(' ')) || '') + 'leaflet-control'; 66 | /** 67 | * Render 68 | */ 69 | return (React.createElement("div", __assign({}, props.container, { ref: controlContainerRef, className: className }), props.children)); 70 | }; 71 | export default Control; 72 | //# sourceMappingURL=Control.js.map -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-leaflet-custom-control 2 | [![npm](https://img.shields.io/npm/v/react-leaflet-custom-control.svg)](https://npmjs.com/package/react-leaflet-custom-control) 3 | [![npm](https://img.shields.io/npm/dt/react-leaflet-custom-control.svg)](https://npmjs.com/package/react-leaflet-custom-control) 4 | [![license](https://img.shields.io/github/license/chris-m92/react-leaflet-custom-control.svg)](https://github.com/chris-m92/react-leaflet-custom-control) 5 | 6 | 7 | A React wrapper to create a custom control for [react-leaflet](https://github.com/PaulLeCam/react-leaflet) using ReactDOM's Portal capabilities 8 | 9 | The current version of this package supports React Leaflet v3 10 | 11 | [Code Sandbox Demo](https://codesandbox.io/s/n1xpv) 12 | 13 | **NOTE** 14 | || 15 | |--| 16 | |Version `^1.2.3` (which adds this note to the README) has updated peer dependencies for React v18. This may be a breaking change depending on your environment. If you are still running React v17 then install version 1.2.2.| 17 | |Version `^1.4.0` now has a dependency on `react-leaflet@^4.2.1`. This allows for the `useMap()` hook. This also requires that this `Control` component **MUST** be a child of your `MapContainer`| 18 | |Version `^1.5.0` has updated react dependencies to include `^react@19.0.0`| 19 | 20 | ## Installation 21 | ```bash 22 | #npm 23 | npm install --save react-leaflet-custom-control 24 | 25 | #yarn 26 | yarn add react-leaflet-custom-control 27 | ``` 28 | 29 | ## Usage 30 | ```jsx 31 | import { MapContainer, TileLayer } from 'react-leaflet' 32 | import Control from 'react-leaflet-custom-control' 33 | import { Button } from '@mui/material' 34 | import { Search as SearchIcon } from '@mui/icons-material' 35 | import 'leaflet.css' 36 | 37 | 38 | 46 | 47 | 50 | 51 | 52 | ``` 53 | ## Order Matters! 54 | Because this uses `React.createPortal` which inherently appends the portal, DOM manipulation is used to append or prepend a container element to the portal target. Because of this, the order of your custom controls matter! The last `Control` element to be prepended to a control position will be at the very top while the last `Control` element to be appended to a control position will be at the very bottom. If mixing with default `React Leaflet` controls, they will be in between your custom controls. 55 | 56 | ### Weird Quirks 57 | However, because of the way that the portal works and re-renders, multiple control elements will shift order after renders, so it's recommended to have a wrapping element be the child of the `Control` to prevent re-ordering each render 58 | 59 | ### Example 60 | ```jsx 61 | import { MapContainer, TileLayer, ZoomControl } from 'react-leaflet' 62 | import Control from 'react-leaflet-custom-control' 63 | import { Button, Stack } from '@mui/material' 64 | import { 65 | Add as AddIcon, 66 | Delete as DeleteIcon, 67 | Search as SearchIcon 68 | } from '@mui/icons-material' 69 | import 'leaflet.css' 70 | 71 | 72 | 80 | {/* Search control is the very top right control */} 81 | 82 | 85 | 86 | 87 | {/* This control will be below the default zoom control. Note the wrapping Stack component */} 88 | 89 | 90 | 93 | 96 | 97 | 98 | 99 | ``` 100 | 101 | ## Props 102 | | Name | Type | Default | Description | 103 | |----------------|----------------------------------------------------------------------|------------------|------------------------------------| 104 | | position | [ControlOptions](https://leafletjs.com/reference-1.7.1.html#control) | **required** | The position of the control | 105 | | children? | any | undefined | Child element to the control | 106 | | ~~style?~~ | ~~`React.CSSProperties`~~ | ~~undefined~~ | ~~CSS Styles to override the control~~ | 107 | | container? | `React.HTMLAttributes` | undefined | The target root container for the portal | 108 | | prepend? | boolean | undefined | Whether the control should be prepended or appended to the position| 109 | 110 | ## Thanks 111 | Huge thanks to @davetapley for contributing to `@1.3.0` and helping to work some of the issues. 112 | Thanks to @samiamlabs for contributing to `@1.3.2` for fixing the infinite `div` issue. 113 | --------------------------------------------------------------------------------