we should make all of these possible somehow:
:active)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:
"fill-color": ["case",
["state": "hover"],
"#ff0000",
"#000000"]
Open questions:
"pointer-events": "none" equivalent?hover and active, what would we want to support? clicked? focus? target?Advantages:
StudioDisadvantages:
We would have to:
stopPropagation)"fill-color": ["case",
["get": "isSelected"],
"#ff0000",
"#000000"]
map.on('click', 'layerid', featureSelector, (e) => {
e.feature.properties.isSelected = !e.feature.properties.isSelected;
e.feature.updateProperties();
});
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:
Advantages:
Disadvantages:
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?
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
I like the idea of doing both, implementing Option 1 using the primitives defined by Option 2.
If we implement only Option 2, I suspect we will disappoint the majority of users, who just want a good-enough out-of-the-box solution for feature interactivity.
If we implement only Option 1, I suspect we will disappoint our power users, who may run into hard limitations with our feature interactivity system.
I wonder if there isn't an Option 3 that's fully declarative and supports all the listed use cases...
/cc @mapbox/studio
Capturing our discussion from last week:
It seems like we're leaning towards doing a combination of 1 and 2:
On 1)
Would states would we track? (hover? active? selected?)
What are the details of when they are set and not?
How does this work or not work on mobile?
Which layers can receive events? Is there a way to explicitly enable/disable this?
We talked about how event though there are a lot of possible types of selectors (checkbox, radio, etc) a simple "zero or one features are selected" would cover a lot of map use cases and might be worth adding.
@mollymerp is looking into this ^
On 2)
We agreed that generally this is needed, but a lot of the questions remain open:
Is there really a clear advantage to per-feature handlers over layer handlers? (I think so, but need to consider this more)
What is the event propagation flow? Can propagation be stopped?
Returning a a feature object that wraps the id vs returning the id directly?
I'm looking into these ^
Below is my proposal for the state tracking work. Very interested in folks' feedback/questions/comments:
setFilter / setData approaches to styling features based on interactivity.$state a protected property name for âinteractive enabledâ features$state property value is tracked and mutated only internally (i.e. not available for runtime property updates)possible values of $state:
default / inactive â donât actually think this is needed â can use $state: null/undefined or remove the $state key from a features properties entirelyhover â triggered by mouseover/mouseleave on web, âshortâ touch? for mobilehighlight)active/selected â triggered by click on web, long(er) touch on mobile, reset when another feature is selectedvisited â not sure on this one, because multiple features could have this state in a given layer and it might add more complexity than weâre comfortable withonly zero or one feature per layer can have $state: hover at any given time
hover feature is reset to inactive / default / null when a new feature is hovered over_or_ the mouse/touch is on an area with no features$state: active at any given timeactive feature is reset to inactive / default / null when a new feature is clicked/touchedoptions:
$state propertylayer style-spec (like interactive: true for example) â could add the ability to easily toggle interactivity at runtime? make $state a protected property name for âinteractive enabledâ features
Would exposing this as an expression (["state"]) avoid the need to protect a property name?
possible values of
$state
With CSS pseudo-classes it is possible for an element to be two things at once, for example both :hover and :visited. Do you think this makes sense or are you thinking it would be better for a feature to have only one at a time? I think the overlap might make sense
Which layers can receive events? Is there a way to explicitly enable/disable this?
options:
...
Which of these do you think we should do? would a combination make sense or would that be weird?
Would exposing this as an expression (["state"]) avoid the need to protect a property name?
I was thinking that if we want gl-js to have full control over $state it would make sense to have it protected (e.g. disallow / ignore user-defined feature $state properties) â if users want to programmatically control a state property they could do so but be responsible for managing their event listeners/data updates. But this is an unnecessary limitation đ€ I was just thinking it could get messy if user-defined event listeners conflicted with internally managed gl-js handlers...
With CSS pseudo-classes it is possible for an element to be two things at once, for example both :hover and :visited. Do you think this makes sense or are you thinking it would be better for a feature to have only one at a time? I think the overlap might make sense
hmm yeah I didn't consider this... do you think it makes sense to store state in multiple property keys then? (e.g. hover: true etc)
Which of these do you think we should do? would a combination make sense or would that be weird?
well option 2 doesn't really make sense by itself â so maybe a combination would make sense for ease-of-use. instead of disabling a hover effect by completely overwriting each interactive property's style rules to remove references to tracked state properties, being able to have a one-liner to enable/disable all interactive properties seems useful
Which layers can receive events? Is there a way to explicitly enable/disable this?
If the hover effects are enabled by explicit requests, does this prevent us from having two features with the same ID on different layers be in a given state at the same time? i.e Can I have a polygon and its label (using a point feature on a separate layer) be hovered together using the style-spec, or does this require using event handlers?
Would exposing this as an expression (["state"]) avoid the need to protect a property name?
In the same way that we moved away from $type in favor of ["geometry-type"], I think this is the route we should go here.
possible values of $state
Given @ansis 's point above about multiple states being possible simultaneously, what about having a separate boolean expression for each kind of state: ["highlighted"], ["active"], ["visited"], etc.? I'm not convinced that the more general state is adding value.
visited â not sure on this one, because multiple features could have this state in a given layer and it might add more complexity than weâre comfortable with
Yeah, I'm hesitant about this one, too: this feels like a more complicated and less well-defined type of state. Is a feature always visited once it's been clicked? "Visited" since when? Can the user programmatically "reset" the map's set of visited features? Etc. I think we can come back to this in the future if we start to see a clear set of use cases for which a precise design could emerge.
@asheemmamoowala
If the hover effects are enabled by explicit requests,
don't understand this bit but,
Can I have a polygon and its label (using a point feature on a separate layer) be hovered together using the style-spec, or does this require using event handlers?
yes â I was assuming so â the symbol layer would just have an expression for whatever paint property would be changed on hover as well.
In the same way that we moved away from $type in favor of ["geometry-type"], I think this is the route we should go here.
@anandthakker is the main difference here that geometry-type is a feature-level property and not nested within properties?
Let's keep state and properties fully distinct and independent, both in how they are accessed in expressions, and how they are stored internally. (AKA follow the React model.)
is the main difference here that geometry-type is a feature-level property and not nested within properties?
The difference I was referring to was between having a 'special' feature property $state, accessed like ["get", "$state"] (or ["get", "$highlighted"]), versus having a separate expression like ["state"] (or ["highlighted"]).
hoverâ triggered bymouseover/mouseleaveon web, âshortâtouch? for mobile
- consider renaming to something that works cross-platform (possibly
highlight)
active/selectedâ triggered byclickon web, long(er)touchon mobile, reset when another feature is selected
I get that hover is highly desirable for the Web, but itâs worth noting the fraught history of hover on the mobile Web, since thereâs no concept of hovering in native applications in the vast majority of mobile form factors (styluses notwithstanding). For example, if a webpage applies a :hover ruleset to a link on a webpage, WebKit on iOS will require two taps, one to âhoverâ the link and another to follow it.
âHoverâ is effectively an âactiveâ state. This behavior allows whatever was associated with :hover to take effect, avoiding information loss, but it also frustrates the user by making the interface feel less responsive. Even âactiveâ is questionable on mobile devices. For example, iOS only has a concept of keyboard focus when a keyboard is attached. Otherwise, in WebKit, âactiveâ is either omitted in favor of âhoverâ or appears only briefly â similar to mousedown on desktops but unlike what tabbing to an element does on a desktop.
Our existing hover examples illustrate the problem well:
At a minimum, the style specification would need a way to distinguish between these intentions. Even if we rename âhoverâ to something less obviously desktop-centric, weâd still need some sort of media query syntax so that developers can choose whether to associate this state with an initial tap on mobile devices. When designing for a touch-enabled device, itâs important to make tappable features always appear tappable from the moment they appear, but that means the effect has to be more subtle than a typical highlight effect. For example, on a desktop, âGet features under the mouse pointerâ might highlight a feature only on hover, whereas on a phone, it might subtly outline the feature at all times, since it isnât possible to scrub a cursor over the map to uncover hidden hit targets.
If we go down the route of media queries, why not rework this proposal into a more general framework for state tracking? The style specification would allow arbitrarily named states, and it would be up to the developer to associate these states with predefined events at runtime via an API. The set of events could vary by platform. This would keep the style JSON file format platform-agnostic while also making it possible for mobile platforms to associate states with mobile-specific gestures like force-touch.
/ref https://github.com/mapbox/mapbox-gl-js/issues/200#issuecomment-360710890
Let's keep state and properties fully distinct and independent, both in how they are accessed in expressions, and how they are stored internally. (AKA follow the React model.)
The link explains that in React "Props are set by the parent and they are fixed throughout the lifetime of a component. For data that is going to change, we have to use state."
@jfirebaugh Do you think we should disallow updating feature properties completely? If a user wants to implement a custom myselected state/prop would this go in the state namespace? Or could it be a property? I'm seeing two possibilities:
It would be nice to be able to update feature properties piecemeal, but I see that as a distinct feature, unrelated to feature interactivity (https://github.com/mapbox/geojson-vt/issues/26).
I think there should be an independent API for updating state, under application control. setData should not set any state. (Except that it might reset it to the empty state, unless we come up with a way to track feature identity across calls to setData.) Nor should vector tiles or GeoJSON be allowed to carry any state.
I'm not sure what you mean by "internally tracked state". hover/active/selected? I think @1ec5 made a convincing case that we should not attempt to track these states automatically.
One major reason to keep properties and state fully separated in this way is to avoid introducing for source data the gnarly refresh/merge challenge we've hit with styles in https://github.com/mapbox/mapbox-gl-js/issues/4225#issuecomment-366089420.
It would be nice to be able to update feature properties piecemeal, but I see that as a distinct feature, unrelated to feature interactivity
Thanks, would there be functional differences between state and properties or would it be mostly just convention? Would state be supported by vectortile and geojson sources while property updates would be geojson-only?
I'm not sure what you mean by "internally tracked state". hover/active/selected? I think @1ec5 made a convincing case that we should not attempt to track these states automatically.
Yep, that's what I meant. State that could potentially be set automatically without the user adding any code
@1ec5 we talked about this a bit a week ago, but could you expand on your thoughts here? I think I remember you saying that these kinds of states might sometimes be platform specific, but that having them could still be useful? and that the iOS sdk has a bit of precedent for this with the built-in annotation selection tracking?
I think @1ec5 made a convincing case that we should not attempt to track these states automatically
I think @1ec5 's argument suggests that we shouldn't _specify_ particular interaction states in the spec, not necessarily that we shouldn't track them automatically (possibly in platform-specific ways)
I'm not sure I understand the distinction. If we track some state automatically, we'll need to document that behavior, including the platform-specific nuances. Is that not equivalent to "specifying" it?
We'd necessarily want to track some state internally so that a style can be used across platforms without having to write code for each.
If we track some state automatically, we'll need to document that behavior, including the platform-specific nuances. Is that not equivalent to "specifying" it?
I don't think it's exactly equivalent. One way or another a user writing their style sheet has to refer _somewhere_ to know that they can use the word "hover" in fill-color: ["case", ["state"], "hover", "red", "blue"]. Putting hover in the style spec is problematic because it's desktop-centric, but having each SDK document the set of state values it provides seems similar to having each SDK document the types of events it supports.
The alternative of having the SDKs only provide an API for updating state is appealing at one level, but I think part of the problem we're trying to solve here is that it feels like it more lines of code than it should to "just" have features be styled differently on hover/tap/click. To what extent would the lower-level state update API deliver on that problem?
I think there should be an independent API for updating state, under application control.
It sounds like there's agreement on having an arbitrary list of named states, with some being predefined (and implemented) for each platform.
Applications should be responsible for enforcing any rules related to these states. For instance, internal mouse event handlers would be responsible for the following hover behavior:
only zero or one feature per layer can have $state: hover at any given time
what about having a separate boolean expression for each kind of state: ["highlighted"], ["active"], ["visited"], etc.? I'm not convinced that the more general state is adding value.
Whether or not we allow externally defined states, the need for different states per-platform makes it hard to support a separate expression for each kind of state. If features could be in multiple states at the same time, then the $state property would need to be treated as an array, which might help with expressing expressions đ.
I think I remember you saying that these kinds of states might sometimes be platform specific, but that having them could still be useful? and that the iOS sdk has a bit of precedent for this with the built-in annotation selection tracking?
Yes, there is some precedent for an SDK tracking state: the iOS and macOS SDKs track which annotation is currently selected. Selection means that the annotation dons its selected appearance and any associated callout (popup) is shown. However, thatâs the extent of it; the other built-in states proposed above get into behaviors that would differ from platform to platform, which is not a problem for selection.
The alternative of having the SDKs only provide an API for updating state is appealing at one level, but I think part of the problem we're trying to solve here is that it feels like it more lines of code than it should to "just" have features be styled differently on hover/tap/click. To what extent would the lower-level state update API deliver on that problem?
Associating a particular state with a particular event handler could be a one-liner on each platform, no? At any rate, it would be less code and hopefully more performant than setting up a gesture recognizer/mouse event listener and querying visible features every time it fires.
If the style specification allows a library to reserve certain states to be associated with platform-specific behaviors (as with hover), then I think my examples above show that weâd need media query expressions to go with that. And media queries wonât solve the problem that a designer in Studio, on the desktop Web, uses states in ways that only make sense on the desktop Web.
Associating a particular state with a particular event handler could be a one-liner on each platform, no?
Hm, yeah I suppose so (maybe a two-liner for hover -- mouseenter / mouseleave), as long as the event listening API and state-updating API were designed with this in mind.
Are there any downsides to
hovered, pressed, selected, ...)This would allow users to design feature interactivity in Studio (which I understand to be a major design goal) and each platform to behave idiomatically.
hovered, pressed, selectedpressed, selected pressed, selectedI'm splitting off my thoughts on per-feature event listeners into a separate issue https://github.com/mapbox/mapbox-gl-js/issues/6215 to avoid breaking up the "what is feature state" conversation here.
Bringing over from #6020(comment)
State should be tracked independently of feature data and assigned per source. This would look like:
//Set a feature_id to a named state
map.setState("source", "state", feature_id);
//Clear state
map.setState("source", "state");
//Set one or more feature Ids to a named state
map.setState("source", "state", [feature_id]);
States are tracked per source in the SourceCache where they can be applied to tiles when preparing them for upload before every frame.
Question: Is there a need for an API to un-set a named state on a single feature or set of features , while preserving other features in that state?
State can be referenced in expressions as part of layer paint properties
// Increase opacity for features in the 'highlight' state
"fill-opacity": [ "case", ["state", "highlight"], ["number", 0.9], ["number", 0.5]]
or queried through an API:
map.getState("state");
// Where the returned object looks like:
{
"source_A" : { "highlight": [1234, ...], "dragging" : [...], ... } ,
"source_B": { "visited" : ["foo", "bar"] }
}
Are there any downsides to
- defining canonical set of feature states (i.e.
hovered,pressed,selected, ...)- exposing them all in Studio
- allowing each platform to support a subset of those states?
This would be reminiscent of how CSS works today: there are certain desktop-centric pseudoclasses built into the language, Web design tools expose them all, and each platform figures out how to map them in appropriate ways. Unfortunately, this approach disadvantages mobile platforms, often making it inconvenient or impossible to access content on a multitouch device. https://github.com/mapbox/mapbox-gl-js/issues/6021#issuecomment-366918031 demonstrates that interactive map features would tend to suffer the same incompatibilities between desktop and mobile devices.
One mitigating factor is that CSS has any-pointer and any-hover media queries (and proprietary predecessors like -moz-touch-enabled), which at least in theory allows developers to remap hovered or pressed behaviors to more touch-friendly affordances. Itâs unclear to me how commonly these media queries are used.
If we were to introduce media query expressions into the style specification, weâd have to coerce designers to take advantage of them. For example, Studio could disallow hover state usage outside a media query expression when the style declares compatibility with the mobile SDKs. Otherwise, I donât see how a GL JSâpowered WYSIWYG environment could possibly encourage designers to give due consideration to mobile users.
As I see it, the choice is essentially to make developers bind states to events:
The first option seems to me like it would ultimately involve less code (if you count expressions as code). I donât know if thereâs been any thought towards how Studio would simulate feature interactivity, but I think the answer would look very different depending on which option we go with.