mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: toasts improvements (#24046)
- more subtle animation when showing a toast - resumes auto close when removing the mouse from the toast - correctly follows reduced motion - uses output with role status as element: https://web.dev/articles/building/a-toast-component - shows toasts inside a section element - prevents toast to all have the same width - fixes a bug on mobile where we would limit the width and the close button wouldn't show correctly aligned I would prefer to have tests for this, but the conjunction of css/animations and our helper changing `discourseLater` to 0 in tests is making it quite challenging for a rather low value. We have system specs using toasts ensuring they show when they should.
This commit is contained in:
parent
2dc9c1b478
commit
552cf56afe
@ -1,27 +1,88 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { on } from "@ember/modifier";
|
import { registerDestructor } from "@ember/destroyable";
|
||||||
|
import { cancel } from "@ember/runloop";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import Modifier from "ember-modifier";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import discourseLater from "discourse-common/lib/later";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
|
const CSS_TRANSITION_DELAY_MS = 300;
|
||||||
|
const TRANSITION_CLASS = "-fade-out";
|
||||||
|
|
||||||
|
class AutoCloseToast extends Modifier {
|
||||||
|
element;
|
||||||
|
close;
|
||||||
|
duration;
|
||||||
|
transitionLaterHandler;
|
||||||
|
closeLaterHandler;
|
||||||
|
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
|
||||||
|
registerDestructor(this, (instance) => instance.cleanup());
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(element, _, { close, duration }) {
|
||||||
|
this.element = element;
|
||||||
|
this.close = close;
|
||||||
|
this.duration = duration;
|
||||||
|
this.element.addEventListener("mouseenter", this.stopTimer, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
this.element.addEventListener("mouseleave", this.startTimer, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
startTimer() {
|
||||||
|
this.transitionLaterHandler = discourseLater(() => {
|
||||||
|
this.element.classList.add(TRANSITION_CLASS);
|
||||||
|
|
||||||
|
this.closeLaterHandler = discourseLater(() => {
|
||||||
|
this.close();
|
||||||
|
}, CSS_TRANSITION_DELAY_MS);
|
||||||
|
}, this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
stopTimer() {
|
||||||
|
cancel(this.transitionLaterHandler);
|
||||||
|
cancel(this.closeLaterHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.stopTimer();
|
||||||
|
this.element.removeEventListener("mouseenter", this.stopTimer);
|
||||||
|
this.element.removeEventListener("mouseleave", this.startTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class DToasts extends Component {
|
export default class DToasts extends Component {
|
||||||
@service toasts;
|
@service toasts;
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="fk-d-toasts">
|
<section class="fk-d-toasts">
|
||||||
{{#each this.toasts.activeToasts as |toast|}}
|
{{#each this.toasts.activeToasts as |toast|}}
|
||||||
<div
|
<output
|
||||||
role={{if toast.options.autoClose "status" "log"}}
|
role={{if toast.options.autoClose "status" "log"}}
|
||||||
key={{toast.id}}
|
key={{toast.id}}
|
||||||
class={{concatClass "fk-d-toast" toast.options.class}}
|
class={{concatClass "fk-d-toast" toast.options.class}}
|
||||||
{{(if toast.options.autoClose (modifier toast.registerAutoClose))}}
|
{{(if
|
||||||
{{on "mouseenter" toast.cancelAutoClose}}
|
toast.options.autoClose
|
||||||
|
(modifier
|
||||||
|
AutoCloseToast close=toast.close duration=toast.options.duration
|
||||||
|
)
|
||||||
|
)}}
|
||||||
>
|
>
|
||||||
<toast.options.component
|
<toast.options.component
|
||||||
@data={{toast.options.data}}
|
@data={{toast.options.data}}
|
||||||
@close={{toast.close}}
|
@close={{toast.close}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</output>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,6 @@ import DDefaultToast from "float-kit/components/d-default-toast";
|
|||||||
export const TOAST = {
|
export const TOAST = {
|
||||||
options: {
|
options: {
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
forceAutoClose: false,
|
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
component: DDefaultToast,
|
component: DDefaultToast,
|
||||||
},
|
},
|
||||||
|
@ -1,38 +1,14 @@
|
|||||||
import { setOwner } from "@ember/application";
|
import { setOwner } from "@ember/application";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { cancel } from "@ember/runloop";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { modifier } from "ember-modifier";
|
|
||||||
import uniqueId from "discourse/helpers/unique-id";
|
import uniqueId from "discourse/helpers/unique-id";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
|
||||||
import { TOAST } from "float-kit/lib/constants";
|
import { TOAST } from "float-kit/lib/constants";
|
||||||
|
|
||||||
const CSS_TRANSITION_DELAY_MS = 500;
|
|
||||||
const TRANSITION_CLASS = "-fade-out";
|
|
||||||
|
|
||||||
export default class DToastInstance {
|
export default class DToastInstance {
|
||||||
@service toasts;
|
@service toasts;
|
||||||
|
|
||||||
options = null;
|
options = null;
|
||||||
id = uniqueId();
|
id = uniqueId();
|
||||||
autoCloseHandler = null;
|
|
||||||
|
|
||||||
registerAutoClose = modifier((element) => {
|
|
||||||
let innerHandler;
|
|
||||||
|
|
||||||
this.autoCloseHandler = discourseLater(() => {
|
|
||||||
element.classList.add(TRANSITION_CLASS);
|
|
||||||
|
|
||||||
innerHandler = discourseLater(() => {
|
|
||||||
this.close();
|
|
||||||
}, CSS_TRANSITION_DELAY_MS);
|
|
||||||
}, this.options.duration || TOAST.options.duration);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancel(innerHandler);
|
|
||||||
cancel(this.autoCloseHandler);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(owner, options = {}) {
|
constructor(owner, options = {}) {
|
||||||
setOwner(this, owner);
|
setOwner(this, owner);
|
||||||
@ -43,14 +19,4 @@ export default class DToastInstance {
|
|||||||
close() {
|
close() {
|
||||||
this.toasts.close(this);
|
this.toasts.close(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
cancelAutoClose() {
|
|
||||||
if (this.options.forceAutoClose) {
|
|
||||||
// Return early so that we do not cancel the autoClose timer.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(this.autoCloseHandler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ export default class Toasts extends Service {
|
|||||||
* @param {Object} [options] - options passed to the toast component as `@toast` argument
|
* @param {Object} [options] - options passed to the toast component as `@toast` argument
|
||||||
* @param {String} [options.duration] - The duration (ms) of the toast, will be closed after this time
|
* @param {String} [options.duration] - The duration (ms) of the toast, will be closed after this time
|
||||||
* @param {Boolean} [options.autoClose=true] - When true, the toast will autoClose after the duration
|
* @param {Boolean} [options.autoClose=true] - When true, the toast will autoClose after the duration
|
||||||
* @param {Boolean} [options.forceAutoClose=false] - When true, toast will still autoClose following mouseover event
|
|
||||||
* @param {ComponentClass} [options.component] - A component to render, will use `DDefaultToast` if not provided
|
* @param {ComponentClass} [options.component] - A component to render, will use `DDefaultToast` if not provided
|
||||||
* @param {String} [options.class] - A class added to the d-toast element
|
* @param {String} [options.class] - A class added to the d-toast element
|
||||||
* @param {Object} [options.data] - An object which will be passed as the `@data` argument to the component
|
* @param {Object} [options.data] - An object which will be passed as the `@data` argument to the component
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
.fk-d-default-toast {
|
.fk-d-default-toast {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
max-width: 350px;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
&__close-container {
|
&__close-container {
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
@keyframes d-toast-opening {
|
@keyframes d-toast-opening {
|
||||||
0% {
|
from {
|
||||||
transform: translateX(0px);
|
transform: translateY(var(--transform-y, 10px));
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateX(-5px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(0px);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fk-d-toasts {
|
.fk-d-toasts {
|
||||||
|
--transform-y: 0;
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
@ -18,6 +14,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px 0;
|
gap: 5px 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
.mobile-view & {
|
.mobile-view & {
|
||||||
left: 5px;
|
left: 5px;
|
||||||
@ -34,7 +31,12 @@
|
|||||||
box-shadow: var(--shadow-menu-panel);
|
box-shadow: var(--shadow-menu-panel);
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
display: flex;
|
display: flex;
|
||||||
animation: d-toast-opening 0.5s ease-in-out;
|
animation: d-toast-opening 0.3s ease-in-out;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
|
.desktop-view & {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--primary-300);
|
border-color: var(--primary-300);
|
||||||
@ -49,3 +51,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.fk-d-toasts {
|
||||||
|
--transform-y: 2vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user