-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: SRS-527 Map Search – Find Me #186
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { CSSProperties, ReactNode, useEffect, useMemo, useState } from 'react'; | ||
import { createPortal } from 'react-dom'; | ||
import { DomUtil } from 'leaflet'; | ||
import clsx from 'clsx'; | ||
|
||
// Allow custom corners like "bottomcenter", "leftmiddle" etc | ||
const POSITION_CLASSES: Record<string, string> = { | ||
bottomleft: 'leaflet-bottom leaflet-left', | ||
bottomright: 'leaflet-bottom leaflet-right', | ||
topleft: 'leaflet-top leaflet-left', | ||
topright: 'leaflet-top leaflet-right', | ||
bottomcenter: 'leaflet-bottom leaflet-center', | ||
topcenter: 'leaflet-top leaflet-center', | ||
leftmiddle: 'leaflet-left leaflet-middle', | ||
rightmiddle: 'leaflet-right leaflet-middle', | ||
}; | ||
|
||
interface Props { | ||
position: string; | ||
children: ReactNode; | ||
className?: string; | ||
style?: CSSProperties; | ||
} | ||
|
||
export function Control({ position, children, className, style }: Props) { | ||
const [container, setContainer] = useState<HTMLElement>( | ||
document.createElement('div'), | ||
); | ||
|
||
useEffect(() => { | ||
// @ts-ignore | ||
let positionClass = POSITION_CLASSES.topright; | ||
if (position && position in POSITION_CLASSES) { | ||
positionClass = POSITION_CLASSES[position]; | ||
} | ||
let targetDiv = document.getElementsByClassName( | ||
positionClass, | ||
)[0] as HTMLElement; | ||
if (!targetDiv) { | ||
// Create custom position | ||
const topRight = document.getElementsByClassName( | ||
POSITION_CLASSES.topright, | ||
)[0]; | ||
targetDiv = DomUtil.create( | ||
'div', | ||
positionClass, | ||
topRight.parentNode as HTMLElement, | ||
); | ||
} | ||
setContainer(targetDiv); | ||
}, [position]); | ||
|
||
// Make the attribution the last child (after map controls) | ||
useEffect(() => { | ||
const len = container.children.length; | ||
if (len > 1) { | ||
const attribution = container.querySelector( | ||
'.leaflet-control-attribution', | ||
); | ||
if (attribution && attribution !== container.children[len - 1]) { | ||
container.removeChild(attribution); | ||
container.appendChild(attribution); | ||
} | ||
} | ||
}, [container, position]); | ||
|
||
const controlContainer = useMemo( | ||
() => ( | ||
<div | ||
className={clsx('leaflet-control leaflet-bar', className)} | ||
style={style} | ||
> | ||
{children} | ||
</div> | ||
), | ||
[children, style], | ||
); | ||
|
||
return createPortal(controlContainer, container); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { useDispatch } from 'react-redux'; | ||
import { IconButton } from '@mui/material'; | ||
import clsx from 'clsx'; | ||
|
||
import { setMyLocationVisible, useMyLocationVisible } from './map-slice'; | ||
import { useGeolocationPermission } from '../../../hooks/useMyLocation'; | ||
|
||
import { FindMe } from '../../components/common/icon'; | ||
|
||
export function FindMeControl() { | ||
const dispatch = useDispatch(); | ||
const isMarkerVisible = useMyLocationVisible(); | ||
const state = useGeolocationPermission(); | ||
// No point in showing the button if the permission has been denied | ||
if (state === 'denied') { | ||
return null; | ||
} | ||
|
||
const onClick = () => { | ||
dispatch(setMyLocationVisible(!isMarkerVisible)); | ||
}; | ||
|
||
return ( | ||
<IconButton | ||
className={clsx( | ||
'map-control-button', | ||
isMarkerVisible && 'map-control-button--active', | ||
)} | ||
onClick={onClick} | ||
title="Show my location on the map" | ||
> | ||
<FindMe /> | ||
</IconButton> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
.map-controls { | ||
display: flex; | ||
flex-direction: column; | ||
gap: 8px; | ||
border: 0 !important; | ||
border-radius: 0; | ||
max-height: calc(100vh - 80px); | ||
transition: margin ease-in-out 0.5s; | ||
position: relative; | ||
} | ||
|
||
.map-controls button.map-control-button { | ||
width: 42px; | ||
height: 42px; | ||
padding: 0; | ||
margin: 0; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
border-radius: 6px; | ||
background: var(--surface-color-background-white); | ||
color: var(--icons-color-secondary); | ||
box-shadow: var(--box-shadow-small); | ||
transition: all ease-in-out 0.3s; | ||
} | ||
|
||
.map-controls button.map-control-button:hover { | ||
color: var(--icons-color-info); | ||
} | ||
|
||
.map-controls button.map-control-button:focus { | ||
outline: 2px solid rgba(46, 93, 215, 0.5); | ||
} | ||
|
||
.map-controls button.map-control-button--active { | ||
background-color: var(--surface-color-primary-active-border); | ||
color: var(--surface-color-background-white); | ||
} | ||
|
||
.map-controls button.map-control-button--active:hover { | ||
color: var(--surface-color-secondary-hover); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { useMemo } from 'react'; | ||
import { useTheme } from '@mui/material/styles'; | ||
import useMediaQuery from '@mui/material/useMediaQuery'; | ||
|
||
import { | ||
MAP_CONTROLS_BOTTOM_LG, | ||
MAP_CONTROLS_BOTTOM_SM, | ||
MAP_CONTROLS_RIGHT_LG, | ||
MAP_CONTROLS_RIGHT_SM, | ||
MAP_CONTROLS_RIGHT_XL, | ||
} from '../../constants/Constant'; | ||
|
||
import { Control } from './Control'; | ||
|
||
import './MapControl.css'; | ||
import { FindMeControl } from './FindMeControl'; | ||
|
||
export function MapControls() { | ||
const theme = useTheme(); | ||
const isSmall = useMediaQuery(theme.breakpoints.down('md')); | ||
const isMedium = useMediaQuery(theme.breakpoints.down('lg')); | ||
const isLarge = useMediaQuery(theme.breakpoints.down('xl')); | ||
|
||
// Shift the controls based on screen size | ||
let right = MAP_CONTROLS_RIGHT_XL; | ||
if (isSmall) { | ||
right = MAP_CONTROLS_RIGHT_SM; | ||
} else if (isLarge) { | ||
right = MAP_CONTROLS_RIGHT_LG; | ||
} | ||
let bottom = MAP_CONTROLS_BOTTOM_LG; | ||
if (isSmall) { | ||
bottom = MAP_CONTROLS_BOTTOM_SM; | ||
} | ||
|
||
const style = useMemo( | ||
() => ({ | ||
marginRight: `${right}px`, | ||
marginBottom: `${bottom}px`, | ||
}), | ||
[right, bottom], | ||
); | ||
|
||
return ( | ||
<Control position="bottomright" className="map-controls" style={style}> | ||
{isMedium && <FindMeControl />} | ||
</Control> | ||
); | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that hooking into redux for a single boolean field is a massive overkill and a regular state field on the level of You mentioned that you were getting map context errors before the redux implementation, but looking at the code just now, I'm 99% certain those were unrelated. Happy to hop on a call to debug this together |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { useSelector } from 'react-redux'; | ||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; | ||
|
||
export interface MapSliceState { | ||
isMyLocationVisible: boolean; | ||
} | ||
|
||
export const initialState: MapSliceState = { | ||
isMyLocationVisible: false, | ||
}; | ||
|
||
export const mapSlice = createSlice({ | ||
name: 'map', | ||
initialState, | ||
reducers: { | ||
setMyLocationVisible: (state, action: PayloadAction<boolean>) => { | ||
state.isMyLocationVisible = action.payload; | ||
}, | ||
}, | ||
}); | ||
|
||
export const { setMyLocationVisible } = mapSlice.actions; | ||
|
||
// Selectors | ||
const selectMyLocationVisible = (state: any) => state.map.isMyLocationVisible; | ||
export const useMyLocationVisible = () => useSelector(selectMyLocationVisible); | ||
|
||
export default mapSlice.reducer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FindMeControl
andFindMeButton
are almost identical. To avoid code duplication, they should be combined into a single component that accepts settings for its appearance/position as props.