@tsirosgeorge/toastnotification 5.4.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -103,6 +103,24 @@ toast('Are you sure?', {
103
103
  });
104
104
  ```
105
105
 
106
+ ### ⌨️ Keyboard and screen readers
107
+
108
+ Confirm dialogs take focus when they open, keep Tab inside themselves while they are
109
+ open, and hand focus back to whatever opened them when they close. Escape cancels, and
110
+ Enter submits a single-line input. Without this the element that opened the dialog kept
111
+ focus behind the backdrop, so Enter or Space re-triggered it and stacked a second dialog.
112
+
113
+ Toast containers are announced as `aria-live="polite"`; `error` and `warning` toasts get
114
+ `role="alert"`. Everything respects `prefers-reduced-motion`.
115
+
116
+ ### ✋ Closing toasts yourself
117
+
118
+ ```javascript
119
+ const t = toast('Working…', { duration: 0 });
120
+ t.close(); // dismiss this one
121
+ toast.dismissAll(); // dismiss everything; open dialogs settle as a cancel
122
+ ```
123
+
106
124
  ### ⏳ Loading → Update
107
125
  ```javascript
108
126
  const t = toast.loading('Uploading…', { type: 'info' });
@@ -114,12 +132,14 @@ t.update('Done!', { type: 'success', duration: 2000 });
114
132
 
115
133
  | Option | Type | Default | Description |
116
134
  |--------------|------------|---------------|-----------------------------------------------------------------------------------------------|
117
- | `position` | `string` | `'top-right'` | Container position. Values: `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'`, `'top-center'`, `'bottom-center'`. |
135
+ | `position` | `string` | `'top-right'` | Container position. Values: `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'`, `'top-center'`, `'bottom-center'`, `'center'`. `'center'` places it dead centre of the viewport and defaults to the `zoom-in` animation; it works for plain toasts and for confirm dialogs. |
118
136
  | `animation` | `string` | `'slide-right'` | Show animation. Examples: `'slide-right'`, `'slide-left'`, `'slide-top'`, `'slide-bottom'`, `'zoom-in'`. Hide uses reverse automatically. |
119
137
  | `type` | `string` | `'info'` | Type of toast, controlling icon and styling. Possible values: `'info'`, `'success'`, `'error'`, `'warning'`. |
120
138
  | `duration` | `number` | `3000` | Duration in milliseconds before the toast automatically dismisses. |
121
139
  | `icon` | `string` or `null` | `null` | Optional custom icon displayed as text (e.g., emoji) before the toast message. If not set, a default GIF icon is used based on the `type`. |
122
140
  | `showLoader` | `boolean` | `false` | Whether to show a loader/progress bar animation on the toast during its visible duration. |
141
+ | `allowHtml` | `boolean` | `true` | `message` is written as HTML. Pass `false` to render it as plain text — do that for anything user-supplied, or you have an XSS hole. |
142
+ | `closeOnEscape` | `boolean` | `true` | Confirm dialogs only. Escape cancels the dialog. |
123
143
  | `onClick` | `function` or `null` | `null` | Callback function executed when the toast is clicked. |
124
144
  | `onShow` | `function` or `null` | `null` | Callback function executed when the toast appears (after it's added to the DOM and shown). |
125
145
  | `onDismiss` | `function` or `null` | `null` | Callback function executed when the toast is dismissed and removed from the DOM. |
@@ -52,6 +52,14 @@
52
52
  align-items: center;
53
53
  }
54
54
 
55
+ /* Dead centre of the viewport, for alerts that should not sit in a corner. */
56
+ .ts-toast-container.center {
57
+ top: 50%;
58
+ left: 50%;
59
+ transform: translate(-50%, -50%);
60
+ align-items: center;
61
+ }
62
+
55
63
  /* General Toast styles */
56
64
  .ts-toast-container .ts-toast {
57
65
  display: flex;
@@ -334,6 +342,7 @@
334
342
  .ts-toast-overlay.bottom-left { align-items: flex-end; justify-content: flex-start; padding: 1rem; }
335
343
  .ts-toast-overlay.bottom-center { align-items: flex-end; justify-content: center; padding: 1rem; }
336
344
  .ts-toast-overlay.bottom-right { align-items: flex-end; justify-content: flex-end; padding: 1rem; }
345
+ .ts-toast-overlay.center { align-items: center; justify-content: center; padding: 1rem; }
337
346
 
338
347
  /* Tiny utility: flex icon container */
339
348
  /* Scoped utility to avoid conflicts */
@@ -1 +1 @@
1
- .ts-toast-container{position:fixed;z-index:9999;display:flex;flex-direction:column;gap:10px;pointer-events:none;width:fit-content;max-width:calc(100% - 2rem)}.ts-toast-container>:not(:last-child){margin-bottom:0!important}.ts-toast-container.top-right{top:1rem;right:1rem;align-items:flex-end}.ts-toast-container.top-left{top:1rem;left:1rem;align-items:flex-start}.ts-toast-container.top-center{top:1rem;left:50%;transform:translateX(-50%);align-items:center}.ts-toast-container.bottom-right{bottom:1rem;right:1rem;align-items:flex-end}.ts-toast-container.bottom-left{bottom:1rem;left:1rem;align-items:flex-start}.ts-toast-container.bottom-center{bottom:1rem;left:50%;transform:translateX(-50%);align-items:center}.ts-toast-container .ts-toast{display:flex;align-items:center;gap:.75rem;background-color:var(--toast-bg,#fff);color:var(--toast-color,#000);padding:.75rem 1rem;border-radius:8px;border:1px solid var(--toast-border,#e5e7eb);box-shadow:var(--toast-shadow,0 10px 15px -3px rgba(0,0,0,.1));font-family:sans-serif;font-size:.95rem;pointer-events:all;position:relative;opacity:0;transform:translateY(20px);transition:opacity .5s ease,transform .5s ease;width:auto!important}.ts-toast.ts-toast-confirm{display:flex;align-items:center;justify-content:center;background-color:var(--toast-bg,#fff);color:var(--toast-color,#000);border:1px solid var(--toast-border,#e5e7eb);border-radius:12px;padding:16px 20px;box-shadow:var(--toast-shadow,0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1));transition:opacity .5s ease,transform .5s ease}.ts-toast.ts-toast-confirm .ts-toast-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center}.ts-toast.ts-toast-confirm .ts-toast-body{text-align:center}.ts-toast-actions{display:flex;gap:10px;justify-content:center;margin-top:12px}.ts-toast-input{width:100%;padding:8px 12px;border:1px solid var(--toast-border,#e5e7eb);border-radius:8px;font-family:inherit;font-size:.95rem;margin-top:12px;background:var(--toast-bg,#fff);color:var(--toast-color,#000);outline:0;transition:border-color .2s ease}.ts-toast-input:focus{border-color:#3b82f6}.ts-toast-container .ts-toast.ts-toast-show{opacity:1;transform:translateY(0)}@keyframes ts-toast-slide-left{from{transform:translateX(-100%)}to{transform:translateX(0)}}@keyframes ts-toast-slide-right{from{transform:translateX(100%)}to{transform:translateX(0)}}@keyframes ts-toast-slide-top{from{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes ts-toast-slide-bottom{from{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes ts-toast-zoom-in{from{transform:scale(0)}to{transform:scale(1)}}@keyframes ts-toast-zoom-out{from{transform:scale(1)}to{transform:scale(0)}}@keyframes ts-toast-flip{from{transform:rotateY(90deg)}to{transform:rotateY(0)}}.ts-toast-container .ts-toast-icon{font-size:1.2rem}.ts-toast-container .ts-toast-success .ts-toast-icon{color:var(--toast-success-color,#66ee78)}.ts-toast-container .ts-toast-error .ts-toast-icon{color:var(--toast-error-color,#ef4444)}.ts-toast-container .ts-toast-loader{width:20px;height:20px;border:3px solid #f3f3f3;border-top:3px solid var(--toast-loader-color,#66ee78);border-radius:50%;animation:ts-toast-spin 1s linear infinite}@keyframes ts-toast-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.ts-toast-container .ts-toast-loader.done{animation:none;opacity:0;transition:opacity .5s ease}.ts-toast-container .ts-toast-icon{font-size:1.2rem;opacity:0;transition:opacity .5s ease}.ts-toast-container .ts-toast.ts-toast-show .ts-toast-icon{opacity:1}@keyframes progress-bar{0%{width:0%}100%{width:100%}}@keyframes ts-toast-slide-top-reverse{from{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes ts-toast-slide-bottom-reverse{from{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes ts-toast-slide-left-reverse{from{transform:translateX(0)}to{transform:translateX(-100%)}}@keyframes ts-toast-slide-right-reverse{from{transform:translateX(0)}to{transform:translateX(100%)}}.ts-toast.ts-toast-slide-out{opacity:0}.ts-toast-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:2147483646}.ts-toast-overlay.top-left{align-items:flex-start;justify-content:flex-start;padding:1rem}.ts-toast-overlay.top-center{align-items:flex-start;justify-content:center;padding:1rem}.ts-toast-overlay.top-right{align-items:flex-start;justify-content:flex-end;padding:1rem}.ts-toast-overlay.bottom-left{align-items:flex-end;justify-content:flex-start;padding:1rem}.ts-toast-overlay.bottom-center{align-items:flex-end;justify-content:center;padding:1rem}.ts-toast-overlay.bottom-right{align-items:flex-end;justify-content:flex-end;padding:1rem}.ts-toast-d-flex{display:inline-flex;align-items:center}body.ts-toast-no-scroll{overflow:hidden}
1
+ .ts-toast-container{position:fixed;z-index:9999;display:flex;flex-direction:column;gap:10px;pointer-events:none;width:fit-content;max-width:calc(100% - 2rem)}.ts-toast-container>:not(:last-child){margin-bottom:0!important}.ts-toast-container.top-right{top:1rem;right:1rem;align-items:flex-end}.ts-toast-container.top-left{top:1rem;left:1rem;align-items:flex-start}.ts-toast-container.top-center{top:1rem;left:50%;transform:translateX(-50%);align-items:center}.ts-toast-container.bottom-right{bottom:1rem;right:1rem;align-items:flex-end}.ts-toast-container.bottom-left{bottom:1rem;left:1rem;align-items:flex-start}.ts-toast-container.bottom-center{bottom:1rem;left:50%;transform:translateX(-50%);align-items:center}.ts-toast-container.center{top:50%;left:50%;transform:translate(-50%,-50%);align-items:center}.ts-toast-container .ts-toast{display:flex;align-items:center;gap:.75rem;background-color:var(--toast-bg,#fff);color:var(--toast-color,#000);padding:.75rem 1rem;border-radius:8px;border:1px solid var(--toast-border,#e5e7eb);box-shadow:var(--toast-shadow,0 10px 15px -3px rgba(0,0,0,.1));font-family:sans-serif;font-size:.95rem;pointer-events:all;position:relative;opacity:0;transform:translateY(20px);transition:opacity .5s ease,transform .5s ease;width:auto!important}.ts-toast.ts-toast-confirm{display:flex;align-items:center;justify-content:center;background-color:var(--toast-bg,#fff);color:var(--toast-color,#000);border:1px solid var(--toast-border,#e5e7eb);border-radius:12px;padding:16px 20px;box-shadow:var(--toast-shadow,0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1));transition:opacity .5s ease,transform .5s ease}.ts-toast.ts-toast-confirm .ts-toast-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center}.ts-toast.ts-toast-confirm .ts-toast-body{text-align:center}.ts-toast-actions{display:flex;gap:10px;justify-content:center;margin-top:12px}.ts-toast-input{width:100%;padding:8px 12px;border:1px solid var(--toast-border,#e5e7eb);border-radius:8px;font-family:inherit;font-size:.95rem;margin-top:12px;background:var(--toast-bg,#fff);color:var(--toast-color,#000);outline:0;transition:border-color .2s ease}.ts-toast-input:focus{border-color:#3b82f6}.ts-toast-container .ts-toast.ts-toast-show{opacity:1;transform:translateY(0)}@keyframes ts-toast-slide-left{from{transform:translateX(-100%)}to{transform:translateX(0)}}@keyframes ts-toast-slide-right{from{transform:translateX(100%)}to{transform:translateX(0)}}@keyframes ts-toast-slide-top{from{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes ts-toast-slide-bottom{from{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes ts-toast-zoom-in{from{transform:scale(0)}to{transform:scale(1)}}@keyframes ts-toast-zoom-out{from{transform:scale(1)}to{transform:scale(0)}}@keyframes ts-toast-flip{from{transform:rotateY(90deg)}to{transform:rotateY(0)}}.ts-toast-container .ts-toast-icon{font-size:1.2rem}.ts-toast-container .ts-toast-success .ts-toast-icon{color:var(--toast-success-color,#66ee78)}.ts-toast-container .ts-toast-error .ts-toast-icon{color:var(--toast-error-color,#ef4444)}.ts-toast-container .ts-toast-loader{width:20px;height:20px;border:3px solid #f3f3f3;border-top:3px solid var(--toast-loader-color,#66ee78);border-radius:50%;animation:ts-toast-spin 1s linear infinite}@keyframes ts-toast-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.ts-toast-container .ts-toast-loader.done{animation:none;opacity:0;transition:opacity .5s ease}.ts-toast-container .ts-toast-icon{font-size:1.2rem;opacity:0;transition:opacity .5s ease}.ts-toast-container .ts-toast.ts-toast-show .ts-toast-icon{opacity:1}@keyframes progress-bar{0%{width:0%}100%{width:100%}}@keyframes ts-toast-slide-top-reverse{from{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes ts-toast-slide-bottom-reverse{from{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes ts-toast-slide-left-reverse{from{transform:translateX(0)}to{transform:translateX(-100%)}}@keyframes ts-toast-slide-right-reverse{from{transform:translateX(0)}to{transform:translateX(100%)}}.ts-toast.ts-toast-slide-out{opacity:0}.ts-toast-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:2147483646}.ts-toast-overlay.top-left{align-items:flex-start;justify-content:flex-start;padding:1rem}.ts-toast-overlay.top-center{align-items:flex-start;justify-content:center;padding:1rem}.ts-toast-overlay.top-right{align-items:flex-start;justify-content:flex-end;padding:1rem}.ts-toast-overlay.bottom-left{align-items:flex-end;justify-content:flex-start;padding:1rem}.ts-toast-overlay.bottom-center{align-items:flex-end;justify-content:center;padding:1rem}.ts-toast-overlay.bottom-right{align-items:flex-end;justify-content:flex-end;padding:1rem}.ts-toast-overlay.center{align-items:center;justify-content:center;padding:1rem}.ts-toast-d-flex{display:inline-flex;align-items:center}body.ts-toast-no-scroll{overflow:hidden}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsirosgeorge/toastnotification",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "a toast notification plugin",
5
5
  "main": "toast.min.js",
6
6
  "module": "toast.module.js",
@@ -21,8 +21,9 @@
21
21
  "build:css": "cleancss -o assets/css/toast.min.css assets/css/toast.css",
22
22
  "build:js": "terser toast.js --compress --mangle --output toast.min.js",
23
23
  "build": "npm run sync:version && npm run build:module && npm run build:css && npm run build:js",
24
+ "version": "npm run build && git add -A",
24
25
  "prepublishOnly": "npm run build",
25
- "release": "npm version ${V:-patch} && npm publish --access public && git push origin main --follow-tags"
26
+ "release": "npm version ${V:-patch} && git push origin main --follow-tags"
26
27
  },
27
28
  "files": [
28
29
  "toast.js",
package/toast.d.ts CHANGED
@@ -2,7 +2,9 @@ export type ToastType = 'info' | 'success' | 'warning' | 'error';
2
2
 
3
3
  export type ToastPosition =
4
4
  | 'top-left' | 'top-right' | 'top-center'
5
- | 'bottom-left' | 'bottom-right' | 'bottom-center';
5
+ | 'bottom-left' | 'bottom-right' | 'bottom-center'
6
+ /** Dead centre of the viewport. Defaults to the zoom-in animation. */
7
+ | 'center';
6
8
 
7
9
  export type ToastAnimation =
8
10
  | 'slide-top' | 'slide-bottom' | 'slide-left' | 'slide-right'
@@ -16,6 +18,8 @@ export type ConfirmResult = boolean | string | null;
16
18
  export interface ToastElement extends HTMLDivElement {
17
19
  /** Present in confirm mode: resolves when the user confirms, cancels or dismisses. */
18
20
  result?: Promise<ConfirmResult>;
21
+ /** Dismiss this toast now. On a confirm dialog this settles as a cancel. */
22
+ close(): void;
19
23
  }
20
24
 
21
25
  export interface ToastOptions {
@@ -27,6 +31,11 @@ export interface ToastOptions {
27
31
  /** Emoji or text used instead of the bundled animated icon. */
28
32
  icon?: string | null;
29
33
  showLoader?: boolean;
34
+ /**
35
+ * `message` is written as HTML by default, for backwards compatibility.
36
+ * Pass `false` to render it as plain text — do that for anything user-supplied.
37
+ */
38
+ allowHtml?: boolean;
30
39
  /** `'confirm'` (alias `'swal'`) renders a modal dialog instead of a toast. */
31
40
  mode?: 'alert' | 'confirm' | 'swal';
32
41
  dismissOnClick?: boolean;
@@ -48,6 +57,8 @@ export interface ConfirmOptions extends ToastOptions {
48
57
  cancelButtonColor?: string | null;
49
58
  useOverlay?: boolean;
50
59
  closeOnOverlayClick?: boolean;
60
+ /** Escape cancels the dialog. Defaults to true. */
61
+ closeOnEscape?: boolean;
51
62
  showClose?: boolean;
52
63
  /** Receives the input's value when `input` is set, otherwise `true`. */
53
64
  onConfirm?: (value: string | true, el: ToastElement) => void;
@@ -74,6 +85,8 @@ export interface Toast {
74
85
  * and `null` on cancel/dismiss — so an empty submission is not read as a cancel.
75
86
  */
76
87
  confirm(message: string, options?: ConfirmOptions): Promise<ConfirmResult>;
88
+ /** Dismiss every toast on screen; open confirm dialogs settle as a cancel. */
89
+ dismissAll(): void;
77
90
  }
78
91
 
79
92
  declare const toast: Toast;
package/toast.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // Single source of truth for the CDN this build points at.
4
4
  // `npm run sync:version` rewrites it from package.json, so it can never go stale.
5
- const TS_TOAST_VERSION = "5.4.0";
5
+ const TS_TOAST_VERSION = "5.5.0";
6
6
  // Point this at your own copy of assets/ to self-host the CSS and icons
7
7
  // (useful offline, behind a strict CSP, or when you don't want a CDN dependency):
8
8
  // window.TS_TOAST_ASSET_BASE = '/vendor/toastnotification';
@@ -10,6 +10,23 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
10
10
  ? String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/, '')
11
11
  : `https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@${TS_TOAST_VERSION}`;
12
12
 
13
+ // How many confirm dialogs are currently open. The body scroll lock belongs to the
14
+ // group, so only the last dialog to close may release it.
15
+ let tsToastOpenModals = 0;
16
+ let tsToastPrevOverflow = '';
17
+
18
+ const tsToastReducedMotion = () =>
19
+ typeof window !== 'undefined' &&
20
+ typeof window.matchMedia === 'function' &&
21
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
22
+
23
+ // Everything inside the dialog a keyboard can reach, in DOM order.
24
+ const tsToastFocusable = (root) => Array.from(
25
+ root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
26
+ ).filter((el) => !el.disabled && el.offsetParent !== null);
27
+
28
+ let tsToastIdCounter = 0;
29
+
13
30
  // Load the stylesheet from the CDN, unless the page opted out by importing it itself
14
31
  // (set window.TS_TOAST_NO_CSS = true before loading, or ship assets/css/toast.css yourself).
15
32
  (function loadStylesheet() {
@@ -33,6 +50,8 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
33
50
  /* Ensure center positions exist even if external CSS lacks them */
34
51
  .ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
35
52
  .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
53
+ .ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }
54
+ .ts-toast-overlay.center { align-items: center; justify-content: center; }
36
55
  .ts-toast-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2147483646; }
37
56
  .ts-toast.ts-toast-confirm { max-width: min(92vw, 440px); width: max(320px, 60%); flex-direction: column; gap: 12px; padding: 16px 20px; background: var(--toast-bg, #fff); color: var(--toast-color, #000); border: 1px solid var(--toast-border, #e5e7eb); border-radius: 12px; box-shadow: var(--toast-shadow, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)); text-align: center; }
38
57
  .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
@@ -84,6 +103,11 @@ const toast = function (message, options = {}) {
84
103
  useOverlay = true,
85
104
  closeOnOverlayClick = true,
86
105
  showClose = false,
106
+ // Escape cancels a confirm dialog
107
+ closeOnEscape = true,
108
+ // `message` is written as HTML for backwards compatibility. Pass false to
109
+ // render it as plain text, which is what you want for anything user-supplied.
110
+ allowHtml = true,
87
111
  // interactions
88
112
  dismissOnClick = true, // ignored if confirm-mode
89
113
  onClick = null, // Custom onClick event listener
@@ -92,6 +116,7 @@ const toast = function (message, options = {}) {
92
116
  } = options;
93
117
 
94
118
  const isConfirm = (mode === 'confirm' || mode === 'swal');
119
+ const reducedMotion = tsToastReducedMotion();
95
120
 
96
121
  // Pick an animation intelligently when one wasn't explicitly provided
97
122
  const resolvedAnimation = (typeof options.animation === 'string' && options.animation.trim())
@@ -107,10 +132,12 @@ const toast = function (message, options = {}) {
107
132
  };
108
133
  return m[a] || a;
109
134
  })(options.animation.trim())
110
- : (isConfirm ? 'ts-toast-zoom-in' : (position.startsWith('top') ? 'ts-toast-slide-top'
135
+ // 'center' has no edge to slide in from, so it zooms like a dialog.
136
+ : (isConfirm || position === 'center' ? 'ts-toast-zoom-in'
137
+ : position.startsWith('top') ? 'ts-toast-slide-top'
111
138
  : position.startsWith('bottom') ? 'ts-toast-slide-bottom'
112
139
  : position.endsWith('left') ? 'ts-toast-slide-left'
113
- : 'ts-toast-slide-right'));
140
+ : 'ts-toast-slide-right');
114
141
 
115
142
  // helper: remove with smooth CSS transition and cleanup (used by alerts)
116
143
  const removeWithAnimation = (el, callback) => {
@@ -143,13 +170,24 @@ const toast = function (message, options = {}) {
143
170
  el.classList.remove('ts-toast-slide-out');
144
171
  if (el.parentNode) el.parentNode.removeChild(el);
145
172
  if (typeof callback === 'function') callback();
146
- }, 500);
173
+ }, reducedMotion ? 0 : 500);
147
174
  };
148
175
 
149
176
  const toastElement = document.createElement('div');
150
177
  toastElement.className = `ts-toast ts-toast-${type}${isConfirm ? ' ts-toast-confirm' : ''}`;
151
178
  toastElement.dataset.anim = resolvedAnimation;
152
- toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
179
+ if (!reducedMotion) toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
180
+
181
+ const uid = `ts-toast-${++tsToastIdCounter}`;
182
+ if (isConfirm) {
183
+ // Without these a screen reader announces nothing, and without tabindex the
184
+ // dialog cannot take focus away from whatever opened it.
185
+ toastElement.setAttribute('role', 'dialog');
186
+ toastElement.setAttribute('aria-modal', 'true');
187
+ toastElement.tabIndex = -1;
188
+ } else if (type === 'error' || type === 'warning') {
189
+ toastElement.setAttribute('role', 'alert');
190
+ }
153
191
  // In confirm mode, we stack content vertically; in alert mode keep original layout
154
192
  if (!isConfirm) {
155
193
  toastElement.style.flexDirection = 'row-reverse';
@@ -181,7 +219,11 @@ const toast = function (message, options = {}) {
181
219
  // Create Body
182
220
  const toastBody = document.createElement('div');
183
221
  toastBody.className = 'ts-toast-body';
184
- toastBody.innerHTML = message; // Allow HTML content in message
222
+ toastBody.id = `${uid}-body`;
223
+ // HTML by default for backwards compatibility; pass allowHtml: false for
224
+ // anything that came from a user.
225
+ if (allowHtml) toastBody.innerHTML = message;
226
+ else toastBody.textContent = message;
185
227
 
186
228
  // Content row for confirm (icon + text side-by-side)
187
229
  let contentRow = null;
@@ -192,10 +234,13 @@ const toast = function (message, options = {}) {
192
234
  if (title) {
193
235
  const titleEl = document.createElement('div');
194
236
  titleEl.className = 'ts-toast-title';
237
+ titleEl.id = `${uid}-title`;
195
238
  titleEl.textContent = title;
196
239
  contentRow.appendChild(titleEl);
240
+ toastElement.setAttribute('aria-labelledby', titleEl.id);
197
241
  }
198
242
  contentRow.appendChild(toastBody);
243
+ toastElement.setAttribute('aria-describedby', toastBody.id);
199
244
  toastElement.appendChild(contentRow);
200
245
  } else {
201
246
  toastElement.appendChild(toastBody);
@@ -214,6 +259,13 @@ const toast = function (message, options = {}) {
214
259
  inputElement.className = 'ts-toast-input';
215
260
  inputElement.placeholder = inputPlaceholder;
216
261
  inputElement.value = inputValue;
262
+ // Enter submits a single-line field, the way a native prompt does.
263
+ // A textarea keeps Enter for newlines.
264
+ if (input !== 'textarea') {
265
+ inputElement.addEventListener('keydown', (e) => {
266
+ if (e.key === 'Enter') { e.preventDefault(); resolveAndClose(true); }
267
+ });
268
+ }
217
269
  toastElement.appendChild(inputElement);
218
270
  }
219
271
 
@@ -223,6 +275,8 @@ const toast = function (message, options = {}) {
223
275
  // Assigned in confirm mode; the overlay and (x) handlers below call it so that
224
276
  // *every* way of dismissing the dialog settles the promise and the callbacks.
225
277
  let resolveAndClose = null;
278
+ // Releases the scroll lock, the key handler and the focus this dialog took.
279
+ let releaseModal = () => {};
226
280
  if (isConfirm) {
227
281
  actionsContainer = document.createElement('div');
228
282
  actionsContainer.className = 'ts-toast-actions';
@@ -262,6 +316,7 @@ const toast = function (message, options = {}) {
262
316
  if (confirmed && typeof onConfirm === 'function') onConfirm(result, toastElement);
263
317
  if (!confirmed && typeof onCancel === 'function') onCancel(toastElement);
264
318
  if (typeof onResult === 'function') onResult(result, toastElement);
319
+ releaseModal();
265
320
  // Use the same slide+fade removal as alerts
266
321
  removeWithAnimation(toastElement, () => {
267
322
  if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
@@ -304,11 +359,17 @@ const toast = function (message, options = {}) {
304
359
  toastElement.appendChild(closeBtn);
305
360
  }
306
361
  if (closeOnOverlayClick) {
362
+ // The cancel must both start and end on the backdrop. Selecting text in
363
+ // the input and releasing the mouse outside the card produced a click
364
+ // whose target was the overlay, which threw the dialog away mid-edit.
365
+ let pressedOnBackdrop = false;
366
+ overlay.addEventListener('pointerdown', (e) => { pressedOnBackdrop = e.target === overlay; });
307
367
  overlay.addEventListener('click', (e) => {
308
368
  // Backdrop click is a cancel: settle the promise and onCancel/onResult,
309
369
  // then close. Previously this resolved only the internal el.result,
310
370
  // so `await toast.confirm(...)` hung forever.
311
- if (e.target === overlay) resolveAndClose(false);
371
+ if (e.target === overlay && pressedOnBackdrop) resolveAndClose(false);
372
+ pressedOnBackdrop = false;
312
373
  });
313
374
  }
314
375
  } else {
@@ -319,9 +380,79 @@ const toast = function (message, options = {}) {
319
380
  container.className = `ts-toast-container ${position}`;
320
381
  document.body.appendChild(container);
321
382
  }
383
+ if (!container.hasAttribute('aria-live')) {
384
+ // Without this a toast is invisible to a screen reader.
385
+ container.setAttribute('role', 'status');
386
+ container.setAttribute('aria-live', 'polite');
387
+ container.setAttribute('aria-relevant', 'additions');
388
+ }
322
389
  container.appendChild(toastElement);
323
390
  }
324
391
 
392
+ if (isConfirm) {
393
+ // The dialog has to own the keyboard while it is open. Without this the
394
+ // element that opened it keeps focus, so pressing Enter or Space activates
395
+ // it again and stacks a second dialog on top of the first — and Tab walks
396
+ // through the page behind the backdrop.
397
+ const previouslyFocused = document.activeElement;
398
+
399
+ tsToastOpenModals += 1;
400
+ if (tsToastOpenModals === 1) {
401
+ tsToastPrevOverflow = document.body.style.overflow;
402
+ document.body.style.overflow = 'hidden';
403
+ }
404
+
405
+ // With dialogs stacked, only the top one should answer the keyboard.
406
+ const isTopmost = () => {
407
+ const open = document.querySelectorAll('.ts-toast.ts-toast-confirm');
408
+ return open.length === 0 || open[open.length - 1] === toastElement;
409
+ };
410
+
411
+ const onKeydown = (e) => {
412
+ if (!isTopmost()) return;
413
+
414
+ if (e.key === 'Escape' && closeOnEscape) {
415
+ e.preventDefault();
416
+ resolveAndClose(false);
417
+ return;
418
+ }
419
+ if (e.key !== 'Tab') return;
420
+
421
+ const focusables = tsToastFocusable(toastElement);
422
+ if (!focusables.length) { e.preventDefault(); return; }
423
+
424
+ const first = focusables[0];
425
+ const last = focusables[focusables.length - 1];
426
+
427
+ // Focus can start outside the dialog (the trigger button); pull it back.
428
+ if (!toastElement.contains(document.activeElement)) {
429
+ e.preventDefault();
430
+ (e.shiftKey ? last : first).focus();
431
+ } else if (e.shiftKey && document.activeElement === first) {
432
+ e.preventDefault();
433
+ last.focus();
434
+ } else if (!e.shiftKey && document.activeElement === last) {
435
+ e.preventDefault();
436
+ first.focus();
437
+ }
438
+ };
439
+ document.addEventListener('keydown', onKeydown, true);
440
+
441
+ releaseModal = () => {
442
+ document.removeEventListener('keydown', onKeydown, true);
443
+ tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
444
+ if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
445
+ // Hand the keyboard back to whatever opened the dialog.
446
+ if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
447
+ document.contains(previouslyFocused)) {
448
+ previouslyFocused.focus();
449
+ }
450
+ };
451
+
452
+ // Land on the input when there is one, otherwise the confirm button.
453
+ (inputElement || toastElement.querySelector('.ts-toast-btn.confirm') || toastElement).focus();
454
+ }
455
+
325
456
  // Trigger the onShow event if provided
326
457
  if (onShow && typeof onShow === 'function') {
327
458
  onShow(toastElement);
@@ -366,8 +497,10 @@ const toast = function (message, options = {}) {
366
497
  if (!isConfirm && dismissOnClick) {
367
498
  toastElement.addEventListener('click', () => {
368
499
  if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
500
+ // onClick belongs to the click, not to the end of the exit animation,
501
+ // which is where it used to fire half a second late.
502
+ if (onClick && typeof onClick === 'function') onClick(toastElement);
369
503
  removeWithAnimation(toastElement, () => {
370
- if (onClick && typeof onClick === 'function') onClick(toastElement);
371
504
  if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
372
505
  });
373
506
  });
@@ -376,15 +509,23 @@ const toast = function (message, options = {}) {
376
509
  // Add swipe event listeners for mobile dismissal
377
510
  if (!isConfirm) {
378
511
  let touchStartX = 0;
512
+ let touchStartY = 0;
379
513
  let touchEndX = 0;
380
514
 
515
+ // Passive: these never preventDefault, and a non-passive touchstart blocks
516
+ // scrolling on the whole toast.
381
517
  toastElement.addEventListener('touchstart', (e) => {
382
518
  touchStartX = e.changedTouches[0].screenX;
383
- });
519
+ touchStartY = e.changedTouches[0].screenY;
520
+ }, { passive: true });
384
521
 
385
522
  toastElement.addEventListener('touchend', (e) => {
386
523
  touchEndX = e.changedTouches[0].screenX;
387
- if (Math.abs(touchStartX - touchEndX) > 50) { // Swipe distance threshold
524
+ const dx = Math.abs(touchStartX - touchEndX);
525
+ const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
526
+ // Only a mostly-horizontal swipe dismisses, so scrolling the page past a
527
+ // toast no longer throws it away on a bit of sideways drift.
528
+ if (dx > 50 && dx > dy) {
388
529
  if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
389
530
  removeWithAnimation(toastElement, () => {
390
531
  if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
@@ -393,6 +534,16 @@ const toast = function (message, options = {}) {
393
534
  });
394
535
  }
395
536
 
537
+ // Let callers dismiss a toast they are holding, instead of only waiting out
538
+ // the duration or making the user click it.
539
+ toastElement.close = () => {
540
+ if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
541
+ if (isConfirm) { resolveAndClose(false); return; }
542
+ removeWithAnimation(toastElement, () => {
543
+ if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
544
+ });
545
+ };
546
+
396
547
  return toastElement;
397
548
  };
398
549
 
@@ -553,15 +704,11 @@ const toast = function (message, options = {}) {
553
704
  showLoader: false // Disable loader when updating the message
554
705
  });
555
706
  },
707
+ // Reuse the element's own close so onDismiss fires, which this
708
+ // hand-rolled copy of the removal never did.
556
709
  close: () => {
557
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
558
- toastElement.classList.add('ts-toast-slide-out');
559
- toastElement.classList.remove('ts-toast-show');
560
- toastElement.style.animation = '';
561
- setTimeout(() => {
562
- toastElement.classList.remove('ts-toast-slide-out');
563
- if (toastElement.parentNode) toastElement.parentNode.removeChild(toastElement);
564
- }, 500);
710
+ toastElement._managedByLoading = false;
711
+ toastElement.close();
565
712
  }
566
713
  };
567
714
  };
@@ -583,6 +730,13 @@ const toast = function (message, options = {}) {
583
730
  });
584
731
  };
585
732
 
733
+ // Close every toast currently on screen. Confirm dialogs settle as a cancel.
734
+ toast.dismissAll = function () {
735
+ document.querySelectorAll('.ts-toast').forEach((el) => {
736
+ if (typeof el.close === 'function') el.close();
737
+ });
738
+ };
739
+
586
740
  // Expose globally for CDN / browser usage
587
741
  if (typeof window !== 'undefined') {
588
742
  window.toast = toast;
package/toast.min.js CHANGED
@@ -1 +1 @@
1
- "use strict";const TS_TOAST_VERSION="5.4.0",TS_TOAST_CDN="undefined"!=typeof window&&window.TS_TOAST_ASSET_BASE?String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/,""):"https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.4.0";!function(){const t="ts-toast-stylesheet";if("undefined"!=typeof window&&window.TS_TOAST_NO_CSS)return;if(document.getElementById(t))return;const e=document.createElement("link");e.id=t,e.rel="stylesheet",e.href=`${TS_TOAST_CDN}/assets/css/toast.min.css`,document.head.appendChild(e)}(),function(){const t="ts-toast-inline-extras";if(document.getElementById(t))return;const e=document.createElement("style");e.id=t,e.textContent="\n /* Ensure center positions exist even if external CSS lacks them */\n .ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }\n .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }\n .ts-toast-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2147483646; }\n .ts-toast.ts-toast-confirm { max-width: min(92vw, 440px); width: max(320px, 60%); flex-direction: column; gap: 12px; padding: 16px 20px; background: var(--toast-bg, #fff); color: var(--toast-color, #000); border: 1px solid var(--toast-border, #e5e7eb); border-radius: 12px; box-shadow: var(--toast-shadow, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)); text-align: center; }\n .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }\n .ts-toast-actions { display: flex; gap: 10px; justify-content: center; margin-top: 12px; }\n .ts-toast-btn { appearance: none; border: 0; padding: 8px 12px; border-radius: 8px; font-weight: 600; cursor: pointer; }\n .ts-toast-btn.cancel { background: #e9ecef; color: #1f2937; }\n .ts-toast-btn.confirm { background: #3b82f6; color: #fff; }\n .ts-toast.ts-toast-error .ts-toast-btn.confirm,\n .ts-toast.ts-toast-warning .ts-toast-btn.confirm { background: #ef4444; color: #fff; }\n .ts-toast.ts-toast-confirm .ts-toast-title { font-weight: 700; font-size: 1.05rem; margin-top: 4px; }\n .ts-toast.ts-toast-confirm .ts-toast-close { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; border-radius: 999px; border: 0; background: transparent; color: #6b7280; font-size: 20px; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }\n .ts-toast.ts-toast-confirm .ts-toast-close:hover { background: rgba(0,0,0,0.06); }\n .ts-toast.ts-toast-confirm .ts-toast-icon { width: 64px; height: 64px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; }\n .ts-toast.ts-toast-confirm .ts-toast-icon img { width: 36px; height: 36px; }\n .ts-toast.ts-toast-confirm.ts-toast-success .ts-toast-icon { background: #dcfce7; }\n .ts-toast.ts-toast-confirm.ts-toast-info .ts-toast-icon { background: #dbeafe; }\n .ts-toast.ts-toast-confirm.ts-toast-warning .ts-toast-icon { background: #fef3c7; }\n .ts-toast.ts-toast-confirm.ts-toast-error .ts-toast-icon { background: #fee2e2; }\n ",document.head.appendChild(e)}();const toast=function(t,e={}){const{position:o="top-right",animation:s="slide-right",type:n="info",duration:a=3e3,icon:i=null,showLoader:l=!1,mode:r="alert",title:c=null,confirmText:d="Yes",cancelText:u="No",input:m=!1,inputPlaceholder:p="",inputValue:f="",confirmButtonBg:h=null,confirmButtonColor:g=null,cancelButtonBg:y=null,cancelButtonColor:x=null,onConfirm:b=null,onCancel:v=null,onResult:w=null,useOverlay:C=!0,closeOnOverlayClick:T=!0,showClose:E=!1,dismissOnClick:S=!0,onClick:L=null,onShow:_=null,onDismiss:N=null}=e,k="confirm"===r||"swal"===r,A="string"==typeof e.animation&&e.animation.trim()?{"slide-top":"ts-toast-slide-top","slide-bottom":"ts-toast-slide-bottom","slide-left":"ts-toast-slide-left","slide-right":"ts-toast-slide-right","zoom-in":"ts-toast-zoom-in","zoom-out":"ts-toast-zoom-out",flip:"ts-toast-flip"}[O=e.animation.trim()]||O:k?"ts-toast-zoom-in":o.startsWith("top")?"ts-toast-slide-top":o.startsWith("bottom")?"ts-toast-slide-bottom":o.endsWith("left")?"ts-toast-slide-left":"ts-toast-slide-right";var O;const B=(t,e)=>{const o=t.dataset&&t.dataset.anim?t.dataset.anim:t.style.animation||"";let s="";o.includes("ts-toast-slide-top")?s="translateY(-100%)":o.includes("ts-toast-slide-bottom")?s="translateY(100%)":(o.includes("ts-toast-slide-left")||o.includes("ts-toast-slide-right"))&&(s="translateX(100%)"),t.classList.add("ts-toast-slide-out"),t.classList.remove("ts-toast-show"),t.style.animation="",s&&(t.style.transform=s),t.style.opacity="0",setTimeout((()=>{t.classList.remove("ts-toast-slide-out"),t.parentNode&&t.parentNode.removeChild(t),"function"==typeof e&&e()}),500)},$=document.createElement("div");$.className=`ts-toast ts-toast-${n}${k?" ts-toast-confirm":""}`,$.dataset.anim=A,$.style.animation=`${A} 0.5s ease`,k||($.style.flexDirection="row-reverse",$.style.justifyContent="flex-end");const R=document.createElement("span");if(R.className="ts-toast-icon",R.style.display="flex",i)R.textContent=i;else{const t=document.createElement("img");t.alt="",t.setAttribute("aria-hidden","true"),t.style.width="30px",t.style.height="30px",t.style.objectFit="contain";const e={success:"success.gif",error:"error.gif",info:"info.gif",warning:"warning.gif"}[n];e&&(t.src=`${TS_TOAST_CDN}/assets/img/${e}`),R.appendChild(t)}const j=document.createElement("div");j.className="ts-toast-body",j.innerHTML=t;let q=null;if(k){if(q=document.createElement("div"),q.className="ts-toast-content",q.appendChild(R),c){const t=document.createElement("div");t.className="ts-toast-title",t.textContent=c,q.appendChild(t)}q.appendChild(j),$.appendChild(q)}else $.appendChild(j);let z=null;k&&m&&("textarea"===m?(z=document.createElement("textarea"),z.rows=3):(z=document.createElement("input"),z.type="text"===m||"email"===m||"password"===m||"number"===m?m:"text"),z.className="ts-toast-input",z.placeholder=p,z.value=f,$.appendChild(z));let D=null,P=null,X=null;if(k){D=document.createElement("div"),D.className="ts-toast-actions";const t=document.createElement("button");t.className="ts-toast-btn cancel",t.textContent=u;const e=document.createElement("button");e.className="ts-toast-btn confirm",e.textContent=d,y&&(t.style.background=y),x&&(t.style.color=x),h&&(e.style.background=h),g&&(e.style.color=g),D.appendChild(t),D.appendChild(e),$.appendChild(D),$.result=new Promise((t=>{P=t}));let o=!1;X=t=>{if(o)return;o=!0;const e=t?!z||z.value:!!z&&null;P&&P(e),t&&"function"==typeof b&&b(e,$),t||"function"!=typeof v||v($),"function"==typeof w&&w(e,$),B($,(()=>{N&&"function"==typeof N&&N($),F&&F.parentNode&&F.parentNode.removeChild(F)}))},t.addEventListener("click",(t=>{t.stopPropagation(),X(!1)})),e.addEventListener("click",(t=>{t.stopPropagation(),X(!0)}))}let M=null;l&&(M=document.createElement("div"),M.className="ts-toast-loader",$.appendChild(M));let F=null;if(k&&C){if(F=document.createElement("div"),F.className="ts-toast-overlay"+(e.position?` ${o}`:""),document.body.appendChild(F),F.appendChild($),E){const t=document.createElement("button");t.className="ts-toast-close",t.setAttribute("aria-label","Close"),t.innerHTML="&times;",t.addEventListener("click",(t=>{t.stopPropagation(),X(!1)})),$.appendChild(t)}T&&F.addEventListener("click",(t=>{t.target===F&&X(!1)}))}else{let t=document.querySelector(`.ts-toast-container.${o}`);t||(t=document.createElement("div"),t.className=`ts-toast-container ${o}`,document.body.appendChild(t)),t.appendChild($)}if(_&&"function"==typeof _&&_($),setTimeout((()=>{$.classList.add("ts-toast-show")}),100),l&&M&&setTimeout((()=>{$._managedByLoading||(M.classList.add("done"),M.remove(),$.contains(R)||(k&&q?q.appendChild(R):$.appendChild(R)))}),2e3),l||k||$.contains(R)||$.appendChild(R),!k&&a>0){const t=setTimeout((()=>{B($,(()=>{N&&"function"==typeof N&&N($)}))}),a);$._autoRemove=t}if(!k&&S&&$.addEventListener("click",(()=>{$._autoRemove&&clearTimeout($._autoRemove),B($,(()=>{L&&"function"==typeof L&&L($),N&&"function"==typeof N&&N($)}))})),!k){let t=0,e=0;$.addEventListener("touchstart",(e=>{t=e.changedTouches[0].screenX})),$.addEventListener("touchend",(o=>{e=o.changedTouches[0].screenX,Math.abs(t-e)>50&&($._autoRemove&&clearTimeout($._autoRemove),B($,(()=>{N&&"function"==typeof N&&N($)})))}))}return $};toast.success=function(t,e){return toast(t,{...e,type:"success"})},toast.error=function(t,e){return toast(t,{...e,type:"error"})},toast.warning=function(t,e){return toast(t,{...e,type:"warning"})},toast.info=function(t,e){return toast(t,{...e,type:"info"})},toast.update=function(t,e,o={}){const{type:s=null,icon:n=null,showLoader:a=!1,duration:i=3e3,onClick:l=null,onShow:r=null,onDismiss:c=null}=o,d=t.querySelector(".ts-toast-loader"),u=t.querySelector(".ts-toast-icon");d&&d.remove(),s&&(["success","error","info","warning"].forEach((e=>t.classList.remove(`ts-toast-${e}`))),t.classList.add("ts-toast",`ts-toast-${s}`,"ts-toast-show"));const m=t.querySelector(".ts-toast-body");m&&(m.innerHTML=e),u&&u.remove();const p=document.createElement("span");if(p.className="ts-toast-icon",p.style.display="flex",n)p.textContent=n;else{const t=document.createElement("img");t.alt="",t.setAttribute("aria-hidden","true"),t.style.width="30px",t.style.height="30px",t.style.objectFit="contain";const e={success:"success.gif",error:"error.gif",info:"info.gif",warning:"warning.gif"}[s];e&&(t.src=`${TS_TOAST_CDN}/assets/img/${e}`),p.appendChild(t)}if((t.querySelector(".ts-toast-content")||t).appendChild(p),a){const e=document.createElement("div");e.className="ts-toast-loader",t.appendChild(e),setTimeout((()=>{e.classList.add("done")}),2e3)}t._autoRemove&&clearTimeout(t._autoRemove);const f=setTimeout((()=>{var e,o;o=()=>{c&&"function"==typeof c&&c(t)},(e=t).classList.add("ts-toast-slide-out"),e.classList.remove("ts-toast-show"),e.style.animation="",setTimeout((()=>{e.classList.remove("ts-toast-slide-out"),e.parentNode&&e.parentNode.removeChild(e),"function"==typeof o&&o()}),500)}),i);t._autoRemove=f},toast.loading=function(t,e={}){const o=toast(t,{...e,type:e.type||"info",duration:0,showLoader:!0,icon:null});requestAnimationFrame((()=>{o.classList.add("ts-toast-show")}));const s=o.querySelector(".ts-toast-loader");let n=o.querySelector(".ts-toast-icon");return n||(n=document.createElement("span"),n.className="ts-toast-icon",n.style.display="flex",o.appendChild(n)),o._managedByLoading=!0,s&&setTimeout((()=>{o._managedByLoading||s.classList.add("done")}),2e3),{update:(t,e={})=>{o._managedByLoading=!1,toast.update(o,t,{...e,showLoader:!1})},close:()=>{o._autoRemove&&clearTimeout(o._autoRemove),o.classList.add("ts-toast-slide-out"),o.classList.remove("ts-toast-show"),o.style.animation="",setTimeout((()=>{o.classList.remove("ts-toast-slide-out"),o.parentNode&&o.parentNode.removeChild(o)}),500)}}},toast.confirm=function(t,e={}){return new Promise((o=>{toast(t,{...e,mode:"confirm",duration:0,dismissOnClick:!1,onResult:t=>o(t)})}))},"undefined"!=typeof window&&(window.toast=toast),"undefined"!=typeof module&&module.exports&&(module.exports=toast);
1
+ "use strict";const TS_TOAST_VERSION="5.5.0",TS_TOAST_CDN="undefined"!=typeof window&&window.TS_TOAST_ASSET_BASE?String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/,""):"https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@5.5.0";let tsToastOpenModals=0,tsToastPrevOverflow="";const tsToastReducedMotion=()=>"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches,tsToastFocusable=t=>Array.from(t.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter((t=>!t.disabled&&null!==t.offsetParent));let tsToastIdCounter=0;!function(){const t="ts-toast-stylesheet";if("undefined"!=typeof window&&window.TS_TOAST_NO_CSS)return;if(document.getElementById(t))return;const e=document.createElement("link");e.id=t,e.rel="stylesheet",e.href=`${TS_TOAST_CDN}/assets/css/toast.min.css`,document.head.appendChild(e)}(),function(){const t="ts-toast-inline-extras";if(document.getElementById(t))return;const e=document.createElement("style");e.id=t,e.textContent="\n /* Ensure center positions exist even if external CSS lacks them */\n .ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }\n .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }\n .ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }\n .ts-toast-overlay.center { align-items: center; justify-content: center; }\n .ts-toast-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2147483646; }\n .ts-toast.ts-toast-confirm { max-width: min(92vw, 440px); width: max(320px, 60%); flex-direction: column; gap: 12px; padding: 16px 20px; background: var(--toast-bg, #fff); color: var(--toast-color, #000); border: 1px solid var(--toast-border, #e5e7eb); border-radius: 12px; box-shadow: var(--toast-shadow, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)); text-align: center; }\n .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }\n .ts-toast-actions { display: flex; gap: 10px; justify-content: center; margin-top: 12px; }\n .ts-toast-btn { appearance: none; border: 0; padding: 8px 12px; border-radius: 8px; font-weight: 600; cursor: pointer; }\n .ts-toast-btn.cancel { background: #e9ecef; color: #1f2937; }\n .ts-toast-btn.confirm { background: #3b82f6; color: #fff; }\n .ts-toast.ts-toast-error .ts-toast-btn.confirm,\n .ts-toast.ts-toast-warning .ts-toast-btn.confirm { background: #ef4444; color: #fff; }\n .ts-toast.ts-toast-confirm .ts-toast-title { font-weight: 700; font-size: 1.05rem; margin-top: 4px; }\n .ts-toast.ts-toast-confirm .ts-toast-close { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; border-radius: 999px; border: 0; background: transparent; color: #6b7280; font-size: 20px; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }\n .ts-toast.ts-toast-confirm .ts-toast-close:hover { background: rgba(0,0,0,0.06); }\n .ts-toast.ts-toast-confirm .ts-toast-icon { width: 64px; height: 64px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; }\n .ts-toast.ts-toast-confirm .ts-toast-icon img { width: 36px; height: 36px; }\n .ts-toast.ts-toast-confirm.ts-toast-success .ts-toast-icon { background: #dcfce7; }\n .ts-toast.ts-toast-confirm.ts-toast-info .ts-toast-icon { background: #dbeafe; }\n .ts-toast.ts-toast-confirm.ts-toast-warning .ts-toast-icon { background: #fef3c7; }\n .ts-toast.ts-toast-confirm.ts-toast-error .ts-toast-icon { background: #fee2e2; }\n ",document.head.appendChild(e)}();const toast=function(t,e={}){const{position:o="top-right",animation:s="slide-right",type:n="info",duration:a=3e3,icon:i=null,showLoader:r=!1,mode:l="alert",title:c=null,confirmText:d="Yes",cancelText:u="No",input:m=!1,inputPlaceholder:f="",inputValue:p="",confirmButtonBg:y=null,confirmButtonColor:h=null,cancelButtonBg:g=null,cancelButtonColor:b=null,onConfirm:v=null,onCancel:x=null,onResult:w=null,useOverlay:T=!0,closeOnOverlayClick:C=!0,showClose:E=!1,closeOnEscape:S=!0,allowHtml:L=!0,dismissOnClick:_=!0,onClick:k=null,onShow:A=null,onDismiss:N=null}=e,O="confirm"===l||"swal"===l,M=tsToastReducedMotion(),$="string"==typeof e.animation&&e.animation.trim()?{"slide-top":"ts-toast-slide-top","slide-bottom":"ts-toast-slide-bottom","slide-left":"ts-toast-slide-left","slide-right":"ts-toast-slide-right","zoom-in":"ts-toast-zoom-in","zoom-out":"ts-toast-zoom-out",flip:"ts-toast-flip"}[B=e.animation.trim()]||B:O||"center"===o?"ts-toast-zoom-in":o.startsWith("top")?"ts-toast-slide-top":o.startsWith("bottom")?"ts-toast-slide-bottom":o.endsWith("left")?"ts-toast-slide-left":"ts-toast-slide-right";var B;const R=(t,e)=>{const o=t.dataset&&t.dataset.anim?t.dataset.anim:t.style.animation||"";let s="";o.includes("ts-toast-slide-top")?s="translateY(-100%)":o.includes("ts-toast-slide-bottom")?s="translateY(100%)":(o.includes("ts-toast-slide-left")||o.includes("ts-toast-slide-right"))&&(s="translateX(100%)"),t.classList.add("ts-toast-slide-out"),t.classList.remove("ts-toast-show"),t.style.animation="",s&&(t.style.transform=s),t.style.opacity="0",setTimeout((()=>{t.classList.remove("ts-toast-slide-out"),t.parentNode&&t.parentNode.removeChild(t),"function"==typeof e&&e()}),M?0:500)},D=document.createElement("div");D.className=`ts-toast ts-toast-${n}${O?" ts-toast-confirm":""}`,D.dataset.anim=$,M||(D.style.animation=`${$} 0.5s ease`);const q="ts-toast-"+ ++tsToastIdCounter;O?(D.setAttribute("role","dialog"),D.setAttribute("aria-modal","true"),D.tabIndex=-1):"error"!==n&&"warning"!==n||D.setAttribute("role","alert"),O||(D.style.flexDirection="row-reverse",D.style.justifyContent="flex-end");const j=document.createElement("span");if(j.className="ts-toast-icon",j.style.display="flex",i)j.textContent=i;else{const t=document.createElement("img");t.alt="",t.setAttribute("aria-hidden","true"),t.style.width="30px",t.style.height="30px",t.style.objectFit="contain";const e={success:"success.gif",error:"error.gif",info:"info.gif",warning:"warning.gif"}[n];e&&(t.src=`${TS_TOAST_CDN}/assets/img/${e}`),j.appendChild(t)}const P=document.createElement("div");P.className="ts-toast-body",P.id=`${q}-body`,L?P.innerHTML=t:P.textContent=t;let z=null;if(O){if(z=document.createElement("div"),z.className="ts-toast-content",z.appendChild(j),c){const t=document.createElement("div");t.className="ts-toast-title",t.id=`${q}-title`,t.textContent=c,z.appendChild(t),D.setAttribute("aria-labelledby",t.id)}z.appendChild(P),D.setAttribute("aria-describedby",P.id),D.appendChild(z)}else D.appendChild(P);let I=null;O&&m&&("textarea"===m?(I=document.createElement("textarea"),I.rows=3):(I=document.createElement("input"),I.type="text"===m||"email"===m||"password"===m||"number"===m?m:"text"),I.className="ts-toast-input",I.placeholder=f,I.value=p,"textarea"!==m&&I.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.preventDefault(),Y(!0))})),D.appendChild(I));let F=null,X=null,Y=null,H=()=>{};if(O){F=document.createElement("div"),F.className="ts-toast-actions";const t=document.createElement("button");t.className="ts-toast-btn cancel",t.textContent=u;const e=document.createElement("button");e.className="ts-toast-btn confirm",e.textContent=d,g&&(t.style.background=g),b&&(t.style.color=b),y&&(e.style.background=y),h&&(e.style.color=h),F.appendChild(t),F.appendChild(e),D.appendChild(F),D.result=new Promise((t=>{X=t}));let o=!1;Y=t=>{if(o)return;o=!0;const e=t?!I||I.value:!!I&&null;X&&X(e),t&&"function"==typeof v&&v(e,D),t||"function"!=typeof x||x(D),"function"==typeof w&&w(e,D),H(),R(D,(()=>{N&&"function"==typeof N&&N(D),W&&W.parentNode&&W.parentNode.removeChild(W)}))},t.addEventListener("click",(t=>{t.stopPropagation(),Y(!1)})),e.addEventListener("click",(t=>{t.stopPropagation(),Y(!0)}))}let K=null;r&&(K=document.createElement("div"),K.className="ts-toast-loader",D.appendChild(K));let W=null;if(O&&T){if(W=document.createElement("div"),W.className="ts-toast-overlay"+(e.position?` ${o}`:""),document.body.appendChild(W),W.appendChild(D),E){const t=document.createElement("button");t.className="ts-toast-close",t.setAttribute("aria-label","Close"),t.innerHTML="&times;",t.addEventListener("click",(t=>{t.stopPropagation(),Y(!1)})),D.appendChild(t)}if(C){let t=!1;W.addEventListener("pointerdown",(e=>{t=e.target===W})),W.addEventListener("click",(e=>{e.target===W&&t&&Y(!1),t=!1}))}}else{let t=document.querySelector(`.ts-toast-container.${o}`);t||(t=document.createElement("div"),t.className=`ts-toast-container ${o}`,document.body.appendChild(t)),t.hasAttribute("aria-live")||(t.setAttribute("role","status"),t.setAttribute("aria-live","polite"),t.setAttribute("aria-relevant","additions")),t.appendChild(D)}if(O){const t=document.activeElement;tsToastOpenModals+=1,1===tsToastOpenModals&&(tsToastPrevOverflow=document.body.style.overflow,document.body.style.overflow="hidden");const e=()=>{const t=document.querySelectorAll(".ts-toast.ts-toast-confirm");return 0===t.length||t[t.length-1]===D},o=t=>{if(!e())return;if("Escape"===t.key&&S)return t.preventDefault(),void Y(!1);if("Tab"!==t.key)return;const o=tsToastFocusable(D);if(!o.length)return void t.preventDefault();const s=o[0],n=o[o.length-1];D.contains(document.activeElement)?t.shiftKey&&document.activeElement===s?(t.preventDefault(),n.focus()):t.shiftKey||document.activeElement!==n||(t.preventDefault(),s.focus()):(t.preventDefault(),(t.shiftKey?n:s).focus())};document.addEventListener("keydown",o,!0),H=()=>{document.removeEventListener("keydown",o,!0),tsToastOpenModals=Math.max(0,tsToastOpenModals-1),0===tsToastOpenModals&&(document.body.style.overflow=tsToastPrevOverflow),t&&"function"==typeof t.focus&&document.contains(t)&&t.focus()},(I||D.querySelector(".ts-toast-btn.confirm")||D).focus()}if(A&&"function"==typeof A&&A(D),setTimeout((()=>{D.classList.add("ts-toast-show")}),100),r&&K&&setTimeout((()=>{D._managedByLoading||(K.classList.add("done"),K.remove(),D.contains(j)||(O&&z?z.appendChild(j):D.appendChild(j)))}),2e3),r||O||D.contains(j)||D.appendChild(j),!O&&a>0){const t=setTimeout((()=>{R(D,(()=>{N&&"function"==typeof N&&N(D)}))}),a);D._autoRemove=t}if(!O&&_&&D.addEventListener("click",(()=>{D._autoRemove&&clearTimeout(D._autoRemove),k&&"function"==typeof k&&k(D),R(D,(()=>{N&&"function"==typeof N&&N(D)}))})),!O){let t=0,e=0,o=0;D.addEventListener("touchstart",(o=>{t=o.changedTouches[0].screenX,e=o.changedTouches[0].screenY}),{passive:!0}),D.addEventListener("touchend",(s=>{o=s.changedTouches[0].screenX;const n=Math.abs(t-o),a=Math.abs(e-s.changedTouches[0].screenY);n>50&&n>a&&(D._autoRemove&&clearTimeout(D._autoRemove),R(D,(()=>{N&&"function"==typeof N&&N(D)})))}))}return D.close=()=>{D._autoRemove&&clearTimeout(D._autoRemove),O?Y(!1):R(D,(()=>{N&&"function"==typeof N&&N(D)}))},D};toast.success=function(t,e){return toast(t,{...e,type:"success"})},toast.error=function(t,e){return toast(t,{...e,type:"error"})},toast.warning=function(t,e){return toast(t,{...e,type:"warning"})},toast.info=function(t,e){return toast(t,{...e,type:"info"})},toast.update=function(t,e,o={}){const{type:s=null,icon:n=null,showLoader:a=!1,duration:i=3e3,onClick:r=null,onShow:l=null,onDismiss:c=null}=o,d=t.querySelector(".ts-toast-loader"),u=t.querySelector(".ts-toast-icon");d&&d.remove(),s&&(["success","error","info","warning"].forEach((e=>t.classList.remove(`ts-toast-${e}`))),t.classList.add("ts-toast",`ts-toast-${s}`,"ts-toast-show"));const m=t.querySelector(".ts-toast-body");m&&(m.innerHTML=e),u&&u.remove();const f=document.createElement("span");if(f.className="ts-toast-icon",f.style.display="flex",n)f.textContent=n;else{const t=document.createElement("img");t.alt="",t.setAttribute("aria-hidden","true"),t.style.width="30px",t.style.height="30px",t.style.objectFit="contain";const e={success:"success.gif",error:"error.gif",info:"info.gif",warning:"warning.gif"}[s];e&&(t.src=`${TS_TOAST_CDN}/assets/img/${e}`),f.appendChild(t)}if((t.querySelector(".ts-toast-content")||t).appendChild(f),a){const e=document.createElement("div");e.className="ts-toast-loader",t.appendChild(e),setTimeout((()=>{e.classList.add("done")}),2e3)}t._autoRemove&&clearTimeout(t._autoRemove);const p=setTimeout((()=>{var e,o;o=()=>{c&&"function"==typeof c&&c(t)},(e=t).classList.add("ts-toast-slide-out"),e.classList.remove("ts-toast-show"),e.style.animation="",setTimeout((()=>{e.classList.remove("ts-toast-slide-out"),e.parentNode&&e.parentNode.removeChild(e),"function"==typeof o&&o()}),500)}),i);t._autoRemove=p},toast.loading=function(t,e={}){const o=toast(t,{...e,type:e.type||"info",duration:0,showLoader:!0,icon:null});requestAnimationFrame((()=>{o.classList.add("ts-toast-show")}));const s=o.querySelector(".ts-toast-loader");let n=o.querySelector(".ts-toast-icon");return n||(n=document.createElement("span"),n.className="ts-toast-icon",n.style.display="flex",o.appendChild(n)),o._managedByLoading=!0,s&&setTimeout((()=>{o._managedByLoading||s.classList.add("done")}),2e3),{update:(t,e={})=>{o._managedByLoading=!1,toast.update(o,t,{...e,showLoader:!1})},close:()=>{o._managedByLoading=!1,o.close()}}},toast.confirm=function(t,e={}){return new Promise((o=>{toast(t,{...e,mode:"confirm",duration:0,dismissOnClick:!1,onResult:t=>o(t)})}))},toast.dismissAll=function(){document.querySelectorAll(".ts-toast").forEach((t=>{"function"==typeof t.close&&t.close()}))},"undefined"!=typeof window&&(window.toast=toast),"undefined"!=typeof module&&module.exports&&(module.exports=toast);
package/toast.module.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // Single source of truth for the CDN this build points at.
4
4
  // `npm run sync:version` rewrites it from package.json, so it can never go stale.
5
- const TS_TOAST_VERSION = "5.4.0";
5
+ const TS_TOAST_VERSION = "5.5.0";
6
6
  // Point this at your own copy of assets/ to self-host the CSS and icons
7
7
  // (useful offline, behind a strict CSP, or when you don't want a CDN dependency):
8
8
  // window.TS_TOAST_ASSET_BASE = '/vendor/toastnotification';
@@ -10,6 +10,23 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
10
10
  ? String(window.TS_TOAST_ASSET_BASE).replace(/\/+$/, '')
11
11
  : `https://cdn.jsdelivr.net/npm/@tsirosgeorge/toastnotification@${TS_TOAST_VERSION}`;
12
12
 
13
+ // How many confirm dialogs are currently open. The body scroll lock belongs to the
14
+ // group, so only the last dialog to close may release it.
15
+ let tsToastOpenModals = 0;
16
+ let tsToastPrevOverflow = '';
17
+
18
+ const tsToastReducedMotion = () =>
19
+ typeof window !== 'undefined' &&
20
+ typeof window.matchMedia === 'function' &&
21
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
22
+
23
+ // Everything inside the dialog a keyboard can reach, in DOM order.
24
+ const tsToastFocusable = (root) => Array.from(
25
+ root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
26
+ ).filter((el) => !el.disabled && el.offsetParent !== null);
27
+
28
+ let tsToastIdCounter = 0;
29
+
13
30
  // Load the stylesheet from the CDN, unless the page opted out by importing it itself
14
31
  // (set window.TS_TOAST_NO_CSS = true before loading, or ship assets/css/toast.css yourself).
15
32
  (function loadStylesheet() {
@@ -33,6 +50,8 @@ const TS_TOAST_CDN = (typeof window !== 'undefined' && window.TS_TOAST_ASSET_BAS
33
50
  /* Ensure center positions exist even if external CSS lacks them */
34
51
  .ts-toast-container.top-center { top: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
35
52
  .ts-toast-container.bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); align-items: center; }
53
+ .ts-toast-container.center { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }
54
+ .ts-toast-overlay.center { align-items: center; justify-content: center; }
36
55
  .ts-toast-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2147483646; }
37
56
  .ts-toast.ts-toast-confirm { max-width: min(92vw, 440px); width: max(320px, 60%); flex-direction: column; gap: 12px; padding: 16px 20px; background: var(--toast-bg, #fff); color: var(--toast-color, #000); border: 1px solid var(--toast-border, #e5e7eb); border-radius: 12px; box-shadow: var(--toast-shadow, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)); text-align: center; }
38
57
  .ts-toast.ts-toast-confirm .ts-toast-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
@@ -84,6 +103,11 @@ const toast = function (message, options = {}) {
84
103
  useOverlay = true,
85
104
  closeOnOverlayClick = true,
86
105
  showClose = false,
106
+ // Escape cancels a confirm dialog
107
+ closeOnEscape = true,
108
+ // `message` is written as HTML for backwards compatibility. Pass false to
109
+ // render it as plain text, which is what you want for anything user-supplied.
110
+ allowHtml = true,
87
111
  // interactions
88
112
  dismissOnClick = true, // ignored if confirm-mode
89
113
  onClick = null, // Custom onClick event listener
@@ -92,6 +116,7 @@ const toast = function (message, options = {}) {
92
116
  } = options;
93
117
 
94
118
  const isConfirm = (mode === 'confirm' || mode === 'swal');
119
+ const reducedMotion = tsToastReducedMotion();
95
120
 
96
121
  // Pick an animation intelligently when one wasn't explicitly provided
97
122
  const resolvedAnimation = (typeof options.animation === 'string' && options.animation.trim())
@@ -107,10 +132,12 @@ const toast = function (message, options = {}) {
107
132
  };
108
133
  return m[a] || a;
109
134
  })(options.animation.trim())
110
- : (isConfirm ? 'ts-toast-zoom-in' : (position.startsWith('top') ? 'ts-toast-slide-top'
135
+ // 'center' has no edge to slide in from, so it zooms like a dialog.
136
+ : (isConfirm || position === 'center' ? 'ts-toast-zoom-in'
137
+ : position.startsWith('top') ? 'ts-toast-slide-top'
111
138
  : position.startsWith('bottom') ? 'ts-toast-slide-bottom'
112
139
  : position.endsWith('left') ? 'ts-toast-slide-left'
113
- : 'ts-toast-slide-right'));
140
+ : 'ts-toast-slide-right');
114
141
 
115
142
  // helper: remove with smooth CSS transition and cleanup (used by alerts)
116
143
  const removeWithAnimation = (el, callback) => {
@@ -143,13 +170,24 @@ const toast = function (message, options = {}) {
143
170
  el.classList.remove('ts-toast-slide-out');
144
171
  if (el.parentNode) el.parentNode.removeChild(el);
145
172
  if (typeof callback === 'function') callback();
146
- }, 500);
173
+ }, reducedMotion ? 0 : 500);
147
174
  };
148
175
 
149
176
  const toastElement = document.createElement('div');
150
177
  toastElement.className = `ts-toast ts-toast-${type}${isConfirm ? ' ts-toast-confirm' : ''}`;
151
178
  toastElement.dataset.anim = resolvedAnimation;
152
- toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
179
+ if (!reducedMotion) toastElement.style.animation = `${resolvedAnimation} 0.5s ease`;
180
+
181
+ const uid = `ts-toast-${++tsToastIdCounter}`;
182
+ if (isConfirm) {
183
+ // Without these a screen reader announces nothing, and without tabindex the
184
+ // dialog cannot take focus away from whatever opened it.
185
+ toastElement.setAttribute('role', 'dialog');
186
+ toastElement.setAttribute('aria-modal', 'true');
187
+ toastElement.tabIndex = -1;
188
+ } else if (type === 'error' || type === 'warning') {
189
+ toastElement.setAttribute('role', 'alert');
190
+ }
153
191
  // In confirm mode, we stack content vertically; in alert mode keep original layout
154
192
  if (!isConfirm) {
155
193
  toastElement.style.flexDirection = 'row-reverse';
@@ -181,7 +219,11 @@ const toast = function (message, options = {}) {
181
219
  // Create Body
182
220
  const toastBody = document.createElement('div');
183
221
  toastBody.className = 'ts-toast-body';
184
- toastBody.innerHTML = message; // Allow HTML content in message
222
+ toastBody.id = `${uid}-body`;
223
+ // HTML by default for backwards compatibility; pass allowHtml: false for
224
+ // anything that came from a user.
225
+ if (allowHtml) toastBody.innerHTML = message;
226
+ else toastBody.textContent = message;
185
227
 
186
228
  // Content row for confirm (icon + text side-by-side)
187
229
  let contentRow = null;
@@ -192,10 +234,13 @@ const toast = function (message, options = {}) {
192
234
  if (title) {
193
235
  const titleEl = document.createElement('div');
194
236
  titleEl.className = 'ts-toast-title';
237
+ titleEl.id = `${uid}-title`;
195
238
  titleEl.textContent = title;
196
239
  contentRow.appendChild(titleEl);
240
+ toastElement.setAttribute('aria-labelledby', titleEl.id);
197
241
  }
198
242
  contentRow.appendChild(toastBody);
243
+ toastElement.setAttribute('aria-describedby', toastBody.id);
199
244
  toastElement.appendChild(contentRow);
200
245
  } else {
201
246
  toastElement.appendChild(toastBody);
@@ -214,6 +259,13 @@ const toast = function (message, options = {}) {
214
259
  inputElement.className = 'ts-toast-input';
215
260
  inputElement.placeholder = inputPlaceholder;
216
261
  inputElement.value = inputValue;
262
+ // Enter submits a single-line field, the way a native prompt does.
263
+ // A textarea keeps Enter for newlines.
264
+ if (input !== 'textarea') {
265
+ inputElement.addEventListener('keydown', (e) => {
266
+ if (e.key === 'Enter') { e.preventDefault(); resolveAndClose(true); }
267
+ });
268
+ }
217
269
  toastElement.appendChild(inputElement);
218
270
  }
219
271
 
@@ -223,6 +275,8 @@ const toast = function (message, options = {}) {
223
275
  // Assigned in confirm mode; the overlay and (x) handlers below call it so that
224
276
  // *every* way of dismissing the dialog settles the promise and the callbacks.
225
277
  let resolveAndClose = null;
278
+ // Releases the scroll lock, the key handler and the focus this dialog took.
279
+ let releaseModal = () => {};
226
280
  if (isConfirm) {
227
281
  actionsContainer = document.createElement('div');
228
282
  actionsContainer.className = 'ts-toast-actions';
@@ -262,6 +316,7 @@ const toast = function (message, options = {}) {
262
316
  if (confirmed && typeof onConfirm === 'function') onConfirm(result, toastElement);
263
317
  if (!confirmed && typeof onCancel === 'function') onCancel(toastElement);
264
318
  if (typeof onResult === 'function') onResult(result, toastElement);
319
+ releaseModal();
265
320
  // Use the same slide+fade removal as alerts
266
321
  removeWithAnimation(toastElement, () => {
267
322
  if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
@@ -304,11 +359,17 @@ const toast = function (message, options = {}) {
304
359
  toastElement.appendChild(closeBtn);
305
360
  }
306
361
  if (closeOnOverlayClick) {
362
+ // The cancel must both start and end on the backdrop. Selecting text in
363
+ // the input and releasing the mouse outside the card produced a click
364
+ // whose target was the overlay, which threw the dialog away mid-edit.
365
+ let pressedOnBackdrop = false;
366
+ overlay.addEventListener('pointerdown', (e) => { pressedOnBackdrop = e.target === overlay; });
307
367
  overlay.addEventListener('click', (e) => {
308
368
  // Backdrop click is a cancel: settle the promise and onCancel/onResult,
309
369
  // then close. Previously this resolved only the internal el.result,
310
370
  // so `await toast.confirm(...)` hung forever.
311
- if (e.target === overlay) resolveAndClose(false);
371
+ if (e.target === overlay && pressedOnBackdrop) resolveAndClose(false);
372
+ pressedOnBackdrop = false;
312
373
  });
313
374
  }
314
375
  } else {
@@ -319,9 +380,79 @@ const toast = function (message, options = {}) {
319
380
  container.className = `ts-toast-container ${position}`;
320
381
  document.body.appendChild(container);
321
382
  }
383
+ if (!container.hasAttribute('aria-live')) {
384
+ // Without this a toast is invisible to a screen reader.
385
+ container.setAttribute('role', 'status');
386
+ container.setAttribute('aria-live', 'polite');
387
+ container.setAttribute('aria-relevant', 'additions');
388
+ }
322
389
  container.appendChild(toastElement);
323
390
  }
324
391
 
392
+ if (isConfirm) {
393
+ // The dialog has to own the keyboard while it is open. Without this the
394
+ // element that opened it keeps focus, so pressing Enter or Space activates
395
+ // it again and stacks a second dialog on top of the first — and Tab walks
396
+ // through the page behind the backdrop.
397
+ const previouslyFocused = document.activeElement;
398
+
399
+ tsToastOpenModals += 1;
400
+ if (tsToastOpenModals === 1) {
401
+ tsToastPrevOverflow = document.body.style.overflow;
402
+ document.body.style.overflow = 'hidden';
403
+ }
404
+
405
+ // With dialogs stacked, only the top one should answer the keyboard.
406
+ const isTopmost = () => {
407
+ const open = document.querySelectorAll('.ts-toast.ts-toast-confirm');
408
+ return open.length === 0 || open[open.length - 1] === toastElement;
409
+ };
410
+
411
+ const onKeydown = (e) => {
412
+ if (!isTopmost()) return;
413
+
414
+ if (e.key === 'Escape' && closeOnEscape) {
415
+ e.preventDefault();
416
+ resolveAndClose(false);
417
+ return;
418
+ }
419
+ if (e.key !== 'Tab') return;
420
+
421
+ const focusables = tsToastFocusable(toastElement);
422
+ if (!focusables.length) { e.preventDefault(); return; }
423
+
424
+ const first = focusables[0];
425
+ const last = focusables[focusables.length - 1];
426
+
427
+ // Focus can start outside the dialog (the trigger button); pull it back.
428
+ if (!toastElement.contains(document.activeElement)) {
429
+ e.preventDefault();
430
+ (e.shiftKey ? last : first).focus();
431
+ } else if (e.shiftKey && document.activeElement === first) {
432
+ e.preventDefault();
433
+ last.focus();
434
+ } else if (!e.shiftKey && document.activeElement === last) {
435
+ e.preventDefault();
436
+ first.focus();
437
+ }
438
+ };
439
+ document.addEventListener('keydown', onKeydown, true);
440
+
441
+ releaseModal = () => {
442
+ document.removeEventListener('keydown', onKeydown, true);
443
+ tsToastOpenModals = Math.max(0, tsToastOpenModals - 1);
444
+ if (tsToastOpenModals === 0) document.body.style.overflow = tsToastPrevOverflow;
445
+ // Hand the keyboard back to whatever opened the dialog.
446
+ if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
447
+ document.contains(previouslyFocused)) {
448
+ previouslyFocused.focus();
449
+ }
450
+ };
451
+
452
+ // Land on the input when there is one, otherwise the confirm button.
453
+ (inputElement || toastElement.querySelector('.ts-toast-btn.confirm') || toastElement).focus();
454
+ }
455
+
325
456
  // Trigger the onShow event if provided
326
457
  if (onShow && typeof onShow === 'function') {
327
458
  onShow(toastElement);
@@ -366,8 +497,10 @@ const toast = function (message, options = {}) {
366
497
  if (!isConfirm && dismissOnClick) {
367
498
  toastElement.addEventListener('click', () => {
368
499
  if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove); // Clear the auto-remove timeout
500
+ // onClick belongs to the click, not to the end of the exit animation,
501
+ // which is where it used to fire half a second late.
502
+ if (onClick && typeof onClick === 'function') onClick(toastElement);
369
503
  removeWithAnimation(toastElement, () => {
370
- if (onClick && typeof onClick === 'function') onClick(toastElement);
371
504
  if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
372
505
  });
373
506
  });
@@ -376,15 +509,23 @@ const toast = function (message, options = {}) {
376
509
  // Add swipe event listeners for mobile dismissal
377
510
  if (!isConfirm) {
378
511
  let touchStartX = 0;
512
+ let touchStartY = 0;
379
513
  let touchEndX = 0;
380
514
 
515
+ // Passive: these never preventDefault, and a non-passive touchstart blocks
516
+ // scrolling on the whole toast.
381
517
  toastElement.addEventListener('touchstart', (e) => {
382
518
  touchStartX = e.changedTouches[0].screenX;
383
- });
519
+ touchStartY = e.changedTouches[0].screenY;
520
+ }, { passive: true });
384
521
 
385
522
  toastElement.addEventListener('touchend', (e) => {
386
523
  touchEndX = e.changedTouches[0].screenX;
387
- if (Math.abs(touchStartX - touchEndX) > 50) { // Swipe distance threshold
524
+ const dx = Math.abs(touchStartX - touchEndX);
525
+ const dy = Math.abs(touchStartY - e.changedTouches[0].screenY);
526
+ // Only a mostly-horizontal swipe dismisses, so scrolling the page past a
527
+ // toast no longer throws it away on a bit of sideways drift.
528
+ if (dx > 50 && dx > dy) {
388
529
  if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
389
530
  removeWithAnimation(toastElement, () => {
390
531
  if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
@@ -393,6 +534,16 @@ const toast = function (message, options = {}) {
393
534
  });
394
535
  }
395
536
 
537
+ // Let callers dismiss a toast they are holding, instead of only waiting out
538
+ // the duration or making the user click it.
539
+ toastElement.close = () => {
540
+ if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
541
+ if (isConfirm) { resolveAndClose(false); return; }
542
+ removeWithAnimation(toastElement, () => {
543
+ if (onDismiss && typeof onDismiss === 'function') onDismiss(toastElement);
544
+ });
545
+ };
546
+
396
547
  return toastElement;
397
548
  };
398
549
 
@@ -553,15 +704,11 @@ const toast = function (message, options = {}) {
553
704
  showLoader: false // Disable loader when updating the message
554
705
  });
555
706
  },
707
+ // Reuse the element's own close so onDismiss fires, which this
708
+ // hand-rolled copy of the removal never did.
556
709
  close: () => {
557
- if (toastElement._autoRemove) clearTimeout(toastElement._autoRemove);
558
- toastElement.classList.add('ts-toast-slide-out');
559
- toastElement.classList.remove('ts-toast-show');
560
- toastElement.style.animation = '';
561
- setTimeout(() => {
562
- toastElement.classList.remove('ts-toast-slide-out');
563
- if (toastElement.parentNode) toastElement.parentNode.removeChild(toastElement);
564
- }, 500);
710
+ toastElement._managedByLoading = false;
711
+ toastElement.close();
565
712
  }
566
713
  };
567
714
  };
@@ -583,6 +730,13 @@ const toast = function (message, options = {}) {
583
730
  });
584
731
  };
585
732
 
733
+ // Close every toast currently on screen. Confirm dialogs settle as a cancel.
734
+ toast.dismissAll = function () {
735
+ document.querySelectorAll('.ts-toast').forEach((el) => {
736
+ if (typeof el.close === 'function') el.close();
737
+ });
738
+ };
739
+
586
740
  // Expose globally for CDN / browser usage
587
741
  if (typeof window !== 'undefined') {
588
742
  window.toast = toast;