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 = '
Lift
'; + 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,