This is the documentation of version 1.0.0-rc.2. Go to the latest version, 1.0.0-rc.5.

User interaction events

Every map module that draws features has an events object for clicks and hovers on them. All modules share one pipeline. Only the top-most feature under the pointer receives an event. Hover and long hover have built-in timing, and handlers receive your original typed data. The demo below opens a place on click, shows an incident's delay on a long hover and reverse geocodes a click on the rest of the map.

Event types

Type Fires when
click The user clicks or taps a feature
contextmenu The user right-clicks a feature
hover The pointer arrives on a feature, or moves on to another one
hover-move The pointer moves along the feature it is already on
long-hover The pointer rests on a feature for the long-hover delay
import { TrafficIncidentsModule } from '@tomtom-org/maps-sdk/map';

const trafficIncidents = await TrafficIncidentsModule.get(map);

const unsubscribe = trafficIncidents.events.on('click', (incident, lngLat, allEventFeatures, sourceWithLayers) => {
    showIncident(incident.properties.description, lngLat);
});

unsubscribe(); // removes this handler only
trafficIncidents.events.off('click'); // removes every click handler of the module

Long-hover events

long-hover fires once the pointer has rested on a feature. Use it for a tooltip that needs a request to fill:

trafficIncidents.events.on('long-hover', (incident, lngLat) => showDelayPopup(incident, lngLat));

Event priority

The event goes to the module of the top-most feature under the pointer, and no further down. If a place pin covers a road, the places module receives the click, not the base map. Two scopes of one module follow the same rule, whichever registered first. If a 3D building covers a road, a buildings3D scope fires and a roads scope does not.

Scoping events

events.where(scope, config?) returns events for part of what a module covers. The optional config is that part's own event configuration. Use it to give two parts of one module different hover cursors.

Module event scopes

A module that draws several kinds of feature has a named scope for each kind, with the same on, off and where. The module's own events covers all of them, plus the lifecycle events, which no scope has.

Module Named scopes
RoutingModule mainLines, waypoints, summaryBubbles, countryCrossings, incidents, vehicleRestricted, ferries, tollRoads, tunnels, instructionLines, plus one <type>Sections scope per generated section type
PlacesModule places, connections, entryPoints
GeometriesModule, ReachableRangesModule geometry, geometryLabel
CustomGeoJSONModule your own sources keys

Modules that draw one kind of feature — BaseMapModule, POIsModule, TrafficFlowModule, TrafficIncidentsModule, TrafficIncidentDetailsModule, TrafficAreaAnalyticsModule — have none, because their events is already that scope. TerrainModule and StylingFoundationsModule draw nothing to interact with, so they have lifecycle events only.

Places module events

places.events.places covers only the pins and hands back the Place you showed. places.events also covers connection lines and entry points. Places events has the details.

places.events.places.on('click', (place) => showPlaceDetails(place.properties.poi?.name));

Routing module events

routing.events.mainLines covers the route lines and hands back each Route. The other scopes cover waypoints and each kind of section. Routing events has the details.

routing.events.mainLines.on('click', async (route) => await routing.selectRoute(route.properties.index));
routing.events.waypoints.on('hover', (waypoint) => showTooltip(waypoint));

Event states

A user event on a feature that a handler covers sets its properties.eventState to click, contextmenu, hover, long-hover or recently-hovered. The SDK draws the state, and your own layers can style it. Every module that draws data you pass to show lets you put, clear and read states yourself, to select a feature from a list or keep the map and your UI in sync:

// Select a route as if clicked, taking the click off any other route
routing.setEventState({ id: routeID, state: 'click' });

// The ids with a state, by state, whoever put them
const { click: [selectedID] = [], hover = [] } = routing.getEventStates();

routing.clearEventState({ id: routeID });
routing.clearEventStates({ states: ['hover'] });
routing.clearEventStates(); // every state

The methods act on the module's main features, which getShown() returns, so they take any id read there or in a handler:

Module Acts on Hovered or clicked look
PlacesModule places, clustered or not a larger pin, its label in the module's color, and its entry points
RoutingModule mainLines, the routes a wider outline
GeometriesModule, ReachableRangesModule geometry a denser fill and wider border
CustomGeoJSONModule every source what your layers draw with ['get', 'eventState'] (example)
TrafficIncidentDetailsModule incidents the focus highlight
TrafficAreaAnalyticsModule hexgrid, square and heatmap an outlined cell, stronger when clicked

Recently hovered features

When the pointer leaves a feature that can carry a state, its hover turns into recently-hovered for hoverGracePeriodMS (400 ms by default), unless it is hovered or clicked again first. A value of 0 clears it at once, on the map or in one module's events configuration.

The demo colours each state, with sliders for the long-hover delay and the grace period:

Event configuration

Map-level configuration

The events option of the map applies to every module:

import { TomTomMap } from '@tomtom-org/maps-sdk/map';

const map = new TomTomMap({
    mapLibre: { container: 'map' },
    events: { precisionMode: 'point-then-box', paddingBoxPx: 10, longHoverDelayOnStillMapMS: 500 },
});
Option Default Effect
precisionMode 'box' 'box' hit-tests a box of paddingBoxPx around the pointer. 'point' tests the exact pixel, for dense data. 'point-then-box' tests the pixel, then the box if that hits nothing
paddingBoxPx 5 Half the box size, in pixels. Use more for touch screens
cursorOnHover 'pointer' Cursor over a feature a handler covers
cursorOnMap 'default' Cursor anywhere else
cursorOnMouseDown 'grabbing' Cursor while the button is down
longHoverDelayAfterMapMoveMS 800 Delay of the first long hover after the map moved
longHoverDelayOnStillMapMS 300 Delay of the next long hovers on a still map
hoverGracePeriodMS 400 How long a left feature stays recently-hovered

Changing the configuration at runtime

updateEventsConfig merges new values into the map's configuration. Values you leave out are unchanged. It applies from the next hover or click. A long hover or grace period already running keeps its delay:

map.updateEventsConfig({ longHoverDelayOnStillMapMS: 600, hoverGracePeriodMS: 1000 });

Module-level configuration

A module's events configuration overrides cursorOnHover and hoverGracePeriodMS for its own features, and updateEventsConfig does not change it. A scope takes its own as the second argument of where.

import { PlacesModule } from '@tomtom-org/maps-sdk/map';

const places = await PlacesModule.create(map, { events: { cursorOnHover: 'help', hoverGracePeriodMS: 0 } });

Handling events on the rest of the map

Using BaseMapModule for background interactions

BaseMapModule covers the style's own land, water, roads, buildings and labels. Only the top-most feature receives an event, so its handlers fire on clicks and hovers that no module drawn above it takes. Use them to clear a selection, close a popup or reverse geocode any point:

import { BaseMapModule } from '@tomtom-org/maps-sdk/map';
import { reverseGeocode } from '@tomtom-org/maps-sdk/services';

const baseMap = await BaseMapModule.get(map, { events: { cursorOnHover: 'default' } });

baseMap.events.on('click', async (feature, lngLat) => {
    clearSelection();
    showAddress(await reverseGeocode({ position: [lngLat.lng, lngLat.lat] }));
});

To make some of the base map interactive and treat the rest as background, give each part a scope over the shared module:

const interactive = ['roads', 'roadLabels', 'buildings3D'];

baseMap.events
    .where({ layerGroups: { show: 'only', values: interactive } }, { cursorOnHover: 'pointer' })
    .on('click', (feature) => showFeatureDetails(feature));

baseMap.events
    .where({ layerGroups: { show: 'all-except', values: interactive } }, { cursorOnHover: 'default' })
    .on('click', () => clearSelection());

Cursor behavior for background interactions

Over every feature a handler covers, the cursor turns into cursorOnHover, which is 'pointer' by default. On the base map, that is the whole map. To keep the pointer meaning "clickable", give the background handlers cursorOnHover: 'default', on the module or on the scope as above. The rest of the map click example combines this with places.

Related guides and examples

Related guides

Related examples