Popups¶
The popup subsystem lives in app/src/core/popups/ (resolution + handlers) and app/src/shell/PopupHost.tsx / PopupContent.tsx (rendering).
Resolution — resolvePopup.ts¶
resolvePopup(layer, uiPopups) merges the popup config for one layer:
shell defaults ←
settings.ui.popups←layer.popup
It returns null for any disabled layer (no popup field, popup: false, or enabled: false) — popups are opt-in. Otherwise it returns a ResolvedPopupConfig carrying the resolved trigger, displayType, title, fields (or null = all fields), the header template, and the layer's group (needed to resolve the {group} header placeholder).
Handlers — popupHandlers.ts¶
registerPopupHandlers(map, layers, uiPopups) wires one click handler and one mousemove handler covering all popup-enabled layers — not per-layer handlers, which would race when a click hits stacked features.
At registration it:
- Calls
resolvePopupfor each layer, sorting them into click-triggered and hover-triggered pools. - Validates
displayTypeat boot — a template name not inpopups.tsxthrows with the available list.
The click handler:
- Bails if
coordinateCaptureArmed(the coordinates widget owns the click). - Queries every click-enabled sublayer at the point, dedupes hits by
(layerId, feature id || stringified props)—queryRenderedFeaturescan return the same polygon once per intersecting tile. - Writes the deduped feature list to the store with trigger
'click'. - An empty click closes a click-popup, gated on
ui.popups.closeOnMapClick(sticky popups skip this).
The mousemove handler drives both the pointer cursor (for any interactive layer) and hover popups (topmost hit wins). Moving off the feature closes hover popups; click popups are left alone (separate lifecycles).
The sublayer ids a handler queries are derived from the layer id (-fill/-line/-point), so they stay valid after re-registration. MapView tears down and re-registers popup handlers alongside registerLayers on every style.load.
Store + rendering¶
The store holds selectedFeatures (always an array — hover writes a single element so the host stays generic), selectedFeatureIndex, and selectedFeatureTrigger. Actions: setSelectedFeatures, advancePopup (wraps), closePopup.
PopupHost(desktop) anchors the popup bottom-left of the map by default, or at a user-dragged position. It applies the size fromui.popups(height/maxHeight/maxWidth) and handles header-drag repositioning. On mobile it no-ops — the bottom sheet renders popups instead.PopupContent(shared by the desktop host and the mobile sheet) renders the header bar, the chosen template body, and — for stacked click features — a pager locked to the bottom. It resolves theheadertemplate's placeholders ({layerName}/{group}/{field:NAME}) per feature, and thetitlevalue fromfeature.properties[title].
Templates themselves live in the user surface (app/popups.tsx) — see Popups configuration.