Skip to content
Open
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
16 changes: 14 additions & 2 deletions src/ui/marker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Point from '@mapbox/point-geometry';
import * as DOM from '../util/dom';
import LngLat from '../geo/lng_lat';
import smartWrap from '../util/smart_wrap';
import {bindAll, radToDeg, smoothstep} from '../util/util';
import {bindAll, radToDeg, smoothstep, warnOnce} from '../util/util';
import {anchorTranslate} from './anchor';
import {Event, Evented} from '../util/evented';
import {GLOBE_ZOOM_THRESHOLD_MAX} from '../geo/projection/globe_constants';
Expand Down Expand Up @@ -52,7 +52,7 @@ type MarkerEvents = {
* Creates a marker component.
*
* @param {Object} [options]
* @param {HTMLElement} [options.element] DOM element to use as a marker. The default is a light blue, droplet-shaped SVG marker.
* @param {HTMLElement} [options.element] DOM element to use as a marker. The default is a light blue, droplet-shaped SVG marker. The marker root is positioned via `transform` and must keep `position: absolute` (provided by the built-in `.mapboxgl-marker` class). If your custom element needs `position: relative` to anchor absolutely-positioned descendants, apply that rule to a child of the marker root instead.
* @param {string} [options.anchor='center'] A string indicating the part of the Marker that should be positioned closest to the coordinate set via {@link Marker#setLngLat}.
* Options are `'center'`, `'top'`, `'bottom'`, `'left'`, `'right'`, `'top-left'`, `'top-right'`, `'bottom-left'`, and `'bottom-right'`.
* @param {PointLike} [options.offset] The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up.
Expand Down Expand Up @@ -260,6 +260,18 @@ export default class Marker extends Evented<MarkerEvents> {
this.setDraggable(this._draggable);
this._update();

// Markers are positioned via `transform: translate(...)` on a `position: absolute`
// root (provided by the `.mapboxgl-marker` class). If a user-supplied element
// overrides `position` to anything that participates in normal flow, additional
// markers stack vertically in the canvas container instead of sharing the same
// origin, and pins after the first land at incorrect screen positions.
if (!this._defaultMarker && typeof window !== 'undefined' && window.getComputedStyle) {
const position = window.getComputedStyle(this._element).position;
if (position !== 'absolute' && position !== 'fixed') {
warnOnce(`Marker element has computed CSS \`position: ${position}\`, expected \`absolute\`. Mapbox positions markers via \`transform\` on a \`position: absolute\` root; overriding it (often via a user style like \`position: relative\` on the marker root) breaks placement of additional markers. Move that rule to a child of the marker element instead.`);
}
}

// If we attached the `click` listener to the marker element, the popup
// would close once the event propogated to `map` due to the
// `Popup#_onClickClose` listener.
Expand Down
22 changes: 22 additions & 0 deletions test/unit/ui/marker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ test('Marker#addTo adds the marker element to the canvas container', () => {
map.remove();
});

test('Marker warns when a custom element overrides position to non-absolute', () => {
const map = createMap();
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const relativeEl = window.document.createElement('div');
relativeEl.style.position = 'relative';
new Marker({element: relativeEl}).setLngLat([0, 0]).addTo(map);
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0][0]).toContain('relative');
expect(warnSpy.mock.calls[0][0]).toContain('absolute');

warnSpy.mockClear();

const absoluteEl = window.document.createElement('div');
absoluteEl.style.position = 'absolute';
new Marker({element: absoluteEl}).setLngLat([0, 0]).addTo(map);
expect(warnSpy).not.toHaveBeenCalled();

warnSpy.mockRestore();
map.remove();
});

test('Marker adds classes from className option, methods for class manipulation work properly', () => {
const map = createMap();
const marker = new Marker({className: 'some classes'})
Expand Down
Loading