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