UX: improves chat message long press and touch (#21984)
This commit attempts to refactor our long press logic to make it more resilient and precise. With this improvement two very UX/UI changes have been made: - scale animation on long press - prevents click on reaction to propagate to the message which would cause the active state of the message to trigger
This commit is contained in:
parent
482ef0782d
commit
6513ca69da
|
@ -47,17 +47,18 @@ export default class ChatMessageReaction extends Component {
|
|||
|
||||
@action
|
||||
handleTouchStart(event) {
|
||||
event.stopPropagation();
|
||||
this.handleClick(event);
|
||||
}
|
||||
|
||||
@action
|
||||
handleClick() {
|
||||
handleClick(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
this.args.onReaction?.(
|
||||
this.args.reaction.emoji,
|
||||
this.args.reaction.reacted ? "remove" : "add"
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get popoverContent() {
|
||||
|
|
|
@ -11,12 +11,14 @@
|
|||
{{did-insert this.decorateCookedMessage}}
|
||||
{{did-update this.decorateCookedMessage @message.id}}
|
||||
{{did-update this.decorateCookedMessage @message.version}}
|
||||
{{on "touchmove" this.handleTouchMove passive=true}}
|
||||
{{on "touchstart" this.handleTouchStart passive=true}}
|
||||
{{on "touchend" this.handleTouchEnd}}
|
||||
{{on "mouseenter" this.onMouseEnter passive=true}}
|
||||
{{on "mouseleave" this.onMouseLeave passive=true}}
|
||||
{{on "mousemove" this.onMouseMove passive=true}}
|
||||
{{chat/on-long-press
|
||||
this.handleLongPressStart
|
||||
this.handleLongPressEnd
|
||||
this.onLongPressCancel
|
||||
}}
|
||||
class={{concat-class
|
||||
"chat-message-container"
|
||||
(if this.pane.selectingMessages "selecting-messages")
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { action } from "@ember/object";
|
||||
import Component from "@glimmer/component";
|
||||
import I18n from "I18n";
|
||||
|
@ -258,47 +257,19 @@ export default class ChatMessage extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
handleTouchStart(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
// if zoomed don't track long press
|
||||
if (isZoomed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when testing this must be triggered immediately because there
|
||||
// is no concept of "long press" there, the Ember `tap` test helper
|
||||
// does send the touchstart/touchend events but immediately, see
|
||||
// https://github.com/emberjs/ember-test-helpers/blob/master/API.md#tap
|
||||
if (isTesting()) {
|
||||
this._handleLongPress();
|
||||
}
|
||||
|
||||
this._touchStartAt = Date.now();
|
||||
this._isPressingHandler = discourseLater(this._handleLongPress, 500);
|
||||
handleLongPressStart(element) {
|
||||
element.classList.add("is-long-pressed");
|
||||
}
|
||||
|
||||
@action
|
||||
handleTouchMove(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
cancel(this._isPressingHandler);
|
||||
onLongPressCancel(element) {
|
||||
element.classList.remove("is-long-pressed");
|
||||
}
|
||||
|
||||
@action
|
||||
handleTouchEnd(event) {
|
||||
event.stopPropagation();
|
||||
handleLongPressEnd(element) {
|
||||
element.classList.remove("is-long-pressed");
|
||||
|
||||
// this is to prevent the long press to register as a click
|
||||
if (Date.now() - this._touchStartAt >= 500) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
cancel(this._isPressingHandler);
|
||||
}
|
||||
|
||||
@action
|
||||
_handleLongPress() {
|
||||
if (isZoomed()) {
|
||||
// if zoomed don't handle long press
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import Modifier from "ember-modifier";
|
||||
import { registerDestructor } from "@ember/destroyable";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
|
||||
function cancelEvent(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
export default class ChatOnLongPress extends Modifier {
|
||||
@service capabilities;
|
||||
@service site;
|
||||
|
||||
constructor(owner, args) {
|
||||
super(owner, args);
|
||||
registerDestructor(this, (instance) => instance.cleanup());
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this.capabilities.touch && this.site.mobileView;
|
||||
}
|
||||
|
||||
modify(element, [onLongPressStart, onLongPressEnd, onLongPressCancel]) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
this.onLongPressStart = onLongPressStart || (() => {});
|
||||
this.onLongPressEnd = onLongPressEnd || (() => {});
|
||||
this.onLongPressCancel = onLongPressCancel || (() => {});
|
||||
|
||||
element.addEventListener("touchstart", this.handleTouchStart, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
onCancel() {
|
||||
cancel(this.timeout);
|
||||
this.element.removeEventListener("touchmove", this.onCancel);
|
||||
this.element.removeEventListener("touchend", this.onCancel);
|
||||
this.element.removeEventListener("touchcancel", this.onCancel);
|
||||
this.onLongPressCancel(this.element);
|
||||
}
|
||||
|
||||
@bind
|
||||
handleTouchStart(event) {
|
||||
if (event.touches.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onLongPressStart(this.element, event);
|
||||
|
||||
this.element.addEventListener("touchmove", this.onCancel);
|
||||
this.element.addEventListener("touchend", this.onCancel);
|
||||
this.element.addEventListener("touchcancel", this.onCancel);
|
||||
|
||||
this.timeout = discourseLater(() => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onLongPressEnd(this.element, event);
|
||||
this.element.addEventListener("touchend", cancelEvent, {
|
||||
once: true,
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
|
@ -8,4 +8,25 @@
|
|||
@include user-select(none);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-container {
|
||||
transition: transform 400ms;
|
||||
transform: scale(1);
|
||||
|
||||
&.is-long-pressed {
|
||||
animation: scale-animation 400ms;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-animation {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
80% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue