-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add utility to handle profusion of clicks #9567
Conversation
@svor, this is an alternative solution to attempting to run a task multiple times (as well as other misinterpretations of clicks). What do you think about this kind of approach? |
@colin-grant-work it looks good to me, thanks! @tsmaeder WDYT? It's related to eclipse-che/che#19821 |
I'd appreciate any thoughts or feedback folks have on this idea. We've had quite a number of issues related to click handling, and this seems like a reasonable approach that we can apply anywhere we need it. |
Just a couple of general thoughts on first approach:
Maybe the interface for clients should be this: onClick(handler) adding a double click handler would be onClicks(2, handler) When handlers for multiple click cardinalities are registered for mouse clicks, the handler with the lowest cardinality (onClick being deemed "0") can decide whether to pass the event to the rest of the handlers. |
Or maybe the correct semantics are like this:
That way, you handlers are exclusive: when you triple click, the single and double click handlers are not invoked. There is always the "immediate" handler that is invoked on any mouse clicked event, regardless of sequence. Since anything < 0.5 secs or so is perceived as immediate, I think we could handle the "execute on single click" scenario that ways, as well. |
Since users of the
It's true, we don't have refer to the
This becomes a question of how far we want to depart from the browser's semantics and our current setup. My approach here is to depart as little as possible while addressing the bugs that have been raised by our users / adopters. The particular problem that's arisen is running a single-click handler (with undesired side effects) when users were targeting a double-click, and that can be solved by just not running those handlers. If you triple-click and invoke the double-click and then the single-click, we can at least blame your finger :-). But if we consider that an important use case, we can also accommodate it. |
I've experimented quickly with my ideas from the comments above and came up with this method (never mind clean typing, etc.) protected createNodeAttributes(node: TreeNode, props: NodeProps): React.Attributes & React.HTMLAttributes<HTMLElement> {
const className = this.createNodeClassNames(node, props).join(' ');
const style = this.createNodeStyle(node, props);
return {
className,
style,
onClick: event => {
this.handleClickEvent(node, event);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const myEvent: any = event as any;
if (event.detail === 1) {
if (myEvent.__timeout) {
// the native multi-click timout is shorter than ours: it's sending us two single clicks
console.info(`clearing timeout ${myEvent.__timeout}`);
clearTimeout(myEvent.__timeout);
myEvent.__timeout = undefined;
this.handleDblClickEvent(node, event);
} else {
myEvent.__timeout = setTimeout(() => {
this.handleSingleClickEvent(node, event);
}, 250);
console.info(`setting timeout ${myEvent.__timeout}`);
}
} else if (event.detail === 2) {
console.info(`clearing timeout ${myEvent.__timeout}`);
clearTimeout(myEvent.__timeout);
myEvent.__timeout = undefined;
this.handleDblClickEvent(node, event);
}
},
onContextMenu: event => this.handleContextMenuEvent(node, event)
};
} I think it's a bit more straightforward and it handles the case where native double-click timeout is actually shorter than what we have. One of the main ideas, though is that "click" and "single click" are really not the same thing, so there is "handleClick" which is called immediately and "handleSingleClick" and "handleDoubleClick" which are mutually exclusive. |
let deferredEvent: T | undefined; | ||
return async (event: T): Promise<void> => { | ||
if (isReactEvent(event)) { | ||
event.persist(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've read somewhere that this is a no-op in newer React versions. From my testing, I don't think we need it. (https://reactjs.org/docs/events.html).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like they say that that's the case as of React 17, but we're still on 16.14.0
according to our yarn.lock
. Good to know, though, and something to look forward to :-).
if (invokeImmediately) { | ||
singleClickHandler(event); | ||
} | ||
await new Promise(resolve => setTimeout(resolve, timeout)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about the use of promises here. Wouldn't a simple setTimout(doStuff(), timeout) be simpler? We're not doing anything with the results of the computation anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had hoped that writing the code this way would get around restrictions on the browser's definition of 'user action' (this was one of Anton's original concerns), but it looks like it doesn't achieve that. It looks like there may not really be any way to delay the handling of an event and still count as a user action (without, say, blocking the thread with an infinite loop :-) ), but if you can see any way, I'm open to suggestions.
@colin-grant-work not sure why this is an advantage. |
This looks interesting to me. I especially like the idea of having an explicitly unconditional handler, along with the single- and double-click handlers. I have two comments:
|
Indeed it does. What bothers me in your code is that you're retaining the superseded event in a closure, but since the tree item is re-rendered on the first click, the closure should be a new one and the event variable An alternative would be to save the state in the tree node: as I understand it it is the stable object that is rendered to html. |
Somewhere in the code I have I comment that points out that React code can't use the |
Since the tree widget is widely used in many different timing scenarios, I think it's worth getting this right. The number is attempts of fixing this problem is the best indicator. |
So two rapid clicks on different elements are a double click or two singles? Stop hurting, my poor head! ;-) |
@colin-grant-work any news on this one? |
@tsmaeder, no, alas - other things have been keeping me busy. I can likely put some work into it today, though. |
Signed-off-by: Colin Grant <[email protected]>
Signed-off-by: Colin Grant <[email protected]>
b1a58ff
to
f7581b2
Compare
Signed-off-by: Colin Grant <[email protected]>
f7581b2
to
0455dff
Compare
@tsmaeder, I've rewritten my code to use a class rather than a closure. Your suggestion re: The new implementation also supports immediate invocation and an arbitrary number of handlers, if people want to set up exclusive quintuple click handlers. One potentially controversial decision is that if the user continues clicking madly on a single element, clicks outside the bounds of the defined handlers are basically ignored. Alternatively we could modulo the |
@colin-grant-work The code looks good to me, and it seems like this is a clean solution for the exclusivity problem of (multi-)clicks. However, I am a bit hesitant to fully support the current proposal, mainly because some trees seem quite unresponsive with the default timeout of 500ms in place, especially since a lot of them have no issues handling the first click of a double click as a single click (e.g. the navigator). The Does that make sense to improve the responsiveness? |
@msujew, re: the click timeout length, I agree the 500ms timeout is too high - I would expect most users to want something lower, so we can adjust that, if we'd like. I chose it just because it happens to be the default double-click timeout for Windows. re: the
I think I do like that better than having one action that you do on every click - I can't think of a lot of contexts in which that makes sense. But I think the whole discussion has maybe gotten a bit too complex. If we assume that we really only need to worry about single vs. double click, which I think is a fair assumption, then it boils down to making the single-click responsive enough - we can always invoke the double-click handler immediately. That means we really have two options: invoke single-click immediately, or wait to see if there will be a second click. If we wait, then the only question is how long to wait. In the context of this implementation, I can certainly add an |
I somehow missed the updated code here. I'll have a look as soon as I can (today is busy). |
@tsmaeder : Ping! |
There doesn't seem to be an excess of interest in this suggestion - I'm going to close for now, but it can be revived if there are additional issues with single- and double-click handling. |
What it does
This is my proposal for a way to handle the problem of double clicks triggering single-click handlers (twice). It's a bit of a sketch. A fuller implementation might:
[ ] - Hook the delay up to a preference so that the user could configure it. (This would be fairly straightforward)
[ ] - Abstract over more event types than click and double click, if there are others that suffer similar problems (maybe key chords?).
Other thoughts have been expressed and proposals made in #9546, #8735, #8168. This approach has at least these advantages:
One drawback, noted in the code, is that the
TreeWidget
, where most of the complaints have focused, can't use this code to invoke the single-click handler immediately and then double-click handler once, because invoking the single-click handler triggers a full rerender of the tree, attaching a new listener to the DOM node and so obviating the state stored in the closure around the handler. I've added (in my second commit) an option to call the single-click handler and then the double-click handler, if that's desirable.How to test
Review checklist
Reminder for reviewers
Signed-off-by: Colin Grant [email protected]