fix(BOOK-138): Fixed several issues after country map functionality was added
* fix(BOOK-138): Fixed issue when hovering markers and info windows for both city cluster marker as city markers. Approved-by: Matilda Landström
This commit is contained in:
49
packages/common/hooks/map/useMarkerHover.ts
Normal file
49
packages/common/hooks/map/useMarkerHover.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useRef } from "react"
|
||||
|
||||
/**
|
||||
* Custom hook to manage hover state across marker and InfoWindow elements.
|
||||
* The Google Maps InfoWindow component does not natively support hover events.
|
||||
* This hook provides a way to track hover state across both elements simultaneously.
|
||||
*
|
||||
* This hook solves the problem where moving from a marker to its InfoWindow
|
||||
* causes the InfoWindow to close prematurely. It uses a counter to track
|
||||
* how many elements (marker, InfoWindow) are currently being hovered over.
|
||||
*
|
||||
* When moving from marker to InfoWindow, the InfoWindow's onMouseEnter fires
|
||||
* before the marker's onMouseLeave, causing the counter to correctly remain > 0.
|
||||
*
|
||||
* @param onHoverChange - Callback function that receives true when hovering starts, false when it ends
|
||||
* @returns Object with handleMouseEnter and handleMouseLeave functions
|
||||
*/
|
||||
export function useMarkerHover(onHoverChange: (isHovering: boolean) => void) {
|
||||
const hoverCountRef = useRef(0)
|
||||
|
||||
function handleMouseEnter() {
|
||||
hoverCountRef.current += 1
|
||||
|
||||
if (hoverCountRef.current >= 1) {
|
||||
onHoverChange(true)
|
||||
|
||||
// In case of rapid mouse movements, or close markers/info windows,
|
||||
// The counter can increase beyond 2, which is unnecessary. We reset
|
||||
// it to 1 which works to track hover state correctly.
|
||||
if (hoverCountRef.current > 2) {
|
||||
hoverCountRef.current = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
hoverCountRef.current -= 1
|
||||
|
||||
if (hoverCountRef.current <= 0) {
|
||||
onHoverChange(false)
|
||||
hoverCountRef.current = 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
InfoWindow as GoogleMapsInfoWindow,
|
||||
type InfoWindowProps as GoogleMapsInfoWindowProps,
|
||||
} from '@vis.gl/react-google-maps'
|
||||
|
||||
import styles from './infoWindow.module.css'
|
||||
|
||||
import type { MouseEventHandler } from 'react'
|
||||
|
||||
interface InfoWindowProps
|
||||
extends React.PropsWithChildren<
|
||||
Omit<GoogleMapsInfoWindowProps, 'pixelOffset'>
|
||||
> {
|
||||
pixelOffsetY?: number
|
||||
onMouseEnter?: MouseEventHandler<HTMLDivElement>
|
||||
onMouseLeave?: MouseEventHandler<HTMLDivElement>
|
||||
}
|
||||
|
||||
export function InfoWindow({
|
||||
children,
|
||||
pixelOffsetY = -12,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
...options
|
||||
}: InfoWindowProps) {
|
||||
function onMouseEnterHandler(e: React.MouseEvent<HTMLDivElement>) {
|
||||
if (onMouseEnter) {
|
||||
onMouseEnter(e)
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeaveHandler(e: React.MouseEvent<HTMLDivElement>) {
|
||||
if (onMouseLeave) {
|
||||
onMouseLeave(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GoogleMapsInfoWindow {...options}>
|
||||
<div
|
||||
className={styles.infoWindow}
|
||||
onMouseEnter={onMouseEnterHandler}
|
||||
onMouseLeave={onMouseLeaveHandler}
|
||||
style={{ paddingBottom: pixelOffsetY ? `${pixelOffsetY * -1}px` : 0 }}
|
||||
>
|
||||
<div className={styles.content}>{children}</div>
|
||||
<span className={styles.arrow} />
|
||||
</div>
|
||||
</GoogleMapsInfoWindow>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
.infoWindow {
|
||||
position: relative;
|
||||
display: grid;
|
||||
padding: 4px 4px 12px 4px;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: relative;
|
||||
height: 12px;
|
||||
width: 25px;
|
||||
filter: drop-shadow(0 4px 2px rgba(0, 0, 0, 0.1));
|
||||
justify-self: center;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
clip-path: polygon(0 0, 50% 100%, 100% 0);
|
||||
height: 12px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
width: 25px;
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,7 @@
|
||||
"./Link": "./lib/components/Link/index.tsx",
|
||||
"./LoadingSpinner": "./lib/components/LoadingSpinner/index.tsx",
|
||||
"./LoginButton": "./lib/components/LoginButton/index.tsx",
|
||||
"./Map/InfoWindow": "./lib/components/Map/InfoWindow/index.tsx",
|
||||
"./Map/InteractiveMap": "./lib/components/Map/InteractiveMap/index.tsx",
|
||||
"./Map/mapConstants": "./lib/components/Map/mapConstants.ts",
|
||||
"./Map/Markers/HotelMarkerByType": "./lib/components/Map/Markers/HotelMarkerByType.tsx",
|
||||
|
||||
@@ -128,8 +128,15 @@ export async function getCityPages(
|
||||
|
||||
const publishedCities = cities[apiCountry].filter((city) => city.isPublished)
|
||||
|
||||
// It happens we receive duplicate cities with the same city identifier.
|
||||
// Remove duplicate cities based on city identifier
|
||||
const uniquePublishedCities = publishedCities.filter(
|
||||
(city, index, self) =>
|
||||
index === self.findIndex((c) => c.cityIdentifier === city.cityIdentifier)
|
||||
)
|
||||
|
||||
const cityPages = await Promise.all(
|
||||
publishedCities.map(async (city) => {
|
||||
uniquePublishedCities.map(async (city) => {
|
||||
if (!city.cityIdentifier) {
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user