Skip to content

Commit

Permalink
Basic InfoWindow implementation (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonayuan authored May 14, 2024
1 parent 156fec2 commit ab5c0ff
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { MigrationMap } from "./maps";
import { MigrationMarker } from "./markers";
import { MigrationAutocompleteService, MigrationPlacesService } from "./places";
import { MigrationInfoWindow } from "./infoWindow";

// Dynamically load the MapLibre stylesheet so that our migration adapter is the only thing our users need to import
// Without this, many MapLibre rendering features won't work (e.g. markers and info windows won't be visible)
Expand Down Expand Up @@ -94,6 +95,7 @@ const migrationInit = async function () {
marker: {
AdvancedMarkerElement: MigrationMarker,
},
InfoWindow: MigrationInfoWindow,
ControlPosition: MigrationControlPosition,

DirectionsRenderer: MigrationDirectionsRenderer,
Expand Down
90 changes: 90 additions & 0 deletions src/infoWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Popup, PopupOptions } from "maplibre-gl";
import { LatLngToLngLat } from "./googleCommon";

class MigrationInfoWindow {
#popup: Popup;

constructor(options?) {
const maplibreOptions: PopupOptions = {};

// maxWidth - set MapLibre 'maxWidth' option
if ("maxWidth" in options) {
maplibreOptions.maxWidth = options.maxWidth + "px";
}

this.#popup = new Popup(maplibreOptions);

// content can be string, HTMLElement, or string containing HTML
if ("content" in options) {
if (typeof options.content === "string") {
if (this._containsOnlyHTMLElements(options.content)) {
this.#popup.setHTML(options.content);
} else {
this.#popup.setText(options.content);
}
} else if (options.content instanceof HTMLElement) {
this.#popup.setDOMContent(options.content);
}
}

if ("position" in options) {
this.setPosition(options.position);
}

// TODO:
// pixelOffset - set 'offset' MapLibre option
// minWidth - set by HTMLElement.style.minWidth
// ariaLabel - add to DOM element
}

// Google:
// - both Marker and LatLng popup/infowindow -> set Marker or LatLng in InfoWindow class and then call InfoWindow.open
// MapLibre:
// - Marker popup/infowindow -> Marker.setPopup then Marker.togglePopup (to open Popup)
// - LatLng popup/infowindow -> Popup.setLngLat (in options when creating popup) then Popup.addTo
open(options?, anchor?) {
if (anchor || options.anchor) {
// Marker specific info window
const marker = anchor !== undefined ? anchor._getMarker() : options.anchor._getMarker();
marker.setPopup(this.#popup);
if (!this.#popup.isOpen()) {
marker.togglePopup();
}
} else if (options.map) {
// LatLng specific info window
this.#popup.addTo(options.map._getMap());
}

// TODO: shouldFocus - focusAfterOpening, use focus method on DOM element
}

setPosition(position?) {
if (position) {
const lnglat = LatLngToLngLat(position);
this.#popup.setLngLat(lnglat);
}
}

// Internal method for manually getting the private #marker property
_getPopup() {
return this.#popup;
}

// Internal method for checking if a string contains valid HTML
_containsOnlyHTMLElements(str: string): boolean {
// Regular expression to match complete HTML elements (opening and closing tags with content)
const htmlElementRegex = /<([a-z][a-z0-9]*)\b[^>]*>(.*?)<\/\1>/gi;

// Check if the string contains any complete HTML elements
const hasHTMLElements = str.match(htmlElementRegex) !== null;

// Check if the string contains any text outside of HTML elements
const textOutsideElements = str.replace(htmlElementRegex, "").trim();
return hasHTMLElements && textOutsideElements.length === 0;
}
}

export { MigrationInfoWindow };
121 changes: 121 additions & 0 deletions test/infoWindow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { MigrationMap } from "../src/maps";
import { MigrationMarker } from "../src/markers";
import { MigrationInfoWindow } from "../src/infoWindow";

// Mock maplibre because it requires a valid DOM container to create a Map
// We don't need to verify maplibre itself, we just need to verify that
// the values we pass to our google migration classes get transformed
// correctly and our called
jest.mock("maplibre-gl");
import { Marker, Popup, PopupOptions } from "maplibre-gl";

const testLat = 30.268193; // Austin, TX :)
const testLng = -97.7457518;

afterEach(() => {
jest.clearAllMocks();
});

test("should set marker options", () => {
const testInfoWindow = new MigrationInfoWindow({
maxWidth: 100,
position: { lat: testLat, lng: testLng },
});

const expectedMaplibreOptions: PopupOptions = {
maxWidth: "100px",
};

expect(testInfoWindow).not.toBeNull();
expect(Popup).toHaveBeenCalledTimes(1);
expect(Popup).toHaveBeenCalledWith(expectedMaplibreOptions);
expect(Popup.prototype.setLngLat).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setLngLat).toHaveBeenCalledWith([testLng, testLat]);
});

test("should set marker content option with string", () => {
const testString = "Hello World!";
const testInfoWindow = new MigrationInfoWindow({
content: testString,
});

expect(testInfoWindow).not.toBeNull();
expect(Popup).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setText).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setText).toHaveBeenCalledWith(testString);
});

test("should set marker content option with string containing HTML", () => {
const htmlString = "<h1>Hello World!</h1>";
const testInfoWindow = new MigrationInfoWindow({
content: htmlString,
});

expect(testInfoWindow).not.toBeNull();
expect(Popup).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setHTML).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setHTML).toHaveBeenCalledWith(htmlString);
});

test("should set marker content option with HTML elements", () => {
const h1Element = document.createElement("h1");
h1Element.textContent = "Hello World!";
const testInfoWindow = new MigrationInfoWindow({
content: h1Element,
});

expect(testInfoWindow).not.toBeNull();
expect(Popup).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setDOMContent).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setDOMContent).toHaveBeenCalledWith(h1Element);
});

test("should call open method on marker with anchor option", () => {
const testInfoWindow = new MigrationInfoWindow({});
const testMarker = new MigrationMarker({});

testInfoWindow.open({
anchor: testMarker,
});

expect(testInfoWindow).not.toBeNull();
expect(testMarker).not.toBeNull();
expect(Marker.prototype.setPopup).toHaveBeenCalledTimes(1);
expect(Marker.prototype.setPopup).toHaveBeenCalledWith(testInfoWindow._getPopup());
expect(Marker.prototype.togglePopup).toHaveBeenCalledTimes(1);
expect(Popup.prototype.isOpen).toHaveBeenCalledTimes(1);
});

test("should call open method on marker with anchor parameter", () => {
const testInfoWindow = new MigrationInfoWindow({});
const testMarker = new MigrationMarker({});

testInfoWindow.open(undefined, testMarker);

expect(testInfoWindow).not.toBeNull();
expect(testMarker).not.toBeNull();
expect(Marker.prototype.setPopup).toHaveBeenCalledTimes(1);
expect(Marker.prototype.setPopup).toHaveBeenCalledWith(testInfoWindow._getPopup());
expect(Marker.prototype.togglePopup).toHaveBeenCalledTimes(1);
expect(Popup.prototype.isOpen).toHaveBeenCalledTimes(1);
});

test("should call open method on marker with lat lng set and map option", () => {
const testInfoWindow = new MigrationInfoWindow({});
const testMarker = new MigrationMarker({});
const testMap = new MigrationMap(null, {});

testInfoWindow.setPosition({ lat: testLat, lng: testLng });
testInfoWindow.open({
map: testMap,
});

expect(testInfoWindow).not.toBeNull();
expect(testMarker).not.toBeNull();
expect(testMap).not.toBeNull();
expect(Popup.prototype.addTo).toHaveBeenCalledTimes(1);
expect(Popup.prototype.addTo).toHaveBeenCalledWith(testMap._getMap());
});

0 comments on commit ab5c0ff

Please sign in to comment.