Merged in feat/SW-2041-map-zoom-buttons (pull request #2550)

Feat(SW-661): Hotel page map zoom restrictions

* fix(SW-2041): update tokens

* chore(SW-2041): restrict zooming

* fix(SW-2041): remove ref

* fix(SW-2041): create map zoom hook


Approved-by: Erik Tiekstra
Approved-by: Hrishikesh Vaipurkar
This commit is contained in:
Matilda Landström
2025-08-13 07:50:39 +00:00
parent 28e9a043b8
commit 5397437628
4 changed files with 75 additions and 33 deletions

View File

@@ -4,10 +4,17 @@ import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { MAP_RESTRICTIONS } from "@/constants/map" import {
DEFAULT_ZOOM,
MAP_RESTRICTIONS,
MAX_ZOOM,
MIN_ZOOM,
} from "@/constants/map"
import { useZoomControls } from "@/hooks/maps/useZoomControls"
import HotelListingMapContent from "./HotelListingMapContent" import HotelListingMapContent from "./HotelListingMapContent"
import PoiMapMarkers from "./PoiMapMarkers" import PoiMapMarkers from "./PoiMapMarkers"
@@ -31,9 +38,12 @@ export default function InteractiveMap({
const intl = useIntl() const intl = useIntl()
const map = useMap() const map = useMap()
const [hasInitializedBounds, setHasInitializedBounds] = useState(false) const [hasInitializedBounds, setHasInitializedBounds] = useState(false)
const { zoomIn, zoomOut, isMaxZoom, isMinZoom } = useZoomControls()
const mapOptions: MapProps = { const mapOptions: MapProps = {
defaultZoom: 14, defaultZoom: DEFAULT_ZOOM,
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
defaultCenter: coordinates, defaultCenter: coordinates,
disableDefaultUI: true, disableDefaultUI: true,
clickableIcons: false, clickableIcons: false,
@@ -42,19 +52,6 @@ export default function InteractiveMap({
restriction: MAP_RESTRICTIONS, restriction: MAP_RESTRICTIONS,
} }
function zoomIn() {
const currentZoom = map && map.getZoom()
if (currentZoom) {
map.setZoom(currentZoom + 1)
}
}
function zoomOut() {
const currentZoom = map && map.getZoom()
if (currentZoom) {
map.setZoom(currentZoom - 1)
}
}
useEffect(() => { useEffect(() => {
if (map && hotelPins?.length && !hasInitializedBounds) { if (map && hotelPins?.length && !hasInitializedBounds) {
if (fitBounds) { if (fitBounds) {
@@ -62,7 +59,6 @@ export default function InteractiveMap({
hotelPins.forEach((marker) => { hotelPins.forEach((marker) => {
bounds.extend(marker.coordinates) bounds.extend(marker.coordinates)
}) })
map.fitBounds(bounds, 100) map.fitBounds(bounds, 100)
} }
setHasInitializedBounds(true) setHasInitializedBounds(true)
@@ -86,32 +82,31 @@ export default function InteractiveMap({
<div className={styles.ctaButtons}> <div className={styles.ctaButtons}>
{closeButton} {closeButton}
<div className={styles.zoomButtons}> <div className={styles.zoomButtons}>
<Button <IconButton
theme="base" theme="Inverted"
intent="inverted" style="Elevated"
variant="icon"
size="small"
className={styles.zoomButton} className={styles.zoomButton}
onClick={zoomOut} onClick={zoomOut}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
defaultMessage: "Zoom in", defaultMessage: "Zoom out",
})} })}
isDisabled={isMinZoom}
> >
<MaterialIcon icon="remove" color="CurrentColor" /> <MaterialIcon icon="remove" color="CurrentColor" />
</Button> </IconButton>
<Button
theme="base" <IconButton
intent="inverted" theme="Inverted"
variant="icon" style="Elevated"
size="small"
className={styles.zoomButton} className={styles.zoomButton}
onClick={zoomIn} onClick={zoomIn}
aria-label={intl.formatMessage({ aria-label={intl.formatMessage({
defaultMessage: "Zoom out", defaultMessage: "Zoom in",
})} })}
isDisabled={isMaxZoom}
> >
<MaterialIcon icon="add" color="CurrentColor" /> <MaterialIcon icon="add" color="CurrentColor" />
</Button> </IconButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -59,8 +59,8 @@
} }
.zoomButton { .zoomButton {
width: var(--Spacing-x5); width: var(--Space-x5);
height: var(--Spacing-x5); height: var(--Space-x5);
padding: 0; padding: 0;
pointer-events: initial; pointer-events: initial;
box-shadow: var(--button-box-shadow); box-shadow: var(--button-box-shadow);

View File

@@ -3,3 +3,7 @@ export const MAP_RESTRICTIONS = {
// See https://developers.google.com/maps/documentation/javascript/reference/map#MapRestriction.latLngBounds // See https://developers.google.com/maps/documentation/javascript/reference/map#MapRestriction.latLngBounds
latLngBounds: { north: 85, south: -85, west: -180, east: 180 }, latLngBounds: { north: 85, south: -85, west: -180, east: 180 },
} }
export const DEFAULT_ZOOM = 14
export const MAX_ZOOM = 18
export const MIN_ZOOM = 8

View File

@@ -0,0 +1,43 @@
import { useMap } from "@vis.gl/react-google-maps"
import { useEffect, useState } from "react"
import { DEFAULT_ZOOM, MAX_ZOOM, MIN_ZOOM } from "@/constants/map"
export function useZoomControls() {
const map = useMap()
const [zoomLevel, setZoomLevel] = useState(DEFAULT_ZOOM)
const zoomIn = () => {
if (map && zoomLevel < MAX_ZOOM) {
map.setZoom(zoomLevel + 1)
}
}
const zoomOut = () => {
if (map && zoomLevel > MIN_ZOOM) {
map.setZoom(zoomLevel - 1)
}
}
useEffect(() => {
if (!map) return
const handleZoomChanged = () => {
const currentZoom = map.getZoom()
if (currentZoom != null) {
setZoomLevel(currentZoom)
}
}
const listener = map.addListener("zoom_changed", handleZoomChanged)
return () => listener.remove()
}, [map])
return {
zoomLevel,
zoomIn,
zoomOut,
isMinZoom: zoomLevel <= MIN_ZOOM,
isMaxZoom: zoomLevel >= MAX_ZOOM,
}
}