Skip to content

Commit

Permalink
Added InfoWindow methods and events (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonayuan authored May 16, 2024
1 parent 9221ad1 commit 6239593
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 10 deletions.
7 changes: 7 additions & 0 deletions src/googleCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ export const MigrationEvent = {
drag: "drag",
dragend: "dragend",
dragstart: "dragstart",
close: "close",
closeclick: "closeclick",
};

// Constant responsible for translating Google Event names to corresponding MapLibre Event names,
Expand All @@ -371,6 +373,8 @@ GoogleToMaplibreEvent[MigrationEvent.zoom_changed] = "zoom";
GoogleToMaplibreEvent[MigrationEvent.drag] = "drag";
GoogleToMaplibreEvent[MigrationEvent.dragend] = "dragend";
GoogleToMaplibreEvent[MigrationEvent.dragstart] = "dragstart";
GoogleToMaplibreEvent[MigrationEvent.close] = "close";
GoogleToMaplibreEvent[MigrationEvent.closeclick] = "click";

// List of Google Map Events that include the MapMouseEvent parameter
export const GoogleMapMouseEvent = [
Expand Down Expand Up @@ -399,6 +403,9 @@ export const GoogleMarkerMouseEvent = [MigrationEvent.drag, MigrationEvent.drags
// (must add event listener using DOM element)
export const GoogleMarkerMouseDOMEvent = [MigrationEvent.click, MigrationEvent.dblclick, MigrationEvent.contextmenu];

// List of Google InfoWindow Events
export const GoogleInfoWindowEvent = [MigrationEvent.close, MigrationEvent.closeclick];

export interface QueryAutocompletePrediction {
description: string;
place_id?: string;
Expand Down
106 changes: 96 additions & 10 deletions src/infoWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

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

const focusQuerySelector = [
"a[href]",
Expand All @@ -17,6 +23,7 @@ const focusQuerySelector = [
class MigrationInfoWindow {
#popup: Popup;
#minWidth: number;
#maxWidth: number;
#ariaLabel: string;

constructor(options?) {
Expand All @@ -25,21 +32,14 @@ class MigrationInfoWindow {
// maxWidth - set MapLibre 'maxWidth' option
if ("maxWidth" in options) {
maplibreOptions.maxWidth = options.maxWidth + "px";
this.#maxWidth = options.maxWidth;
}

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);
}
this.setContent(options.content);
}

if ("position" in options) {
Expand All @@ -57,6 +57,27 @@ class MigrationInfoWindow {
}
}

addListener(eventName, handler) {
if (GoogleInfoWindowEvent.includes(eventName)) {
// if close then use 'on' method on popup instance
if (eventName === MigrationEvent.close) {
this.#popup.on(GoogleToMaplibreEvent[eventName], () => {
handler();
});
} else if (eventName === MigrationEvent.closeclick) {
// if closeclick then use 'addEventListener' method on button element
const closeButton = this.#popup.getElement().querySelector("button.maplibregl-popup-close-button");
closeButton.addEventListener(GoogleToMaplibreEvent[eventName], () => {
handler();
});
}
}
}

close() {
this.#popup.remove();
}

focus() {
const container = this.#popup.getElement();
// popup/infowindow not yet rendered
Expand All @@ -70,6 +91,16 @@ class MigrationInfoWindow {
}
}

getContent() {
return this.#popup._content;
}

getPosition() {
const position = this.#popup.getLngLat();

return new MigrationLatLng(position?.lat, position?.lng);
}

// Google:
// - both Marker and LatLng popup/infowindow -> set Marker or LatLng in InfoWindow class and then call InfoWindow.open
// MapLibre:
Expand Down Expand Up @@ -97,12 +128,57 @@ class MigrationInfoWindow {
this.#popup.getElement().style.minWidth = this.#minWidth + "px";
}

// use setMaxWidth MapLibre method if maxWidth is set in setOptions Google method
if (this.#maxWidth !== undefined) {
this.#popup.setMaxWidth(this.#maxWidth + "px");
}

// set ariaLabel property to local variable once popup is opened
if (this.#ariaLabel !== undefined) {
this.#popup.getElement().ariaLabel = this.#ariaLabel;
}
}

setContent(content?) {
if (typeof content === "string") {
if (this._containsOnlyHTMLElements(content)) {
this.#popup.setHTML(content);
} else {
this.#popup.setText(content);
}
} else if (content instanceof HTMLElement) {
this.#popup.setDOMContent(content);
}
}

setOptions(options?) {
// have to close and reopen infowindowto register new minWidth
if ("minWidth" in options) {
this.#minWidth = options.minWidth;
}

// have to close and reopen infowindow to register new maxWidth
if ("maxWidth" in options) {
this.#maxWidth = options.maxWidth;
}

// applies new ariaLabel immediately
if ("ariaLabel" in options) {
this.#popup.getElement().ariaLabel = options.ariaLabel;
this.#ariaLabel = options.ariaLabel;
}

// applies new content immediately
if ("content" in options) {
this.setContent(options.content);
}

// applies new position immediately
if ("position" in options) {
this.setPosition(options.position);
}
}

setPosition(position?) {
if (position) {
const lnglat = LatLngToLngLat(position);
Expand All @@ -120,6 +196,11 @@ class MigrationInfoWindow {
return this.#minWidth;
}

// Internal method for manually getting the private #maxWidth property
_getMaxWidth() {
return this.#maxWidth;
}

// Internal method for manually getting the private #ariaLabel property
_getAriaLabel() {
return this.#ariaLabel;
Expand All @@ -130,6 +211,11 @@ class MigrationInfoWindow {
this.#popup = popup;
}

// Internal method for manually setting the private #maxWidth property
_setMaxWidth(maxWidth) {
this.#maxWidth = maxWidth;
}

// 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)
Expand Down
139 changes: 139 additions & 0 deletions test/infoWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { MigrationMap } from "../src/maps";
import { MigrationMarker } from "../src/markers";
import { MigrationInfoWindow } from "../src/infoWindow";
import { MigrationLatLng } from "../src/googleCommon";

// 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
Expand Down Expand Up @@ -168,6 +169,15 @@ test("should call open method on infowindow with minWidth set", () => {
expect(spy).toHaveBeenCalledWith("50px");
});

test("should call open method on infowindow with maxWidth set", () => {
const testInfoWindow = new MigrationInfoWindow({});
testInfoWindow._setMaxWidth(200);

testInfoWindow.open({});
expect(Popup.prototype.setMaxWidth).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setMaxWidth).toHaveBeenCalledWith("200px");
});

test("should call open method on infowindow with ariaLabel set", () => {
const mockAriaLabel = {
_ariaLabel: undefined,
Expand Down Expand Up @@ -220,3 +230,132 @@ test("should call focus method on infowindow with early return due to unrendered
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith("InfoWindow is not visible");
});

test("should call close method on infowindow", () => {
const mockInfoWindow = {
remove: jest.fn(),
};
const testInfoWindow = new MigrationInfoWindow({});
testInfoWindow._setPopup(mockInfoWindow);

testInfoWindow.close();

expect(testInfoWindow).not.toBeNull();
expect(mockInfoWindow.remove).toHaveBeenCalledTimes(1);
});

test("should call getContent method on infowindow", () => {
const content = "testContent";
const mockInfoWindow = {
_content: content,
};
const testInfoWindow = new MigrationInfoWindow({});
testInfoWindow._setPopup(mockInfoWindow);

const resultContent = testInfoWindow.getContent();

expect(testInfoWindow).not.toBeNull();
expect(resultContent).toBe(content);
});

test("should call getPosition method on infowindow", () => {
const mockInfoWindow = {
getLngLat: jest.fn().mockReturnValue({ lat: testLat, lng: testLng }),
};
const testInfoWindow = new MigrationInfoWindow({});
testInfoWindow._setPopup(mockInfoWindow);

const resultPosition = testInfoWindow.getPosition();

expect(testInfoWindow).not.toBeNull();
expect(resultPosition).toStrictEqual(new MigrationLatLng(testLat, testLng));
});

test("should call setOptions on infowindow", () => {
const testContent = "content";
const testInfoWindow = new MigrationInfoWindow({});

testInfoWindow.setOptions({
minWidth: 100,
maxWidth: 200,
content: testContent,
position: { lat: testLat, lng: testLng },
});

expect(testInfoWindow._getMinWidth()).toBe(100);
expect(testInfoWindow._getMaxWidth()).toBe(200);
expect(Popup.prototype.setText).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setText).toHaveBeenCalledWith(testContent);
expect(Popup.prototype.setLngLat).toHaveBeenCalledTimes(1);
expect(Popup.prototype.setLngLat).toHaveBeenCalledWith([testLng, testLat]);
});

test("should call setOptions on infowindow with ariaLabel", () => {
const testLabel = "label";
const mockAriaLabel = {
_ariaLabel: undefined,
set ariaLabel(value: string) {
this._ariaLabel = value;
},
get ariaLabel() {
return this._ariaLabel;
},
};
const mockInfoWindow = {
getElement: jest.fn().mockReturnValue(mockAriaLabel),
};
const testInfoWindow = new MigrationInfoWindow({});
testInfoWindow._setPopup(mockInfoWindow);

testInfoWindow.setOptions({
ariaLabel: testLabel,
});

expect(testInfoWindow._getAriaLabel()).toBe(testLabel);
});

test("should call handler after close", () => {
// mock infowindow so that we can mock on so that we can mock close
const mockInfoWindow = {
on: jest.fn(),
};
const migrationInfoWindow = new MigrationInfoWindow({});
migrationInfoWindow._setPopup(mockInfoWindow);

// add spy as handler
const handlerSpy = jest.fn();
migrationInfoWindow.addListener("close", handlerSpy);

// mock close
mockInfoWindow.on.mock.calls[0][1]();

expect(handlerSpy).toHaveBeenCalledTimes(1);
});

test("should call handler after closeclick", () => {
// mock button so that we can mock addEventListener so that we can mock click
const mockButton = {
addEventListener: jest.fn(),
};

// mock container so that we can mock the button
const mockContainer = {
querySelector: jest.fn().mockReturnValue(mockButton),
};

// mock marker to return mockElement when getElement is called
const mockInfoWindow = {
getElement: jest.fn().mockReturnValue(mockContainer),
};
const migrationInfoWindow = new MigrationInfoWindow({});
migrationInfoWindow._setPopup(mockInfoWindow);

// add spy as handler
const handlerSpy = jest.fn();
migrationInfoWindow.addListener("closeclick", handlerSpy);

// mock click button
mockButton.addEventListener.mock.calls[0][1]();

expect(handlerSpy).toHaveBeenCalledTimes(1);
});

0 comments on commit 6239593

Please sign in to comment.