Skip to content

Commit 749433a

Browse files
chrisgervangclaude
andcommitted
feat(mapbox): Add widget support to MapboxOverlay via IControl adapter
Enable deck widgets to coexist with native Mapbox/MapLibre controls without DOM overlap by rendering widgets with viewId: 'mapbox' into the map's control container system. - Add DeckWidgetControl class that wraps deck widgets as IControls - Process widgets with viewId: 'mapbox' in MapboxOverlay - Set widget._container to Mapbox-positioned element via IControl - Widgets stack correctly with native controls in same container Closes #9962 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 1510545 commit 749433a

2 files changed

Lines changed: 132 additions & 1 deletion

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
import type {Widget} from '@deck.gl/core';
6+
import type {IControl, ControlPosition, Map} from './types';
7+
8+
/**
9+
* Wraps a deck.gl Widget as a Mapbox/MapLibre IControl.
10+
*
11+
* This enables deck widgets to be positioned alongside native map controls
12+
* in the same DOM container, preventing overlap issues.
13+
*
14+
* Used internally by MapboxOverlay for widgets with `viewId: 'mapbox'`.
15+
* Can also be used directly for more control over widget positioning.
16+
*
17+
* @example
18+
* ```typescript
19+
* const zoomWidget = new ZoomWidget({placement: 'top-right'});
20+
* const control = new DeckWidgetControl(zoomWidget);
21+
* map.addControl(control, 'top-right');
22+
* ```
23+
*/
24+
export class DeckWidgetControl implements IControl {
25+
private _widget: Widget;
26+
private _container: HTMLDivElement | null = null;
27+
28+
constructor(widget: Widget) {
29+
this._widget = widget;
30+
}
31+
32+
/**
33+
* Called when the control is added to the map.
34+
* Creates a container element that will be positioned by Mapbox/MapLibre,
35+
* and sets the widget's _container prop so WidgetManager appends the widget here.
36+
*/
37+
onAdd(map: Map): HTMLElement {
38+
this._container = document.createElement('div');
39+
this._container.className = 'maplibregl-ctrl mapboxgl-ctrl deck-widget-ctrl';
40+
41+
// Set _container so WidgetManager appends the widget's rootElement here
42+
// instead of in its own overlay container
43+
this._widget.props._container = this._container;
44+
45+
return this._container;
46+
}
47+
48+
/**
49+
* Called when the control is removed from the map.
50+
*/
51+
onRemove(): void {
52+
// Clear the _container reference so widget doesn't try to append there
53+
if (this._widget.props._container === this._container) {
54+
this._widget.props._container = null;
55+
}
56+
this._container?.remove();
57+
this._container = null;
58+
}
59+
60+
/**
61+
* Returns the default position for this control.
62+
* Uses the widget's placement, which conveniently matches Mapbox control positions.
63+
* Note: 'fill' placement is not supported by Mapbox controls, defaults to 'top-left'.
64+
*/
65+
getDefaultPosition(): ControlPosition {
66+
const placement = this._widget.placement;
67+
// 'fill' is not a valid Mapbox control position
68+
if (!placement || placement === 'fill') {
69+
return 'top-left';
70+
}
71+
return placement;
72+
}
73+
74+
/** Returns the wrapped widget */
75+
get widget(): Widget {
76+
return this._widget;
77+
}
78+
}

modules/mapbox/src/mapbox-overlay.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import {
1111
getDefaultParameters,
1212
getProjection
1313
} from './deck-utils';
14+
import {DeckWidgetControl} from './deck-widget-control';
1415

1516
import type {Map, IControl, MapMouseEvent, ControlPosition} from './types';
1617
import type {MjolnirGestureEvent, MjolnirPointerEvent} from 'mjolnir.js';
17-
import type {DeckProps, LayersList} from '@deck.gl/core';
18+
import type {DeckProps, LayersList, Widget} from '@deck.gl/core';
1819

1920
import {resolveLayers} from './resolve-layers';
2021
import {resolveLayerGroups} from './resolve-layer-groups';
@@ -55,6 +56,8 @@ export default class MapboxOverlay implements IControl {
5556
private _interleaved: boolean;
5657
private _renderLayersInGroups: boolean;
5758
private _lastMouseDownPoint?: {x: number; y: number; clientX: number; clientY: number};
59+
/** IControl wrappers for widgets with viewId: 'mapbox' */
60+
private _widgetControls: DeckWidgetControl[] = [];
5861

5962
constructor(props: MapboxOverlayProps) {
6063
const {interleaved = false} = props;
@@ -79,6 +82,12 @@ export default class MapboxOverlay implements IControl {
7982
this._resolveLayers(this._map, this._deck, this._props.layers, props.layers);
8083
}
8184

85+
// Process widgets with viewId: 'mapbox' before updating props
86+
// This must happen before deck.setProps so _container is set
87+
if (props.widgets !== undefined) {
88+
this._processWidgets(props.widgets);
89+
}
90+
8291
Object.assign(this._props, this.filterProps(props));
8392

8493
if (this._deck && this._map) {
@@ -112,6 +121,10 @@ export default class MapboxOverlay implements IControl {
112121
});
113122
this._container = container;
114123

124+
// Process widgets with viewId: 'mapbox' BEFORE creating Deck
125+
// so _container is set when WidgetManager initializes
126+
this._processWidgets(this._props.widgets);
127+
115128
this._deck = new Deck<any>({
116129
...this._props,
117130
parent: container,
@@ -143,6 +156,11 @@ export default class MapboxOverlay implements IControl {
143156
'Incompatible basemap library. See: https://deck.gl/docs/api-reference/mapbox/overview#compatibility'
144157
)();
145158
}
159+
160+
// Process widgets with viewId: 'mapbox' BEFORE creating Deck
161+
// so _container is set when WidgetManager initializes
162+
this._processWidgets(this._props.widgets);
163+
146164
this._deck = getDeckInstance({
147165
map,
148166
deck: new Deck({
@@ -171,11 +189,46 @@ export default class MapboxOverlay implements IControl {
171189
}
172190
}
173191

192+
/**
193+
* Process widgets and wrap those with viewId: 'mapbox' as IControls.
194+
* This enables deck widgets to be positioned in Mapbox's control container
195+
* alongside native map controls, preventing overlap.
196+
*/
197+
private _processWidgets(widgets: Widget[] | undefined): void {
198+
const map = this._map;
199+
if (!map) return;
200+
201+
// Remove old widget controls
202+
for (const control of this._widgetControls) {
203+
map.removeControl(control);
204+
}
205+
this._widgetControls = [];
206+
207+
if (!widgets) return;
208+
209+
// Wrap widgets with viewId: 'mapbox' as IControls
210+
for (const widget of widgets) {
211+
if (widget.viewId === 'mapbox') {
212+
const control = new DeckWidgetControl(widget);
213+
// Add to map - this calls onAdd() synchronously, setting _container
214+
// Use control.getDefaultPosition() which handles 'fill' -> 'top-left' conversion
215+
map.addControl(control, control.getDefaultPosition());
216+
this._widgetControls.push(control);
217+
}
218+
}
219+
}
220+
174221
/** Called when the control is removed from a map */
175222
onRemove(): void {
176223
const map = this._map;
177224

178225
if (map) {
226+
// Remove widget controls
227+
for (const control of this._widgetControls) {
228+
map.removeControl(control);
229+
}
230+
this._widgetControls = [];
231+
179232
if (this._interleaved) {
180233
this._onRemoveInterleaved(map);
181234
} else {

0 commit comments

Comments
 (0)