Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 0 additions & 134 deletions src/hooks/useConstrainedFloatingButton.ts

This file was deleted.

46 changes: 19 additions & 27 deletions src/pages/components/map-related/MapShell.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,45 @@
import { CloseFullscreenTwoTone, OpenInFullTwoTone } from '@mui/icons-material'
import { IconButton } from '@mui/material'
import { PropsWithChildren, ReactNode, useCallback, useRef, useState } from 'react'
import { PropsWithChildren, ReactNode, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AttributionControl, MapContainer, MapContainerProps, ZoomControl } from 'react-leaflet'
import { useConstrainedFloatingButton } from 'src/hooks/useConstrainedFloatingButton'
import { AttributionControl, MapContainer, MapContainerProps } from 'react-leaflet'
import { useTheme } from 'src/layout/ThemeContext'
import { MapZoomBar } from './MapZoomBar'

/**
* Shared shell for every map page: the `.map-info` wrapper (with the
* expand/collapse + dark-theme classes), the floating expand button, and a
* MapContainer whose zoom/attribution controls are placed direction-aware
* (zoom at the inline-end, attribution at the inline-start). Pages pass the
* MapContainer props (center/zoom/…) and the layers as children. An optional
* `legend` renders in the shared `.map-legend` box (top inline-start corner)
* so every map page places and themes its legend the same way — pages provide
* only the legend content.
* expand/collapse + dark-theme classes) and a MapContainer whose controls are
* placed direction-aware — the zoom bar at the inline-end with the
* expand/collapse button as its bottom segment, attribution at the
* inline-start. Pages pass the MapContainer props (center/zoom/…) and the
* layers as children. An optional `legend` renders in the shared `.map-legend`
* box (top inline-start corner) so every map page places and themes its legend
* the same way — pages provide only the legend content.
*/
type MapShellProps = PropsWithChildren<MapContainerProps & { legend?: ReactNode }>

export function MapShell({ children, legend, ...mapProps }: MapShellProps) {
const [isExpanded, setIsExpanded] = useState<boolean>(false)
const toggleExpanded = useCallback(() => setIsExpanded((expanded) => !expanded), [])

const mapContainerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
useConstrainedFloatingButton(mapContainerRef, buttonRef, isExpanded)

const { isDarkTheme } = useTheme()
const { i18n } = useTranslation()
const isRtl = i18n.dir() === 'rtl'

return (
<div
ref={mapContainerRef}
className={`map-info ${isExpanded ? 'expanded' : 'collapsed'}${isDarkTheme ? ' dark' : ''}`}>
<IconButton
ref={buttonRef}
color="primary"
className="expand-button"
onClick={toggleExpanded}>
{isExpanded ? (
<CloseFullscreenTwoTone fontSize="large" />
) : (
<OpenInFullTwoTone fontSize="large" />
)}
</IconButton>
{legend && <div className="map-legend">{legend}</div>}
<MapContainer {...mapProps} zoomControl={false} attributionControl={false}>
<ZoomControl position={isRtl ? 'topleft' : 'topright'} />
<MapZoomBar position={isRtl ? 'topleft' : 'topright'}>
<IconButton className="expand-button" onClick={toggleExpanded}>
{isExpanded ? (
<CloseFullscreenTwoTone fontSize="small" />
) : (
<OpenInFullTwoTone fontSize="small" />
)}
</IconButton>
</MapZoomBar>
<AttributionControl position={isRtl ? 'bottomright' : 'bottomleft'} prefix={false} />
{children}
</MapContainer>
Expand Down
39 changes: 39 additions & 0 deletions src/pages/components/map-related/MapZoomBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Control, ControlPosition, DomEvent, DomUtil } from 'leaflet'
import { PropsWithChildren, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { useMap } from 'react-leaflet'

type MapZoomBarProps = PropsWithChildren<{ position: ControlPosition }>

/**
* Leaflet's zoom control with extra buttons appended into the same
* `.leaflet-bar`, so they read as one connected control rather than separate
* boxes stacked in a corner. Zoom in/out stays Leaflet's own: it keeps the
* disabled states at the zoom limits, the button titles and the keyboard
* handling that a hand-rolled pair of buttons would have to reimplement.
*
* The slot is a stable node appended after `addTo`, so the container Leaflet
* builds is never queried or patched from the outside — changing `position`
* just rebuilds the control and re-appends the same slot.
*/
export function MapZoomBar({ position, children }: MapZoomBarProps) {
const map = useMap()
const [slot] = useState(() => DomUtil.create('div', 'map-zoom-bar-slot'))

useEffect(() => {
// Without this a click on the button also reaches the map and pans/zooms it.
DomEvent.disableClickPropagation(slot)
DomEvent.disableScrollPropagation(slot)
}, [slot])

useEffect(() => {
const control = new Control.Zoom({ position })
control.addTo(map)
control.getContainer()?.appendChild(slot)
return () => {
control.remove()
}
}, [map, position, slot])

return createPortal(children, slot)
}
76 changes: 53 additions & 23 deletions src/resources/map.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ $button-shadow:
$button-border: 1px solid rgb(0 0 0 / 25%);
$button-border-dark: 1px solid rgb(255 255 255 / 25%);

// Divider between the segments of the zoom bar (+ / − / expand). Deliberately
// stronger than the frame: Leaflet's own #ccc all but disappears against the
// button background, which made the three segments read as one blank slab.
$bar-divider: rgb(0 0 0 / 40%);
$bar-divider-dark: rgb(255 255 255 / 40%);

main,
main > div,
.map-container,
Expand All @@ -38,41 +44,64 @@ main > div,
position: relative;
margin-bottom: 1em;

.expand-button {
inset-inline-end: 5px;
bottom: 5px;
color: #1976d2 !important;
position: fixed;
z-index: 2000;
width: 2.6rem;
height: 2.6rem;
background: $button-bg;
border: $button-border;
box-shadow: $button-shadow;

&:hover {
background: rgb(255 255 255 / 95%);
}

&:active {
background: rgb(255 255 255 / 100%);
}
// The slot MapZoomBar appends into the zoom bar must not generate a box of
// its own, or the button would be nested one level deeper than the zoom
// links and stop lining up with them.
.map-zoom-bar-slot {
display: contents;
}

// Match the expand button's frame so both buttons read with equal weight.
.leaflet-control-zoom.leaflet-bar {
border: $button-border;
box-shadow: $button-shadow;
}

.leaflet-control-zoom a {
background: $button-bg;
border-bottom-color: $bar-divider;

&:hover {
background: rgb(255 255 255 / 95%);
}
}

// The expand button is the bar's bottom segment, so it drops every MUI
// button default (round, padded, its own frame) and takes the zoom links'
// box instead: same size, same background, square but for the bar's bottom
// corners. The separator above it is the zoom-out link's own bottom border,
// which Leaflet only removes while that link is the bar's :last-child.
.leaflet-control-zoom .expand-button {
display: flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: none;
border-radius: 0 0 4px 4px;
background: $button-bg;
color: #1976d2 !important;

&:hover {
background: rgb(255 255 255 / 95%);
}

&:active {
background: rgb(255 255 255 / 100%);
}

svg {
font-size: 16px;
}
}

// Leaflet grows its bar buttons on touch-capable displays; track it so the
// expand button never ends up a different size from the zoom links.
.leaflet-touch .leaflet-control-zoom .expand-button {
width: 30px;
height: 30px;
}

.leaflet-control-attribution {
background: $control-bg;
}
Expand Down Expand Up @@ -185,7 +214,7 @@ main > div,
.leaflet-control-zoom a {
background: $button-bg-dark;
color: #fff;
border-color: #444;
border-bottom-color: $bar-divider-dark;

&:hover {
background: rgb(40 40 40 / 100%);
Expand All @@ -201,9 +230,10 @@ main > div,
}
}

.expand-button {
// Background only — the accent color, frame and divider are all inherited
// from the light rules, which already hold in both themes.
.leaflet-control-zoom .expand-button {
background: $button-bg-dark;
border: $button-border-dark;

&:hover {
background: rgb(40 40 40 / 95%);
Expand Down
Loading