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
|
@action
|
||||||
handleTouchStart(event) {
|
handleTouchStart(event) {
|
||||||
event.stopPropagation();
|
this.handleClick(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleClick() {
|
handleClick(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
this.args.onReaction?.(
|
this.args.onReaction?.(
|
||||||
this.args.reaction.emoji,
|
this.args.reaction.emoji,
|
||||||
this.args.reaction.reacted ? "remove" : "add"
|
this.args.reaction.reacted ? "remove" : "add"
|
||||||
);
|
);
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get popoverContent() {
|
get popoverContent() {
|
||||||
|
|
|
@ -11,12 +11,14 @@
|
||||||
{{did-insert this.decorateCookedMessage}}
|
{{did-insert this.decorateCookedMessage}}
|
||||||
{{did-update this.decorateCookedMessage @message.id}}
|
{{did-update this.decorateCookedMessage @message.id}}
|
||||||
{{did-update this.decorateCookedMessage @message.version}}
|
{{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 "mouseenter" this.onMouseEnter passive=true}}
|
||||||
{{on "mouseleave" this.onMouseLeave passive=true}}
|
{{on "mouseleave" this.onMouseLeave passive=true}}
|
||||||
{{on "mousemove" this.onMouseMove passive=true}}
|
{{on "mousemove" this.onMouseMove passive=true}}
|
||||||
|
{{chat/on-long-press
|
||||||
|
this.handleLongPressStart
|
||||||
|
this.handleLongPressEnd
|
||||||
|
this.onLongPressCancel
|
||||||
|
}}
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-message-container"
|
"chat-message-container"
|
||||||
(if this.pane.selectingMessages "selecting-messages")
|
(if this.pane.selectingMessages "selecting-messages")
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
@ -258,47 +257,19 @@ export default class ChatMessage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTouchStart(event) {
|
handleLongPressStart(element) {
|
||||||
event.stopPropagation();
|
element.classList.add("is-long-pressed");
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTouchMove(event) {
|
onLongPressCancel(element) {
|
||||||
event.stopPropagation();
|
element.classList.remove("is-long-pressed");
|
||||||
|
|
||||||
cancel(this._isPressingHandler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTouchEnd(event) {
|
handleLongPressEnd(element) {
|
||||||
event.stopPropagation();
|
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 (isZoomed()) {
|
||||||
// if zoomed don't handle long press
|
// if zoomed don't handle long press
|
||||||
return;
|
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);
|
@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