Skip to content

refactor(google-maps)!: OverlayView class extraction and reactive position rendering#697

Merged
harlan-zw merged 1 commit intomainfrom
refactor/google-maps-overlay-view-class-extraction
Apr 9, 2026
Merged

refactor(google-maps)!: OverlayView class extraction and reactive position rendering#697
harlan-zw merged 1 commit intomainfrom
refactor/google-maps-overlay-view-class-extraction

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

🔗 Linked issue

Related to #689 (PR E in the umbrella split-up plan; final piece, depends on #696).

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

Internal restructure of <ScriptGoogleMapsOverlayView> that replaces imperative DOM manipulation with reactive Vue bindings, with one behaviour-visible side effect (DOM structure change).

The motivation: the previous implementation wrote el.style.left/top/visibility/transform and el.dataset.state imperatively from inside OverlayView.draw(), then propagated data-state to the slot's first child manually because Vue couldn't patch elements that had been reparented out of the component tree. The reactive pattern in this PR demonstrates that Vue's :style and :data-state bindings actually do patch moved DOM nodes correctly, so we can drop the imperative writes entirely and rely on the framework.

⚠️ Breaking Changes

The slot now sits inside an additional overlay-anchor div.

The DOM hierarchy is now anchor div → content div → slot (previously content div → slot). The anchor handles positioning and gets reparented into a Google Maps pane on onAdd(); the content div carries data-state plus any forwarded class/attrs.

Selectors targeting [data-state="open"] on the slot's first child will not match anymore, because data-state lives on the internal content div now and is no longer propagated into the slot.

📝 Migration

Move the class onto the OverlayView component itself; it gets forwarded onto the content div via v-bind=\"\$attrs\". Use :deep() in scoped styles to reach across the component boundary:

```diff

  • <ScriptGoogleMapsOverlayView v-model:open="open">
  •  ...
    
  • <ScriptGoogleMapsOverlayView class="overlay-popup" v-model:open="open">
  • ...
<style scoped> -.overlay-popup[data-state=\"open\"] { animation: ... } +:deep(.overlay-popup[data-state=\"open\"]) { animation: ... } </style>

```

🛠 Implementation notes

  • Reactive position. A new overlayPosition shallowRef is written by draw(). The template binds :style=\"overlayStyle\" on the anchor element, where overlayStyle is a computed that derives left/top/transform/visibility from the position + offset/anchor/zIndex props. Vue's patcher updates the moved DOM node directly.
  • Reactive data-state. dataState is a computed bound on the content div. No imperative dataset.state writes, no need to propagate to slot children.
  • Class factory. class CustomOverlay extends mapsApi.OverlayView is wrapped in a makeOverlayClass(mapsApi, map) factory at script-setup top level. The class can't be a true module-top-level declaration (it depends on the runtime base class from the loaded Maps API), but the factory keeps create() focused on instantiation and listener wiring.
  • Anchor moves between parents. The anchor div starts inside a display: none wrapper in the component template, then onAdd() calls panes[pane].appendChild(anchorEl) to move it into the Google Maps pane. Vue's reactivity continues to patch the moved element because the vdom reference doesn't depend on the parent.
  • Animation-friendly close path. Setting visibility via the reactive :style binding (instead of imperative writes) keeps the element in the DOM after open flips false, so CSS animations targeting [data-state=\"closed\"] can actually run before the element is hidden. (Not yet wired up to wait for transitionend, but the structural prerequisite is now in place.)

🛡 Protected behaviours (regression-tested)

All v1 protected behaviours remain green:

  • Skip-if-equal setCenter watcher (parent map)
  • map.setOptions exclusion of zoom/center
  • defineModel('open', { default: undefined }) continues to treat missing v-model:open as undefined, not false
  • data-state=\"open\"/\"closed\" toggling
  • InfoWindow group close
  • Marker drag listeners forwarding to draw()

🧪 Tests

Five new mount-based tests in test/nuxt-runtime/google-maps-overlay-view.nuxt.test.ts:

  • DOM structure: asserts anchor → content → slot hierarchy
  • Anchor inline style reflects projected pixel position + offset
  • Reactive visibility transition when controlled open flips to false
  • Anchor stays in the DOM when overlay closes (animation prerequisite)
  • data-state is exposed on the content div for CSS targeting

100/100 google-maps-* tests pass; lint and typecheck clean.

Playground demo update

playground/pages/third-parties/google-maps/overlay-animated.vue moves .overlay-popup from a slot child div onto the <ScriptGoogleMapsOverlayView> itself and uses :deep() in scoped styles to reach the now-internal content element. This is the canonical demonstration of the new class-on-component animation pattern.

…ition rendering

Internal restructure of <ScriptGoogleMapsOverlayView> with one
behaviour-visible side effect (DOM structure change).

Highlights:

- Reactive position. Position updates from `draw()` write to a
  `overlayPosition` shallowRef that the template binds via `:style` on
  the anchor element. Vue's patcher updates the moved DOM node, so
  imperative `el.style.left/top/visibility/...` writes are gone. The
  `data-state` attribute is also a reactive template binding now,
  removing the imperative `setDataState` helper that previously
  propagated state to both the wrapper and its first child.
- Anchor + content split. The overlay renders a two-element structure:
  the anchor div (positioned, moved into a Google Maps pane) wraps the
  content div (carries `data-state` + forwarded `class`/attrs + the
  slot). This is the DOM-level breaking change; consumers that target
  `[data-state]` on the slot's first child should move that class to
  the OverlayView component itself.
- Class factory. The `CustomOverlay extends mapsApi.OverlayView`
  declaration is lifted out of `useGoogleMapsResource`'s `create()`
  callback into a `makeOverlayClass(mapsApi, map)` factory at script
  setup top level, so the `create()` callback is just wiring.
- Playground demo update. `overlay-animated.vue` moves the
  `.overlay-popup` class from a slot child div onto the
  `<ScriptGoogleMapsOverlayView>` itself and uses `:deep()` in scoped
  styles to reach the now-internal content element.

Tests:

Five new mount-based tests in google-maps-overlay-view.nuxt.test.ts
covering the anchor → content → slot DOM hierarchy, the projected
pixel position landing on the anchor's inline style, the reactive
visibility transition when the controlled `open` prop flips to false,
the anchor staying in the DOM during the close path (so CSS
animations targeting `[data-state="closed"]` can run), and data-state
exposure on the content div for CSS targeting.

All v1 protected behaviours remain green:

- skip-if-equal setCenter watcher
- map.setOptions exclusion of zoom/center
- defineModel('open', { default: undefined })
- data-state="open"/"closed" toggling
- InfoWindow group close

100/100 google-maps-* tests pass; lint and typecheck clean.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 9, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@697

commit: 8b6cf55

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment Apr 9, 2026 2:54pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

This change refactors the <ScriptGoogleMapsOverlayView> component to transition from imperative DOM manipulation to reactive, declarative state management. The component now uses a two-element DOM structure (anchor div containing content div containing slot) with inline styles driven by reactive overlayPosition binding. The data-state attribute is computed and bound directly to the content element rather than being imperatively set. Supporting documentation is updated to reflect the new DOM hierarchy and styling approach using :deep() for scoped CSS selectors. Comprehensive tests are added to validate the new DOM structure, reactivity, and state behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactoring: extracting the OverlayView class and implementing reactive position rendering instead of imperative DOM manipulation.
Description check ✅ Passed The description comprehensively explains the refactoring's motivation, breaking changes, migration path, implementation details, and testing coverage—all directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/google-maps-overlay-view-class-extraction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue`:
- Around line 237-244: The pan logic triggers even when the overlay is closed or
unpositioned; change the block using panOnOpen so it first checks the overlay's
open state and that overlayAnchor.value is positioned before scheduling
panMapToFitOverlay. Specifically, update the conditional around panOnOpen to
also verify the component's open flag (open) and that overlayAnchor.value exists
and has a valid position (e.g., overlayAnchor.value.getPosition() or similar)
before calling requestAnimationFrame and panMapToFitOverlay; keep the existing
padding calculation and only call panMapToFitOverlay when those guards pass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99b015f1-0ebf-474c-a9bb-5796c8f56462

📥 Commits

Reviewing files that changed from the base of the PR and between 7bfc780 and 8b6cf55.

📒 Files selected for processing (4)
  • docs/content/docs/4.migration-guide/1.v0-to-v1.md
  • packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue
  • playground/pages/third-parties/google-maps/overlay-animated.vue
  • test/nuxt-runtime/google-maps-overlay-view.nuxt.test.ts

Comment on lines +237 to 244
if (panOnOpen) {
// Wait for draw() to position the element, then pan
const padding = typeof panOnOpen === 'number' ? panOnOpen : 40
requestAnimationFrame(() => {
if (overlayAnchor.value)
panMapToFitOverlay(overlayAnchor.value, map, padding)
})
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard pan-on-open so closed/unpositioned overlays do not pan the map.

On Line 237, panMapToFitOverlay is scheduled whenever panOnOpen is enabled, even if open is false or draw() cannot resolve a position. That can cause unexpected map panning on initial mount/remount while the overlay is closed.

🔧 Suggested fix
-      if (panOnOpen) {
+      if (panOnOpen && open.value !== false) {
         // Wait for draw() to position the element, then pan
         const padding = typeof panOnOpen === 'number' ? panOnOpen : 40
         requestAnimationFrame(() => {
-          if (overlayAnchor.value)
+          if (overlayAnchor.value && overlayPosition.value)
             panMapToFitOverlay(overlayAnchor.value, map, padding)
         })
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue`
around lines 237 - 244, The pan logic triggers even when the overlay is closed
or unpositioned; change the block using panOnOpen so it first checks the
overlay's open state and that overlayAnchor.value is positioned before
scheduling panMapToFitOverlay. Specifically, update the conditional around
panOnOpen to also verify the component's open flag (open) and that
overlayAnchor.value exists and has a valid position (e.g.,
overlayAnchor.value.getPosition() or similar) before calling
requestAnimationFrame and panMapToFitOverlay; keep the existing padding
calculation and only call panMapToFitOverlay when those guards pass.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — verified this is pre-existing on main: the if (panOnOpen) { requestAnimationFrame(...) } block had no open/position guard before this PR either, the refactor just moved it into the new factory. Keeping PR E focused on the class extraction + reactive position rendering and tracking the pan-on-open guard as a separate follow-up so the fix lands with its own regression test.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harlan-zw, thanks for verifying the pre-existing behaviour — makes sense to keep the fix out of scope here and ship it alongside a dedicated regression test. Would you like me to open a GitHub issue to track the pan-on-open guard fix (adding the open.value !== false and overlayPosition.value guards with a regression test)?

@harlan-zw harlan-zw merged commit c33473c into main Apr 9, 2026
17 checks passed
@harlan-zw harlan-zw deleted the refactor/google-maps-overlay-view-class-extraction branch April 9, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant