Toast.tsx 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import React from "react";
  2. import { render } from "react-dom";
  3. import Stack from "./Stack";
  4. import { Island } from "./Island";
  5. import "./Toast.css";
  6. import { close } from "./icons";
  7. const TOAST_TIMEOUT = 7000;
  8. function ToastRenderer(props: {
  9. toasts: Map<number, React.ReactNode>;
  10. onCloseRequest: (id: number) => void;
  11. }) {
  12. return (
  13. <Stack.Col gap={2} align="center">
  14. {[...props.toasts.entries()].map(([id, toast]) => (
  15. <Island key={id} padding={3}>
  16. <div className="Toast">
  17. <div className="Toast__content">{toast}</div>
  18. <button
  19. className="Toast__close"
  20. onClick={() => props.onCloseRequest(id)}
  21. >
  22. {close}
  23. </button>
  24. </div>
  25. </Island>
  26. ))}
  27. </Stack.Col>
  28. );
  29. }
  30. let toastsRootNode: HTMLDivElement;
  31. function getToastsRootNode() {
  32. return toastsRootNode || (toastsRootNode = initToastsRootNode());
  33. }
  34. function initToastsRootNode() {
  35. const div = window.document.createElement("div");
  36. document.body.appendChild(div);
  37. div.className = "Toast__container";
  38. return div;
  39. }
  40. function renderToasts(
  41. toasts: Map<number, React.ReactNode>,
  42. onCloseRequest: (id: number) => void,
  43. ) {
  44. render(
  45. <ToastRenderer toasts={toasts} onCloseRequest={onCloseRequest} />,
  46. getToastsRootNode(),
  47. );
  48. }
  49. let incrementalId = 0;
  50. function getToastId() {
  51. return incrementalId++;
  52. }
  53. class ToastManager {
  54. private toasts = new Map<number, React.ReactNode>();
  55. private timers = new Map<number, number>();
  56. public push(message: React.ReactNode, shiftAfterMs: number) {
  57. const id = getToastId();
  58. this.toasts.set(id, message);
  59. if (isFinite(shiftAfterMs)) {
  60. const handle = window.setTimeout(() => this.pop(id), shiftAfterMs);
  61. this.timers.set(id, handle);
  62. }
  63. this.render();
  64. }
  65. private pop = (id: number) => {
  66. const handle = this.timers.get(id);
  67. if (handle) {
  68. window.clearTimeout(handle);
  69. this.timers.delete(id);
  70. }
  71. this.toasts.delete(id);
  72. this.render();
  73. };
  74. private render() {
  75. renderToasts(this.toasts, this.pop);
  76. }
  77. }
  78. let toastManagerInstance: ToastManager;
  79. function getToastManager(): ToastManager {
  80. return toastManagerInstance ?? (toastManagerInstance = new ToastManager());
  81. }
  82. export function push(message: React.ReactNode, manualClose = false) {
  83. const toastManager = getToastManager();
  84. toastManager.push(message, manualClose ? Infinity : TOAST_TIMEOUT);
  85. }