diff --git a/public/img/home.png b/public/img/home.png
new file mode 100644
index 000000000..45383507c
Binary files /dev/null and b/public/img/home.png differ
diff --git a/src/__tests__/components/fields/location-field/location-field.spec.tsx b/src/__tests__/components/fields/location-field/location-field.spec.tsx
index a8108c0f9..77aed8655 100644
--- a/src/__tests__/components/fields/location-field/location-field.spec.tsx
+++ b/src/__tests__/components/fields/location-field/location-field.spec.tsx
@@ -1,12 +1,14 @@
import { Breakpoint, LifeSGTheme } from "@lifesg/react-design-system/theme";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { MockViewport, mockIntersectionObserver, mockViewport, mockViewportForTestGroup } from "jsdom-testing-mocks";
+import { Marker } from "leaflet";
import { useEffect, useRef, useState } from "react";
import { FrontendEngine, IFrontendEngineData, IFrontendEngineProps, IFrontendEngineRef } from "../../../../components";
import { ILocationFieldSchema, TSetCurrentLocationDetail } from "../../../../components/fields";
import { LocationHelper } from "../../../../components/fields/location-field/location-helper";
import { ERROR_SVG } from "../../../../components/fields/location-field/location-modal/location-modal.data";
import { ErrorImage } from "../../../../components/fields/location-field/location-modal/location-modal.styles";
+import * as markerHelper from "../../../../components/fields/location-field/location-modal/location-picker/helper";
import { IMapPin } from "../../../../components/fields/location-field/location-modal/location-picker/types";
import { ERROR_MESSAGES, Prompt } from "../../../../components/shared";
import { GeoLocationHelper, TestHelper } from "../../../../utils";
@@ -33,6 +35,7 @@ import {
mockReverseGeoCodeResponse,
mockStaticMapDataUri,
} from "./mock-values";
+
jest.mock("../../../../services/onemap/onemap-service.ts");
window.HTMLElement.prototype.scrollTo = jest.fn; // required for .scrollTo in location-search
@@ -51,6 +54,7 @@ const LEGEND_ITEMS = [
const setCurrentLocationSpy = jest.fn();
const editButtonOnClickSpy = jest.fn();
const confirmLocationOnClickSpy = jest.fn();
+const handleClickSelectablePin = jest.fn();
enum ELocationInputEvents {
"SET_CURRENT_LOCATION" = "set-current-location",
@@ -66,6 +70,7 @@ enum ELocationInputEvents {
"HIDE_PERMISSION_MODAL" = "hide-permission-modal",
"DISMISS_LOCATION_MODAL" = "dismiss-location-modal",
"CLICK_REFRESH_CURRENT_LOCATION" = "click-refresh-current-location",
+ "CLICK_SELECTABLE_PIN" = "click-selectable-pin",
}
interface ICustomFrontendEngineProps extends IFrontendEngineProps {
locationDetails?: TSetCurrentLocationDetail;
@@ -124,6 +129,12 @@ const FrontendEngineWithEventListener = ({
handleShowConfirmLocationPrompt(e);
})
);
+ addFieldEventListener(
+ UI_TYPE,
+ ELocationInputEvents.CLICK_SELECTABLE_PIN,
+ COMPONENT_ID,
+ handleClickSelectablePin
+ );
};
const handleRemoveFieldEventListener = () => {
@@ -134,6 +145,7 @@ const FrontendEngineWithEventListener = ({
COMPONENT_ID,
confirmLocationOnClickSpy
);
+ removeFieldEventListener(ELocationInputEvents.CLICK_SELECTABLE_PIN, COMPONENT_ID, handleClickSelectablePin);
};
handleAddFieldEventListener();
@@ -808,6 +820,12 @@ describe("location-input-group", () => {
// Selectable Pin Events
describe("Selectable Pin events", () => {
const getSelectablePinsEvent = "get-selectable-pins";
+ const createMarkerStub = () =>
+ ({
+ addTo: jest.fn().mockReturnThis(),
+ on: jest.fn().mockReturnThis(),
+ remove: jest.fn(),
+ } as unknown as Marker);
describe("get-selectable-pins", () => {
const getSelectablePins = jest.fn();
@@ -817,7 +835,7 @@ describe("location-input-group", () => {
};
beforeEach(() => {
- jest.clearAllMocks;
+ jest.clearAllMocks();
});
it("should fire get-selectable-pins event if default location is set", async () => {
@@ -861,6 +879,14 @@ describe("location-input-group", () => {
});
describe("set-selectable-pins", () => {
+ let markerFromSpy: jest.SpyInstance;
+ let markerFromHtmlSpy: jest.SpyInstance;
+
+ afterEach(() => {
+ markerFromHtmlSpy?.mockRestore();
+ markerFromSpy?.mockRestore();
+ });
+
it("should show error modal if selectable pins is not an array", async () => {
renderComponent({
eventType: getSelectablePinsEvent,
@@ -922,6 +948,87 @@ describe("location-input-group", () => {
expect(screen.queryByText("address 2")).toBeDefined();
});
});
+
+ it("should render custom HTML markers when markerHtml is provided", async () => {
+ const customMarkerHtml = '

';
+ markerFromSpy = jest.spyOn(markerHelper, "markerFrom").mockImplementation(() => createMarkerStub());
+ markerFromHtmlSpy = jest
+ .spyOn(markerHelper, "markerFromHtml")
+ .mockImplementation(() => createMarkerStub());
+ renderComponent({
+ eventType: getSelectablePinsEvent,
+ eventListener: (formRef) => () => {
+ formRef.dispatchFieldEvent(UI_TYPE, "set-selectable-pins", COMPONENT_ID, {
+ pins: [
+ {
+ lat: 1.21,
+ lng: 103.78,
+ resultListItemText: "address 1",
+ address: "address 1",
+ markerHtml: customMarkerHtml,
+ },
+ ],
+ });
+ },
+ overrideSchema: {
+ defaultValues: {
+ [COMPONENT_ID]: {
+ lat: 1.29994179707526,
+ lng: 103.789404349716,
+ },
+ },
+ },
+ });
+
+ getLocationInput()?.focus();
+
+ await waitFor(() => {
+ expect(markerFromHtmlSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ markerHtml: expect.stringContaining(customMarkerHtml),
+ }),
+ expect.any(String)
+ );
+ });
+ });
+ });
+ describe("click-selectable-pin", () => {
+ it("should fire click-selectable-pin event when selectable pin is clicked", async () => {
+ const selectablePin = {
+ lat: 1.21,
+ lng: 103.78,
+ resultListItemText: "address 1",
+ address: "address 1",
+ };
+ renderComponent({
+ withEvents: true,
+ locationDetails: { payload: { lat: 1, lng: 1 } },
+ eventType: getSelectablePinsEvent,
+ eventListener: (formRef) => () => {
+ formRef.dispatchFieldEvent(UI_TYPE, "set-selectable-pins", COMPONENT_ID, {
+ pins: [selectablePin],
+ });
+ },
+ overrideSchema: {
+ defaultValues: {
+ [COMPONENT_ID]: {
+ lat: 1.29994179707526,
+ lng: 103.789404349716,
+ },
+ },
+ },
+ });
+
+ getLocationInput()?.focus();
+
+ await waitFor(() => {
+ const markerIcon = document.querySelector(".leaflet-marker-icon");
+ expect(markerIcon).toBeInTheDocument();
+
+ markerIcon && fireEvent.click(markerIcon);
+ expect(handleClickSelectablePin).toHaveBeenCalled();
+ });
+ });
});
});
diff --git a/src/components/fields/location-field/location-modal/location-picker/helper.ts b/src/components/fields/location-field/location-modal/location-picker/helper.ts
index 59aeb48e2..2c306c3ff 100644
--- a/src/components/fields/location-field/location-modal/location-picker/helper.ts
+++ b/src/components/fields/location-field/location-modal/location-picker/helper.ts
@@ -23,3 +23,13 @@ export const removeMarkers = (markers: L.Marker[] | undefined) => {
if (!markers) return;
markers.forEach((marker) => marker.remove());
};
+
+export const markerFromHtml = ({ lat, lng }: IMapPin, html: string, className?: string | undefined): L.Marker => {
+ return L.marker([lat, lng], {
+ icon: L.divIcon({
+ html,
+ className: className || "",
+ iconAnchor: [0, 0],
+ }),
+ });
+};
diff --git a/src/components/fields/location-field/location-modal/location-picker/location-picker.tsx b/src/components/fields/location-field/location-modal/location-picker/location-picker.tsx
index b83e61951..b5b245829 100644
--- a/src/components/fields/location-field/location-modal/location-picker/location-picker.tsx
+++ b/src/components/fields/location-field/location-modal/location-picker/location-picker.tsx
@@ -6,6 +6,7 @@ import { NavigationFillIcon } from "@lifesg/react-icons/navigation-fill";
import { ICircleFillIcon } from "@lifesg/react-icons";
import { PinFillIcon } from "@lifesg/react-icons/pin-fill";
import * as L from "leaflet";
+import sanitizeHtml from "sanitize-html";
import "leaflet/dist/leaflet.css";
import { useContext, useEffect, useRef } from "react";
import ReactDOMServer from "react-dom/server";
@@ -14,7 +15,7 @@ import { TestHelper } from "../../../../../utils";
import { useFieldEvent } from "../../../../../utils/hooks";
import { LocationHelper } from "../../location-helper";
import { ILocationCoord } from "../../types";
-import { markerFrom, removeMarkers } from "./helper";
+import { markerFrom, markerFromHtml, removeMarkers } from "./helper";
import { Legend } from "./legend";
import {
Banner,
@@ -134,10 +135,17 @@ export const LocationPicker = ({
*/
useEffect(() => {
if (!selectedLocationCoord?.lat || !selectedLocationCoord?.lng) {
- // If there is no selected location, zoom to default address if available, else reset to default view
- if (defaultAddress?.lat && defaultAddress?.lng) {
- zoomWithMarkers([defaultAddress], false);
- return;
+ // When no location is selected, center the map on the selectable pin that matches
+ // defaultAddress so we keep the selectable pin markers (including markerHtml) visible.
+ if (selectablePins.length > 0 && defaultAddress?.lat && defaultAddress?.lng) {
+ const zoomCenter = selectablePins.find(
+ ({ lat, lng }) => lat === defaultAddress.lat && lng === defaultAddress.lng
+ );
+
+ if (zoomCenter) {
+ zoomWithMarkers(selectablePins, true, zoomCenter, true, false);
+ return;
+ }
}
resetView();
return;
@@ -195,15 +203,22 @@ export const LocationPicker = ({
const mapPinIcon =
"data:image/svg+xml;base64," +
btoa(ReactDOMServer.renderToString());
- const marker = markerFrom(
- target,
- interactiveMapPinIconUrl && !target.isCurrentLocation ? interactiveMapPinIconUrl : mapPinIcon,
- isSelected,
- target.isCurrentLocation
- ).addTo(map);
+ const resolvedPinIcon =
+ interactiveMapPinIconUrl && !target.isCurrentLocation ? interactiveMapPinIconUrl : mapPinIcon;
+ const marker = target.markerHtml
+ ? markerFromHtml(
+ target,
+ sanitizeHtml(target.markerHtml, {
+ allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
+ allowedAttributes: false,
+ })
+ ).addTo(map)
+ : markerFrom(target, resolvedPinIcon, isSelected, target.isCurrentLocation).addTo(map);
return shouldSelectOnClick && !target.isCurrentLocation
? marker.on("click", () => {
+ const shouldPreventDefault = !dispatchFieldEvent("click-selectable-pin", id, { pin: target });
+ if (shouldPreventDefault) return;
// To accurately identify the pin, use the original lat & lng from the pin
// instead of the ones from the LeafletMouseEvent.
zoomAndCenter(map, { lat: target.lat, lng: target.lng });
@@ -252,7 +267,10 @@ export const LocationPicker = ({
const shouldPreventDefault = !dispatchFieldEvent("click-refresh-current-location", id);
if (!shouldPreventDefault) {
if (defaultAddress?.lat && defaultAddress?.lng) {
- zoomWithMarkers([defaultAddress], false);
+ const zoomCenter = selectablePins.find(
+ ({ lat, lng }) => lat === defaultAddress.lat && lng === defaultAddress.lng
+ );
+ zoomWithMarkers(selectablePins, true, zoomCenter ?? defaultAddress, true, false);
return;
}
getCurrentLocation();
diff --git a/src/components/fields/location-field/location-modal/location-picker/types.ts b/src/components/fields/location-field/location-modal/location-picker/types.ts
index f36f08a6f..c72ff443f 100644
--- a/src/components/fields/location-field/location-modal/location-picker/types.ts
+++ b/src/components/fields/location-field/location-modal/location-picker/types.ts
@@ -5,6 +5,7 @@ export interface IMapPin extends ILocationCoord {
address?: string | undefined;
resultListItemText?: string | undefined;
isCurrentLocation?: boolean | undefined;
+ markerHtml?: string | undefined;
}
export interface ILocationPickerProps extends React.InputHTMLAttributes {
diff --git a/src/components/fields/location-field/types.ts b/src/components/fields/location-field/types.ts
index b5e4dc3c5..622ee4cd1 100644
--- a/src/components/fields/location-field/types.ts
+++ b/src/components/fields/location-field/types.ts
@@ -110,6 +110,7 @@ export type TLocationFieldEvents = {
error: CustomEvent;
"error-end": CustomEvent;
"set-selectable-pins": CustomEvent<{ pins: IMapPin[] }>;
+ "click-selectable-pin": CustomEvent<{ pin: IMapPin }>;
};
export class GeolocationPositionErrorWrapper extends Error {
@@ -242,6 +243,18 @@ function locationFieldEvent(
listener: TFieldEventListener,
options?: boolean | AddEventListenerOptions | undefined
): void;
+/**
+ * fired on clicking a selectable pin on the map
+ *
+ * `event.preventDefault()` will stop the default selection behavior
+ * */
+function locationFieldEvent(
+ uiType: "location-field",
+ type: "click-selectable-pin",
+ id: string,
+ listener: TFieldEventListener<{ pin: IMapPin }>,
+ options?: boolean | AddEventListenerOptions | undefined
+): void;
function locationFieldEvent() {
//
}
diff --git a/src/stories/3-fields/location-field/custom-pin/index.styles.ts b/src/stories/3-fields/location-field/custom-pin/index.styles.ts
new file mode 100644
index 000000000..5ec0a625c
--- /dev/null
+++ b/src/stories/3-fields/location-field/custom-pin/index.styles.ts
@@ -0,0 +1,51 @@
+import { Colour, Font, Radius, Shadow, Spacing } from "@lifesg/react-design-system/theme";
+import styled from "styled-components";
+
+export const CustomPinContainer = styled.div`
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ gap: ${Spacing["spacing-4"]};
+ padding: ${Spacing["spacing-4"]};
+ background: ${Colour["bg-primary"]};
+ border-radius: ${Radius.full};
+ box-shadow: ${Shadow["sm-subtle"]};
+ color: ${Colour["text-inverse"]};
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: calc(100% - 1px);
+ left: 50%;
+ transform: translateX(-50%);
+ border-width: 8px;
+ border-style: solid;
+ border-color: ${Colour["bg-primary"]} transparent transparent transparent;
+ }
+`;
+
+export const PinIconWrapper = styled.span`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+`;
+
+export const PinIconImage = styled.img`
+ width: ${Spacing["spacing-24"]};
+ height: ${Spacing["spacing-24"]};
+ object-fit: contain;
+ flex-shrink: 0;
+`;
+
+export const PinCount = styled.span`
+ ${Font["body-sm-semibold"]};
+ font-weight: ${Font.Spec["weight-semibold"]};
+`;
+
+export const HomePinImage = styled.img`
+ display: block;
+ width: ${Spacing["spacing-40"]};
+ height: ${Spacing["spacing-40"]};
+ object-fit: contain;
+`;
diff --git a/src/stories/3-fields/location-field/custom-pin/index.tsx b/src/stories/3-fields/location-field/custom-pin/index.tsx
new file mode 100644
index 000000000..4daee3e2a
--- /dev/null
+++ b/src/stories/3-fields/location-field/custom-pin/index.tsx
@@ -0,0 +1,28 @@
+import { CustomPinContainer, HomePinImage, PinCount, PinIconImage, PinIconWrapper } from "./index.styles";
+
+export interface ICustomPinProps {
+ icons?: string[];
+ count?: number | undefined;
+}
+
+export interface ICustomHomePinProps {
+ iconUrl?: string | undefined;
+}
+
+export const CustomPin = ({ icons = [], count }: ICustomPinProps) => {
+ const showCount = count !== undefined && count !== null;
+
+ return (
+
+
+ {icons.map((icon, index) => (
+
+ ))}
+
+
+ {showCount && {count}}
+
+ );
+};
+
+export const CustomHomePin = ({ iconUrl = "" }: ICustomHomePinProps) => ;
diff --git a/src/stories/3-fields/location-field/location-field-events.stories.tsx b/src/stories/3-fields/location-field/location-field-events.stories.tsx
index a70f24e3b..d69a492ff 100644
--- a/src/stories/3-fields/location-field/location-field-events.stories.tsx
+++ b/src/stories/3-fields/location-field/location-field-events.stories.tsx
@@ -686,17 +686,35 @@ HidePermissionModal.args = {
mapApi: defaultMapApi,
};
-const SetSelectablePinsTemplate = () =>
+const createSelectablePinsTemplate = (listenClickSelectablePin = false) =>
((args) => {
const id = "location-enable-map-click";
const formRef = useRef();
useEffect(() => {
const currentFormRef = formRef.current;
- currentFormRef.addFieldEventListener("location-field", "get-selectable-pins", id, getPins);
+ const handleClickSelectablePin = (e: TLocationFieldEvents["click-selectable-pin"]) =>
+ action("click-selectable-pin")(e);
+ if (listenClickSelectablePin) {
+ currentFormRef?.addFieldEventListener(
+ "location-field",
+ "click-selectable-pin",
+ id,
+ handleClickSelectablePin
+ );
+ }
+ currentFormRef?.addFieldEventListener("location-field", "get-selectable-pins", id, getPins);
return () => {
- currentFormRef.removeFieldEventListener("location-field", "get-selectable-pins", id, getPins);
+ currentFormRef?.removeFieldEventListener("location-field", "get-selectable-pins", id, getPins);
+ if (listenClickSelectablePin) {
+ currentFormRef?.removeFieldEventListener(
+ "location-field",
+ "click-selectable-pin",
+ id,
+ handleClickSelectablePin
+ );
+ }
};
}, []);
@@ -716,7 +734,7 @@ const SetSelectablePinsTemplate = () =>
},
];
setTimeout(() => {
- formRef.current.dispatchFieldEvent("location-field", "set-selectable-pins", id, {
+ formRef.current?.dispatchFieldEvent("location-field", "set-selectable-pins", id, {
pins: res.map(
(r) =>
({
@@ -755,13 +773,20 @@ const SetSelectablePinsTemplate = () =>
}) as StoryFn;
/* eslint-enable react-hooks/rules-of-hooks */
-export const SetSelectablePins = SetSelectablePinsTemplate().bind({});
+export const SetSelectablePins = createSelectablePinsTemplate().bind({});
SetSelectablePins.args = {
uiType: "location-field",
label: "Set Selectable Pins",
mapApi: defaultMapApi,
};
+export const ClickSelectablePin = createSelectablePinsTemplate(true).bind({});
+ClickSelectablePin.args = {
+ uiType: "location-field",
+ label: "Click Selectable Pin",
+ mapApi: defaultMapApi,
+};
+
/* eslint-disable react-hooks/rules-of-hooks */
const RefreshLocationAndTriggerGetCurrentLocationTemplate = () =>
((args) => {
diff --git a/src/stories/3-fields/location-field/location-field.stories.tsx b/src/stories/3-fields/location-field/location-field.stories.tsx
index 41832d560..b6d6d166e 100644
--- a/src/stories/3-fields/location-field/location-field.stories.tsx
+++ b/src/stories/3-fields/location-field/location-field.stories.tsx
@@ -1,6 +1,7 @@
import { ArgTypes, Stories, Title } from "@storybook/addon-docs";
import { Meta, StoryFn } from "@storybook/react";
import { useEffect, useRef } from "react";
+import ReactDOMServer from "react-dom/server";
import { ILocationCoord, ILocationFieldSchema, ILocationFieldValues } from "../../../components/fields";
import { IMapPin } from "../../../components/fields/location-field/location-modal/location-picker/types";
import { IFrontendEngineRef } from "../../../components/frontend-engine";
@@ -13,6 +14,24 @@ import {
SUBMIT_BUTTON_SCHEMA,
WarningStoryTemplate,
} from "../../common";
+import { CustomHomePin, CustomPin } from "./custom-pin";
+
+type TSelectablePinConfig = {
+ name?: string;
+ carParkName?: string;
+ id?: string;
+ carParkNumber?: string;
+ carParkType?: string;
+ parkingSystem?: string;
+ xCoord?: number;
+ yCoord?: number;
+ distanceFromReference?: number;
+ latitude: number;
+ longitude: number;
+ icons?: string[];
+ count?: number;
+ isHomeAddress?: boolean;
+};
const recaptchaSiteKey = "6LfCjocsAAAAALM6wuZN3bqarbgbdaLuJIgFSrXT";
@@ -20,6 +39,82 @@ const reverseGeocode = "https://api.dev.lifesg.io/onemap/revgeocode";
const convertLatLngToXY = "https://api.dev.lifesg.io/onemap/4326to3414";
const search = "https://api.dev.lifesg.io/onemap/search";
+const defaultSelectablePins: TSelectablePinConfig[] = [
+ {
+ carParkName: "BLK 120 TO 124 PAYA LEBAR WAY",
+ carParkNumber: "M32",
+ carParkType: "SURFACE CAR PARK",
+ parkingSystem: "ELECTRONIC PARKING",
+ latitude: 1.3223122045708784,
+ longitude: 103.88279263612282,
+ xCoord: 33506.0078,
+ yCoord: 33840.2109,
+ distanceFromReference: 109.9831233911331,
+ },
+];
+
+const pinsWithIcons: TSelectablePinConfig[] = [
+ {
+ carParkName: "KAMPONG UBI VIEW",
+ carParkNumber: "358",
+ carParkType: "SURFACE CAR PARK",
+ parkingSystem: "ELECTRONIC PARKING",
+ xCoord: 35501.9607216,
+ yCoord: 34191.1578935,
+ latitude: 1.325486284730739,
+ longitude: 103.90072773995409,
+ icons: ["/img/home.png"],
+ count: 1,
+ isHomeAddress: true,
+ },
+ {
+ carParkName: "KAMPONG UBI VIEW",
+ carParkNumber: "351",
+ carParkType: "SURFACE CAR PARK",
+ parkingSystem: "ELECTRONIC PARKING",
+ xCoord: 35501.9607216,
+ yCoord: 34191.1578935,
+ latitude: 1.326386284730739,
+ longitude: 103.90162773995409,
+ icons: ["/img/lift.png", "/img/reno.png"],
+ count: 3,
+ },
+ {
+ carParkName: "KAMPONG UBI VIEW",
+ carParkNumber: "349",
+ carParkType: "SURFACE CAR PARK",
+ parkingSystem: "ELECTRONIC PARKING",
+ xCoord: 35485.6405577,
+ yCoord: 34228.9805541,
+ latitude: 1.3258283432645641,
+ longitude: 103.90058110378057,
+ icons: ["/img/lift.png"],
+ count: 2,
+ },
+ {
+ carParkName: "KAMPONG UBI VIEW",
+ carParkNumber: "352",
+ carParkType: "SURFACE CAR PARK",
+ parkingSystem: "ELECTRONIC PARKING",
+ xCoord: 35354.2225417,
+ yCoord: 34162.4855169,
+ latitude: 1.3252270180708603,
+ longitude: 103.89940022649942,
+ icons: ["/img/reno.png"],
+ count: 1,
+ },
+];
+
+const getMarkerHtml = (pin: TSelectablePinConfig) => {
+ return ReactDOMServer.renderToString(
+ pin?.isHomeAddress ? (
+
+ ) : (
+
+ )
+ );
+};
+
const defaultMapApi = {
reverseGeocode,
convertLatLngToXY,
@@ -408,46 +503,30 @@ locationSelectionMode.args = {
};
/* eslint-disable react-hooks/rules-of-hooks */
-const IndicateCurrentLocationTemplate = () =>
+const IndicateCurrentLocationTemplate = (res: TSelectablePinConfig[]) =>
((args) => {
const id = "location-enable-map-click";
const formRef = useRef();
useEffect(() => {
const currentFormRef = formRef.current;
- currentFormRef.addFieldEventListener("location-field", "get-selectable-pins", id, getPins);
+ currentFormRef?.addFieldEventListener("location-field", "get-selectable-pins", id, getPins);
return () => {
- currentFormRef.removeFieldEventListener("location-field", "get-selectable-pins", id, getPins);
+ currentFormRef?.removeFieldEventListener("location-field", "get-selectable-pins", id, getPins);
};
}, []);
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const getPins = (e: CustomEvent) => {
- const res = [
- {
- carParkName: "BLK 120 TO 124 PAYA LEBAR WAY",
- carParkNumber: "M32",
- carParkType: "SURFACE CAR PARK",
- parkingSystem: "ELECTRONIC PARKING",
- latitude: 1.3223122045708784,
- longitude: 103.88279263612282,
- xCoord: 33506.0078,
- yCoord: 33840.2109,
- distanceFromReference: 109.9831233911331,
- },
- ];
+ const getPins = () => {
setTimeout(() => {
- formRef.current.dispatchFieldEvent("location-field", "set-selectable-pins", id, {
- pins: res.map(
- (r) =>
- ({
- lat: r.latitude,
- lng: r.longitude,
- resultListItemText: r.carParkName,
- address: `${r.carParkName} (${r.carParkNumber})`,
- } as IMapPin)
- ),
+ formRef.current?.dispatchFieldEvent("location-field", "set-selectable-pins", id, {
+ pins: res.map((r) => ({
+ lat: r.latitude,
+ lng: r.longitude,
+ resultListItemText: r.carParkName,
+ address: `${r.carParkName} (${r.carParkNumber})`,
+ markerHtml: r.icons ? getMarkerHtml(r) : undefined,
+ })),
});
}, 2000);
};
@@ -475,7 +554,7 @@ const IndicateCurrentLocationTemplate = () =>
);
}) as StoryFn;
-export const pinsOnlyIndicateCurrentLocation = IndicateCurrentLocationTemplate().bind({});
+export const pinsOnlyIndicateCurrentLocation = IndicateCurrentLocationTemplate(defaultSelectablePins).bind({});
pinsOnlyIndicateCurrentLocation.args = {
uiType: "location-field",
label: "Indicate Current Location",
@@ -486,6 +565,37 @@ pinsOnlyIndicateCurrentLocation.args = {
"https://dev.eservices.lifesg.io/report-neighbourhood-issue/img/icons/car-location-pin.svg",
};
+export const CustomHtmlMarkers = IndicateCurrentLocationTemplate(pinsWithIcons).bind({});
+CustomHtmlMarkers.args = {
+ uiType: "location-field",
+ label: "Custom HTML Markers",
+ mapApi: defaultMapApi,
+ locationSelectionMode: "pins-only",
+ legendItems: [
+ {
+ id: "lift",
+ label: "Lift fault",
+ icon: "/img/lift.png",
+ },
+ {
+ id: "reno",
+ label: "Renovation",
+ icon: "/img/reno.png",
+ },
+ ],
+ defaultAddress: {
+ lat: 1.325486284730739,
+ lng: 103.90072773995409,
+ },
+};
+CustomHtmlMarkers.parameters = {
+ docs: {
+ description: {
+ story: "Custom HTML markers can be rendered by passing a markerHtml property for each marker. This HTML string is used to display a custom marker, enabling dynamic rendering based on pin data—for example, showing different icons or badges for various location types.",
+ },
+ },
+};
+
export const LegendComponent = DefaultStoryTemplate(
"location-field-legend",
false,