Open dandclark opened 2 years ago
The CSS Working Group just discussed dispatching highlight events
.
So something like we were mentioning was:
Highlight
inherit from EventTarget
So that we can dispatch events on them.
Something like:
partial interface HighlightRegistry {
// For parallelism with elementsFromPoint.
// Returned values in (z-order, priority) order.
sequence<Highlight> highlightsFromPoint(long x, long y);
};
The idea is that the page could, either before or after handling DOM events, do something like:
document.addEventListener("click", function(e) {
if (e.defaultPrevented)
return;
for (let highlight of CSS.highlights.highlightsFromPoint(e.clientX, e.clientY)) {
highlight.dispatchEvent(e);
if (e.defaultPrevented)
return;
}
});
touch-action
@dbaron / @flackr pointed out that encouraging document-level event listeners may be problematic (because having something like, e.g, a non-passive pointerdown
event-listener might cause performance issues with scrolling).
@flackr proposed to support touch-action: none
on highlights, so that you can make those event listeners passive as needed.
Per #7512, there are use cases to know the specific ranges that are hit. Which means that maybe the API should do something a bit more complex:
dictionary HighlightHitTestResult {
Highlight highlight;
sequence<AbstrangeRange> ranges;
}
partial interface HighlightRegistry {
// For parallelism with elementsFromPoint.
// Returned values in (z-order, priority) order.
sequence<HighlightHitTestResult> highlightsFromPoint(long x, long y);
};
Or so. We could then encourage to do something like putting the highlights in the expando, something like:
document.addEventListener("click", function(e) {
if (e.defaultPrevented)
return;
for (let { highlight, ranges } of CSS.highlights.highlightsFromPoint(e.clientX, e.clientY)) {
e.ranges = ranges;
highlight.dispatchEvent(e);
if (e.defaultPrevented)
break;
}
delete e.ranges;
});
But that feels rather clunky... Not sure I have a better proposal atm tho.
That example doesn't work. One can't dispatch an event which is already being dispatched.
That example doesn't work. One can't dispatch an event which is already being dispatched.
Hmm, that certainly poses a problem. Unless we'd be willing to tell devs to do setTimeout(() => { e.ranges = ranges; highlight.dispatchEvent(e); }, 0)
in the Document's event handler, I guess we'd need to add another API to hand off the event to Highlights:
partial interface Highlight {
// Once the event has finished dispatching, dispatch it again against this Highlight
// with event.ranges set to ranges.
void handleEventAsync(Event event, sequence<AbstrangeRange> ranges);
}
This would be called in the for
loop of the Document's event handler, instead of calling dispatchEvent
directly. Setting e.ranges
during the initial for
loop no longer works since this is asynchronous, hence the extra ranges
parameter.
Is there a better way to work around this problem? This is becoming sufficiently cumbersome that I'm thinking this approach's initial goal of simplicity is no longer being achieved.
I noticed another problem in the case where the page has two or more highlighting libraries that don't coordinate with each other. In this scenario, both libraries independently set this event listener in order to ensure that events get routed to their highlights:
document.addEventListener("click", function(e) {
if (e.defaultPrevented)
return;
for (let { highlight, ranges } of CSS.highlights.highlightsFromPoint(e.clientX, e.clientY)) {
e.ranges = ranges;
highlight.dispatchEvent(e);
if (e.defaultPrevented)
break;
}
delete e.ranges;
});
If the Highlight
event handlers for both libraries call preventDefault
, then the defaultPrevented
checking means we won't repeat the events. But if neither highlighting library wants to preventDefault
, then each Highlight
will get the event twice for each user interactions: once from each Document "click" handler set by each respective highlighting library.
It seems hard to get this right when there are multiple non-coordinating highlight libraries. I'd like to reconsider whether an approach that provides platform support closer to a traditional event path makes sense here:
[when the user performs a pointer interaction over a highlight range], dispatch an event against the top-priority intersected highlight such that the event path includes any overlapping highlights in descending priority order. Secondly, the "normal" pointer event is dispatched against the originating element. In this case we could consider defining preventDefault on the highlight-dispatched event to prevent the subsequent dispatch of the "normal" pointer event.
So highlight would get something like highlightpointerdown/up events? and if one called preventDefault() on those, pointerdown/up (and possible mousedown/up, touchdown/up) events wouldn't fire?
Another thing to remember, if highlights can be accessed from the event, is that we'd need to tweak event state when events pass shadow DOM boundary, or stop propagation at shadow DOM boundary. Highlights in shadow DOM shouldn't be exposed to light DOM. Perhaps the events just wouldn't be composed.
To me CSS.highlights.highlightsFromPoint() smells like the minimum viable API. Highlights wouldn't need to be event targets. If we later figure out a good way to dispatch events to Highlights themselves, that could be added separately.
So highlight would get something like highlightpointerdown/up events? and if one called preventDefault() on those, pointerdown/up (and possible mousedown/up, touchdown/up) events wouldn't fire?
Basically. It wouldn't need to be new event name though (like highlightpointerdown/up); it could just be a separate instance of pointerdown/pointerup that gets delivered to highlights before the pointerdown/up that gets delivered to DOM elements.
Another thing to remember, if highlights can be accessed from the event, is that we'd need to tweak event state when events pass shadow DOM boundary, or stop propagation at shadow DOM boundary. Highlights in shadow DOM shouldn't be exposed to light DOM. Perhaps the events just wouldn't be composed.
This is a good point. We also need to think this through for CSS.highlights.highlightsFromPoint()
, though. Would it be a problem if that API returned highlights from inside closed shadow DOMs? In a way that would break shadow DOM encapsulation, but it's not necessarily a new issue since those ranges are available via CSS.highlights.get()
anyway. So shadow DOM encapsulation is basically already broken as long as there are active Highlights in the shadow.
Currently document.elementsFromPoint
excludes content inside shadows, so for consistency's sake it would seem appropriate that highlightsFromPoint
do the same. That would make the API useless in a scenario with shadow DOM though -- and in that case it might be better to go straight to a highlight eventing approach that would support shadow DOM properly.
To me CSS.highlights.highlightsFromPoint() smells like the minimum viable API. Highlights wouldn't need to be event targets. If we later figure out a good way to dispatch events to Highlights themselves, that could be added separately.
I'm inclined to agree with this, but only if we can work around the potential performance issues and the shadow DOM questions above, and if the lack of ability to coordinate between different highlight libraries doesn't turn out to be a hard requirement.
I can think of two ways to expose shadow DOM highlights with highlightsFromPoint()
.
The first is to handle it like getInnerHTML()
does for Declarative Shadow DOM, in which it takes an includeShadowRoots
option as well as a list of closed shadow roots so that highlights inside these can be returned without breaking encapsulation: https://web.dev/declarative-shadow-dom/#serialization. Highlights could do something similar:
const hitTestResults = CSS.highlights.highlightsFromPoint({
includeShadowRoots: true,
closedRoots: [shadowRoot1, shadowRoot2, ...]
});
Alternatively, we could put highlightsFromPoint()
on DocumentOrShadowRoot
. When called on a Document, it would return only highlights in that document (not in its shadow roots). When called on a shadow root, it would return only highlights in that shadow, or perhaps highlights in that shadow plus highlights in the document (but not highlights in any nested shadow roots). This is analogous to how elementsFromPoint()
works in Blink and Gecko today, though the spec doesn't yet reflect that elementsFromPoint()
is callable on shadow DOM and I see some cross-browser inconsistencies in which elements are returned.
The CSS Working Group just discussed Approaches for dispatching highlight pointer events
, and agreed to the following:
RESOLVED: Add the highlightFromPoint() API to CSS.highlights or DocumentOrShadowRoot
I filed a new issue (#7766) for the highlightsFromPoint()
shadow DOM question, since there's already a lot going on in this thread.
Hi all, we are planning on creating a spec and implementing the highlightsFromPoint()
API on CSS.highlights
(not DocumentOrShadowRoot
).
The issue of how the API interacts with shadow DOM is detailed in #7766. In summary, we proposed that the API has an optional dictionary parameter with a key that maps to an array of ShadowRoot
objects that can return highlights, similar to caretPositionFromPoint()
and getHTML()
. Please let us know if there are any questions or concerns. Thanks!
cc: @sanketj
7512 discusses whether a new pointer event type should be added for use with
Highlight
s. A related task is to develop the details about how pointer events should be dispatched to highlights.It's important to consider what should happen if there are multiple overlapping highlights. Consider for example an in-page editor implementing find-on-page, where a spell-checking extension is also running. Both might have actions they want to take when the user clicks a highlighted range, for example the find-on-page might want to change the color of the clicked word and update the selection, and the spell-checker might want to show a popup with spelling suggestions. When the spell checker decides to show its popup, it might be reasonable to allow it to block actions that another highlight type might take (like updating find-on-page highlight colors or selection).
With that in mind, there are a few potential approaches for what to do when a user interacts with (potentially overlapping) highlighted content.
Approach A: Dispatch a separate event against each
Highlight
under the cursor, in descending priority order.Additionally, we could define the default action of each event to be the generation of the event for the next Highlight. This way, a Highlight's event handler can call
preventDefault
and prevent events from reaching subsequentHighlight
s.It is a bit suspicious to potentially fire an arbitrary number of pointer events for a single user interaction, but this might be fine.
Approach B: Dispatch a single event against the top-priority highlight, whose event path includes any overlapping highlights in descending priority order, followed by the containing element and its parents.
There's a web compatibility problem with this one. Currently a
PointerEvent
'starget
can only be an element, soPointerEvent
handlers will probably expect this and encounter problems if they receive an event whosetarget
is aHighlight
.The MSEdgeExplainer proposes a variation of Approach B that works around this by setting the event's
target
to the element rather than the Highlight, even though the event path is ordered as if the Highlight is the target. But as @smaug---- points out at https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/588, this raises its own questions about how eventPath would work.I'm also interested in a potential combination of Approach A and Approach B, where we dispatch exactly two events per user action. Firstly, dispatch an event against the top-priority highlight such that the event path includes any overlapping highlights in descending priority order. Secondly, the "normal" pointer event is dispatched against the originating element. In this case we could consider defining
preventDefault
on the highlight-dispatched event to prevent the subsequent dispatch of the "normal" pointer event. This approach seems to avoid a lot of the difficulties around Approach A and Approach B separately, and allows a great deal of flexibility in how Highlights can prevent propagation to lower-priority highlights and to the page content.cc @frivoal @luisjuansp