Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,18 @@ function panMapToFitOverlay(el: HTMLElement, map: google.maps.Map, padding: numb
// available after the script loads, so this stays a function rather than
// a top-level class declaration.
function makeOverlayClass(mapsApi: typeof google.maps, map: google.maps.Map) {
// Capture the anchor element at onAdd time so onRemove can detach it even
// after Vue has nulled the template ref during component unmount. Without
// this, `v-if="false"` leaves the reparented element in the Google Maps
// pane because `overlayAnchor.value` is already null when setMap(null)
// triggers onRemove.
let attachedEl: HTMLElement | null = null
return class CustomOverlay extends mapsApi.OverlayView {
override onAdd() {
const panes = this.getPanes()
const el = overlayAnchor.value
if (panes && el) {
attachedEl = el
panes[pane].appendChild(el)
if (blockMapInteraction)
mapsApi.OverlayView.preventMapHitsAndGesturesFrom(el)
Expand Down Expand Up @@ -274,8 +281,8 @@ function makeOverlayClass(mapsApi: typeof google.maps, map: google.maps.Map) {
}

override onRemove() {
const el = overlayAnchor.value
el?.parentNode?.removeChild(el)
attachedEl?.parentNode?.removeChild(attachedEl)
attachedEl = null
}
}
}
Expand Down
48 changes: 48 additions & 0 deletions test/nuxt-runtime/google-maps-overlay-view.nuxt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,4 +413,52 @@ describe('scriptGoogleMapsOverlayView', () => {
expect(mocks.mockMap.panBy).not.toHaveBeenCalled()
})
})

describe('unmount cleanup', () => {
// Regression: https://github.com/nuxt/scripts/issues/735
// `<ScriptGoogleMapsOverlayView v-if="x">` did not detach its overlay
// element from the Google Maps pane on unmount, leaving a stale node
// visible on the map. Cause: `onRemove()` read the anchor via
// `useTemplateRef`, which Vue nulls during component unmount before
// `onUnmounted` fires (and thus before `setMap(null)` triggers
// `onRemove`). The fix captures the element at `onAdd` time.
it('detaches the anchor element from its pane when v-if toggles false', async () => {
const mocks = createOverlayMocks()
const Provider = createMapProvider(mocks)
const show = shallowRef(true)

const wrapper = await mountSuspended(Provider, {
slots: {
default: () => (show.value
? h(
ScriptGoogleMapsOverlayView,
{ position: { lat: 10, lng: 20 } },
() => h('div', { class: 'overlay-content' }),
)
: null),
},
})

await nextTick()
await nextTick()
await nextTick()

const overlayWrapper = wrapper.findComponent(ScriptGoogleMapsOverlayView)
const anchor = (overlayWrapper.vm as any).$refs['overlay-anchor'] as HTMLElement
expect(anchor).toBeTruthy()
// After onAdd, the anchor is reparented into a Google Maps pane, so it
// has a parentNode that is not the component's hidden wrapper.
expect(anchor.parentNode).toBeTruthy()
const paneBeforeUnmount = anchor.parentNode!

// Unmount the component via v-if. The cleanup must remove the anchor
// from the pane so it does not linger on the map.
show.value = false
await wrapper.setProps({})
await nextTick()
await nextTick()

expect(paneBeforeUnmount.contains(anchor)).toBe(false)
})
})
})
Loading