mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 20:36:39 +08:00
FEATURE: add progress bar to toast notifications (#26483)
This change adds a progress bar to toast notifications when autoClose is enabled (true by default). The progress bar allows users to visually see how long is left before the notification disappears. When hovered on desktop, the progress and autoclose timer will be paused, it will resume again once the mouse is moved away from the toast notification.
This commit is contained in:
parent
38d39b6c34
commit
f75d119cd3
|
@ -25,6 +25,27 @@ module(
|
||||||
assert.dom(".fk-d-default-toast__icon-container").doesNotExist();
|
assert.dom(".fk-d-default-toast__icon-container").doesNotExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("progress bar", async function (assert) {
|
||||||
|
this.toast = new DToastInstance(this, {});
|
||||||
|
this.noop = () => {};
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<DDefaultToast @data={{this.toast.options.data}} @autoClose={{true}} @onRegisterProgressBar={{this.noop}} />`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.dom(".fk-d-default-toast__progress-bar").exists();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no progress bar", async function (assert) {
|
||||||
|
this.toast = new DToastInstance(this, {});
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<DDefaultToast @data={{this.toast.options.data}} @autoClose={{false}} />`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.dom(".fk-d-default-toast__progress-bar").doesNotExist();
|
||||||
|
});
|
||||||
|
|
||||||
test("title", async function (assert) {
|
test("title", async function (assert) {
|
||||||
this.toast = new DToastInstance(this, { data: { title: "Title" } });
|
this.toast = new DToastInstance(this, { data: { title: "Title" } });
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { concat, fn, hash } from "@ember/helper";
|
import { concat, fn, hash } from "@ember/helper";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
import { or } from "truth-helpers";
|
import { or } from "truth-helpers";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
@ -12,6 +13,12 @@ const DDefaultToast = <template>
|
||||||
}}
|
}}
|
||||||
...attributes
|
...attributes
|
||||||
>
|
>
|
||||||
|
{{#if @autoClose}}
|
||||||
|
<div
|
||||||
|
class="fk-d-default-toast__progress-bar"
|
||||||
|
{{didInsert @onRegisterProgressBar}}
|
||||||
|
></div>
|
||||||
|
{{/if}}
|
||||||
{{#if @data.icon}}
|
{{#if @data.icon}}
|
||||||
<div class="fk-d-default-toast__icon-container">
|
<div class="fk-d-default-toast__icon-container">
|
||||||
{{icon @data.icon}}
|
{{icon @data.icon}}
|
||||||
|
|
135
app/assets/javascripts/float-kit/addon/components/d-toast.gjs
Normal file
135
app/assets/javascripts/float-kit/addon/components/d-toast.gjs
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { registerDestructor } from "@ember/destroyable";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { cancel } from "@ember/runloop";
|
||||||
|
import Modifier from "ember-modifier";
|
||||||
|
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;
|
||||||
|
progressBar;
|
||||||
|
progressAnimation;
|
||||||
|
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
|
||||||
|
registerDestructor(this, (instance) => instance.cleanup());
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(element, _, { close, duration, progressBar }) {
|
||||||
|
this.element = element;
|
||||||
|
this.close = close;
|
||||||
|
this.duration = duration;
|
||||||
|
this.timeRemaining = duration;
|
||||||
|
this.progressBar = progressBar;
|
||||||
|
this.element.addEventListener("mouseenter", this.stopTimer, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
this.element.addEventListener("mouseleave", this.startTimer, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
startTimer() {
|
||||||
|
this.startProgressAnimation();
|
||||||
|
|
||||||
|
this.transitionLaterHandler = discourseLater(() => {
|
||||||
|
this.element.classList.add(TRANSITION_CLASS);
|
||||||
|
|
||||||
|
this.closeLaterHandler = discourseLater(() => {
|
||||||
|
this.close();
|
||||||
|
}, CSS_TRANSITION_DELAY_MS);
|
||||||
|
}, this.timeRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
stopTimer() {
|
||||||
|
this.pauseProgressAnimation();
|
||||||
|
cancel(this.transitionLaterHandler);
|
||||||
|
cancel(this.closeLaterHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
startProgressAnimation() {
|
||||||
|
if (!this.progressBar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.progressAnimation) {
|
||||||
|
this.progressAnimation.play();
|
||||||
|
this.progressBar.style.opacity = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progressAnimation = this.progressBar.animate(
|
||||||
|
{ transform: `scaleX(0)` },
|
||||||
|
{ duration: this.duration, fill: "forwards" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
pauseProgressAnimation() {
|
||||||
|
if (
|
||||||
|
!this.progressAnimation ||
|
||||||
|
this.progressAnimation.currentTime === this.duration
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progressAnimation.pause();
|
||||||
|
this.progressBar.style.opacity = 0.5;
|
||||||
|
this.timeRemaining = this.duration - this.progressAnimation.currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.stopTimer();
|
||||||
|
this.element.removeEventListener("mouseenter", this.stopTimer);
|
||||||
|
this.element.removeEventListener("mouseleave", this.startTimer);
|
||||||
|
this.progressBar = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DToast extends Component {
|
||||||
|
@tracked progressBar;
|
||||||
|
|
||||||
|
@action
|
||||||
|
registerProgressBar(element) {
|
||||||
|
this.progressBar = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<output
|
||||||
|
role={{if @toast.options.autoClose "status" "log"}}
|
||||||
|
key={{@toast.id}}
|
||||||
|
class={{concatClass "fk-d-toast" @toast.options.class}}
|
||||||
|
{{(if
|
||||||
|
@toast.options.autoClose
|
||||||
|
(modifier
|
||||||
|
AutoCloseToast
|
||||||
|
close=@toast.close
|
||||||
|
duration=@toast.options.duration
|
||||||
|
progressBar=this.progressBar
|
||||||
|
)
|
||||||
|
)}}
|
||||||
|
>
|
||||||
|
<@toast.options.component
|
||||||
|
@data={{@toast.options.data}}
|
||||||
|
@close={{@toast.close}}
|
||||||
|
@autoClose={{@toast.options.autoClose}}
|
||||||
|
@onRegisterProgressBar={{this.registerProgressBar}}
|
||||||
|
/>
|
||||||
|
</output>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,64 +1,6 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { registerDestructor } from "@ember/destroyable";
|
|
||||||
import { cancel } from "@ember/runloop";
|
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import Modifier from "ember-modifier";
|
import DToast from "float-kit/components/d-toast";
|
||||||
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;
|
||||||
|
@ -66,22 +8,7 @@ export default class DToasts extends Component {
|
||||||
<template>
|
<template>
|
||||||
<section class="fk-d-toasts">
|
<section class="fk-d-toasts">
|
||||||
{{#each this.toasts.activeToasts as |toast|}}
|
{{#each this.toasts.activeToasts as |toast|}}
|
||||||
<output
|
<DToast @toast={{toast}} />
|
||||||
role={{if toast.options.autoClose "status" "log"}}
|
|
||||||
key={{toast.id}}
|
|
||||||
class={{concatClass "fk-d-toast" toast.options.class}}
|
|
||||||
{{(if
|
|
||||||
toast.options.autoClose
|
|
||||||
(modifier
|
|
||||||
AutoCloseToast close=toast.close duration=toast.options.duration
|
|
||||||
)
|
|
||||||
)}}
|
|
||||||
>
|
|
||||||
<toast.options.component
|
|
||||||
@data={{toast.options.data}}
|
|
||||||
@close={{toast.close}}
|
|
||||||
/>
|
|
||||||
</output>
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -70,6 +70,20 @@
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 5px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--success);
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fk-d-default-toast:has(&__progress-bar) {
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
&__texts {
|
&__texts {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -96,5 +110,7 @@
|
||||||
|
|
||||||
&__message {
|
&__message {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: var(--primary-high);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user