@trackunit/react-map 0.2.224 → 0.2.226

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.
Files changed (48) hide show
  1. package/index.cjs.js +276 -84
  2. package/index.esm.js +277 -85
  3. package/migrations/entry.js.map +1 -0
  4. package/package.json +11 -11
  5. package/src/appearance/AppearancePreview.d.ts +10 -1
  6. package/src/appearance/AppearancePreviewGrid.d.ts +8 -1
  7. package/src/controls/renderers/menu/utils/menuTooltipLabel.d.ts +3 -0
  8. package/src/controls/renderers/shared/StandaloneControlTooltip.d.ts +12 -3
  9. package/src/controls/renderers/shared/useDisabledReason.d.ts +28 -0
  10. package/src/controls/renderers/shared/utils/isStandaloneTooltipWrapped.d.ts +8 -0
  11. package/src/controls/types.d.ts +9 -0
  12. package/src/draw/useDraw.d.ts +33 -1
  13. package/src/index.d.ts +1 -1
  14. package/src/layers/utils/useGatedLayerInteraction.d.ts +25 -0
  15. package/translation.cjs.js +4 -0
  16. package/translation.esm.js +4 -0
  17. package/translation10.cjs.js +4 -0
  18. package/translation10.esm.js +4 -0
  19. package/translation11.cjs.js +4 -0
  20. package/translation11.esm.js +4 -0
  21. package/translation12.cjs.js +4 -0
  22. package/translation12.esm.js +4 -0
  23. package/translation13.cjs.js +4 -0
  24. package/translation13.esm.js +4 -0
  25. package/translation14.cjs.js +4 -0
  26. package/translation14.esm.js +4 -0
  27. package/translation15.cjs.js +4 -0
  28. package/translation15.esm.js +4 -0
  29. package/translation16.cjs.js +4 -0
  30. package/translation16.esm.js +4 -0
  31. package/translation17.cjs.js +4 -0
  32. package/translation17.esm.js +4 -0
  33. package/translation2.cjs.js +4 -0
  34. package/translation2.esm.js +4 -0
  35. package/translation3.cjs.js +4 -0
  36. package/translation3.esm.js +4 -0
  37. package/translation4.cjs.js +4 -0
  38. package/translation4.esm.js +4 -0
  39. package/translation5.cjs.js +4 -0
  40. package/translation5.esm.js +4 -0
  41. package/translation6.cjs.js +4 -0
  42. package/translation6.esm.js +4 -0
  43. package/translation7.cjs.js +4 -0
  44. package/translation7.esm.js +4 -0
  45. package/translation8.cjs.js +4 -0
  46. package/translation8.esm.js +4 -0
  47. package/translation9.cjs.js +4 -0
  48. package/translation9.esm.js +4 -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
- * Wraps standalone map controls with a right-aligned tooltip when `overlaySide` is `"right"`.
1836
- * Callers should omit `title` on {@link IconButton} in that mode so tooltips do not stack.
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 StandaloneControlTooltip = ({ children, disabled = false, label, overlaySide = undefined, }) => {
1839
- if (overlaySide !== "right") {
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 = config.disabled === true && config.disabledReason !== undefined
1862
- ? config.disabledReason
1863
- : t("controls.boolean.tooltip", { label });
1864
- const useRightOverlay = overlaySide === "right";
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.jsx("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: label, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: jsxRuntime.jsx(reactComponents.IconButton, { ariaLabel: label, "data-testid": dataTestId, 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 }) })] })) }) }));
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
- return (jsxRuntime.jsx("div", { className: className ?? "w-fit", ref: triggerRef, children: jsxRuntime.jsxs(reactComponents.Popover, { placement: popoverPlacement, children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx(reactComponents.IconButton, { "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, title: label, 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 }) })] }) }));
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
- return (jsxRuntime.jsxs("label", { className: "cursor-pointer", "data-testid": dataTestId, children: [jsxRuntime.jsxs("div", { className: reactComponents.cvaInteractableItem({
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, onChange: handleAppearanceChange, options: previewOptions })),
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
- const hasPolygons = value.features.length > 0;
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
- }, [layers.handles, adaptiveByLayer, viewportStyleOverrides, fillGeometryOverrides, zIndexOverrides, queryAt]);
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: layers.hover,
12476
- select: coordinatedSelect,
12477
- dblClick: handleDblClick,
12478
- onClick: onEntityClick,
12663
+ hover: interaction.hover,
12664
+ select: interaction.select,
12665
+ dblClick: interaction.dblClick,
12666
+ onClick: interaction.click,
12479
12667
  });
12480
- const coordinatedHover = react.useCallback((entity) => {
12481
- setDomHoverActive(entity !== null);
12482
- setDecorationHoverActiveRef.current(entity !== null);
12483
- if (entity !== null && entity.type === "shape" && entity.handleId !== undefined) {
12484
- promoteFeatureRef.current(entity.handleId, entity.id);
12485
- }
12486
- layers.hover(entity);
12487
- }, [layers, setDomHoverActive]);
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: layers.select,
12498
- hover: coordinatedHover,
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 interaction = interactionMap.get(sourceId);
12586
- return { descriptor, rendered, interaction, zIndex };
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, interaction }) => {
12601
- if (interaction !== undefined) {
12602
- return reactDom.createPortal(jsxRuntime.jsx(InteractiveDecoration, { entity: interaction.entity, hover: coordinatedHover, onClick: interaction.onClick, onDblClick: handleDblClick, onHover: interaction.onHover, select: layers.select, children: rendered }), descriptor.container, descriptor.key);
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
  }) }));