Skip to content

Commit f23bda4

Browse files
dfallingclaude
andcommitted
Add location control button component
Add lit-google-map-location-button component that provides a native-looking button for centering the map on the user's current location. The component: - Uses the browser's Geolocation API to get user location - Adds a control button to the map at a configurable position - Centers the map on user location when clicked - Dispatches events for location-found, location-error, location-requested - Supports disabled state and loading animation The main map component now supports a "controls" slot for adding custom controls following the same pattern as markers and shapes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent fe11492 commit f23bda4

8 files changed

Lines changed: 775 additions & 9 deletions

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This project is a fork of [lit-google-map](https://github.com/arkadiuszwojcik/li
88
- update dependencies and keep current with Dependabot
99
- add `zoom_changed`, `center_changed`, and `view_changed` events
1010
- move to [AdvancedMarkerElement](https://developers.google.com/maps/documentation/javascript/advanced-markers/migration)
11+
- add location button control for centering map on user's current location
1112

1213
## Table of contents
1314

@@ -23,6 +24,8 @@ This project is a fork of [lit-google-map](https://github.com/arkadiuszwojcik/li
2324

2425
[Polygon shape element attributes](#Polygon-shape-element-attributes)
2526

27+
[Location button control](#Location-button-control)
28+
2629
[How to build](#How-to-build)
2730

2831
[License](#License)
@@ -208,6 +211,51 @@ Example:
208211
</lit-google-map-polygon>
209212
```
210213

214+
## Location button control
215+
216+
The location button control adds a native-looking button to the map that centers the map on the user's current location using the browser's Geolocation API.
217+
218+
**Note:** Geolocation requires HTTPS (or localhost for development).
219+
220+
### Location button attributes
221+
222+
- '_position_' - Control position on map (default: 'RIGHT_BOTTOM')
223+
- Valid values: 'TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT', 'LEFT_TOP', 'LEFT_CENTER', 'LEFT_BOTTOM', 'RIGHT_TOP', 'RIGHT_CENTER', 'RIGHT_BOTTOM', 'BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT'
224+
- '_label_' - Accessible label for screen readers (default: 'My Location')
225+
- '_disabled_' - Disable the button (default: false)
226+
227+
### Location button events
228+
229+
- '_location-requested_' - Fired when button is clicked and location request begins
230+
- '_location-found_' - Fired when location is successfully obtained
231+
- Detail: `{lat: number, lng: number}`
232+
- '_location-error_' - Fired when geolocation fails
233+
- Detail: `{code: number, message: string}`
234+
235+
### Example
236+
237+
Basic usage:
238+
239+
```html
240+
<lit-google-map api-key="YOUR_GOOGLE_MAPS_API_KEY">
241+
<lit-google-map-location-button slot="controls">
242+
</lit-google-map-location-button>
243+
</lit-google-map>
244+
```
245+
246+
With custom position:
247+
248+
```html
249+
<lit-google-map api-key="YOUR_GOOGLE_MAPS_API_KEY">
250+
<lit-google-map-location-button
251+
slot="controls"
252+
position="TOP_RIGHT"
253+
label="Find Me"
254+
>
255+
</lit-google-map-location-button>
256+
</lit-google-map>
257+
```
258+
211259
## How to build
212260

213261
Before build install all required packages:

dist/lit-google-map.bundle.js

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ var LitGoogleMap = (function (exports) {
163163
});
164164
this.updateMarkers();
165165
this.updateShapes();
166+
this.updateControls();
166167
}
167168
getMapOptions() {
168169
return {
@@ -257,6 +258,15 @@ var LitGoogleMap = (function (exports) {
257258
s.attachToMap(this.map);
258259
}
259260
}
261+
updateControls() {
262+
const controlsSelector = this.shadowRoot.getElementById("controls-selector");
263+
if (!controlsSelector)
264+
return;
265+
this.controls = controlsSelector.items;
266+
for (const c of this.controls) {
267+
c.changeMap(this.map);
268+
}
269+
}
260270
fitToMarkersChanged(retryAttempt = 0) {
261271
if (this.map && this.fitToMarkers && this.markers.length > 0) {
262272
const latLngBounds = new google.maps.LatLngBounds();
@@ -316,6 +326,13 @@ var LitGoogleMap = (function (exports) {
316326
>
317327
<slot id="shapes" name="shapes"></slot>
318328
</lit-selector>
329+
<lit-selector
330+
id="controls-selector"
331+
selected-attribute="open"
332+
activate-event="google-map-control-activate"
333+
>
334+
<slot id="controls" name="controls"></slot>
335+
</lit-selector>
319336
<div id="map"></div>
320337
`;
321338
}
@@ -471,6 +488,210 @@ var LitGoogleMap = (function (exports) {
471488
t("lit-google-map-circle")
472489
], exports.LitGoogleMapCircle);
473490

491+
exports.LitGoogleMapLocationButton = class LitGoogleMapLocationButton extends i {
492+
constructor() {
493+
super(...arguments);
494+
this.position = "RIGHT_BOTTOM";
495+
this.label = "My Location";
496+
this.disabled = false;
497+
this.map = null;
498+
this.controlDiv = null;
499+
this.controlButton = null;
500+
this.isRequesting = false;
501+
}
502+
changeMap(newMap) {
503+
this.map = newMap;
504+
this.mapChanged();
505+
}
506+
mapChanged() {
507+
if (this.controlDiv && this.map) {
508+
this.controlDiv = null;
509+
this.controlButton = null;
510+
}
511+
if (this.map && this.map instanceof google.maps.Map) {
512+
this.mapReady();
513+
}
514+
}
515+
mapReady() {
516+
this.controlDiv = this.createControlButton();
517+
const controlPosition = google.maps.ControlPosition[this.position];
518+
if (controlPosition !== undefined) {
519+
this.map.controls[controlPosition].push(this.controlDiv);
520+
}
521+
}
522+
createControlButton() {
523+
const controlDiv = document.createElement("div");
524+
controlDiv.style.margin = "10px";
525+
const controlButton = document.createElement("button");
526+
controlButton.type = "button";
527+
controlButton.title = this.label;
528+
controlButton.setAttribute("aria-label", this.label);
529+
controlButton.innerHTML = `
530+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18">
531+
<path fill="currentColor" d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3A8.994 8.994 0 0 0 13 3.06V1h-2v2.06A8.994 8.994 0 0 0 3.06 11H1v2h2.06A8.994 8.994 0 0 0 11 20.94V23h2v-2.06A8.994 8.994 0 0 0 20.94 13H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/>
532+
</svg>
533+
`;
534+
Object.assign(controlButton.style, {
535+
backgroundColor: "#fff",
536+
border: "0",
537+
borderRadius: "2px",
538+
boxShadow: "0 1px 4px rgba(0,0,0,0.3)",
539+
cursor: "pointer",
540+
padding: "10px",
541+
display: "flex",
542+
alignItems: "center",
543+
justifyContent: "center",
544+
width: "40px",
545+
height: "40px",
546+
color: "#666",
547+
});
548+
controlButton.addEventListener("mouseenter", () => {
549+
if (!this.disabled && !this.isRequesting) {
550+
controlButton.style.backgroundColor = "#f8f8f8";
551+
}
552+
});
553+
controlButton.addEventListener("mouseleave", () => {
554+
if (!this.disabled && !this.isRequesting) {
555+
controlButton.style.backgroundColor = "#fff";
556+
}
557+
});
558+
controlButton.addEventListener("click", () => {
559+
if (!this.disabled && !this.isRequesting) {
560+
this.handleLocationRequest();
561+
}
562+
});
563+
this.controlButton = controlButton;
564+
controlDiv.appendChild(controlButton);
565+
return controlDiv;
566+
}
567+
async handleLocationRequest() {
568+
if (!navigator.geolocation) {
569+
this.dispatchEvent(new CustomEvent("location-error", {
570+
detail: {
571+
code: -1,
572+
message: "Geolocation is not supported by your browser",
573+
},
574+
bubbles: true,
575+
composed: true,
576+
}));
577+
return;
578+
}
579+
this.isRequesting = true;
580+
this.setLoadingState(true);
581+
this.dispatchEvent(new CustomEvent("location-requested", {
582+
bubbles: true,
583+
composed: true,
584+
}));
585+
try {
586+
const position = await this.getCurrentPosition();
587+
const lat = position.coords.latitude;
588+
const lng = position.coords.longitude;
589+
this.map.setCenter({ lat, lng });
590+
this.map.setZoom(14);
591+
this.dispatchEvent(new CustomEvent("location-found", {
592+
detail: { lat, lng },
593+
bubbles: true,
594+
composed: true,
595+
}));
596+
}
597+
catch (error) {
598+
let message = "Unable to retrieve your location";
599+
let code = -1;
600+
if (error instanceof GeolocationPositionError) {
601+
code = error.code;
602+
if (error.code === error.PERMISSION_DENIED) {
603+
message =
604+
"Location access denied. Please enable location permissions.";
605+
}
606+
else if (error.code === error.TIMEOUT) {
607+
message = "Location request timed out. Please try again.";
608+
}
609+
else if (error.code === error.POSITION_UNAVAILABLE) {
610+
message = "Location information is unavailable.";
611+
}
612+
}
613+
this.dispatchEvent(new CustomEvent("location-error", {
614+
detail: { code, message },
615+
bubbles: true,
616+
composed: true,
617+
}));
618+
}
619+
finally {
620+
this.isRequesting = false;
621+
this.setLoadingState(false);
622+
}
623+
}
624+
getCurrentPosition() {
625+
return new Promise((resolve, reject) => {
626+
navigator.geolocation.getCurrentPosition(resolve, reject, {
627+
timeout: 10000,
628+
enableHighAccuracy: true,
629+
});
630+
});
631+
}
632+
setLoadingState(loading) {
633+
if (!this.controlButton)
634+
return;
635+
if (loading) {
636+
this.controlButton.style.backgroundColor = "#f0f0f0";
637+
this.controlButton.style.cursor = "wait";
638+
const svg = this.controlButton.querySelector("svg");
639+
if (svg) {
640+
svg.style.animation = "spin 1s linear infinite";
641+
const style = document.createElement("style");
642+
style.textContent = `
643+
@keyframes spin {
644+
from { transform: rotate(0deg); }
645+
to { transform: rotate(360deg); }
646+
}
647+
`;
648+
if (!document.querySelector("style[data-location-button-spin]")) {
649+
style.setAttribute("data-location-button-spin", "true");
650+
document.head.appendChild(style);
651+
}
652+
}
653+
}
654+
else {
655+
this.controlButton.style.backgroundColor = "#fff";
656+
this.controlButton.style.cursor = "pointer";
657+
const svg = this.controlButton.querySelector("svg");
658+
if (svg) {
659+
svg.style.animation = "";
660+
}
661+
}
662+
}
663+
attributeChangedCallback(name, oldval, newval) {
664+
super.attributeChangedCallback(name, oldval, newval);
665+
if (name === "disabled" && this.controlButton) {
666+
if (this.disabled) {
667+
this.controlButton.style.backgroundColor = "#f0f0f0";
668+
this.controlButton.style.cursor = "not-allowed";
669+
this.controlButton.style.opacity = "0.6";
670+
}
671+
else {
672+
this.controlButton.style.backgroundColor = "#fff";
673+
this.controlButton.style.cursor = "pointer";
674+
this.controlButton.style.opacity = "1";
675+
}
676+
}
677+
}
678+
};
679+
__decorate([
680+
n({ type: String, reflect: true }),
681+
__metadata("design:type", Object)
682+
], exports.LitGoogleMapLocationButton.prototype, "position", void 0);
683+
__decorate([
684+
n({ type: String, reflect: true }),
685+
__metadata("design:type", Object)
686+
], exports.LitGoogleMapLocationButton.prototype, "label", void 0);
687+
__decorate([
688+
n({ type: Boolean, reflect: true }),
689+
__metadata("design:type", Object)
690+
], exports.LitGoogleMapLocationButton.prototype, "disabled", void 0);
691+
exports.LitGoogleMapLocationButton = __decorate([
692+
t("lit-google-map-location-button")
693+
], exports.LitGoogleMapLocationButton);
694+
474695
exports.LitGoogleMapMarker = class LitGoogleMapMarker extends i {
475696
constructor() {
476697
super(...arguments);

0 commit comments

Comments
 (0)