This is the documentation of version 1.0.0-rc.4. 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, hovers and drags 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
dragstart A pressed feature starts to move with the pointer. See Dragging features
drag The pointer moves during a drag
dragend The user releases a dragged feature
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));

Dragging features

A dragstart, drag or dragend handler makes the features it covers draggable, with the mouse or one finger. It works on every module and scope. To move a place pin, show it again where the pointer is:

places.events.places.on('drag', async (place, lngLat) => {
    const geometry = { type: 'Point', coordinates: lngLat.toArray() };
    myPlaces = myPlaces.map((shown) => (shown.id === place.id ? { ...shown, geometry } : shown));
    await places.show(myPlaces);
});
places.events.places.on('dragend', (place, lngLat) => savePosition(place.id, lngLat));

Editing a route by dragging moves waypoints and pulls new stops onto a route line. The route dragging plugin does it for you.

Event priority

The event goes to the module of the top-most feature under the pointer, and no further down. A drag starts the same way, on the top-most feature. Only the features a handler covers count. If a place pin covers a road, a places handler receives the click and the base map doesn't. With no places handler, the click reaches the road. 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
    .where((route) => route.properties.routeState === 'deselected')
    .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:

// Highlight 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: [clickedID] = [], hover = [] } = routing.getEventStates();

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

A route's click state is not its selection: selectRoute clears it, and the selected route has routeState: 'selected'.

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:

Custom layer objects

Register a hit tester for a custom layer, and its objects get the same click, hover and cursor handling as module features. Pass it as the hitTest of map.addCustomLayer, then listen on the events of the layer it returns:

const vehicles = map.addCustomLayer(vehiclesLayer, {
    beforeLayerConfig: 'lowest3D',
    hitTest: (point) => pickVehicles(point), // [{ id, properties }], the topmost first
    setState: (id, state) => highlightVehicle(id, state),
});

vehicles.events.on('click', (vehicle) => showVehicle(vehicle.properties));

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
cursorOnDraggable 'grab' Cursor over a feature a drag handler covers, instead of cursorOnHover
cursorOnMap 'default' Cursor anywhere else
cursorOnMouseDown 'grabbing' Cursor while the button is down, and during a drag
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, cursorOnDraggable 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());

Ending a hover

No event says the pointer left a feature. It either moved on to another feature, which fires a hover there, or it left the map. To keep your own UI in step, such as a card lit up while its route is hovered, end the hover on the base map's hover under the routes and on MapLibre's mouseout:

const baseMap = await BaseMapModule.get(map, { events: { cursorOnHover: 'default' } });
const underRoutes = baseMap.events.where({
    layerGroups: {
        show: 'only',
        values: ['land', 'water', 'buildings2D', 'roads', 'railways', 'ferries', 'borders'],
    },
});

routing.events.mainLines.on('hover', (route) => highlightCard(route.properties.index));
underRoutes.on('hover', () => highlightCard(undefined));
map.mapLibreMap.on('mouseout', () => {
    // The pointer left the map, say for your panel
    highlightCard(undefined);
    routing.clearEventStates({ states: ['hover'] });
});

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 pin interaction example combines this with places.

Related guides and examples

Related guides

Related examples