mapbox / mapbox/mapbox-gl-js

RFC: Tracking feature state for interactivity

Open
#6,021 27 comments 6 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature :green_apple:
Dominant language
TypeScript
Stars
12.4k
Forks
2.4k
PR merge metrics
No merged PRs in 30d

Description

## Motivation

#6020 provides a way to update the appearance of features. But how do you know if a feature is being hovered on? if is being activated? if it's currently selected? if it was selected at some point? We need to have an approach to tracking state.

we should make all of these possible *somehow*:
- style the topmost feature on hover
- style the topmost feature on hover, ignoring some layers
- style all the features under the mouse on hover
- style a polygon label and fill when the fill is hovered on
- style the polygon fill, but not label, when the fill is hovered on
- style a feature on activation (`:active`)
- style a selected feature (only one allowed)
- style all the selected features (multiple allowed)
- style which have been selected at some point

## Design Alternatives

### Option 1: track state internally

We could define several possible feature interactivity states including `hover` and `active`. We'd listen to mousemove events internally, query for the features under the mouse and update state. Users would access the state using a data-driven expression:
```js
"fill-color": ["case",
["state": "hover"],
"#ff0000",
"#000000"]
```

Open questions:
- does only the topmost feature get hovered on?
- or all features under the mouse?
- or the topmost excluding layers with no event listeners? with some `"pointer-events": "none"` equivalent?
- could this handle selection?
- would this highlight the feature in this layer only (fill but not label)? or all layers (fill and label)? could you choose?
- besides `hover` and `active`, what would we want to support? `clicked`? `focus`? `target`?

Advantages:
- completely declarative
- makes basic cases easy to add
- handles both mouse and touch events properly automatically
- could potentially have special treatment in `Studio`

Disadvantages:
- hard/impossible to cover all the use cases described at the top
- can't support things like various possible approaches to selection without expanding the spec a lot
- will never support everything users want to do

### Option 2: provide the tools to let users track state themselves
We would have to:
- provide event listeners for individual features!
- provide a way to update feature properties (#6020)
We would *maybe* have to:
- provide some way of preventing other event listeners from being called (`stopPropagation`)
- support data joining

```
"fill-color": ["case",
["get": "isSelected"],
"#ff0000",
"#000000"]
```

#### multi-selection
```js
map.on('click', 'layerid', featureSelector, (e) => {
e.feature.properties.isSelected = !e.feature.properties.isSelected;
e.feature.updateProperties();
});
```

#### single-selection

```js
let currentlySelected = null;

map.on('click', 'layerid', featureSelector, (e) => {
// toggle selection
e.feature.properties.isSelected = !e.feature.properties.isSelected;

if (currentlySelected) {
// unselect previously selected feature
currentlySelected.properties.isSelected = false;
currentlySelected.updateProperties();
currentlySelected = null;
}

if (e.feature.properties.isSelected) {
currentlySelected = e.feature;
}
});
```

Open questions:
- what order are events called? is this backwards compatible?
- what information are events called with?
- do we want to create "RenderedFeature" objects that are provided by events?
- how are listeners assigned to individual features? with selectors?
- can events be cancelled?
- what does the api for updating feature properties look like?

Advantages:
- can support a huge amount of possibilities with a smaller api

Disadvantages:
- not declarative
- requires more effort from the user
- handling multiple intertwined interactions could get messy
- need to handle desktop/mobile differences yourself
- user bugs can leave you with corrupted feature state

## Design

I think we need to implement Option 2 to cover all the cases we want to cover. But I think it makes sense to also implement Option 1 to provide a solid foundation for basic use cases. So... both?

### Concepts

Option 1 introduces new concepts like *hover* and *active* which would need to be documented. Option 2 would introduce more complexity around events.

**What existing precedents support the new concepts?** Pseudo-classes from the web are a precedent for Option 1. Interactivity event listeners in Option 2 have precedents both on the web and mobile.

@kkaefer @asheemmamoowala @mollymerp @anandthakker @mourner @lucaswoj

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No implementation files, tests, or entry points are named. Start by reviewing #6020 and the Option 1 and Option 2 alternatives, then resolve the listed API and interaction questions; done requires an agreed, implementable approach, which this RFC does not currently define.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
frontend, web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.