discourse/app/assets/javascripts/float-kit/addon/components/d-toasts.gjs
Joffrey JAFFEUX 552cf56afe
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.
2023-10-23 15:23:10 +02:00

89 lines
2.3 KiB
Plaintext

import Component from "@glimmer/component";
import { registerDestructor } from "@ember/destroyable";
import { cancel } from "@ember/runloop";
import { inject as service } from "@ember/service";
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;
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 {
@service toasts;
<template>
<section class="fk-d-toasts">
{{#each this.toasts.activeToasts as |toast|}}
<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
)
)}}
>
<toast.options.component
@data={{toast.options.data}}
@close={{toast.close}}
/>
</output>
{{/each}}
</section>
</template>
}