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:
@@ -1,9 +1,7 @@
|
||||
.wrapper {
|
||||
background: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
||||
.cityMapCard {
|
||||
display: flex;
|
||||
position: relative;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
.name {
|
||||
@@ -28,8 +26,13 @@
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.link {
|
||||
justify-content: center;
|
||||
.exploreLink {
|
||||
justify-self: center;
|
||||
color: var(--Text-Interactive-Secondary);
|
||||
|
||||
&:hover {
|
||||
color: var(--Text-Interactive-Secondary-Hover);
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
@@ -37,6 +40,13 @@
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 949px) {
|
||||
.cityMapCard {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.content {
|
||||
min-width: 220px;
|
||||
@@ -44,7 +54,7 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.link {
|
||||
.exploreLink {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -33,92 +34,75 @@ export default function CityMapCard({
|
||||
}: CityMapCardProps) {
|
||||
const intl = useIntl()
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const { setActiveCityMarker, setHoveredCityMarker } =
|
||||
const { setActiveCityMarker, resetActiveAndHoveredState } =
|
||||
useDestinationPageCitiesMapStore()
|
||||
|
||||
function handleClose() {
|
||||
setActiveCityMarker(null)
|
||||
setHoveredCityMarker(null)
|
||||
resetActiveAndHoveredState()
|
||||
}
|
||||
|
||||
const cityMapUrl = setMapUrlFromCountryPage(url)
|
||||
|
||||
return (
|
||||
<article className={className}>
|
||||
<div className={styles.wrapper}>
|
||||
<IconButton
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
className={styles.closeButton}
|
||||
onPress={handleClose}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="close"
|
||||
size={16}
|
||||
className={styles.closeIcon}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</IconButton>
|
||||
{image ? (
|
||||
<DialogImage
|
||||
image={image.src}
|
||||
altText={image.alt}
|
||||
imageError={imageError}
|
||||
setImageError={setImageError}
|
||||
/>
|
||||
) : (
|
||||
<ImageFallback />
|
||||
)}
|
||||
<article className={cx(styles.cityMapCard, className)}>
|
||||
<IconButton
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
className={styles.closeButton}
|
||||
onPress={handleClose}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="close"
|
||||
size={16}
|
||||
className={styles.closeIcon}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</IconButton>
|
||||
{image ? (
|
||||
<DialogImage
|
||||
image={image.src}
|
||||
altText={image.alt}
|
||||
imageError={imageError}
|
||||
setImageError={setImageError}
|
||||
/>
|
||||
) : (
|
||||
<ImageFallback />
|
||||
)}
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<h4>{cityName}</h4>
|
||||
</Typography>
|
||||
</div>
|
||||
{url && cityMapUrl && (
|
||||
<span className={styles.links}>
|
||||
<ButtonLink
|
||||
href={cityMapUrl}
|
||||
variant="Secondary"
|
||||
color="Primary"
|
||||
size="Small"
|
||||
onClick={() => setActiveCityMarker(null)}
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "See hotels on map",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</ButtonLink>
|
||||
|
||||
<Link
|
||||
href={url}
|
||||
color="Text/Interactive/Secondary"
|
||||
variant="icon"
|
||||
className={styles.link}
|
||||
>
|
||||
<Typography variant="Link/sm">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Explore city",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
<MaterialIcon
|
||||
icon="open_in_new"
|
||||
size={20}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<h4>{cityName}</h4>
|
||||
</Typography>
|
||||
</div>
|
||||
{url && cityMapUrl && (
|
||||
<span className={styles.links}>
|
||||
<ButtonLink
|
||||
href={cityMapUrl}
|
||||
variant="Secondary"
|
||||
color="Primary"
|
||||
size="Small"
|
||||
onClick={() => setActiveCityMarker(null)}
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "See hotels on map",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</ButtonLink>
|
||||
|
||||
<Link href={url} className={styles.exploreLink} size="small">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Explore city",
|
||||
})}
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
|
||||
@@ -48,6 +48,14 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.exploreLink {
|
||||
color: var(--Text-Interactive-Secondary);
|
||||
|
||||
&:hover {
|
||||
color: var(--Text-Interactive-Secondary-Hover);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 950px) {
|
||||
.content {
|
||||
min-width: 220px;
|
||||
|
||||
@@ -104,15 +104,10 @@ export function CityListItem({
|
||||
<h3>{cityName}</h3>
|
||||
</Typography>
|
||||
<div>
|
||||
<Link href={url} color="Text/Interactive/Secondary" variant="icon">
|
||||
<Typography variant="Link/sm">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Explore city",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
<MaterialIcon icon="open_in_new" size={20} color="CurrentColor" />
|
||||
<Link href={url} className={styles.exploreLink} size="small">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Explore city",
|
||||
})}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
.content {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
padding: var(--Space-x15);
|
||||
display: grid;
|
||||
gap: var(--Space-x1);
|
||||
list-style: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 949px) {
|
||||
.content {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,11 +68,18 @@
|
||||
|
||||
/* Overriding Google maps infoWindow styles */
|
||||
.mapWrapper :global(.gm-style .gm-style-iw-c) {
|
||||
background-color: transparent !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.mapWrapper :global(.gm-style .gm-style-iw-d) {
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.mapWrapper :global(.gm-style .gm-style-iw-tc) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.cityMarker {
|
||||
display: block;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
background-color: var(--Base-Text-High-contrast);
|
||||
@@ -7,13 +8,12 @@
|
||||
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.cityMarker:hover,
|
||||
.hoveredChild {
|
||||
background:
|
||||
linear-gradient(rgba(31, 28, 27, 0.3), rgba(31, 28, 27, 0.3)),
|
||||
var(--Surface-Brand-Primary-2-Default);
|
||||
width: var(--Space-x4) !important;
|
||||
height: var(--Space-x4) !important;
|
||||
&.active {
|
||||
background:
|
||||
linear-gradient(rgba(31, 28, 27, 0.3), rgba(31, 28, 27, 0.3)),
|
||||
var(--Surface-Brand-Primary-2-Default);
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
InfoWindow,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useCallback, useState } from "react"
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useCallback } from "react"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { useMarkerHover } from "@scandic-hotels/common/hooks/map/useMarkerHover"
|
||||
import { InfoWindow } from "@scandic-hotels/design-system/Map/InfoWindow"
|
||||
|
||||
import { useDestinationPageCitiesMapStore } from "@/stores/destination-page-cities-map"
|
||||
|
||||
import { trackMapClick } from "@/utils/tracking/destinationPage"
|
||||
@@ -31,63 +34,55 @@ export default function CityMarker({ position, properties }: CityMarkerProps) {
|
||||
setActiveCityMarker,
|
||||
} = useDestinationPageCitiesMapStore()
|
||||
|
||||
const [infoWindowHovered, setInfoWindowHovered] = useState<boolean>(false)
|
||||
|
||||
const isDesktop = useMediaQuery("(min-width: 950px)")
|
||||
|
||||
const { handleMouseEnter, handleMouseLeave } = useMarkerHover((isHovered) => {
|
||||
if (isHovered) {
|
||||
if (activeCityMarker?.cityId !== properties.id) {
|
||||
setActiveCityMarker(null)
|
||||
}
|
||||
setHoveredCityMarker(properties.id)
|
||||
} else {
|
||||
setHoveredCityMarker(null)
|
||||
}
|
||||
})
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setActiveCityMarker({ cityId: properties.id, location: position })
|
||||
trackMapClick(`city with id: ${properties.id}`)
|
||||
}, [position, properties.id, setActiveCityMarker])
|
||||
|
||||
function handleMouseEnter() {
|
||||
if (activeCityMarker?.cityId !== hoveredCityMarker) {
|
||||
setActiveCityMarker(null)
|
||||
}
|
||||
setHoveredCityMarker(properties.id)
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
setTimeout(() => {
|
||||
if (!infoWindowHovered) {
|
||||
setHoveredCityMarker(null)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const isHovered = hoveredCityMarker === properties.id || infoWindowHovered
|
||||
const isHovered = hoveredCityMarker === properties.id
|
||||
const isActive = activeCityMarker?.cityId === properties.id
|
||||
|
||||
return (
|
||||
<AdvancedMarker
|
||||
position={position}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={`${styles.cityMarker} ${isActive || isHovered ? styles.hoveredChild : ""}`}
|
||||
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
|
||||
>
|
||||
<span
|
||||
className={cx(styles.cityMarker, {
|
||||
[styles.active]: isActive || isHovered,
|
||||
})}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
{isDesktop && (isActive || isHovered) ? (
|
||||
<span
|
||||
onMouseEnter={() => setInfoWindowHovered(true)}
|
||||
onMouseLeave={() => setInfoWindowHovered(false)}
|
||||
<InfoWindow
|
||||
position={position}
|
||||
headerDisabled={true}
|
||||
minWidth={450}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<InfoWindow
|
||||
position={position}
|
||||
pixelOffset={[0, -24]}
|
||||
headerDisabled={true}
|
||||
minWidth={450}
|
||||
>
|
||||
<CityMapCard
|
||||
cityName={properties.name}
|
||||
image={properties.image}
|
||||
url={properties.url}
|
||||
/>
|
||||
</InfoWindow>
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<CityMapCard
|
||||
cityName={properties.name}
|
||||
image={properties.image}
|
||||
url={properties.url}
|
||||
/>
|
||||
</InfoWindow>
|
||||
) : null}
|
||||
</AdvancedMarker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
InfoWindow,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useCallback, useState } from "react"
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useCallback } from "react"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { useMarkerHover } from "@scandic-hotels/common/hooks/map/useMarkerHover"
|
||||
import { InfoWindow } from "@scandic-hotels/design-system/Map/InfoWindow"
|
||||
|
||||
import { useDestinationPageCitiesMapStore } from "@/stores/destination-page-cities-map"
|
||||
|
||||
import { trackMapClick } from "@/utils/tracking/destinationPage"
|
||||
@@ -33,18 +36,25 @@ export default function CityClusterMarker({
|
||||
onMarkerClick,
|
||||
cities,
|
||||
}: ClusterMarkerProps) {
|
||||
const { hoveredCityMarker, activeCityMarker, setActiveCityMarker } =
|
||||
useDestinationPageCitiesMapStore()
|
||||
const {
|
||||
hoveredCityMarker,
|
||||
setHoveredCityMarker,
|
||||
activeCityMarker,
|
||||
setActiveCityMarker,
|
||||
} = useDestinationPageCitiesMapStore()
|
||||
const isDesktop = useMediaQuery("(min-width: 950px)")
|
||||
const cityIdsAsString = cities.map((city) => city.id).join(",")
|
||||
|
||||
const [isHoveredOnMap, setIsHoveredOnMap] = useState(false)
|
||||
const [infoWindowHovered, setInfoWindowHovered] = useState<boolean>(false)
|
||||
|
||||
const isActive =
|
||||
cities.find(
|
||||
(city) =>
|
||||
city.id === hoveredCityMarker || city.id === activeCityMarker?.cityId
|
||||
) || infoWindowHovered
|
||||
const { handleMouseEnter, handleMouseLeave } = useMarkerHover((isHovered) => {
|
||||
if (isHovered) {
|
||||
if (activeCityMarker?.cityId !== cityIdsAsString) {
|
||||
setActiveCityMarker(null)
|
||||
}
|
||||
setHoveredCityMarker(cityIdsAsString)
|
||||
} else {
|
||||
setHoveredCityMarker(null)
|
||||
}
|
||||
})
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (onMarkerClick) {
|
||||
@@ -55,20 +65,7 @@ export default function CityClusterMarker({
|
||||
)
|
||||
}, [onMarkerClick, position, cities])
|
||||
|
||||
function handleMouseEnter() {
|
||||
if (activeCityMarker?.cityId !== hoveredCityMarker) {
|
||||
setActiveCityMarker(null)
|
||||
}
|
||||
setIsHoveredOnMap(true)
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
setTimeout(() => {
|
||||
if (!infoWindowHovered) {
|
||||
setIsHoveredOnMap(false)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
const isHovered = hoveredCityMarker === cityIdsAsString
|
||||
|
||||
return (
|
||||
<AdvancedMarker
|
||||
@@ -77,23 +74,22 @@ export default function CityClusterMarker({
|
||||
onMouseLeave={handleMouseLeave}
|
||||
zIndex={size}
|
||||
onClick={handleClick}
|
||||
className={`${styles.clusterMarker} ${isActive ? styles.hoveredChild : ""}`}
|
||||
className={cx(styles.clusterMarker, {
|
||||
[styles.active]: isHovered,
|
||||
})}
|
||||
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
|
||||
>
|
||||
<span className={styles.count}>{sizeAsText}</span>
|
||||
{isDesktop && (isHoveredOnMap || infoWindowHovered) ? (
|
||||
<span
|
||||
onMouseEnter={() => setInfoWindowHovered(true)}
|
||||
onMouseLeave={() => setInfoWindowHovered(false)}
|
||||
{isDesktop && isHovered ? (
|
||||
<InfoWindow
|
||||
position={position}
|
||||
headerDisabled={true}
|
||||
pixelOffsetY={-20}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<InfoWindow
|
||||
position={position}
|
||||
pixelOffset={[0, -24]}
|
||||
headerDisabled={true}
|
||||
>
|
||||
<LocationsList locations={cities} />
|
||||
</InfoWindow>
|
||||
</span>
|
||||
<LocationsList locations={cities} />
|
||||
</InfoWindow>
|
||||
) : null}
|
||||
</AdvancedMarker>
|
||||
)
|
||||
|
||||
@@ -11,15 +11,14 @@
|
||||
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.clusterMarker:hover,
|
||||
.hoveredChild {
|
||||
background:
|
||||
linear-gradient(rgba(31, 28, 27, 0.3), rgba(31, 28, 27, 0.3)),
|
||||
var(--Surface-Brand-Primary-2-Default);
|
||||
width: 46px !important;
|
||||
height: 46px !important;
|
||||
&.active {
|
||||
background:
|
||||
linear-gradient(rgba(31, 28, 27, 0.3), rgba(31, 28, 27, 0.3)),
|
||||
var(--Surface-Brand-Primary-2-Default);
|
||||
width: 46px !important;
|
||||
height: 46px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
InfoWindow,
|
||||
useAdvancedMarkerRef,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { InfoWindow } from "@scandic-hotels/design-system/Map/InfoWindow"
|
||||
import { HotelMarkerByType } from "@scandic-hotels/design-system/Map/Markers/HotelMarkerByType"
|
||||
|
||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||
@@ -69,12 +69,7 @@ export default function HotelMarker({
|
||||
/>
|
||||
|
||||
{isActive && isDesktop && (
|
||||
<InfoWindow
|
||||
position={position}
|
||||
pixelOffset={[0, -24]}
|
||||
headerDisabled={true}
|
||||
minWidth={450}
|
||||
>
|
||||
<InfoWindow position={position} headerDisabled={true} minWidth={450}>
|
||||
<HotelMapCard
|
||||
amenities={properties.amenities.slice(0, 3)}
|
||||
tripadvisorRating={properties.tripadvisor}
|
||||
|
||||
@@ -7,16 +7,28 @@ export type SelectedMarker = {
|
||||
|
||||
interface DestinationPageCitiesMapState {
|
||||
hoveredCityMarker: string | null
|
||||
hoveredInfoWindow: string | null
|
||||
activeCityMarker: SelectedMarker
|
||||
setHoveredInfoWindow: (hovered: string | null) => void
|
||||
setHoveredCityMarker: (cityId: string | null) => void
|
||||
setActiveCityMarker: (marker: SelectedMarker) => void
|
||||
resetActiveAndHoveredState: () => void
|
||||
}
|
||||
|
||||
export const useDestinationPageCitiesMapStore =
|
||||
create<DestinationPageCitiesMapState>((set) => ({
|
||||
hoveredCityMarker: null,
|
||||
hoveredInfoWindow: null,
|
||||
activeCityMarker: null,
|
||||
setHoveredInfoWindow: (hovered) =>
|
||||
set(() => ({ hoveredInfoWindow: hovered })),
|
||||
setHoveredCityMarker: (cityId) => set({ hoveredCityMarker: cityId }),
|
||||
setActiveCityMarker: (selectedMarker) =>
|
||||
set({ activeCityMarker: selectedMarker }),
|
||||
resetActiveAndHoveredState: () =>
|
||||
set({
|
||||
activeCityMarker: null,
|
||||
hoveredCityMarker: null,
|
||||
hoveredInfoWindow: null,
|
||||
}),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user