Skip to content

Commit b65e5db

Browse files
authored
fix(google-maps): fix OverlayView, InfoWindow, and AdvancedMarkerElement DX issues (#660)
1 parent 5d95da2 commit b65e5db

File tree

9 files changed

+299
-59
lines changed

9 files changed

+299
-59
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
const selected = ref<number | null>(null)
5+
6+
const places = [
7+
{ id: 1, name: 'Sydney Opera House', desc: 'Iconic performing arts venue on Bennelong Point.', rating: 4.7, reviews: '82k', position: { lat: -33.8568, lng: 151.2153 } },
8+
{ id: 2, name: 'Harbour Bridge', desc: 'Steel arch bridge opened in 1932, connecting the CBD to the North Shore.', rating: 4.8, reviews: '41k', position: { lat: -33.8523, lng: 151.2108 } },
9+
{ id: 3, name: 'Bondi Beach', desc: 'Famous crescent beach popular with surfers and sunbathers.', rating: 4.6, reviews: '29k', position: { lat: -33.8908, lng: 151.2743 } },
10+
]
11+
</script>
12+
13+
<template>
14+
<div>
15+
<h2 class="text-lg font-bold mb-2">
16+
OverlayView Popup (click to toggle)
17+
</h2>
18+
<p class="mb-4 text-sm text-gray-600">
19+
Click a marker to show a fully custom popup. Click again or press × to close. Uses v-if for multiple markers.
20+
</p>
21+
<ScriptGoogleMaps
22+
:center="{ lat: -33.8688, lng: 151.2093 }"
23+
:zoom="12"
24+
:width="800"
25+
:height="500"
26+
above-the-fold
27+
:map-options="{ mapId: 'DEMO_MAP_ID' }"
28+
>
29+
<ScriptGoogleMapsAdvancedMarkerElement
30+
v-for="place in places"
31+
:key="place.id"
32+
:position="place.position"
33+
@click="selected = selected === place.id ? null : place.id"
34+
>
35+
<ScriptGoogleMapsOverlayView
36+
v-if="selected === place.id"
37+
anchor="bottom-center"
38+
:offset="{ x: 0, y: -50 }"
39+
>
40+
<div class="w-64 rounded-xl bg-white p-4 shadow-lg ring-1 ring-black/5">
41+
<div class="flex items-start justify-between gap-2">
42+
<h3 class="text-sm font-semibold text-gray-900">
43+
{{ place.name }}
44+
</h3>
45+
<button
46+
class="shrink-0 rounded-full p-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
47+
@click.stop="selected = null"
48+
>
49+
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 20 20" fill="currentColor">
50+
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
51+
</svg>
52+
</button>
53+
</div>
54+
<div class="mt-1 flex items-center gap-1 text-xs text-gray-500">
55+
<span class="font-medium text-yellow-500">★ {{ place.rating }}</span>
56+
<span>({{ place.reviews }} reviews)</span>
57+
</div>
58+
<p class="mt-2 text-xs leading-relaxed text-gray-600">
59+
{{ place.desc }}
60+
</p>
61+
<button class="mt-3 w-full rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700">
62+
View details
63+
</button>
64+
</div>
65+
</ScriptGoogleMapsOverlayView>
66+
</ScriptGoogleMapsAdvancedMarkerElement>
67+
</ScriptGoogleMaps>
68+
</div>
69+
</template>

playground/pages/third-parties/google-maps/styled.vue

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,5 @@ const mapOptions = {
1616
:map-options="mapOptions"
1717
/>
1818
</div>
19-
<div class="button-container">
20-
<button
21-
class="button"
22-
@click="changeQuery"
23-
>
24-
change query
25-
</button>
26-
</div>
2719
</div>
2820
</template>
29-
30-
<style>
31-
.button-container {
32-
margin: 20px 0;
33-
}
34-
35-
.button {
36-
background-color: orange;
37-
border-radius: 8px;
38-
padding: 4px 8px;
39-
cursor: pointer;
40-
}
41-
42-
.button:not(:last-child) {
43-
margin-right: 8px;
44-
}
45-
</style>

src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,18 @@ const googleMaps = {
334334
335335
defineExpose(googleMaps)
336336
337-
provide(MAP_INJECTION_KEY, { map, mapsApi })
337+
// Shared InfoWindow group: only one InfoWindow open at a time within this map
338+
let activeInfoWindow: google.maps.InfoWindow | undefined
339+
provide(MAP_INJECTION_KEY, {
340+
map,
341+
mapsApi,
342+
activateInfoWindow(iw: google.maps.InfoWindow) {
343+
if (activeInfoWindow && activeInfoWindow !== iw) {
344+
activeInfoWindow.close()
345+
}
346+
activeInfoWindow = iw
347+
},
348+
})
338349
339350
onMounted(() => {
340351
watch(ready, (v) => {

src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,22 @@ const props = defineProps<{
1414
options?: Omit<google.maps.marker.AdvancedMarkerElementOptions, 'map'>
1515
}>()
1616
17-
const emit = defineEmits<{
18-
(event: typeof eventsWithoutPayload[number]): void
19-
(event: typeof eventsWithMapMouseEventPayload[number], payload: google.maps.MapMouseEvent): void
20-
}>()
21-
22-
// AdvancedMarkerElement supported events only
23-
// See https://developers.google.com/maps/documentation/javascript/reference/advanced-markers
24-
const eventsWithoutPayload = [] as const
25-
26-
const eventsWithMapMouseEventPayload = [
27-
'click',
28-
'drag',
29-
'dragend',
30-
'dragstart',
31-
] as const
32-
17+
const emit = defineEmits(['click', 'drag', 'dragend', 'dragstart'])
18+
const dragEvents = ['drag', 'dragend', 'dragstart'] as const
3319
const slots = useSlots()
3420
const markerContent = useTemplateRef('marker-content')
3521
const markerClustererContext = inject(MARKER_CLUSTERER_INJECTION_KEY, undefined)
3622
23+
// gmp-click handler for cleanup (AdvancedMarkerElement uses DOM gmp-click instead of Maps addListener click)
24+
let gmpClickHandler: ((e: any) => void) | undefined
25+
3726
const advancedMarkerElement = useGoogleMapsResource<google.maps.marker.AdvancedMarkerElement>({
3827
ready: () => !slots.content || !!markerContent.value,
3928
async create({ mapsApi, map }) {
4029
await mapsApi.importLibrary('marker')
4130
const marker = new mapsApi.marker.AdvancedMarkerElement({
4231
...props.options,
32+
gmpClickable: true,
4333
...(props.position ? { position: props.position } : {}),
4434
})
4535
@@ -48,21 +38,30 @@ const advancedMarkerElement = useGoogleMapsResource<google.maps.marker.AdvancedM
4838
marker.content = markerContent.value
4939
}
5040
51-
bindGoogleMapsEvents(marker, emit, {
52-
noPayload: eventsWithoutPayload,
53-
withPayload: eventsWithMapMouseEventPayload,
54-
})
55-
5641
if (markerClustererContext?.markerClusterer.value) {
5742
markerClustererContext.markerClusterer.value.addMarker(marker, true)
5843
markerClustererContext.requestRerender()
5944
}
6045
else {
6146
marker.map = map
6247
}
48+
49+
// AdvancedMarkerElement: use gmp-click DOM event (addListener('click') is deprecated)
50+
gmpClickHandler = (e: any) => emit('click', e)
51+
marker.addEventListener('gmp-click', gmpClickHandler)
52+
53+
// Drag events still use Maps API addListener
54+
bindGoogleMapsEvents(marker, emit, {
55+
withPayload: dragEvents,
56+
})
57+
6358
return marker
6459
},
6560
cleanup(marker, { mapsApi }) {
61+
if (gmpClickHandler) {
62+
marker.removeEventListener('gmp-click', gmpClickHandler)
63+
gmpClickHandler = undefined
64+
}
6665
mapsApi.event.clearInstanceListeners(marker)
6766
if (markerClustererContext?.markerClusterer.value) {
6867
markerClustererContext.markerClusterer.value.removeMarker(marker, true)

src/runtime/components/GoogleMaps/ScriptGoogleMapsInfoWindow.vue

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { inject, useTemplateRef, watch } from 'vue'
33
import { bindGoogleMapsEvents } from './bindGoogleMapsEvents'
4-
import { ADVANCED_MARKER_ELEMENT_INJECTION_KEY, MARKER_INJECTION_KEY } from './injectionKeys'
4+
import { ADVANCED_MARKER_ELEMENT_INJECTION_KEY, MAP_INJECTION_KEY, MARKER_INJECTION_KEY } from './injectionKeys'
55
import { useGoogleMapsResource } from './useGoogleMapsResource'
66
77
const props = defineProps<{
@@ -24,13 +24,16 @@ const infoWindowEvents = [
2424
'zindex_changed',
2525
] as const
2626
27+
const mapContext = inject(MAP_INJECTION_KEY, undefined)
2728
const markerContext = inject(MARKER_INJECTION_KEY, undefined)
2829
const advancedMarkerElementContext = inject(ADVANCED_MARKER_ELEMENT_INJECTION_KEY, undefined)
2930
3031
const infoWindowContainer = useTemplateRef('info-window-container')
3132
3233
// Track click listener on parent marker so it can be removed on cleanup
3334
let markerClickListener: google.maps.MapsEventListener | undefined
35+
let gmpClickHandler: ((e: any) => void) | undefined
36+
let isOpen = false
3437
3538
const infoWindow = useGoogleMapsResource<google.maps.InfoWindow>({
3639
ready: () => !!infoWindowContainer.value,
@@ -40,36 +43,57 @@ const infoWindow = useGoogleMapsResource<google.maps.InfoWindow>({
4043
...props.options,
4144
})
4245
46+
// Track open state for toggle behavior
47+
iw.addListener('closeclick', () => {
48+
isOpen = false
49+
})
50+
iw.addListener('close', () => {
51+
isOpen = false
52+
})
53+
4354
bindGoogleMapsEvents(iw, emit, { noPayload: infoWindowEvents })
4455
56+
const toggleOpen = (anchor: any) => {
57+
if (isOpen) {
58+
iw.close()
59+
isOpen = false
60+
}
61+
else {
62+
mapContext?.activateInfoWindow(iw)
63+
iw.open({ anchor, map })
64+
isOpen = true
65+
}
66+
}
67+
4568
if (markerContext?.marker.value) {
4669
markerClickListener = markerContext.marker.value.addListener('click', () => {
47-
iw.open({
48-
anchor: markerContext.marker.value,
49-
map,
50-
})
70+
toggleOpen(markerContext.marker.value)
5171
})
5272
}
5373
else if (advancedMarkerElementContext?.advancedMarkerElement.value) {
54-
markerClickListener = advancedMarkerElementContext.advancedMarkerElement.value.addListener('click', () => {
55-
iw.open({
56-
anchor: advancedMarkerElementContext.advancedMarkerElement.value,
57-
map,
58-
})
59-
})
74+
const ame = advancedMarkerElementContext.advancedMarkerElement.value
75+
ame.gmpClickable = true
76+
gmpClickHandler = () => toggleOpen(ame)
77+
ame.addEventListener('gmp-click', gmpClickHandler)
6078
}
6179
else {
6280
iw.setPosition(props.options?.position)
6381
iw.open(map)
82+
isOpen = true
6483
}
6584
6685
return iw
6786
},
6887
cleanup(iw, { mapsApi }) {
6988
markerClickListener?.remove()
7089
markerClickListener = undefined
90+
if (gmpClickHandler && advancedMarkerElementContext?.advancedMarkerElement.value) {
91+
advancedMarkerElementContext.advancedMarkerElement.value.removeEventListener('gmp-click', gmpClickHandler)
92+
gmpClickHandler = undefined
93+
}
7194
mapsApi.event.clearInstanceListeners(iw)
7295
iw.close()
96+
isOpen = false
7397
},
7498
})
7599

src/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const props = withDefaults(defineProps<{
2323
blockMapInteraction: true,
2424
})
2525
26-
const open = defineModel<boolean>('open')
26+
const open = defineModel<boolean>('open', { default: undefined })
2727
2828
const markerContext = inject(MARKER_INJECTION_KEY, undefined)
2929
const advancedMarkerElementContext = inject(ADVANCED_MARKER_ELEMENT_INJECTION_KEY, undefined)

src/runtime/components/GoogleMaps/injectionKeys.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { InjectionKey, Ref, ShallowRef } from 'vue'
33
export const MAP_INJECTION_KEY = Symbol('map') as InjectionKey<{
44
map: ShallowRef<google.maps.Map | undefined>
55
mapsApi: Ref<typeof google.maps | undefined>
6+
/** Close the previously active InfoWindow and register a new one as active */
7+
activateInfoWindow: (iw: google.maps.InfoWindow) => void
68
}>
79

810
export const ADVANCED_MARKER_ELEMENT_INJECTION_KEY = Symbol('marker') as InjectionKey<{

test/unit/__mocks__/google-maps-api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ export function createMockAdvancedMarkerElement() {
1313
map: null,
1414
content: null,
1515
position: null,
16+
gmpClickable: false,
1617
addListener: vi.fn(),
18+
addEventListener: vi.fn(),
19+
removeEventListener: vi.fn(),
1720
}
1821
}
1922

0 commit comments

Comments
 (0)