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
24 changes: 17 additions & 7 deletions src/ui/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import {extend, bindAll} from '../util/util.js';
import {Event, Evented} from '../util/evented.js';
import {MapMouseEvent} from '../ui/events.js';
import {MapMouseEvent} from './events.js';
import * as DOM from '../util/dom.js';
import LngLat from '../geo/lng_lat.js';
import Point from '@mapbox/point-geometry';
Expand All @@ -19,7 +19,7 @@ const defaultOptions = {
closeOnClick: true,
focusAfterOpen: true,
className: '',
maxWidth: "240px"
maxWidth: "240px",
};

export type Offset = number | PointLike | {[_: Anchor]: PointLike};
Expand All @@ -32,7 +32,8 @@ export type PopupOptions = {
anchor?: Anchor,
offset?: Offset,
className?: string,
maxWidth?: string
maxWidth?: string,
fixedAnchor: boolean
};

const focusQuerySelector = [
Expand All @@ -57,6 +58,9 @@ const focusQuerySelector = [
* map moves.
* @param {boolean} [options.focusAfterOpen=true] If `true`, the popup will try to focus the
* first focusable element inside the popup.
* @param {boolean} [options.fixedAnchor] - If 'true', the popup will fix to the value set by
* `options.anchor`. If 'false' the anchor will be dynamically set to ensure the popup falls within
* the map container with a preference for the value set by `options.anchor`.
* @param {string} [options.anchor] - A string indicating the part of the popup that should
* be positioned closest to the coordinate, set via {@link Popup#setLngLat}.
* Options are `'center'`, `'top'`, `'bottom'`, `'left'`, `'right'`, `'top-left'`,
Expand All @@ -67,7 +71,7 @@ const focusQuerySelector = [
* A pixel offset applied to the popup's location specified as:
* - a single number specifying a distance from the popup's location
* - a {@link PointLike} specifying a constant offset
* - an object of {@link Point}s specifing an offset for each anchor position.
* - an object of {@link Point}s specifying an offset for each anchor position.
*
* Negative offsets indicate left and up.
* @param {string} [options.className] Space-separated CSS class names to add to popup container.
Expand Down Expand Up @@ -540,13 +544,19 @@ export default class Popup extends Evented {
}

_getAnchor(bottomY: number): Anchor {
if (this.options.anchor) { return this.options.anchor; }
const fallbackPosition = 'bottom';
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const fallbackPosition = 'bottom';
const fallbackPosition = this.options.anchor || 'bottom';

I think this could help clean up the later return statements

if (
this.options.fixedAnchor ||
(typeof (this.options.fixedAnchor) === 'undefined' && this.options.anchor)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit:

Suggested change
(typeof (this.options.fixedAnchor) === 'undefined' && this.options.anchor)
this.options.fixedAnchor === undefined && this.options.anchor)

) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be more readable if broken into two if statements?

return this.options.anchor || fallbackPosition;
}

const map = this._map;
const container = this._container;
const pos = this._pos;

if (!map || !container || !pos) return 'bottom';
if (!map || !container || !pos) return fallbackPosition;

const width = container.offsetWidth;
const height = container.offsetHeight;
Expand All @@ -568,7 +578,7 @@ export default class Popup extends Evented {
if (isLeft) return 'left';
if (isRight) return 'right';

return 'bottom';
return this.options.anchor || fallbackPosition;
}

_updateClassList() {
Expand Down
99 changes: 80 additions & 19 deletions test/unit/ui/popup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,32 @@ test('Popup anchors as specified by the anchor option', (t) => {
t.end();
});

test('Popup anchors as specified by the anchor option when fixedAnchor is true', (t) => {
const map = createMap(t);
const popup = new Popup({anchor: 'top-left', fixedAnchor: true})
.setLngLat([0, 0])
.setText('Test')
.addTo(map);
map._domRenderTaskQueue.run();

t.ok(popup.getElement().classList.contains('mapboxgl-popup-anchor-top-left'));
t.end();
});

[true, false].forEach((fixedAnchor) => {
test(`Popup anchors as bottom when anchor is undefined and fixedAnchor is ${fixedAnchor}`, (t) => {
const map = createMap(t);
const popup = new Popup({fixedAnchor})
.setLngLat([0, 0])
.setText('Test')
.addTo(map);
map._domRenderTaskQueue.run();

t.ok(popup.getElement().classList.contains('mapboxgl-popup-anchor-bottom'));
t.end();
});
});

[
['top-left', new Point(10, 10), 'translate(0,0) translate(7px,7px)'],
['top', new Point(containerWidth / 2, 10), 'translate(-50%,0) translate(0px,10px)'],
Expand All @@ -413,25 +439,26 @@ test('Popup anchors as specified by the anchor option', (t) => {
const anchor = args[0];
const point = args[1];
const transform = args[2];

test(`Popup automatically anchors to ${anchor}`, (t) => {
const map = createMap(t);
const popup = new Popup()
.setLngLat([0, 0])
.setText('Test')
.addTo(map);
map._domRenderTaskQueue.run();

Object.defineProperty(popup.getElement(), 'offsetWidth', {value: 100});
Object.defineProperty(popup.getElement(), 'offsetHeight', {value: 100});

t.stub(map, 'project').returns(point);
t.stub(map.transform, 'locationPoint3D').returns(point);
popup.setLngLat([0, 0]);
map._domRenderTaskQueue.run();

t.ok(popup.getElement().classList.contains(`mapboxgl-popup-anchor-${anchor}`));
t.end();
[undefined, false].forEach((fixedAnchor) => {
test(`Popup automatically anchors to ${anchor} when fixed anchor is ${fixedAnchor ?? 'undefined'}`, (t) => {
const map = createMap(t);
const popup = new Popup({fixedAnchor})
.setLngLat([0, 0])
.setText('Test')
.addTo(map);
map._domRenderTaskQueue.run();

Object.defineProperty(popup.getElement(), 'offsetWidth', {value: 100});
Object.defineProperty(popup.getElement(), 'offsetHeight', {value: 100});

t.stub(map, 'project').returns(point);
t.stub(map.transform, 'locationPoint3D').returns(point);
popup.setLngLat([0, 0]);
map._domRenderTaskQueue.run();

t.ok(popup.getElement().classList.contains(`mapboxgl-popup-anchor-${anchor}`));
t.end();
});
});

test(`Popup translation reflects offset and ${anchor} anchor`, (t) => {
Expand Down Expand Up @@ -474,6 +501,40 @@ test('Popup automatically anchors to top if its bottom offset would push it off-
t.end();
});

[ 'center',
'top',
'bottom',
'left',
'right',
'top-left',
'top-right',
'bottom-left',
'bottom-right'].forEach((anchor) => {
test(`Popup automatically anchors to value (${anchor}) position if will be on-screen`, (t) => {
const map = createMap(t);
const point = new Point(containerWidth / 2, containerHeight / 2);
const options = {offset: {
'bottom': [0, 0],
'top': [0, 0]
}, anchor, fixedAnchor: false};
const popup = new Popup(options)
.setLngLat([0, 0])
.setText('Test')
.addTo(map);
map._domRenderTaskQueue.run();

Object.defineProperty(popup.getElement(), 'offsetWidth', {value: containerWidth / 2});
Object.defineProperty(popup.getElement(), 'offsetHeight', {value: containerHeight / 2});

t.stub(map, 'project').returns(point);
popup.setLngLat([0, 0]);
map._domRenderTaskQueue.run();

t.ok(popup.getElement().classList.contains(`mapboxgl-popup-anchor-${anchor}`));
t.end();
});
});

test('Popup is offset via a PointLike offset option', (t) => {
const map = createMap(t);
t.stub(map, 'project').returns(new Point(0, 0));
Expand Down