Skip to content
Open
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
114 changes: 60 additions & 54 deletions src/components/src/map/split-map-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@

import React, {ComponentType, useCallback, useMemo, useState, useRef, useEffect} from 'react';
import classnames from 'classnames';
import styled from 'styled-components';
import {MapControlButton} from '../common/styled-components';
import {Delete, Split} from '../common/icons';
import ToolbarItem from '../common/toolbar-item';
import MapControlTooltipFactory from './map-control-tooltip';
import MapControlToolbarFactory from './map-control-toolbar';
import {MapControlItem, MapControls, MapState} from '@kepler.gl/types';
import {MapSplitMode} from '@kepler.gl/constants';

SplitMapButtonFactory.deps = [MapControlTooltipFactory];
SplitMapButtonFactory.deps = [MapControlTooltipFactory, MapControlToolbarFactory];

interface SplitMapButtonIcons {
delete: ComponentType<any>;
Expand All @@ -22,39 +23,13 @@ export type SplitMapButtonProps = {
mapIndex: number;
onToggleSplitMap: (index?: number) => void;
onSetMapSplitMode?: (payload: {mapSplitMode: MapSplitMode}) => void;
onToggleMapControl?: (control: string) => void;
Comment thread
igorDykhta marked this conversation as resolved.
actionIcons: SplitMapButtonIcons;
readOnly: boolean;
mapControls: MapControls;
mapState?: MapState;
};

const StyledSplitModeMenu = styled.div`
position: absolute;
top: 0;
left: -160px;
background: ${props => props.theme.dropdownListBgd || '#3A414C'};
border-radius: 4px;
box-shadow: 0 6px 12px 0 rgba(0, 0, 0, 0.16);
padding: 4px 0;
z-index: 1000;
min-width: 140px;
`;

const StyledMenuItem = styled.div<{$active?: boolean}>`
padding: 8px 16px;
color: ${props =>
props.$active
? props.theme.activeColor || '#1FBAD6'
: props.theme.textColor || '#A0A7B4'};
cursor: pointer;
font-size: 12px;
font-weight: ${props => (props.$active ? 500 : 400)};
&:hover {
background: ${props => props.theme.dropdownListHighlightBg || '#4B5464'};
color: ${props => props.theme.textColorHl || '#FFFFFF'};
}
`;

const SwipeCompareIcon: React.FC<{height?: string}> = ({height = '18px'}) => (
<svg height={height} viewBox="0 0 16 16" fill="currentColor">
<path
Expand All @@ -69,12 +44,15 @@ const SwipeCompareIcon: React.FC<{height?: string}> = ({height = '18px'}) => (
);

const SPLIT_MODE_OPTIONS = [
{id: MapSplitMode.SINGLE_MAP, label: 'Single'},
{id: MapSplitMode.DUAL_MAP, label: 'Dual'},
{id: MapSplitMode.SWIPE_COMPARE, label: 'Swipe'}
{id: MapSplitMode.SINGLE_MAP, label: 'tooltip.singleView'},
{id: MapSplitMode.DUAL_MAP, label: 'tooltip.dualView'},
{id: MapSplitMode.SWIPE_COMPARE, label: 'tooltip.swipeView'}
];

function SplitMapButtonFactory(MapControlTooltip) {
function SplitMapButtonFactory(
MapControlTooltip: ReturnType<typeof MapControlTooltipFactory>,
MapControlToolbar: ReturnType<typeof MapControlToolbarFactory>
) {
const defaultActionIcons = {
delete: Delete,
split: Split
Expand All @@ -86,43 +64,72 @@ function SplitMapButtonFactory(MapControlTooltip) {
mapIndex,
onToggleSplitMap,
onSetMapSplitMode,
onToggleMapControl,
actionIcons = defaultActionIcons,
mapControls,
readOnly,
mapState
}) => {
const splitMap = mapControls?.splitMap || ({} as MapControlItem);
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);

// The split-mode menu open state is normally kept in redux
// (mapControls.splitMap.active) so it participates in the "only one
// map-control menu open at a time" logic in toggleMapControlUpdater.
// If a consumer supplies onSetMapSplitMode without onToggleMapControl, we
// fall back to local component state so the menu still opens/closes.
const useReduxMenuState = Boolean(onToggleMapControl);
const [localMenuOpen, setLocalMenuOpen] = useState(false);

const toggleMenu = useCallback(() => {
if (useReduxMenuState) {
onToggleMapControl?.('splitMap');
} else {
setLocalMenuOpen(prev => !prev);
}
}, [useReduxMenuState, onToggleMapControl]);

const closeMenu = useCallback(() => {
if (useReduxMenuState) {
if (splitMap.active) {
onToggleMapControl?.('splitMap');
}
} else {
setLocalMenuOpen(false);
}
}, [useReduxMenuState, onToggleMapControl, splitMap.active]);

const menuOpen =
Boolean(onSetMapSplitMode) && (useReduxMenuState ? Boolean(splitMap.active) : localMenuOpen);

const currentMode = mapState?.mapSplitMode || MapSplitMode.SINGLE_MAP;

const onClick = useCallback(
event => {
event.preventDefault();
if (onSetMapSplitMode) {
setMenuOpen(prev => !prev);
toggleMenu();
} else {
onToggleSplitMap(isSplit ? mapIndex : undefined);
}
},
[isSplit, mapIndex, onToggleSplitMap, onSetMapSplitMode]
[isSplit, mapIndex, onToggleSplitMap, onSetMapSplitMode, toggleMenu]
);

const handleModeSelect = useCallback(
(mode: string) => {
if (onSetMapSplitMode) {
onSetMapSplitMode({mapSplitMode: mode as MapSplitMode});
}
setMenuOpen(false);
closeMenu();
},
[onSetMapSplitMode]
[onSetMapSplitMode, closeMenu]
);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setMenuOpen(false);
closeMenu();
}
};
if (menuOpen) {
Expand All @@ -131,15 +138,27 @@ function SplitMapButtonFactory(MapControlTooltip) {
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [menuOpen]);
}, [menuOpen, closeMenu]);

const isVisible = useMemo(() => splitMap.show && readOnly !== true, [splitMap.show, readOnly]);

if (!splitMap.show) {
return null;
}
return isVisible ? (
<div style={{position: 'relative'}} ref={menuRef}>
<div className="split-map-controls" style={{position: 'relative'}} ref={menuRef}>
{menuOpen && onSetMapSplitMode ? (
<MapControlToolbar $show={menuOpen}>
{SPLIT_MODE_OPTIONS.map(option => (
<ToolbarItem
key={option.id}
onClick={() => handleModeSelect(option.id)}
label={option.label}
active={currentMode === option.id}
/>
))}
</MapControlToolbar>
) : null}
<MapControlTooltip
id="action-toggle"
message={
Expand All @@ -164,19 +183,6 @@ function SplitMapButtonFactory(MapControlTooltip) {
)}
</MapControlButton>
</MapControlTooltip>
{menuOpen && onSetMapSplitMode && (
<StyledSplitModeMenu>
{SPLIT_MODE_OPTIONS.map(option => (
<StyledMenuItem
key={option.id}
$active={currentMode === option.id}
onClick={() => handleModeSelect(option.id)}
>
{option.label}
</StyledMenuItem>
))}
</StyledSplitModeMenu>
)}
</div>
) : null;
};
Expand Down
3 changes: 3 additions & 0 deletions src/localization/src/translations/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@ export default {
closePanel: 'Tanca panel actual',
switchToDualView: 'Canvia a la vista de mapa dual',
selectSplitMode: "Selecciona el mode de vista del mapa",
singleView: "Únic",
dualView: "Doble",
swipeView: "Comparar",
showLegend: 'mostra llegenda',
disable3DMap: 'Desactiva mapa 3D',
DrawOnMap: 'Dibuixa al mapa',
Expand Down
3 changes: 3 additions & 0 deletions src/localization/src/translations/cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ export default {
closePanel: '关闭当前面板',
switchToDualView: '切换到双地图视图',
selectSplitMode: '选择地图视图模式',
singleView: '单个',
dualView: '双屏',
swipeView: '对比',
showLegend: '显示图例',
disable3DMap: '禁用 3D 地图',
DrawOnMap: '在地图上绘制',
Expand Down
3 changes: 3 additions & 0 deletions src/localization/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ export default {
closePanel: 'Close current panel',
switchToDualView: 'Switch to dual map view',
selectSplitMode: 'Select map view mode',
singleView: 'Single',
dualView: 'Dual',
swipeView: 'Swipe',
showLegend: 'Show legend',
disable3DMap: 'Disable 3D Map',
globeMap: 'Globe Map',
Expand Down
3 changes: 3 additions & 0 deletions src/localization/src/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ export default {
closePanel: 'Cerrar el panel actual',
switchToDualView: 'Cambiar a la vista de mapa dual',
selectSplitMode: 'Seleccionar modo de vista del mapa',
singleView: 'Único',
dualView: 'Doble',
swipeView: 'Comparar',
showLegend: 'Mostrar leyenda',
disable3DMap: 'Desactivar mapa 3D',
DrawOnMap: 'Dibujar en el mapa',
Expand Down
3 changes: 3 additions & 0 deletions src/localization/src/translations/fi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ export default {
closePanel: 'Sulje paneeli',
switchToDualView: 'Vaihda kaksoiskarrtanäkymään',
selectSplitMode: 'Valitse karttanäkymätila',
singleView: 'Yksi',
dualView: 'Kaksois',
swipeView: 'Vertaa',
showLegend: 'Näytä selite',
disable3DMap: 'Poistu 3D-näkymästä',
DrawOnMap: 'Piirrä kartalle',
Expand Down
3 changes: 3 additions & 0 deletions src/localization/src/translations/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ export default {
closePanel: 'このパネルを閉じる',
switchToDualView: 'デュアルビューに切り替え',
selectSplitMode: 'マップ表示モードを選択',
singleView: 'シングル',
dualView: 'デュアル',
swipeView: 'スワイプ',
showLegend: '凡例を表示',
disable3DMap: '3D地図を無効化',
DrawOnMap: '地図上に図形を描画',
Expand Down
3 changes: 3 additions & 0 deletions src/localization/src/translations/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ export default {
closePanel: 'Fechar painel atual',
switchToDualView: 'Trocar para visualização dupla de mapa',
selectSplitMode: 'Selecionar modo de visualização do mapa',
singleView: 'Único',
dualView: 'Duplo',
swipeView: 'Comparar',
showLegend: 'mostrar legenda',
disable3DMap: 'Desabilitar Mapa 3D',
DrawOnMap: 'Desenhar no mapa',
Expand Down
3 changes: 3 additions & 0 deletions src/localization/src/translations/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ export default {
closePanel: 'Закрыть текущую панель',
switchToDualView: 'Перейти в режим двойной карты',
selectSplitMode: 'Выбрать режим отображения карты',
singleView: 'Одна',
dualView: 'Двойная',
swipeView: 'Сравнение',
showLegend: 'Показать легенду',
disable3DMap: 'Отключить 3D Карту',
DrawOnMap: 'Рисовать на карте',
Expand Down
45 changes: 25 additions & 20 deletions src/reducers/src/ui-state-updaters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,16 @@ export const toggleSidePanelCloseButtonUpdater = (
isSidePanelCloseButtonVisible: show
});

// Map control dropdowns/menus that overlap each other visually and therefore
// should be mutually exclusive: opening one closes all the others.
// (split/view-mode menu, top/3d/globe menu, polygon draw tool, language menu)
export const MUTUALLY_EXCLUSIVE_MAP_CONTROLS: string[] = [
MAP_CONTROLS.splitMap,
MAP_CONTROLS.toggle3d,
MAP_CONTROLS.mapDraw,
MAP_CONTROLS.mapLocale
];

/**
* Toggle active map control panel
* @memberof uiStateUpdaters
Expand All @@ -410,15 +420,6 @@ export const toggleMapControlUpdater = (
? MAP_CONTROLS.effect
: null;

// To to toggle the mapDraw and mapLocal dropdowns
// We have to deactivate the other active dropdown
const dropdownToDeactivate =
panelId === MAP_CONTROLS.mapDraw
? MAP_CONTROLS.mapLocale
: panelId === MAP_CONTROLS.mapLocale
? MAP_CONTROLS.mapDraw
: null;

// If we need to deactivate a competing panel and it's currently active
if (panelToDeactivate && state.mapControls[panelToDeactivate]?.active) {
updatedState = {
Expand All @@ -433,18 +434,22 @@ export const toggleMapControlUpdater = (
};
}

// If we need to deactivate a competing dropdown and it's currently active
if (dropdownToDeactivate && state.mapControls[dropdownToDeactivate]?.active) {
updatedState = {
...state,
mapControls: {
...updatedState.mapControls,
[dropdownToDeactivate]: {
...updatedState.mapControls[dropdownToDeactivate],
active: false
}
// The overlapping map control menus should be mutually exclusive: when one of
// them is being opened, deactivate every other one that is currently active
// so their dropdowns never overlap on screen.
const isOpening = !updatedState.mapControls[panelId]?.active;
if (isOpening && MUTUALLY_EXCLUSIVE_MAP_CONTROLS.includes(panelId)) {
const nextMapControls = {...updatedState.mapControls};
let didDeactivate = false;
MUTUALLY_EXCLUSIVE_MAP_CONTROLS.forEach(controlId => {
if (controlId !== panelId && nextMapControls[controlId]?.active) {
nextMapControls[controlId] = {...nextMapControls[controlId], active: false};
didDeactivate = true;
}
};
});
if (didDeactivate) {
updatedState = {...updatedState, mapControls: nextMapControls};
}
}

return {
Expand Down
Loading