@trackunit/react-map 0.2.224 → 0.2.225
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +276 -84
- package/index.esm.js +277 -85
- package/package.json +11 -11
- package/src/appearance/AppearancePreview.d.ts +10 -1
- package/src/appearance/AppearancePreviewGrid.d.ts +8 -1
- package/src/controls/renderers/menu/utils/menuTooltipLabel.d.ts +3 -0
- package/src/controls/renderers/shared/StandaloneControlTooltip.d.ts +12 -3
- package/src/controls/renderers/shared/useDisabledReason.d.ts +28 -0
- package/src/controls/renderers/shared/utils/isStandaloneTooltipWrapped.d.ts +8 -0
- package/src/controls/types.d.ts +9 -0
- package/src/draw/useDraw.d.ts +33 -1
- package/src/index.d.ts +1 -1
- package/src/layers/utils/useGatedLayerInteraction.d.ts +25 -0
package/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import { useNamespaceTranslation, registerTranslations } from '@trackunit/i18n-library-translation';
|
|
3
|
-
import { INITIAL_CAMERA_STATE, INITIAL_MAP_STATUS, KEYBOARD_PAN_AMOUNT, KEYBOARD_ZOOM_AMOUNT, mapAppearanceSchema, DEFAULT_MAP_APPEARANCE, EMPTY_DRAW_VALUE, WORLD_BBOX, INITIAL_INTERACTION_STATE, mapThemeSchema, mergeAntimeridianFeatures, extractSourceData, computeMarkerDomPortalZIndex, densifyGeodesicFeatures, angularDistance, intermediatePoint, GEODESIC_MAX_SEGMENT_KM, geometryTypeToShapeType, resolveStrokeColors } from '@trackunit/react-map-adapter-shared';
|
|
3
|
+
import { INITIAL_CAMERA_STATE, INITIAL_MAP_STATUS, KEYBOARD_PAN_AMOUNT, KEYBOARD_ZOOM_AMOUNT, mapAppearanceSchema, DEFAULT_MAP_APPEARANCE, EMPTY_DRAW_VALUE, NO_DRAW_ERRORS, WORLD_BBOX, INITIAL_INTERACTION_STATE, mapThemeSchema, mergeAntimeridianFeatures, extractSourceData, computeMarkerDomPortalZIndex, densifyGeodesicFeatures, angularDistance, intermediatePoint, GEODESIC_MAX_SEGMENT_KM, geometryTypeToShapeType, resolveStrokeColors } from '@trackunit/react-map-adapter-shared';
|
|
4
4
|
export { ANCHOR_SELECTOR, DEFAULT_MAP_APPEARANCE, EMPTY_DRAW_VALUE, HIT_SURFACE_SELECTOR, INITIAL_CAMERA_STATE, INITIAL_INTERACTION_STATE, INITIAL_MAP_STATE, INITIAL_MAP_STATUS, computeMarkerDomPortalZIndex, defineAdapter, geometryTypeToShapeType, mapAppearanceSchema, mapThemeSchema, mapTypeSchema } from '@trackunit/react-map-adapter-shared';
|
|
5
5
|
import { useSyncExternalStore, useMemo, useState, useRef, useCallback, useLayoutEffect, useEffect, useReducer, createContext, useContext, memo, Suspense, useId, Fragment as Fragment$1, forwardRef } from 'react';
|
|
6
6
|
import { cva } from '@trackunit/css-class-variance-utilities';
|
|
@@ -32,6 +32,7 @@ var defaultTranslations = {
|
|
|
32
32
|
"controls.stepper.increase": "Increase",
|
|
33
33
|
"controls.zoom": "Zoom",
|
|
34
34
|
"draw.clear": "Clear drawing",
|
|
35
|
+
"draw.limitReached": "Polygon limit reached. Clear the drawing to draw again",
|
|
35
36
|
"draw.polygon": "Draw polygon",
|
|
36
37
|
"draw.select": "Edit polygon",
|
|
37
38
|
"draw.selectDisabled": "Draw a polygon first",
|
|
@@ -40,6 +41,7 @@ var defaultTranslations = {
|
|
|
40
41
|
"map.loadingErrorMessage": "Unable to load the map. Please try again later.",
|
|
41
42
|
"multiGeometry.badge.tooltip": "Part of a multi-part shape",
|
|
42
43
|
"settings.mapStyle.dark": "Dark",
|
|
44
|
+
"settings.mapStyle.disabledWhileDrawing": "Finish drawing to change the map style",
|
|
43
45
|
"settings.mapStyle.light": "Light",
|
|
44
46
|
"settings.mapStyle.roadmap": "Road map",
|
|
45
47
|
"settings.mapStyle.roads": "Roads",
|
|
@@ -1831,14 +1833,44 @@ const RIGHT_CONTROL_POPOVER_PLACEMENT = "right-start";
|
|
|
1831
1833
|
const RIGHT_CONTROL_TOOLTIP_PLACEMENT = "right";
|
|
1832
1834
|
|
|
1833
1835
|
/**
|
|
1834
|
-
*
|
|
1835
|
-
*
|
|
1836
|
+
* Whether `StandaloneControlTooltip` wraps its child: always on the right overlay
|
|
1837
|
+
* side, and on any side when the caller requires it (a disabled `IconButton` drops
|
|
1838
|
+
* its own tooltip, so a disabled reason needs the wrapper). Callers omit `title`
|
|
1839
|
+
* on the `IconButton` whenever this is true so tooltips do not stack.
|
|
1836
1840
|
*/
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1841
|
+
const isStandaloneTooltipWrapped = (overlaySide, required) => overlaySide === "right" || required;
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* Wraps standalone map controls with a right-aligned tooltip when `overlaySide` is `"right"`,
|
|
1845
|
+
* or on any side when `required`. Callers should omit `title` on {@link IconButton} whenever
|
|
1846
|
+
* this wraps so tooltips do not stack; `isStandaloneTooltipWrapped` tells them.
|
|
1847
|
+
*/
|
|
1848
|
+
const StandaloneControlTooltip = ({ children, disabled = false, label, overlaySide = undefined, required = false, }) => {
|
|
1849
|
+
if (!isStandaloneTooltipWrapped(overlaySide, required)) {
|
|
1839
1850
|
return children;
|
|
1840
1851
|
}
|
|
1841
|
-
return (jsx(Tooltip, { delayed: true, disabled: disabled, interactable: false, label: label, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: children }));
|
|
1852
|
+
return (jsx(Tooltip, { delayed: true, disabled: disabled, interactable: false, label: label, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : "auto", children: children }));
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* A disabled control is not focusable and its tooltip is hover-only, so the reason it is
|
|
1857
|
+
* disabled is also announced to assistive technology: a visually hidden copy of the reason,
|
|
1858
|
+
* linked from the control through `aria-describedby`. One hook for every control that shows a
|
|
1859
|
+
* `disabledReason`, so the pattern has one home (BooleanButton, Menu, AppearancePreview).
|
|
1860
|
+
*/
|
|
1861
|
+
const useDisabledReason = ({ disabled = false, disabledReason, }) => {
|
|
1862
|
+
const id = useId();
|
|
1863
|
+
return useMemo(() => {
|
|
1864
|
+
if (!disabled || disabledReason === undefined) {
|
|
1865
|
+
return { showsReason: false, reason: undefined, describedBy: undefined, description: null };
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
showsReason: true,
|
|
1869
|
+
reason: disabledReason,
|
|
1870
|
+
describedBy: id,
|
|
1871
|
+
description: (jsx("span", { className: "sr-only", id: id, children: disabledReason })),
|
|
1872
|
+
};
|
|
1873
|
+
}, [disabled, disabledReason, id]);
|
|
1842
1874
|
};
|
|
1843
1875
|
|
|
1844
1876
|
/**
|
|
@@ -1853,15 +1885,15 @@ const BooleanButton = ({ config, overlaySide = undefined, resolved }) => {
|
|
|
1853
1885
|
const { className, "data-testid": dataTestId, label, onChange: onClickChange, value } = config;
|
|
1854
1886
|
const { icon, size } = resolved;
|
|
1855
1887
|
const [t] = useTranslation();
|
|
1888
|
+
const { showsReason, reason, describedBy, description } = useDisabledReason(config);
|
|
1856
1889
|
if (icon === undefined) {
|
|
1857
1890
|
return null;
|
|
1858
1891
|
}
|
|
1859
1892
|
// A disabled control's tooltip explains what unblocks it rather than what clicking would do
|
|
1860
|
-
const tooltipLabel =
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
return (jsx(StandaloneControlTooltip, { label: tooltipLabel, overlaySide: overlaySide, children: jsx(IconButton, { ...(useRightOverlay ? { ariaLabel: label } : { title: tooltipLabel }), "aria-pressed": value, className: className, "data-testid": dataTestId, disabled: config.disabled ?? false, icon: jsx(Icon, { name: icon, size: "small" }), onClick: () => onClickChange(!value), size: size, variant: value ? "primary" : "secondary" }) }));
|
|
1893
|
+
const tooltipLabel = showsReason ? reason : t("controls.boolean.tooltip", { label });
|
|
1894
|
+
// IconButton drops its own tooltip while disabled, so the reason needs the wrapper on every side.
|
|
1895
|
+
const wrapped = isStandaloneTooltipWrapped(overlaySide, showsReason);
|
|
1896
|
+
return (jsxs(Fragment, { children: [jsx(StandaloneControlTooltip, { label: tooltipLabel, overlaySide: overlaySide, required: showsReason, children: jsx(IconButton, { ...(wrapped ? { ariaLabel: label } : { title: tooltipLabel }), "aria-describedby": describedBy, "aria-pressed": value, className: className, "data-testid": dataTestId, disabled: config.disabled ?? false, icon: jsx(Icon, { name: icon, size: "small" }), onClick: () => onClickChange(!value), size: size, variant: value ? "primary" : "secondary" }) }), description] }));
|
|
1865
1897
|
};
|
|
1866
1898
|
|
|
1867
1899
|
/**
|
|
@@ -2376,6 +2408,9 @@ const SectionHeader = ({ title, className }) => (jsx("div", { className: twMerge
|
|
|
2376
2408
|
*/
|
|
2377
2409
|
const Separator = ({ className }) => (jsx("div", { className: twMerge("my-1 h-px bg-neutral-200", className), role: "separator" }));
|
|
2378
2410
|
|
|
2411
|
+
/** A disabled menu's tooltip explains what unblocks it rather than what it opens. */
|
|
2412
|
+
const menuTooltipLabel = (config) => config.disabled === true && config.disabledReason !== undefined ? config.disabledReason : config.label;
|
|
2413
|
+
|
|
2379
2414
|
/**
|
|
2380
2415
|
* Renders a menu item based on its type.
|
|
2381
2416
|
* Calls resolveControl with placement: "menu" for each item.
|
|
@@ -2450,8 +2485,11 @@ const MenuContent = ({ availableSpace, customPortalId, isFullScreen, items }) =>
|
|
|
2450
2485
|
/** Popover opens right; tooltip is suppressed while the menu is open. */
|
|
2451
2486
|
const MenuRightPopover = ({ availableSpace, config, customPortalId, resolved, }) => {
|
|
2452
2487
|
const { className, "data-testid": dataTestId, items, label } = config;
|
|
2488
|
+
const disabled = config.disabled ?? false;
|
|
2489
|
+
const tooltipLabel = menuTooltipLabel(config);
|
|
2490
|
+
const { describedBy, description } = useDisabledReason(config);
|
|
2453
2491
|
const isFullScreen = useIsFullscreen();
|
|
2454
|
-
return (
|
|
2492
|
+
return (jsxs("div", { className: className ?? "w-fit", children: [jsx(Popover, { placement: RIGHT_CONTROL_POPOVER_PLACEMENT, children: ({ isOpen }) => (jsxs(Fragment$1, { children: [jsx(PopoverTrigger, { children: jsx(Tooltip, { delayed: true, disabled: isOpen, interactable: false, label: tooltipLabel, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: jsx(IconButton, { "aria-describedby": describedBy, ariaLabel: label, "data-testid": dataTestId, disabled: disabled, icon: jsx(Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, variant: "secondary" }) }) }), jsx(PopoverContent, { className: "overflow-clip", portalId: isFullScreen ? customPortalId : undefined, children: jsx(MenuContent, { availableSpace: availableSpace, customPortalId: customPortalId, isFullScreen: isFullScreen, items: items }) })] })) }), description] }));
|
|
2455
2493
|
};
|
|
2456
2494
|
/** Popover presentation -- renders a position-aware popover anchored to the trigger button. */
|
|
2457
2495
|
const MenuAutoPopover = ({ availableSpace, config, containerRef, customPortalId, resolved, }) => {
|
|
@@ -2459,7 +2497,13 @@ const MenuAutoPopover = ({ availableSpace, config, containerRef, customPortalId,
|
|
|
2459
2497
|
const isFullScreen = useIsFullscreen();
|
|
2460
2498
|
const triggerRef = useRef(null);
|
|
2461
2499
|
const popoverPlacement = usePopoverPlacement(triggerRef, containerRef);
|
|
2462
|
-
|
|
2500
|
+
const disabled = config.disabled ?? false;
|
|
2501
|
+
const tooltipLabel = menuTooltipLabel(config);
|
|
2502
|
+
const { showsReason, describedBy, description } = useDisabledReason(config);
|
|
2503
|
+
// IconButton drops its own tooltip while disabled, so a disabled reason needs the wrapper;
|
|
2504
|
+
// the accessible name then moves from `title` to `ariaLabel` so the tooltips do not stack.
|
|
2505
|
+
const trigger = (jsx(IconButton, { ...(showsReason ? { ariaLabel: label, "aria-describedby": describedBy } : { title: tooltipLabel }), "data-testid": dataTestId, disabled: disabled, icon: jsx(Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, variant: "secondary" }));
|
|
2506
|
+
return (jsxs("div", { className: className ?? "w-fit", ref: triggerRef, children: [jsxs(Popover, { placement: popoverPlacement, children: [jsx(PopoverTrigger, { children: showsReason ? (jsx(StandaloneControlTooltip, { label: tooltipLabel, required: true, children: trigger })) : (trigger) }), jsx(PopoverContent, { className: "overflow-clip", portalId: isFullScreen ? customPortalId : undefined, children: jsx(MenuContent, { availableSpace: availableSpace, customPortalId: customPortalId, isFullScreen: isFullScreen, items: items }) })] }), description] }));
|
|
2463
2507
|
};
|
|
2464
2508
|
/**
|
|
2465
2509
|
* Renders a menu control as a popover -- either anchored to the trigger (auto-placed) or
|
|
@@ -2778,11 +2822,17 @@ const cvaMapPreview = cva({
|
|
|
2778
2822
|
*
|
|
2779
2823
|
* @internal
|
|
2780
2824
|
*/
|
|
2781
|
-
const AppearancePreview = ({ adapterConfig, mapType, label, selected, onSelect, "data-testid": dataTestId, }) => {
|
|
2825
|
+
const AppearancePreview = ({ adapterConfig, mapType, label, selected, onSelect, disabled = false, disabledReason, "data-testid": dataTestId, }) => {
|
|
2782
2826
|
const { Map } = usePreviewMap(adapterConfig, { mapType });
|
|
2783
|
-
|
|
2827
|
+
const { showsReason, describedBy, description } = useDisabledReason({ disabled, disabledReason });
|
|
2828
|
+
const cell = (jsxs("label", { className: disabled ? undefined : "cursor-pointer", "data-testid": dataTestId, children: [jsxs("div", { className: cvaInteractableItem({
|
|
2829
|
+
disabled,
|
|
2784
2830
|
className: cvaMapPreview({ selected }),
|
|
2785
|
-
}), children: [jsx("input", { checked: selected, className: "sr-only", name: "map-style-preview", onChange: onSelect, type: "radio" }), jsx("div", { className: "absolute inset-px overflow-clip rounded-[calc(var(--radius)-1px)]", inert: true, children: jsx(Map, { "aria-label": label, style: { width: "72px", height: "96px", pointerEvents: "none" } }) })] }), jsx("span", { className: "line-clamp-2 w-[72px] text-center text-xs leading-tight text-neutral-700", children: label })] }));
|
|
2831
|
+
}), children: [jsx("input", { "aria-describedby": describedBy, "aria-disabled": disabled || undefined, checked: selected, className: "sr-only", name: "map-style-preview", onChange: disabled ? undefined : onSelect, readOnly: disabled, type: "radio" }), jsx("div", { className: "absolute inset-px overflow-clip rounded-[calc(var(--radius)-1px)]", inert: true, children: jsx(Map, { "aria-label": label, style: { width: "72px", height: "96px", pointerEvents: "none" } }) })] }), jsx("span", { className: "line-clamp-2 w-[72px] text-center text-xs leading-tight text-neutral-700", children: label }), description] }));
|
|
2832
|
+
if (showsReason) {
|
|
2833
|
+
return jsx(Tooltip, { label: disabledReason, children: cell });
|
|
2834
|
+
}
|
|
2835
|
+
return cell;
|
|
2786
2836
|
};
|
|
2787
2837
|
|
|
2788
2838
|
/**
|
|
@@ -2797,7 +2847,7 @@ const AppearancePreview = ({ adapterConfig, mapType, label, selected, onSelect,
|
|
|
2797
2847
|
*
|
|
2798
2848
|
* @internal
|
|
2799
2849
|
*/
|
|
2800
|
-
const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme, currentMapType, onChange, }) => {
|
|
2850
|
+
const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme, currentMapType, onChange, disabled = false, disabledReason, }) => {
|
|
2801
2851
|
const derivedOptions = useMemo(() => options.map(option => ({
|
|
2802
2852
|
option,
|
|
2803
2853
|
// Pin the full appearance per option: the main map's config may carry a
|
|
@@ -2811,7 +2861,7 @@ const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme
|
|
|
2811
2861
|
showTraffic: false,
|
|
2812
2862
|
}),
|
|
2813
2863
|
})), [adapterConfig, options]);
|
|
2814
|
-
return (jsx("div", { "aria-label": ariaLabel, className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: derivedOptions.map(({ option, config }) => (jsx(AppearancePreview, { adapterConfig: config, "data-testid": `map-style-preview-${option.key}`, label: option.label, mapType: option.mapType, onSelect: () => onChange(option.theme, option.mapType), selected: option.theme === currentTheme && option.mapType === currentMapType }, option.key))) }));
|
|
2864
|
+
return (jsx("div", { "aria-disabled": disabled || undefined, "aria-label": ariaLabel, className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: derivedOptions.map(({ option, config }) => (jsx(AppearancePreview, { adapterConfig: config, "data-testid": `map-style-preview-${option.key}`, disabled: disabled, disabledReason: disabledReason, label: option.label, mapType: option.mapType, onSelect: () => onChange(option.theme, option.mapType), selected: option.theme === currentTheme && option.mapType === currentMapType }, option.key))) }));
|
|
2815
2865
|
};
|
|
2816
2866
|
|
|
2817
2867
|
// ============================================================================
|
|
@@ -3077,7 +3127,12 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
3077
3127
|
}, [dispatch]);
|
|
3078
3128
|
const previewOptions = useMemo(() => buildAppearanceOptions(themes, mapTypes, t), [themes, mapTypes, t]);
|
|
3079
3129
|
const adapterConfig = api.adapterConfig;
|
|
3130
|
+
// While a polygon is being drawn, a style change would recreate the map (Google) or
|
|
3131
|
+
// reload the style (Mapbox) and drop the in-progress polygon, so the whole control is
|
|
3132
|
+
// disabled with a reason. The persisted preferences keep displaying (ADR 0030).
|
|
3133
|
+
const drawing = api.state.drawing === true;
|
|
3080
3134
|
return useMemo(() => {
|
|
3135
|
+
const drawingReason = drawing ? t("settings.mapStyle.disabledWhileDrawing") : undefined;
|
|
3081
3136
|
const menuItems = [
|
|
3082
3137
|
{
|
|
3083
3138
|
type: "custom",
|
|
@@ -3089,7 +3144,9 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
3089
3144
|
section: t("settings.mapStyle.title"),
|
|
3090
3145
|
render: () => (jsx(AppearancePreviewGrid, { adapterConfig: adapterConfig,
|
|
3091
3146
|
// `SectionHeader` is role="presentation", so the header above names nothing.
|
|
3092
|
-
ariaLabel: t("settings.mapStyle.title"), currentMapType: localState.mapType, currentTheme: localState.theme,
|
|
3147
|
+
ariaLabel: t("settings.mapStyle.title"), currentMapType: localState.mapType, currentTheme: localState.theme,
|
|
3148
|
+
// Items disable on their own too: a flattened responsive menu has no trigger.
|
|
3149
|
+
disabled: drawing, disabledReason: drawingReason, onChange: handleAppearanceChange, options: previewOptions })),
|
|
3093
3150
|
},
|
|
3094
3151
|
];
|
|
3095
3152
|
const isSatelliteOrHybrid = localState.mapType === "satellite" || localState.mapType === "hybrid";
|
|
@@ -3103,6 +3160,8 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
3103
3160
|
id: "roads-overlay",
|
|
3104
3161
|
label: t("settings.mapStyle.roads"),
|
|
3105
3162
|
value: localState.showRoads,
|
|
3163
|
+
disabled: drawing,
|
|
3164
|
+
disabledReason: drawingReason,
|
|
3106
3165
|
onChange: handleRoadsChange,
|
|
3107
3166
|
"data-testid": "map-style-roads-toggle",
|
|
3108
3167
|
});
|
|
@@ -3113,15 +3172,16 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
3113
3172
|
// are off but keeps DISPLAYING the persisted preference — the setting is
|
|
3114
3173
|
// still "on", it just cannot apply until roads return, and the tooltip says
|
|
3115
3174
|
// so. Explicit "hybrid" always renders roads (and traffic), so the gate
|
|
3116
|
-
// applies to "satellite" only.
|
|
3175
|
+
// applies to "satellite" only. Drawing mode disables it as well (a traffic
|
|
3176
|
+
// toggle recreates the Google map); the drawing reason wins while it lasts.
|
|
3117
3177
|
const trafficBlockedByRoads = localState.mapType === "satellite" && !localState.showRoads;
|
|
3118
3178
|
menuItems.push({
|
|
3119
3179
|
type: "toggle",
|
|
3120
3180
|
id: "traffic-overlay",
|
|
3121
3181
|
label: t("settings.mapStyle.traffic"),
|
|
3122
3182
|
value: localState.showTraffic,
|
|
3123
|
-
disabled: trafficBlockedByRoads,
|
|
3124
|
-
disabledReason: t("settings.mapStyle.trafficRequiresRoads"),
|
|
3183
|
+
disabled: drawing || trafficBlockedByRoads,
|
|
3184
|
+
disabledReason: drawingReason ?? t("settings.mapStyle.trafficRequiresRoads"),
|
|
3125
3185
|
onChange: handleTrafficChange,
|
|
3126
3186
|
"data-testid": "map-style-traffic-toggle",
|
|
3127
3187
|
});
|
|
@@ -3133,12 +3193,15 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
|
|
|
3133
3193
|
icon: "Map",
|
|
3134
3194
|
label: t("settings.mapStyle.title"),
|
|
3135
3195
|
items: menuItems,
|
|
3196
|
+
disabled: drawing,
|
|
3197
|
+
disabledReason: drawingReason,
|
|
3136
3198
|
"data-testid": "map-style-menu",
|
|
3137
3199
|
},
|
|
3138
3200
|
];
|
|
3139
3201
|
}, [
|
|
3140
3202
|
t,
|
|
3141
3203
|
adapterConfig,
|
|
3204
|
+
drawing,
|
|
3142
3205
|
localState.mapType,
|
|
3143
3206
|
localState.theme,
|
|
3144
3207
|
localState.showRoads,
|
|
@@ -3388,10 +3451,20 @@ const useControls = (base, areas) => {
|
|
|
3388
3451
|
}, [base, areas]);
|
|
3389
3452
|
};
|
|
3390
3453
|
|
|
3454
|
+
/**
|
|
3455
|
+
* Single dev-warning channel for react-map hooks (invalid data, invalid style, unhandled render errors).
|
|
3456
|
+
* Messages are prefixed with the emitting hook, e.g. `[useHeatmap]`.
|
|
3457
|
+
*/
|
|
3458
|
+
const consoleWarnMap = (source, message, ...details) => {
|
|
3459
|
+
// eslint-disable-next-line no-console -- Intentional: the one react-map warning channel; invalid input is dropped or defaulted and render errors without onError must stay visible
|
|
3460
|
+
console.warn(`[${source}] ${message}`, ...details);
|
|
3461
|
+
};
|
|
3462
|
+
|
|
3391
3463
|
const noopUnsubscribe$1 = () => undefined;
|
|
3392
3464
|
const subscribeNoop = () => noopUnsubscribe$1;
|
|
3393
3465
|
const getIdle = () => "idle";
|
|
3394
3466
|
const getEmpty = () => EMPTY_DRAW_VALUE;
|
|
3467
|
+
const getNoErrors = () => NO_DRAW_ERRORS;
|
|
3395
3468
|
/**
|
|
3396
3469
|
* Feature-facing hook for the optional drawing capability (ADR 0030).
|
|
3397
3470
|
*
|
|
@@ -3423,13 +3496,56 @@ const useDraw = (api, options) => {
|
|
|
3423
3496
|
const subscribe = useMemo(() => (port === undefined ? subscribeNoop : (listener) => port.subscribe(listener)), [port]);
|
|
3424
3497
|
const getMode = useMemo(() => (port === undefined ? getIdle : () => port.getMode()), [port]);
|
|
3425
3498
|
const getValue = useMemo(() => (port === undefined ? getEmpty : () => port.getValue()), [port]);
|
|
3499
|
+
const getErrors = useMemo(() => (port === undefined ? getNoErrors : () => port.getErrors()), [port]);
|
|
3426
3500
|
const mode = useSyncExternalStore(subscribe, getMode, getIdle);
|
|
3427
3501
|
const value = useSyncExternalStore(subscribe, getValue, getEmpty);
|
|
3502
|
+
const errors = useSyncExternalStore(subscribe, getErrors, getNoErrors);
|
|
3503
|
+
const error = errors.engine ?? errors.value;
|
|
3428
3504
|
// A disabled hook must not leave the map in a drawing mode it can no longer exit.
|
|
3429
3505
|
useEffect(() => {
|
|
3430
3506
|
if (!enabled)
|
|
3431
3507
|
port?.setMode("idle");
|
|
3432
3508
|
}, [enabled, port]);
|
|
3509
|
+
const onError = options?.onError;
|
|
3510
|
+
const onErrorRef = useRef(onError);
|
|
3511
|
+
useEffect(() => {
|
|
3512
|
+
onErrorRef.current = onError;
|
|
3513
|
+
}, [onError]);
|
|
3514
|
+
const errorHandler = useErrorHandlerOrNull();
|
|
3515
|
+
// The app's error handler at most once per code per hook mount, like useHeatmap: a
|
|
3516
|
+
// rejected stored boundary re-reports on every reconnect otherwise. A `value-rejected`
|
|
3517
|
+
// verdict is reported too, deliberately: it is the only count of legacy boundaries
|
|
3518
|
+
// the engine cannot render, kept until the Phase 2b pilot has run (ADR 0030).
|
|
3519
|
+
const reportedCodesRef = useRef(new Set());
|
|
3520
|
+
// Each slot reports once per new object: an engine error coming and going must not
|
|
3521
|
+
// re-report the value verdict that stayed in place the whole time.
|
|
3522
|
+
const reportedRef = useRef(NO_DRAW_ERRORS);
|
|
3523
|
+
useEffect(() => {
|
|
3524
|
+
const previous = reportedRef.current;
|
|
3525
|
+
reportedRef.current = errors;
|
|
3526
|
+
const fresh = [errors.engine, errors.value].flatMap(slot => slot !== null && slot !== previous.engine && slot !== previous.value ? [slot] : []);
|
|
3527
|
+
for (const next of fresh) {
|
|
3528
|
+
const detail = next.code === "value-rejected" ? next.reasons : next.cause;
|
|
3529
|
+
if (errorHandler !== null && !reportedCodesRef.current.has(next.code)) {
|
|
3530
|
+
reportedCodesRef.current.add(next.code);
|
|
3531
|
+
errorHandler.captureException(new Error(`Map drawing failed: ${next.code}`, { cause: detail }), {
|
|
3532
|
+
level: "warning",
|
|
3533
|
+
fingerprint: ["react-map", "draw", next.code],
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
const handler = onErrorRef.current;
|
|
3537
|
+
if (handler === undefined) {
|
|
3538
|
+
consoleWarnMap("useDraw", `Drawing failed (${next.code}).`, detail);
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
try {
|
|
3542
|
+
handler(next);
|
|
3543
|
+
}
|
|
3544
|
+
catch (handlerError) {
|
|
3545
|
+
consoleWarnMap("useDraw", "onError threw.", handlerError, detail);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
}, [errors, errorHandler]);
|
|
3433
3549
|
const setMode = useCallback((nextMode) => {
|
|
3434
3550
|
port?.setMode(nextMode);
|
|
3435
3551
|
}, [port]);
|
|
@@ -3440,7 +3556,20 @@ const useDraw = (api, options) => {
|
|
|
3440
3556
|
port?.setValue(EMPTY_DRAW_VALUE);
|
|
3441
3557
|
port?.setMode("idle");
|
|
3442
3558
|
}, [port]);
|
|
3443
|
-
|
|
3559
|
+
// Rejected features are in the value but not on the map, so there is nothing to
|
|
3560
|
+
// select among them; the edit toggle follows what the map shows, whatever the
|
|
3561
|
+
// engine slot says.
|
|
3562
|
+
const rejectedCount = errors.value?.rejectedFeatureIndices.length ?? 0;
|
|
3563
|
+
const hasPolygons = value.features.length - rejectedCount > 0;
|
|
3564
|
+
const maxPolygons = options?.maxPolygons;
|
|
3565
|
+
// A limit below one would lock the toggle with "clear the drawing" and nothing to clear.
|
|
3566
|
+
const limitReached = maxPolygons !== undefined && maxPolygons >= 1 && value.features.length >= maxPolygons;
|
|
3567
|
+
// Closing the polygon that reaches the limit ends polygon mode, so the next click
|
|
3568
|
+
// on the map does not start another one.
|
|
3569
|
+
useEffect(() => {
|
|
3570
|
+
if (limitReached && mode === "polygon")
|
|
3571
|
+
port?.setMode("idle");
|
|
3572
|
+
}, [limitReached, mode, port]);
|
|
3444
3573
|
const controls = useMemo(() => {
|
|
3445
3574
|
if (!isSupported || !enabled)
|
|
3446
3575
|
return [];
|
|
@@ -3452,6 +3581,10 @@ const useDraw = (api, options) => {
|
|
|
3452
3581
|
section: t("controls.category.tools"),
|
|
3453
3582
|
value: mode === "polygon",
|
|
3454
3583
|
onChange: on => setMode(on ? "polygon" : "idle"),
|
|
3584
|
+
// The limit effect above ends polygon mode as soon as it is reached, so the
|
|
3585
|
+
// toggle is never both active and disabled for longer than one render.
|
|
3586
|
+
disabled: limitReached,
|
|
3587
|
+
disabledReason: limitReached ? t("draw.limitReached") : undefined,
|
|
3455
3588
|
"data-testid": "map-draw-polygon-toggle",
|
|
3456
3589
|
};
|
|
3457
3590
|
const selectToggle = {
|
|
@@ -3477,17 +3610,19 @@ const useDraw = (api, options) => {
|
|
|
3477
3610
|
"data-testid": "map-draw-clear-button",
|
|
3478
3611
|
};
|
|
3479
3612
|
return [polygonToggle, selectToggle, clearButton];
|
|
3480
|
-
}, [clear, enabled, hasPolygons, isSupported, mode, setMode, t]);
|
|
3613
|
+
}, [clear, enabled, hasPolygons, isSupported, limitReached, mode, setMode, t]);
|
|
3481
3614
|
return useMemo(() => ({
|
|
3482
3615
|
isSupported,
|
|
3483
3616
|
isDrawing: mode !== "idle",
|
|
3484
3617
|
mode,
|
|
3485
3618
|
value,
|
|
3619
|
+
error,
|
|
3620
|
+
errors,
|
|
3486
3621
|
setMode,
|
|
3487
3622
|
setValue,
|
|
3488
3623
|
clear,
|
|
3489
3624
|
controls,
|
|
3490
|
-
}), [clear, controls, isSupported, mode, setMode, setValue, value]);
|
|
3625
|
+
}), [clear, controls, error, errors, isSupported, mode, setMode, setValue, value]);
|
|
3491
3626
|
};
|
|
3492
3627
|
|
|
3493
3628
|
/**
|
|
@@ -5994,15 +6129,6 @@ const useFitToContent = (api, handles, options) => {
|
|
|
5994
6129
|
return useMemo(() => ({ hasFitted, isWaiting, fitNow }), [hasFitted, isWaiting, fitNow]);
|
|
5995
6130
|
};
|
|
5996
6131
|
|
|
5997
|
-
/**
|
|
5998
|
-
* Single dev-warning channel for react-map hooks (invalid data, invalid style, unhandled render errors).
|
|
5999
|
-
* Messages are prefixed with the emitting hook, e.g. `[useHeatmap]`.
|
|
6000
|
-
*/
|
|
6001
|
-
const consoleWarnMap = (source, message, ...details) => {
|
|
6002
|
-
// eslint-disable-next-line no-console -- Intentional: the one react-map warning channel; invalid input is dropped or defaulted and render errors without onError must stay visible
|
|
6003
|
-
console.warn(`[${source}] ${message}`, ...details);
|
|
6004
|
-
};
|
|
6005
|
-
|
|
6006
6132
|
/**
|
|
6007
6133
|
* Converts consumer items into weighted GeoJSON `Point` features.
|
|
6008
6134
|
*
|
|
@@ -12331,6 +12457,26 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
12331
12457
|
return useMemo(() => ({ queryAt, promoteFeature, clearPromotion, setDecorationHoverActive }), [queryAt, promoteFeature, clearPromotion, setDecorationHoverActive]);
|
|
12332
12458
|
};
|
|
12333
12459
|
|
|
12460
|
+
const noop$1 = () => undefined;
|
|
12461
|
+
/** The handler set every consumer gets while drawing: nothing reaches the layers. */
|
|
12462
|
+
const INERT_LAYER_INTERACTION = {
|
|
12463
|
+
hover: noop$1,
|
|
12464
|
+
select: noop$1,
|
|
12465
|
+
dblClick: noop$1,
|
|
12466
|
+
click: undefined,
|
|
12467
|
+
settleHover: noop$1,
|
|
12468
|
+
decorationHover: noop$1,
|
|
12469
|
+
decorationSelect: noop$1,
|
|
12470
|
+
};
|
|
12471
|
+
/**
|
|
12472
|
+
* The one choke point for "drawing mode suppresses layer interaction" (ADR 0030).
|
|
12473
|
+
* `<Layers>` builds its live handlers once, passes them through here, and hands the
|
|
12474
|
+
* result to every consumer, so a new interaction path is gated by default instead
|
|
12475
|
+
* of needing its own `if (drawing) return`. Programmatic `layers.select` and
|
|
12476
|
+
* `layers.hover` calls from outside `<Layers>` are not affected.
|
|
12477
|
+
*/
|
|
12478
|
+
const useGatedLayerInteraction = (live, drawing) => useMemo(() => (drawing ? INERT_LAYER_INTERACTION : live), [drawing, live]);
|
|
12479
|
+
|
|
12334
12480
|
const applyPortalZIndex = (container, zIndex) => {
|
|
12335
12481
|
container.style.zIndex = String(zIndex);
|
|
12336
12482
|
};
|
|
@@ -12358,6 +12504,13 @@ const applyPortalZIndex = (container, zIndex) => {
|
|
|
12358
12504
|
*/
|
|
12359
12505
|
const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
12360
12506
|
const layerPort = useLayerPort();
|
|
12507
|
+
// Drawing mode suppresses layer interaction (ADR 0030): shapes render non-interactive
|
|
12508
|
+
// so Terra Draw gets every click without competing with existing polygons, and
|
|
12509
|
+
// marker, cluster and decoration clicks and hovers are dropped so no panel opens and
|
|
12510
|
+
// no hover state lingers under the draw tool. Every interaction consumer below gets
|
|
12511
|
+
// its handlers from one gated set (`interaction`), so the rule lives in one place.
|
|
12512
|
+
// Programmatic `layers.select` still works.
|
|
12513
|
+
const drawing = api?.state.drawing === true;
|
|
12361
12514
|
const [decorationLayers, setDecorationLayers] = useState([]);
|
|
12362
12515
|
const [viewportStyleOverrides, setViewportStyleOverrides] = useState(new Map());
|
|
12363
12516
|
const [fillGeometryOverrides, setFillGeometryOverrides] = useState(new Map());
|
|
@@ -12383,6 +12536,59 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
12383
12536
|
// Fill tiling only fires on polygonal features — shapeType is always "polygon".
|
|
12384
12537
|
layers.hover({ type: "shape", id: featureId, handleId, shapeType: "polygon" });
|
|
12385
12538
|
}, [layers]);
|
|
12539
|
+
// The remaining live handlers are defined here, before the hooks that consume them,
|
|
12540
|
+
// so one gated set can feed every consumer. `setDomHoverActive` comes from
|
|
12541
|
+
// `useEntityInteraction` further down, hence the ref.
|
|
12542
|
+
const setDomHoverActiveRef = useRef(() => undefined);
|
|
12543
|
+
const promoteFeatureRef = useRef(() => undefined);
|
|
12544
|
+
const setDecorationHoverActiveRef = useRef(() => undefined);
|
|
12545
|
+
const coordinatedSelect = useCallback((entity) => {
|
|
12546
|
+
const hovered = hoveredEntityRef.current;
|
|
12547
|
+
if (entity === null || hovered === null) {
|
|
12548
|
+
layers.select(entity);
|
|
12549
|
+
return;
|
|
12550
|
+
}
|
|
12551
|
+
// Default to the entity the click actually carried — it is the freshest,
|
|
12552
|
+
// most direct signal of what the user hit, and it hit-tests per element
|
|
12553
|
+
// for markers/clusters/routes. The single exception is a shape-on-shape
|
|
12554
|
+
// conflict: the adapter resolves polygon clicks with its own z-order and
|
|
12555
|
+
// full (unclipped) hit areas, so it cannot honor the fill-tiling ownership
|
|
12556
|
+
// model that decides which overlapping site is visually on top (ADR-0021).
|
|
12557
|
+
// There the settle-resolved hovered shape is the precise winner, so we
|
|
12558
|
+
// trust it instead. This is also why an asset pill overlapping a site
|
|
12559
|
+
// selects the asset: a marker click is never a shape, so it bypasses the
|
|
12560
|
+
// exception even when a stale site shape is still the hovered entity.
|
|
12561
|
+
if (entity.type === "shape" && hovered.type === "shape") {
|
|
12562
|
+
layers.select(hovered);
|
|
12563
|
+
return;
|
|
12564
|
+
}
|
|
12565
|
+
layers.select(entity);
|
|
12566
|
+
}, [layers]);
|
|
12567
|
+
const fitOnDblClick = useDblClickFit(api, layers.handles);
|
|
12568
|
+
const handleDblClick = useCallback((entity) => {
|
|
12569
|
+
onEntityDblClick?.(entity);
|
|
12570
|
+
fitOnDblClick?.(entity);
|
|
12571
|
+
}, [onEntityDblClick, fitOnDblClick]);
|
|
12572
|
+
const coordinatedHover = useCallback((entity) => {
|
|
12573
|
+
setDomHoverActiveRef.current(entity !== null);
|
|
12574
|
+
setDecorationHoverActiveRef.current(entity !== null);
|
|
12575
|
+
if (entity !== null && entity.type === "shape" && entity.handleId !== undefined) {
|
|
12576
|
+
promoteFeatureRef.current(entity.handleId, entity.id);
|
|
12577
|
+
}
|
|
12578
|
+
layers.hover(entity);
|
|
12579
|
+
}, [layers]);
|
|
12580
|
+
const liveInteraction = useMemo(() => ({
|
|
12581
|
+
hover: layers.hover,
|
|
12582
|
+
select: coordinatedSelect,
|
|
12583
|
+
dblClick: handleDblClick,
|
|
12584
|
+
click: onEntityClick,
|
|
12585
|
+
settleHover: onSettledHover,
|
|
12586
|
+
decorationHover: coordinatedHover,
|
|
12587
|
+
decorationSelect: layers.select,
|
|
12588
|
+
}), [layers.hover, layers.select, coordinatedSelect, handleDblClick, onEntityClick, onSettledHover, coordinatedHover]);
|
|
12589
|
+
// The one choke point for ADR 0030's interaction rule: while drawing, every consumer
|
|
12590
|
+
// below receives handlers that do nothing (see useGatedLayerInteraction).
|
|
12591
|
+
const interaction = useGatedLayerInteraction(liveInteraction, drawing);
|
|
12386
12592
|
const suppressedFillIdsByHandle = useMemo(() => {
|
|
12387
12593
|
const result = new Map();
|
|
12388
12594
|
for (const [handleId, featureStyles] of viewportStyleOverrides) {
|
|
@@ -12401,16 +12607,14 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
12401
12607
|
handles: layers.handles,
|
|
12402
12608
|
onFillGeometriesChange: setFillGeometryOverrides,
|
|
12403
12609
|
onZIndexOverridesChange: setZIndexOverrides,
|
|
12404
|
-
onSettledHover,
|
|
12610
|
+
onSettledHover: interaction.settleHover,
|
|
12405
12611
|
selectedFeatureId: layers.interaction.selectedEntity?.type === "shape" ? String(layers.interaction.selectedEntity.id) : null,
|
|
12406
12612
|
selectedHandleId: layers.interaction.selectedEntity?.type === "shape" ? layers.interaction.selectedEntity.handleId : null,
|
|
12407
12613
|
suppressedFillIdsByHandle,
|
|
12408
12614
|
});
|
|
12409
|
-
const promoteFeatureRef = useRef(promoteFeature);
|
|
12410
12615
|
useEffect(() => {
|
|
12411
12616
|
promoteFeatureRef.current = promoteFeature;
|
|
12412
12617
|
}, [promoteFeature]);
|
|
12413
|
-
const setDecorationHoverActiveRef = useRef(setDecorationHoverActive);
|
|
12414
12618
|
useEffect(() => {
|
|
12415
12619
|
setDecorationHoverActiveRef.current = setDecorationHoverActive;
|
|
12416
12620
|
}, [setDecorationHoverActive]);
|
|
@@ -12422,11 +12626,14 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
12422
12626
|
const fillOverrides = fillGeometryOverrides.get(handle.id);
|
|
12423
12627
|
const zIndexOverridesForHandle = zIndexOverrides.get(handle.id);
|
|
12424
12628
|
const queryShapesAt = (position) => queryAt(handle.id, position);
|
|
12629
|
+
// The existing "none" mode removes the adapter's polygon hit-testing entirely.
|
|
12630
|
+
const interactiveWhileDrawing = drawing ? { interactive: "none" } : {};
|
|
12425
12631
|
if (styleOverrides === undefined && fillOverrides === undefined && zIndexOverridesForHandle === undefined) {
|
|
12426
|
-
return { ...handle, queryShapesAt };
|
|
12632
|
+
return { ...handle, ...interactiveWhileDrawing, queryShapesAt };
|
|
12427
12633
|
}
|
|
12428
12634
|
return {
|
|
12429
12635
|
...handle,
|
|
12636
|
+
...interactiveWhileDrawing,
|
|
12430
12637
|
queryShapesAt,
|
|
12431
12638
|
...(styleOverrides !== undefined ? { featureStyles: styleOverrides } : {}),
|
|
12432
12639
|
...(fillOverrides !== undefined ? { featureFillGeometries: fillOverrides } : {}),
|
|
@@ -12440,50 +12647,35 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
12440
12647
|
return handle;
|
|
12441
12648
|
return { ...handle, adaptiveResolution };
|
|
12442
12649
|
});
|
|
12443
|
-
}, [
|
|
12650
|
+
}, [
|
|
12651
|
+
layers.handles,
|
|
12652
|
+
adaptiveByLayer,
|
|
12653
|
+
viewportStyleOverrides,
|
|
12654
|
+
fillGeometryOverrides,
|
|
12655
|
+
zIndexOverrides,
|
|
12656
|
+
queryAt,
|
|
12657
|
+
drawing,
|
|
12658
|
+
]);
|
|
12444
12659
|
const { renderedSourceIds, hasSourceReadiness, readyRevision } = useLayerHandleSync(layerPort, enrichedHandles, layers.interaction, decorationLayers);
|
|
12445
|
-
const coordinatedSelect = useCallback((entity) => {
|
|
12446
|
-
const hovered = hoveredEntityRef.current;
|
|
12447
|
-
if (entity === null || hovered === null) {
|
|
12448
|
-
layers.select(entity);
|
|
12449
|
-
return;
|
|
12450
|
-
}
|
|
12451
|
-
// Default to the entity the click actually carried — it is the freshest,
|
|
12452
|
-
// most direct signal of what the user hit, and it hit-tests per element
|
|
12453
|
-
// for markers/clusters/routes. The single exception is a shape-on-shape
|
|
12454
|
-
// conflict: the adapter resolves polygon clicks with its own z-order and
|
|
12455
|
-
// full (unclipped) hit areas, so it cannot honor the fill-tiling ownership
|
|
12456
|
-
// model that decides which overlapping site is visually on top (ADR-0021).
|
|
12457
|
-
// There the settle-resolved hovered shape is the precise winner, so we
|
|
12458
|
-
// trust it instead. This is also why an asset pill overlapping a site
|
|
12459
|
-
// selects the asset: a marker click is never a shape, so it bypasses the
|
|
12460
|
-
// exception even when a stale site shape is still the hovered entity.
|
|
12461
|
-
if (entity.type === "shape" && hovered.type === "shape") {
|
|
12462
|
-
layers.select(hovered);
|
|
12463
|
-
return;
|
|
12464
|
-
}
|
|
12465
|
-
layers.select(entity);
|
|
12466
|
-
}, [layers]);
|
|
12467
|
-
const fitOnDblClick = useDblClickFit(api, layers.handles);
|
|
12468
|
-
const handleDblClick = useCallback((entity) => {
|
|
12469
|
-
onEntityDblClick?.(entity);
|
|
12470
|
-
fitOnDblClick?.(entity);
|
|
12471
|
-
}, [onEntityDblClick, fitOnDblClick]);
|
|
12472
12660
|
const setDomHoverActive = useEntityInteraction({
|
|
12473
12661
|
layerPort,
|
|
12474
|
-
hover:
|
|
12475
|
-
select:
|
|
12476
|
-
dblClick:
|
|
12477
|
-
onClick:
|
|
12662
|
+
hover: interaction.hover,
|
|
12663
|
+
select: interaction.select,
|
|
12664
|
+
dblClick: interaction.dblClick,
|
|
12665
|
+
onClick: interaction.click,
|
|
12478
12666
|
});
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12667
|
+
useEffect(() => {
|
|
12668
|
+
setDomHoverActiveRef.current = setDomHoverActive;
|
|
12669
|
+
}, [setDomHoverActive]);
|
|
12670
|
+
// Entering drawing mode drops whatever was hovered, so nothing stays highlighted
|
|
12671
|
+
// under the draw tool; selection is left alone (the wizard may show the stored site).
|
|
12672
|
+
useEffect(() => {
|
|
12673
|
+
if (!drawing)
|
|
12674
|
+
return;
|
|
12675
|
+
setDomHoverActive(false);
|
|
12676
|
+
setDecorationHoverActiveRef.current(false);
|
|
12677
|
+
layers.hover(null);
|
|
12678
|
+
}, [drawing, layers, setDomHoverActive]);
|
|
12487
12679
|
const interactionMapStore = useShapeDecorations({
|
|
12488
12680
|
onDecorationsChange: setDecorationLayers,
|
|
12489
12681
|
onStyleOverridesChange: setViewportStyleOverrides,
|
|
@@ -12493,8 +12685,8 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
12493
12685
|
hasSourceReadiness,
|
|
12494
12686
|
readyRevision,
|
|
12495
12687
|
interaction: layers.interaction,
|
|
12496
|
-
select:
|
|
12497
|
-
hover:
|
|
12688
|
+
select: interaction.decorationSelect,
|
|
12689
|
+
hover: interaction.decorationHover,
|
|
12498
12690
|
});
|
|
12499
12691
|
const interactionMap = useSyncExternalStore(interactionMapStore.subscribe, interactionMapStore.getSnapshot, interactionMapStore.getSnapshot);
|
|
12500
12692
|
const markerPortalStore = layerPort?.markerPortals ?? null;
|
|
@@ -12581,8 +12773,8 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
12581
12773
|
}
|
|
12582
12774
|
: itemState;
|
|
12583
12775
|
const rendered = descriptor.renderFn(descriptor.sourceData, callState);
|
|
12584
|
-
const
|
|
12585
|
-
return { descriptor, rendered,
|
|
12776
|
+
const decorationInteraction = interactionMap.get(sourceId);
|
|
12777
|
+
return { descriptor, rendered, decorationInteraction, zIndex };
|
|
12586
12778
|
};
|
|
12587
12779
|
const portalEntries = [
|
|
12588
12780
|
...markerPortalDescriptors.map(d => buildPortalEntry(d, "marker")),
|
|
@@ -12596,9 +12788,9 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
12596
12788
|
});
|
|
12597
12789
|
if (portalEntries.length === 0)
|
|
12598
12790
|
return null;
|
|
12599
|
-
return (jsx(Fragment, { children: portalEntries.map(({ descriptor, rendered,
|
|
12600
|
-
if (
|
|
12601
|
-
return createPortal(jsx(InteractiveDecoration, { entity:
|
|
12791
|
+
return (jsx(Fragment, { children: portalEntries.map(({ descriptor, rendered, decorationInteraction }) => {
|
|
12792
|
+
if (decorationInteraction !== undefined) {
|
|
12793
|
+
return createPortal(jsx(InteractiveDecoration, { entity: decorationInteraction.entity, hover: interaction.decorationHover, onClick: decorationInteraction.onClick, onDblClick: interaction.dblClick, onHover: decorationInteraction.onHover, select: interaction.decorationSelect, children: rendered }), descriptor.container, descriptor.key);
|
|
12602
12794
|
}
|
|
12603
12795
|
return createPortal(rendered, descriptor.container, descriptor.key);
|
|
12604
12796
|
}) }));
|