From afd90c6d215a78879f1d9424f5c5a51fafc7942e Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 16 Sep 2026 20:52:40 +1000 Subject: [PATCH] fix(maplibre): keep credits from every replaced attribution control Only the first replaced control's `customAttribution` carried over, so a second attribution control lost its credits while the component was mounted. The credits of all replaced controls are now merged. --- .../ScriptMapLibreAttributionControl.vue | 9 +++++++-- .../maplibre-controls.nuxt.test.ts | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/script/src/runtime/components/MapLibre/ScriptMapLibreAttributionControl.vue b/packages/script/src/runtime/components/MapLibre/ScriptMapLibreAttributionControl.vue index 53fb68291..95b3d54e3 100644 --- a/packages/script/src/runtime/components/MapLibre/ScriptMapLibreAttributionControl.vue +++ b/packages/script/src/runtime/components/MapLibre/ScriptMapLibreAttributionControl.vue @@ -27,15 +27,20 @@ function restore(map: MapLibre.Map): void { replaced = [] } +function mergeCredits(controls: MapLibre.AttributionControl[]): string[] | undefined { + const credits = [...new Set(controls.flatMap(existing => existing.options.customAttribution ?? []))] + return credits.length ? credits : undefined +} + const control = useMapLibreResource({ create({ maplibre, map }) { // `_controls` is the list that `hasControl()` reads. `filter` copies it before removal. replaced = map._controls.filter((existing): existing is MapLibre.AttributionControl => existing instanceof maplibre.AttributionControl) // The component options override the map's `attributionControl` options. - // Credits the map already configured stay unless the component sets its own. + // Credits from every replaced control stay unless the component sets its own. const inherited = replaced[0]?.options const options = inherited || props.options - ? { ...inherited, ...props.options, customAttribution: props.options?.customAttribution ?? inherited?.customAttribution } + ? { ...inherited, ...props.options, customAttribution: props.options?.customAttribution ?? mergeCredits(replaced) } : undefined const instance = new maplibre.AttributionControl(options) for (const existing of replaced) diff --git a/test/nuxt-runtime/maplibre-controls.nuxt.test.ts b/test/nuxt-runtime/maplibre-controls.nuxt.test.ts index a4f455e1d..4c44e48ad 100644 --- a/test/nuxt-runtime/maplibre-controls.nuxt.test.ts +++ b/test/nuxt-runtime/maplibre-controls.nuxt.test.ts @@ -306,7 +306,7 @@ describe('mapLibre attribution control', () => { global: provideMap(maplibre, map), }) await nextTick() - expect(controlsOf(map, 'attribution')[0]!.options).toEqual({ compact: false, customAttribution: 'Data: Hobart City Council' }) + expect(controlsOf(map, 'attribution')[0]!.options).toEqual({ compact: false, customAttribution: ['Data: Hobart City Council'] }) inheriting.unmount() const overriding = mount(ScriptMapLibreAttributionControl, { @@ -318,6 +318,22 @@ describe('mapLibre attribution control', () => { overriding.unmount() }) + it('keeps the credits of every replaced attribution control', async () => { + const maplibre = createMapLibre() + const { map } = mapWithDefaultAttribution(maplibre, { compact: true, customAttribution: 'MapLibre' }) + map.addControl(new (maplibre.AttributionControl as any)({ customAttribution: ['Data: Hobart City Council', 'MapLibre'] })) + const wrapper = mount(ScriptMapLibreAttributionControl, { + global: provideMap(maplibre, map), + }) + await nextTick() + + const [mounted] = controlsOf(map, 'attribution') + expect(controlsOf(map, 'attribution')).toHaveLength(1) + expect((mounted!.options as { customAttribution: unknown }).customAttribution).toEqual(['MapLibre', 'Data: Hobart City Council']) + wrapper.unmount() + expect(controlsOf(map, 'attribution')).toHaveLength(2) + }) + it('restores the map default after consumer code removed the component control', async () => { const maplibre = createMapLibre() const { map, builtIn } = mapWithDefaultAttribution(maplibre)