Skip to content

Commit 49b5416

Browse files
authored
fix(google-maps): detach overlay element on unmount (#736)
1 parent f0e4c06 commit 49b5416

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,18 @@ function panMapToFitOverlay(el: HTMLElement, map: google.maps.Map, padding: numb
225225
// available after the script loads, so this stays a function rather than
226226
// a top-level class declaration.
227227
function makeOverlayClass(mapsApi: typeof google.maps, map: google.maps.Map) {
228+
// Capture the anchor element at onAdd time so onRemove can detach it even
229+
// after Vue has nulled the template ref during component unmount. Without
230+
// this, `v-if="false"` leaves the reparented element in the Google Maps
231+
// pane because `overlayAnchor.value` is already null when setMap(null)
232+
// triggers onRemove.
233+
let attachedEl: HTMLElement | null = null
228234
return class CustomOverlay extends mapsApi.OverlayView {
229235
override onAdd() {
230236
const panes = this.getPanes()
231237
const el = overlayAnchor.value
232238
if (panes && el) {
239+
attachedEl = el
233240
panes[pane].appendChild(el)
234241
if (blockMapInteraction)
235242
mapsApi.OverlayView.preventMapHitsAndGesturesFrom(el)
@@ -274,8 +281,8 @@ function makeOverlayClass(mapsApi: typeof google.maps, map: google.maps.Map) {
274281
}
275282
276283
override onRemove() {
277-
const el = overlayAnchor.value
278-
el?.parentNode?.removeChild(el)
284+
attachedEl?.parentNode?.removeChild(attachedEl)
285+
attachedEl = null
279286
}
280287
}
281288
}

test/nuxt-runtime/google-maps-overlay-view.nuxt.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,4 +413,52 @@ describe('scriptGoogleMapsOverlayView', () => {
413413
expect(mocks.mockMap.panBy).not.toHaveBeenCalled()
414414
})
415415
})
416+
417+
describe('unmount cleanup', () => {
418+
// Regression: https://github.com/nuxt/scripts/issues/735
419+
// `<ScriptGoogleMapsOverlayView v-if="x">` did not detach its overlay
420+
// element from the Google Maps pane on unmount, leaving a stale node
421+
// visible on the map. Cause: `onRemove()` read the anchor via
422+
// `useTemplateRef`, which Vue nulls during component unmount before
423+
// `onUnmounted` fires (and thus before `setMap(null)` triggers
424+
// `onRemove`). The fix captures the element at `onAdd` time.
425+
it('detaches the anchor element from its pane when v-if toggles false', async () => {
426+
const mocks = createOverlayMocks()
427+
const Provider = createMapProvider(mocks)
428+
const show = shallowRef(true)
429+
430+
const wrapper = await mountSuspended(Provider, {
431+
slots: {
432+
default: () => (show.value
433+
? h(
434+
ScriptGoogleMapsOverlayView,
435+
{ position: { lat: 10, lng: 20 } },
436+
() => h('div', { class: 'overlay-content' }),
437+
)
438+
: null),
439+
},
440+
})
441+
442+
await nextTick()
443+
await nextTick()
444+
await nextTick()
445+
446+
const overlayWrapper = wrapper.findComponent(ScriptGoogleMapsOverlayView)
447+
const anchor = (overlayWrapper.vm as any).$refs['overlay-anchor'] as HTMLElement
448+
expect(anchor).toBeTruthy()
449+
// After onAdd, the anchor is reparented into a Google Maps pane, so it
450+
// has a parentNode that is not the component's hidden wrapper.
451+
expect(anchor.parentNode).toBeTruthy()
452+
const paneBeforeUnmount = anchor.parentNode!
453+
454+
// Unmount the component via v-if. The cleanup must remove the anchor
455+
// from the pane so it does not linger on the map.
456+
show.value = false
457+
await wrapper.setProps({})
458+
await nextTick()
459+
await nextTick()
460+
461+
expect(paneBeforeUnmount.contains(anchor)).toBe(false)
462+
})
463+
})
416464
})

0 commit comments

Comments
 (0)