Consolidate the floating popover into the shared tool library
Follow-up accepted in review of #317.
Motivation
Three tools — Timeline, LayerManager, and (as of #317) MapControl — each carry their own private copy of the same floating popover component: the surface that anchors to a trigger button, portals out of clipping containers, positions itself against the viewport, and owns Escape, outside-click, and focus placement/restoration.
The copies were byte-identical until #317 extended only MapControl's with a tooltip variant that nothing uses. That is the drift risk made concrete: a fix or capability added to one copy — a positioning bug, an accessibility improvement — silently doesn't reach the other two. Per-tool copies exist because tools may not import from each other, but a shared home for cross-tool code already exists and is already in use.
How it should work
- One popover implementation lives in the shared area every tool may draw from; Timeline, LayerManager, and MapControl all use it.
- The per-tool copies are deleted.
- The unused tooltip variant is either adopted by a real consumer or dropped, so the shared component carries no speculative surface.
- While rewiring MapControl: the map bar should only maintain its search surface when search is actually configured, matching how its trigger button already behaves.
- On screen, nothing changes: popovers open and position as before, close on Escape and outside click, and return focus to their trigger.
Done when
Out of scope
Draft implementation plan — written as of 1821dc2 (PR #317 head) on 2026-08-13. Rough guide; re-verify against latest code, and land after #317 merges.
Current behavior
src/essence/Tools/{Timeline,LayerManager,MapControl}/lib/FloatingPopover/FloatingPopover.tsx — three copies. Timeline and LayerManager are identical; MapControl's adds a role?: 'dialog' | 'tooltip' prop (default 'dialog') that no call site sets.
- Consumers: Timeline and LayerManager import from their own
lib/FloatingPopover; MapControl uses it in lib/geo/MapControlBar/MapControlBar.tsx for the basemap and search panels.
- Shared-code precedent:
src/essence/Tools/_shared/ (e.g. _shared/share/ShareMenu.tsx + share-menu.scss), imported by multiple tools.
- The search popover in
MapControlBar.tsx renders unconditionally while its trigger button is gated on the onSearchSelect prop (basemap popover is gated on hasStyles).
Where the change lands & rough plan
- Move one copy (MapControl's, minus the
role prop unless a consumer appears) to src/essence/Tools/_shared/FloatingPopover/.
- Point the three tools' imports at it; delete the three
lib/FloatingPopover/ directories and their index.ts re-exports (including the one in MapControl's lib/index.ts).
- Wrap MapControl's search
<FloatingPopover> in {onSearchSelect && …}.
- Add unit coverage in the shared home — positioning clamps, Escape close, focus restoration are jsdom-testable;
tests/unit/ and LayerManager/__tests__/popoverMenu.spec.js show the local patterns.
⚠️ Gotcha: the copies may have drifted further by pickup time — diff all three before choosing the survivor, don't assume Timeline/LayerManager are still identical.
⚠️ Gotcha: focus restoration reads a focusin-tracked ref because document.activeElement has already fallen back to <body> by portal cleanup — preserve that mechanism when touching the component.
References
Consolidate the floating popover into the shared tool library
Follow-up accepted in review of #317.
Motivation
Three tools — Timeline, LayerManager, and (as of #317) MapControl — each carry their own private copy of the same floating popover component: the surface that anchors to a trigger button, portals out of clipping containers, positions itself against the viewport, and owns Escape, outside-click, and focus placement/restoration.
The copies were byte-identical until #317 extended only MapControl's with a tooltip variant that nothing uses. That is the drift risk made concrete: a fix or capability added to one copy — a positioning bug, an accessibility improvement — silently doesn't reach the other two. Per-tool copies exist because tools may not import from each other, but a shared home for cross-tool code already exists and is already in use.
How it should work
Done when
Out of scope
Draft implementation plan — written as of 1821dc2 (PR #317 head) on 2026-08-13. Rough guide; re-verify against latest code, and land after #317 merges.
Current behavior
src/essence/Tools/{Timeline,LayerManager,MapControl}/lib/FloatingPopover/FloatingPopover.tsx— three copies. Timeline and LayerManager are identical; MapControl's adds arole?: 'dialog' | 'tooltip'prop (default'dialog') that no call site sets.lib/FloatingPopover; MapControl uses it inlib/geo/MapControlBar/MapControlBar.tsxfor the basemap and search panels.src/essence/Tools/_shared/(e.g._shared/share/ShareMenu.tsx+share-menu.scss), imported by multiple tools.MapControlBar.tsxrenders unconditionally while its trigger button is gated on theonSearchSelectprop (basemap popover is gated onhasStyles).Where the change lands & rough plan
roleprop unless a consumer appears) tosrc/essence/Tools/_shared/FloatingPopover/.lib/FloatingPopover/directories and theirindex.tsre-exports (including the one in MapControl'slib/index.ts).<FloatingPopover>in{onSearchSelect && …}.tests/unit/andLayerManager/__tests__/popoverMenu.spec.jsshow the local patterns.References
src/essence/Tools/_shared/share/as the shared-home pattern to mirror.