toastify-pro 1.4.0 → 1.6.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.
@@ -9,17 +9,21 @@
9
9
  * - Position-aware car swipe exit animations
10
10
  * - Description support for enhanced messaging
11
11
  * - Six theme variants (success, error, info, warning, dark, light)
12
+ * - Custom color toasts with gradient support (custom method)
12
13
  * - Progress bar with shimmer effects
13
14
  * - Responsive design for mobile devices
14
15
  * - Framework agnostic (works with React, Vue, Angular, etc.)
15
16
  * - Confirmation dialogs with customizable buttons and callbacks
17
+ * - Confirmation overlay with blur effect for focus
16
18
  * - Center position support for enhanced focus
17
19
  * - Independent positioning for confirmations
18
- * - Loading states for async operations
19
- * - Custom gradient colors with primaryColor/secondaryColor
20
- * - Single instance mode with shake animation
20
+ * - Action buttons in toasts with customizable callbacks
21
+ * - Pause on hover functionality
22
+ * - Queue management (maxToasts, newestOnTop)
23
+ * - Full accessibility support (ARIA, keyboard navigation, reduced motion)
24
+ * - Focus management for confirmation dialogs
21
25
  *
22
- * @version 1.4.0
26
+ * @version 1.6.0
23
27
  * @author ToastifyPro Team
24
28
  * @license MIT
25
29
  */
@@ -35,6 +39,12 @@ class ToastifyPro {
35
39
  * @param {number} options.timeout - Auto-dismiss timeout in milliseconds (0 to disable)
36
40
  * @param {boolean} options.allowClose - Whether to show close button
37
41
  * @param {number} options.maxLength - Maximum message length
42
+ * @param {string} options.primaryColor - Primary color for custom() method
43
+ * @param {string} options.secondaryColor - Secondary color for gradient in custom() method
44
+ * @param {boolean} options.pauseOnHover - Pause timeout when hovering over toast (default: true)
45
+ * @param {number} options.maxToasts - Maximum number of visible toasts (0 for unlimited)
46
+ * @param {boolean} options.newestOnTop - Show newest toasts on top (default: true)
47
+ * @param {boolean} options.ariaLive - ARIA live region setting: 'polite' or 'assertive' (default: 'polite')
38
48
  */
39
49
  constructor(options = {}) {
40
50
  // Validate options parameter
@@ -49,7 +59,16 @@ class ToastifyPro {
49
59
  timeout: options.timeout || 3000,
50
60
  allowClose: options.allowClose !== false, // default true
51
61
  maxLength: options.maxLength || 100,
62
+ primaryColor: options.primaryColor || null, // Custom primary color for custom() method
63
+ secondaryColor: options.secondaryColor || null, // Custom secondary color for gradient
64
+ pauseOnHover: options.pauseOnHover !== false, // default true - pause timeout on hover
65
+ maxToasts: options.maxToasts || 0, // 0 = unlimited
66
+ newestOnTop: options.newestOnTop !== false, // default true
67
+ ariaLive: options.ariaLive || 'polite', // 'polite' or 'assertive'
52
68
  };
69
+
70
+ // Track active toasts for queue management
71
+ this.activeToasts = [];
53
72
 
54
73
  // Validate position
55
74
  const validPositions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'center'];
@@ -82,6 +101,46 @@ class ToastifyPro {
82
101
 
83
102
  // Inject styles once
84
103
  this.injectStyles();
104
+
105
+ // Setup global keyboard event listener for accessibility
106
+ this.setupKeyboardNavigation();
107
+ }
108
+
109
+ /**
110
+ * Sets up keyboard navigation for accessibility
111
+ * - Escape key dismisses the most recent toast or confirmation
112
+ * - Tab key cycles through focusable elements in confirmations
113
+ */
114
+ setupKeyboardNavigation() {
115
+ // Only setup once globally
116
+ if (window._toastifyProKeyboardSetup) return;
117
+ window._toastifyProKeyboardSetup = true;
118
+
119
+ document.addEventListener('keydown', (e) => {
120
+ // Escape key - dismiss toast or confirmation
121
+ if (e.key === 'Escape') {
122
+ // First check for active confirmation
123
+ if (globalActiveConfirmation && globalActiveConfirmation.element) {
124
+ const loadingBtn = globalActiveConfirmation.element.querySelector('.toast-btn-confirm.loading');
125
+ if (!loadingBtn) {
126
+ globalActiveConfirmation.close();
127
+ }
128
+ return;
129
+ }
130
+
131
+ // Otherwise dismiss the most recent toast
132
+ const containers = document.querySelectorAll('.toastify-pro-container');
133
+ containers.forEach(container => {
134
+ const toasts = container.querySelectorAll('.toastify-pro:not(.confirmation)');
135
+ if (toasts.length > 0) {
136
+ const lastToast = toasts[toasts.length - 1];
137
+ if (lastToast && lastToast._toastInstance) {
138
+ lastToast._toastInstance.removeToast(lastToast);
139
+ }
140
+ }
141
+ });
142
+ }
143
+ });
85
144
  }
86
145
 
87
146
  /**
@@ -476,6 +535,7 @@ class ToastifyPro {
476
535
  transition: all 0.2s ease;
477
536
  flex-shrink: 0;
478
537
  width: 32px;
538
+ border: none;
479
539
  height: 32px;
480
540
  display: flex;
481
541
  align-items: center;
@@ -707,6 +767,9 @@ class ToastifyPro {
707
767
  }
708
768
 
709
769
  .toast-btn-confirm {
770
+ display: flex;
771
+ align-items: center;
772
+ justify-content: center;
710
773
  color: white;
711
774
  font-weight: 700;
712
775
  border: 2px solid rgba(255, 255, 255, 0.4);
@@ -745,17 +808,20 @@ class ToastifyPro {
745
808
 
746
809
  .toast-btn-confirm .btn-spinner {
747
810
  display: none;
748
- width: 16px;
749
- height: 16px;
750
- border: 2px solid rgba(255, 255, 255, 0.3);
751
- border-top-color: white;
752
- border-radius: 50%;
753
- animation: spin 0.6s linear infinite;
754
- margin-right: 8px;
811
+ align-items: center;
812
+ justify-content: center;
813
+ margin-left: 8px;
814
+ }
815
+
816
+ .toast-btn-confirm .btn-spinner svg {
817
+ width: 25px;
818
+ height: 25px;
819
+ animation: spin 1s linear infinite;
820
+ color: currentColor;
755
821
  }
756
822
 
757
823
  .toast-btn-confirm.loading .btn-spinner {
758
- display: inline-block;
824
+ display: inline-flex;
759
825
  }
760
826
 
761
827
  .toast-btn-confirm.loading .btn-text {
@@ -793,6 +859,169 @@ class ToastifyPro {
793
859
  max-width: calc(100vw - 32px);
794
860
  }
795
861
  }
862
+
863
+ /* Custom toast type */
864
+ .toastify-pro.custom {
865
+ border-color: rgba(255, 255, 255, 0.2);
866
+ }
867
+
868
+ .toastify-pro.custom.light-text {
869
+ color: #1e293b;
870
+ }
871
+
872
+ .toastify-pro.custom.light-text .toast-icon {
873
+ background: rgba(15, 23, 42, 0.1);
874
+ }
875
+
876
+ .toastify-pro.custom.light-text .close-btn {
877
+ background: rgba(15, 23, 42, 0.08);
878
+ }
879
+
880
+ .toastify-pro.custom.light-text .close-btn:hover {
881
+ background: rgba(15, 23, 42, 0.15);
882
+ }
883
+
884
+ .toastify-pro.custom.light-text::before {
885
+ background: linear-gradient(90deg,
886
+ rgba(30, 41, 59, 0.8) 0%,
887
+ rgba(30, 41, 59, 0.4) 50%,
888
+ rgba(30, 41, 59, 0.8) 100%);
889
+ }
890
+
891
+ .toastify-pro.custom.light-text::after {
892
+ background: rgba(30, 41, 59, 0.6);
893
+ }
894
+
895
+ /* Confirmation Overlay */
896
+ .toastify-pro-overlay {
897
+ position: fixed;
898
+ top: 0;
899
+ left: 0;
900
+ right: 0;
901
+ bottom: 0;
902
+ background: rgba(0, 0, 0, 0.5);
903
+ backdrop-filter: blur(8px);
904
+ -webkit-backdrop-filter: blur(8px);
905
+ z-index: 9998;
906
+ opacity: 0;
907
+ transition: opacity 0.3s ease;
908
+ pointer-events: auto;
909
+ }
910
+
911
+ .toastify-pro-overlay.show {
912
+ opacity: 1;
913
+ }
914
+
915
+ /* Action Button Styles */
916
+ .toastify-pro .toast-action {
917
+ display: inline-flex;
918
+ align-items: center;
919
+ gap: 6px;
920
+ padding: 6px 12px;
921
+ margin-top: 8px;
922
+ border: none;
923
+ border-radius: 8px;
924
+ font-weight: 600;
925
+ font-size: 13px;
926
+ cursor: pointer;
927
+ transition: all 0.2s ease;
928
+ background: rgba(255, 255, 255, 0.2);
929
+ color: inherit;
930
+ backdrop-filter: blur(10px);
931
+ }
932
+
933
+ .toastify-pro .toast-action:hover {
934
+ background: rgba(255, 255, 255, 0.3);
935
+ transform: translateY(-1px);
936
+ }
937
+
938
+ .toastify-pro .toast-action:active {
939
+ transform: translateY(0);
940
+ }
941
+
942
+ .toastify-pro.light .toast-action {
943
+ background: rgba(15, 23, 42, 0.1);
944
+ }
945
+
946
+ .toastify-pro.light .toast-action:hover {
947
+ background: rgba(15, 23, 42, 0.15);
948
+ }
949
+
950
+ /* Paused state - pause progress bar */
951
+ .toastify-pro.paused::after {
952
+ animation-play-state: paused;
953
+ }
954
+
955
+ /* Focus styles for accessibility */
956
+ .toastify-pro .close-btn:focus,
957
+ .toastify-pro .toast-action:focus,
958
+ .toast-btn:focus {
959
+ outline: 1px solid rgba(255, 255, 255, 0.8);
960
+ }
961
+
962
+ .toastify-pro.light .close-btn:focus,
963
+ .toastify-pro.light .toast-action:focus {
964
+ outline-color: 1px solid rgba(15, 23, 42, 0.5);
965
+ }
966
+
967
+ /* Screen reader only class */
968
+ .sr-only {
969
+ position: absolute;
970
+ width: 1px;
971
+ height: 1px;
972
+ padding: 0;
973
+ margin: -1px;
974
+ overflow: hidden;
975
+ clip: rect(0, 0, 0, 0);
976
+ white-space: nowrap;
977
+ border: 0;
978
+ }
979
+
980
+ /* Reduced motion support */
981
+ @media (prefers-reduced-motion: reduce) {
982
+ .toastify-pro {
983
+ transition: opacity 0.3s ease;
984
+ transform: none !important;
985
+ }
986
+
987
+ .toastify-pro.show {
988
+ animation: none !important;
989
+ opacity: 1;
990
+ transform: none !important;
991
+ }
992
+
993
+ .toastify-pro .toast-icon {
994
+ animation: none !important;
995
+ }
996
+
997
+ .toastify-pro::before {
998
+ animation: none !important;
999
+ }
1000
+
1001
+ .toastify-pro::after {
1002
+ animation: progress var(--duration, 5s) linear !important;
1003
+ }
1004
+
1005
+ .toastify-pro-overlay {
1006
+ transition: opacity 0.2s ease;
1007
+ }
1008
+
1009
+ .toast-btn::after {
1010
+ display: none;
1011
+ }
1012
+
1013
+ .toast-btn:hover {
1014
+ transform: none;
1015
+ }
1016
+
1017
+ .toastify-pro.confirmation .conf-close-btn:hover {
1018
+ transform: scale(1.05);
1019
+ }
1020
+
1021
+ .btn-spinner svg {
1022
+ animation: spin 1.5s linear infinite !important;
1023
+ }
1024
+ }
796
1025
  `;
797
1026
  document.head.appendChild(style);
798
1027
  } catch (error) {
@@ -809,6 +1038,9 @@ class ToastifyPro {
809
1038
  * @param {number} opts.timeout - Override default timeout
810
1039
  * @param {boolean} opts.allowClose - Override close button setting
811
1040
  * @param {number} opts.maxLength - Override max message length
1041
+ * @param {Object} opts.action - Action button configuration { label, onClick }
1042
+ * @param {boolean} opts.pauseOnHover - Pause timeout on hover
1043
+ * @param {string} opts.ariaLive - ARIA live region type ('polite' or 'assertive')
812
1044
  */
813
1045
  show(message, type = "dark", opts = {}) {
814
1046
  // Input validation
@@ -838,10 +1070,30 @@ class ToastifyPro {
838
1070
  const options = { ...this.defaultOptions, ...opts };
839
1071
 
840
1072
  try {
1073
+ // Queue management - remove oldest toasts if limit exceeded
1074
+ if (options.maxToasts > 0 && this.activeToasts.length >= options.maxToasts) {
1075
+ const toastsToRemove = this.activeToasts.length - options.maxToasts + 1;
1076
+ for (let i = 0; i < toastsToRemove; i++) {
1077
+ const oldestToast = this.activeToasts.shift();
1078
+ if (oldestToast && oldestToast.element) {
1079
+ this.removeToast(oldestToast.element);
1080
+ }
1081
+ }
1082
+ }
1083
+
841
1084
  // Create toast element
842
1085
  const toast = document.createElement("div");
843
1086
  toast.className = `toastify-pro ${type}`;
844
1087
 
1088
+ // Store reference to this instance for keyboard navigation
1089
+ toast._toastInstance = this;
1090
+
1091
+ // ARIA accessibility attributes
1092
+ const ariaLive = type === 'error' || type === 'warning' ? 'assertive' : (options.ariaLive || 'polite');
1093
+ toast.setAttribute('role', type === 'error' ? 'alert' : 'status');
1094
+ toast.setAttribute('aria-live', ariaLive);
1095
+ toast.setAttribute('aria-atomic', 'true');
1096
+
845
1097
  // Set duration for progress bar animation
846
1098
  if (options.timeout > 0) {
847
1099
  toast.style.setProperty('--duration', `${options.timeout}ms`);
@@ -850,6 +1102,7 @@ class ToastifyPro {
850
1102
  // Create icon wrapper
851
1103
  const iconWrapper = document.createElement("div");
852
1104
  iconWrapper.className = "toast-icon";
1105
+ iconWrapper.setAttribute('aria-hidden', 'true');
853
1106
  iconWrapper.innerHTML = this.getIconSVG(type);
854
1107
  toast.appendChild(iconWrapper);
855
1108
 
@@ -871,20 +1124,50 @@ class ToastifyPro {
871
1124
  contentWrapper.appendChild(descriptionElement);
872
1125
  }
873
1126
 
1127
+ // Action button support
1128
+ if (options.action && typeof options.action === 'object') {
1129
+ const actionBtn = document.createElement("button");
1130
+ actionBtn.className = "toast-action";
1131
+ actionBtn.textContent = options.action.label || 'Action';
1132
+ actionBtn.setAttribute('type', 'button');
1133
+ if (typeof options.action.onClick === 'function') {
1134
+ actionBtn.onclick = (e) => {
1135
+ e.stopPropagation();
1136
+ options.action.onClick({ close: () => this.removeToast(toast), event: e });
1137
+ };
1138
+ }
1139
+ contentWrapper.appendChild(actionBtn);
1140
+ }
1141
+
874
1142
  toast.appendChild(contentWrapper);
875
1143
 
876
1144
  // Add close button if enabled
877
1145
  if (options.allowClose) {
878
- const closeBtn = document.createElement("span");
1146
+ const closeBtn = document.createElement("button");
879
1147
  closeBtn.className = "close-btn";
880
1148
  closeBtn.innerHTML = "&times;";
1149
+ closeBtn.setAttribute('type', 'button');
881
1150
  closeBtn.setAttribute('aria-label', 'Close notification');
882
1151
  closeBtn.onclick = () => this.removeToast(toast);
883
1152
  toast.appendChild(closeBtn);
884
1153
  }
885
1154
 
886
- // Add toast to container
887
- this.container.appendChild(toast);
1155
+ // Add toast to container (respect newestOnTop setting)
1156
+ if (options.newestOnTop && this.container.firstChild) {
1157
+ this.container.insertBefore(toast, this.container.firstChild);
1158
+ } else {
1159
+ this.container.appendChild(toast);
1160
+ }
1161
+
1162
+ // Track toast for queue management
1163
+ const toastData = {
1164
+ element: toast,
1165
+ timeout: null,
1166
+ remainingTime: options.timeout,
1167
+ startTime: null,
1168
+ isPaused: false
1169
+ };
1170
+ this.activeToasts.push(toastData);
888
1171
 
889
1172
  // Apple AirDrop-style entrance animation
890
1173
  setTimeout(() => {
@@ -896,16 +1179,90 @@ class ToastifyPro {
896
1179
  }
897
1180
  }, 10);
898
1181
 
1182
+ // Pause on hover functionality
1183
+ if (options.pauseOnHover && options.timeout > 0) {
1184
+ toast.addEventListener('mouseenter', () => {
1185
+ if (toastData.timeout) {
1186
+ clearTimeout(toastData.timeout);
1187
+ toastData.isPaused = true;
1188
+ toastData.remainingTime -= (Date.now() - toastData.startTime);
1189
+ toast.classList.add('paused');
1190
+ }
1191
+ });
1192
+
1193
+ toast.addEventListener('mouseleave', () => {
1194
+ if (toastData.isPaused && toastData.remainingTime > 0) {
1195
+ toastData.isPaused = false;
1196
+ toastData.startTime = Date.now();
1197
+ toast.classList.remove('paused');
1198
+ // Update CSS variable for remaining progress
1199
+ toast.style.setProperty('--duration', `${toastData.remainingTime}ms`);
1200
+ // Restart the progress animation
1201
+ const afterElement = toast.querySelector('::after');
1202
+ toast.style.animation = 'none';
1203
+ void toast.offsetHeight; // Force reflow
1204
+ toast.style.animation = '';
1205
+
1206
+ toastData.timeout = setTimeout(() => this.removeToast(toast), toastData.remainingTime);
1207
+ }
1208
+ });
1209
+ }
1210
+
899
1211
  // Auto-remove after timeout
900
1212
  if (options.timeout > 0) {
901
- setTimeout(() => this.removeToast(toast), options.timeout);
1213
+ toastData.startTime = Date.now();
1214
+ toastData.timeout = setTimeout(() => this.removeToast(toast), options.timeout);
902
1215
  }
903
1216
 
904
- return toast; // Return element for potential future manipulation
1217
+ // Return toast control object
1218
+ return {
1219
+ element: toast,
1220
+ dismiss: () => this.removeToast(toast),
1221
+ update: (newMessage, newOpts) => this.updateToast(toast, newMessage, newOpts)
1222
+ };
905
1223
  } catch (error) {
906
1224
  console.error('ToastifyPro: Failed to create toast:', error);
907
1225
  }
908
1226
  }
1227
+
1228
+ /**
1229
+ * Updates an existing toast's content
1230
+ * @param {HTMLElement} toast - Toast element to update
1231
+ * @param {string} message - New message text
1232
+ * @param {Object} opts - Options to update
1233
+ */
1234
+ updateToast(toast, message, opts = {}) {
1235
+ if (!toast || !toast.parentNode) return;
1236
+
1237
+ const messageEl = toast.querySelector('.toast-message');
1238
+ const descEl = toast.querySelector('.toast-description');
1239
+
1240
+ if (message && messageEl) {
1241
+ messageEl.textContent = message;
1242
+ }
1243
+
1244
+ if (opts.description && descEl) {
1245
+ descEl.textContent = opts.description;
1246
+ } else if (opts.description) {
1247
+ const descriptionElement = document.createElement("div");
1248
+ descriptionElement.className = "toast-description";
1249
+ descriptionElement.textContent = opts.description;
1250
+ toast.querySelector('.toast-content')?.appendChild(descriptionElement);
1251
+ }
1252
+
1253
+ // Update type/style if provided
1254
+ if (opts.type) {
1255
+ const validTypes = ['success', 'error', 'info', 'warning', 'dark', 'light'];
1256
+ if (validTypes.includes(opts.type)) {
1257
+ validTypes.forEach(t => toast.classList.remove(t));
1258
+ toast.classList.add(opts.type);
1259
+ const iconWrapper = toast.querySelector('.toast-icon');
1260
+ if (iconWrapper) {
1261
+ iconWrapper.innerHTML = this.getIconSVG(opts.type);
1262
+ }
1263
+ }
1264
+ }
1265
+ }
909
1266
 
910
1267
  /**
911
1268
  * Removes a toast with position-aware car swipe animation
@@ -918,6 +1275,16 @@ class ToastifyPro {
918
1275
  }
919
1276
 
920
1277
  try {
1278
+ // Remove from active toasts tracking
1279
+ const toastIndex = this.activeToasts.findIndex(t => t.element === toast);
1280
+ if (toastIndex > -1) {
1281
+ const toastData = this.activeToasts[toastIndex];
1282
+ if (toastData.timeout) {
1283
+ clearTimeout(toastData.timeout);
1284
+ }
1285
+ this.activeToasts.splice(toastIndex, 1);
1286
+ }
1287
+
921
1288
  // Detect position to choose the right swipe direction
922
1289
  const container = toast.parentNode;
923
1290
  const position = container.className.split(' ')[1]; // get position class
@@ -960,6 +1327,33 @@ class ToastifyPro {
960
1327
  }
961
1328
  }
962
1329
  }
1330
+
1331
+ /**
1332
+ * Dismisses all active toasts
1333
+ * @param {string} type - Optional: only dismiss toasts of this type
1334
+ */
1335
+ dismissAll(type = null) {
1336
+ const toastsCopy = [...this.activeToasts];
1337
+ toastsCopy.forEach(toastData => {
1338
+ if (toastData.element) {
1339
+ if (type) {
1340
+ if (toastData.element.classList.contains(type)) {
1341
+ this.removeToast(toastData.element);
1342
+ }
1343
+ } else {
1344
+ this.removeToast(toastData.element);
1345
+ }
1346
+ }
1347
+ });
1348
+ }
1349
+
1350
+ /**
1351
+ * Gets the count of active toasts
1352
+ * @returns {number} Number of active toasts
1353
+ */
1354
+ getActiveCount() {
1355
+ return this.activeToasts.length;
1356
+ }
963
1357
 
964
1358
  /**
965
1359
  * Shows a success toast notification
@@ -1034,6 +1428,220 @@ class ToastifyPro {
1034
1428
  this.show(msg, "light", opts);
1035
1429
  }
1036
1430
 
1431
+ /**
1432
+ * Shows a custom-colored toast notification with gradient support
1433
+ * @param {string} msg - Main message
1434
+ * @param {string|Object} opts - Description string or options object
1435
+ * @param {string} opts.primaryColor - Primary color for the toast
1436
+ * @param {string} opts.secondaryColor - Secondary color for gradient (optional)
1437
+ */
1438
+ custom(msg, opts) {
1439
+ if (typeof opts === 'string') {
1440
+ opts = { description: opts };
1441
+ }
1442
+
1443
+ opts = opts || {};
1444
+
1445
+ // Get colors from options or use default options
1446
+ const primaryColor = opts.primaryColor || this.defaultOptions.primaryColor;
1447
+ const secondaryColor = opts.secondaryColor || this.defaultOptions.secondaryColor;
1448
+
1449
+ // If no custom colors provided, fallback to success style
1450
+ if (!primaryColor) {
1451
+ return this.success(msg, opts);
1452
+ }
1453
+
1454
+ // Helper function to determine if a color is light
1455
+ const isLightColor = (color) => {
1456
+ if (!color) return false;
1457
+ const hex = color.replace('#', '');
1458
+ const r = parseInt(hex.substr(0, 2), 16);
1459
+ const g = parseInt(hex.substr(2, 2), 16);
1460
+ const b = parseInt(hex.substr(4, 2), 16);
1461
+ const brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000;
1462
+ return brightness > 155;
1463
+ };
1464
+
1465
+ // Helper function to lighten or darken a color
1466
+ const adjustColor = (color, percent) => {
1467
+ const hex = color.replace('#', '');
1468
+ const r = parseInt(hex.substr(0, 2), 16);
1469
+ const g = parseInt(hex.substr(2, 2), 16);
1470
+ const b = parseInt(hex.substr(4, 2), 16);
1471
+
1472
+ const adjust = (c) => {
1473
+ const adjusted = Math.round(c + (percent / 100) * (percent > 0 ? (255 - c) : c));
1474
+ return Math.max(0, Math.min(255, adjusted));
1475
+ };
1476
+
1477
+ const newR = adjust(r).toString(16).padStart(2, '0');
1478
+ const newG = adjust(g).toString(16).padStart(2, '0');
1479
+ const newB = adjust(b).toString(16).padStart(2, '0');
1480
+
1481
+ return `#${newR}${newG}${newB}`;
1482
+ };
1483
+
1484
+ // Determine gradient colors
1485
+ let gradientStart = primaryColor;
1486
+ let gradientEnd;
1487
+
1488
+ if (secondaryColor) {
1489
+ // Both colors provided
1490
+ gradientEnd = secondaryColor;
1491
+ } else {
1492
+ // Only primary color - create gradient with lighter/darker shade
1493
+ const isLight = isLightColor(primaryColor);
1494
+ gradientEnd = isLight ? adjustColor(primaryColor, -25) : adjustColor(primaryColor, 25);
1495
+ }
1496
+
1497
+ // Determine text color
1498
+ const needsLightText = isLightColor(primaryColor);
1499
+
1500
+ // Create custom options
1501
+ const customOpts = {
1502
+ ...opts,
1503
+ customGradient: `linear-gradient(135deg, ${gradientStart} 0%, ${gradientEnd} 100%)`,
1504
+ customTextLight: needsLightText
1505
+ };
1506
+
1507
+ this.showCustom(msg, customOpts);
1508
+ }
1509
+
1510
+ /**
1511
+ * Internal method to show a custom-styled toast
1512
+ * @param {string} message - Main message text
1513
+ * @param {Object} opts - Options including customGradient and customTextLight
1514
+ */
1515
+ showCustom(message, opts = {}) {
1516
+ if (typeof message !== 'string') {
1517
+ message = String(message);
1518
+ }
1519
+
1520
+ if (!message.trim()) {
1521
+ console.warn('ToastifyPro: Empty message provided.');
1522
+ return;
1523
+ }
1524
+
1525
+ const options = { ...this.defaultOptions, ...opts };
1526
+
1527
+ try {
1528
+ const toast = document.createElement("div");
1529
+ toast.className = `toastify-pro custom${options.customTextLight ? ' light-text' : ''}`;
1530
+
1531
+ // Store reference to this instance
1532
+ toast._toastInstance = this;
1533
+
1534
+ // ARIA accessibility attributes
1535
+ toast.setAttribute('role', 'status');
1536
+ toast.setAttribute('aria-live', options.ariaLive || 'polite');
1537
+ toast.setAttribute('aria-atomic', 'true');
1538
+
1539
+ // Apply custom gradient
1540
+ if (options.customGradient) {
1541
+ toast.style.background = options.customGradient;
1542
+ }
1543
+
1544
+ if (options.timeout > 0) {
1545
+ toast.style.setProperty('--duration', `${options.timeout}ms`);
1546
+ }
1547
+
1548
+ // Create icon wrapper
1549
+ const iconWrapper = document.createElement("div");
1550
+ iconWrapper.className = "toast-icon";
1551
+ iconWrapper.setAttribute('aria-hidden', 'true');
1552
+ iconWrapper.innerHTML = this.getIconSVG('success'); // Use success icon for custom
1553
+ toast.appendChild(iconWrapper);
1554
+
1555
+ // Create content wrapper
1556
+ const contentWrapper = document.createElement("div");
1557
+ contentWrapper.className = "toast-content";
1558
+
1559
+ const messageElement = document.createElement("div");
1560
+ messageElement.className = "toast-message";
1561
+ messageElement.textContent = message.substring(0, options.maxLength);
1562
+ contentWrapper.appendChild(messageElement);
1563
+
1564
+ if (options.description && typeof options.description === 'string') {
1565
+ const descriptionElement = document.createElement("div");
1566
+ descriptionElement.className = "toast-description";
1567
+ descriptionElement.textContent = options.description.substring(0, options.maxLength * 2);
1568
+ contentWrapper.appendChild(descriptionElement);
1569
+ }
1570
+
1571
+ toast.appendChild(contentWrapper);
1572
+
1573
+ if (options.allowClose) {
1574
+ const closeBtn = document.createElement("button");
1575
+ closeBtn.className = "close-btn";
1576
+ closeBtn.innerHTML = "&times;";
1577
+ closeBtn.setAttribute('type', 'button');
1578
+ closeBtn.setAttribute('aria-label', 'Close notification');
1579
+ closeBtn.onclick = () => this.removeToast(toast);
1580
+ toast.appendChild(closeBtn);
1581
+ }
1582
+
1583
+ // Add toast to container (respect newestOnTop setting)
1584
+ if (options.newestOnTop && this.container.firstChild) {
1585
+ this.container.insertBefore(toast, this.container.firstChild);
1586
+ } else {
1587
+ this.container.appendChild(toast);
1588
+ }
1589
+
1590
+ // Track toast for queue management
1591
+ const toastData = {
1592
+ element: toast,
1593
+ timeout: null,
1594
+ remainingTime: options.timeout,
1595
+ startTime: null,
1596
+ isPaused: false
1597
+ };
1598
+ this.activeToasts.push(toastData);
1599
+
1600
+ // Pause on hover functionality
1601
+ if (options.pauseOnHover && options.timeout > 0) {
1602
+ toast.addEventListener('mouseenter', () => {
1603
+ if (toastData.timeout) {
1604
+ clearTimeout(toastData.timeout);
1605
+ toastData.isPaused = true;
1606
+ toastData.remainingTime -= (Date.now() - toastData.startTime);
1607
+ toast.classList.add('paused');
1608
+ }
1609
+ });
1610
+
1611
+ toast.addEventListener('mouseleave', () => {
1612
+ if (toastData.isPaused && toastData.remainingTime > 0) {
1613
+ toastData.isPaused = false;
1614
+ toastData.startTime = Date.now();
1615
+ toast.classList.remove('paused');
1616
+ toast.style.setProperty('--duration', `${toastData.remainingTime}ms`);
1617
+ toastData.timeout = setTimeout(() => this.removeToast(toast), toastData.remainingTime);
1618
+ }
1619
+ });
1620
+ }
1621
+
1622
+ setTimeout(() => {
1623
+ toast.classList.add("show");
1624
+ const icon = toast.querySelector('.toast-icon');
1625
+ if (icon) {
1626
+ icon.style.animation = 'iconBounce 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
1627
+ }
1628
+ }, 10);
1629
+
1630
+ if (options.timeout > 0) {
1631
+ toastData.startTime = Date.now();
1632
+ toastData.timeout = setTimeout(() => this.removeToast(toast), options.timeout);
1633
+ }
1634
+
1635
+ return {
1636
+ element: toast,
1637
+ dismiss: () => this.removeToast(toast),
1638
+ update: (newMessage, newOpts) => this.updateToast(toast, newMessage, newOpts)
1639
+ };
1640
+ } catch (error) {
1641
+ console.error('ToastifyPro: Failed to create custom toast:', error);
1642
+ }
1643
+ }
1644
+
1037
1645
  /**
1038
1646
  * Shows a confirmation toast with confirm/cancel buttons
1039
1647
  * @param {string} message - Main confirmation question
@@ -1161,6 +1769,34 @@ class ToastifyPro {
1161
1769
  let isLoading = false;
1162
1770
  let useLoading = false; // Track if user wants loading behavior
1163
1771
  let toastElement = null; // Reference to toast element
1772
+ let overlayElement = null; // Reference to overlay element
1773
+
1774
+ // Create overlay for confirmation
1775
+ const createOverlay = () => {
1776
+ overlayElement = document.createElement("div");
1777
+ overlayElement.className = "toastify-pro-overlay";
1778
+ document.body.appendChild(overlayElement);
1779
+
1780
+ // Trigger show animation
1781
+ setTimeout(() => {
1782
+ overlayElement.classList.add("show");
1783
+ }, 10);
1784
+
1785
+ return overlayElement;
1786
+ };
1787
+
1788
+ // Remove overlay
1789
+ const removeOverlay = () => {
1790
+ if (overlayElement && overlayElement.parentNode) {
1791
+ overlayElement.classList.remove("show");
1792
+ setTimeout(() => {
1793
+ if (overlayElement && overlayElement.parentNode) {
1794
+ overlayElement.remove();
1795
+ }
1796
+ overlayElement = null;
1797
+ }, 300);
1798
+ }
1799
+ };
1164
1800
 
1165
1801
  const setLoading = (loading) => {
1166
1802
  useLoading = true; // User is manually controlling loading
@@ -1190,6 +1826,7 @@ class ToastifyPro {
1190
1826
  const closeConfirmation = () => {
1191
1827
  if (toastElement && toastElement.parentNode) {
1192
1828
  globalActiveConfirmation = null;
1829
+ removeOverlay(); // Remove the overlay when closing
1193
1830
  this.removeToast(toastElement);
1194
1831
  }
1195
1832
  };
@@ -1277,6 +1914,9 @@ class ToastifyPro {
1277
1914
  };
1278
1915
 
1279
1916
  try {
1917
+ // Create overlay first
1918
+ createOverlay();
1919
+
1280
1920
  // Create confirmation toast element
1281
1921
  const toast = document.createElement("div");
1282
1922
  toast.className = `toastify-pro confirmation ${confirmOptions.theme}`;
@@ -1306,9 +1946,10 @@ class ToastifyPro {
1306
1946
  }
1307
1947
 
1308
1948
  // Create close button for confirmation
1309
- const closeBtn = document.createElement("span");
1949
+ const closeBtn = document.createElement("button");
1310
1950
  closeBtn.className = "conf-close-btn";
1311
1951
  closeBtn.innerHTML = "&times;";
1952
+ closeBtn.setAttribute('type', 'button');
1312
1953
  closeBtn.setAttribute('aria-label', 'Cancel confirmation');
1313
1954
  closeBtn.onclick = () => {
1314
1955
  if (!isLoading) {
@@ -1324,6 +1965,7 @@ class ToastifyPro {
1324
1965
  // Create icon wrapper
1325
1966
  const iconWrapper = document.createElement("div");
1326
1967
  iconWrapper.className = "toast-icon";
1968
+ iconWrapper.setAttribute('aria-hidden', 'true');
1327
1969
  iconWrapper.innerHTML = this.getIconSVG('info'); // Default to info icon
1328
1970
  if (confirmOptions.primaryColor) {
1329
1971
  iconWrapper.style.color = textColor;
@@ -1344,8 +1986,9 @@ class ToastifyPro {
1344
1986
  contentWrapper.appendChild(messageElement);
1345
1987
 
1346
1988
  // Optional description
1989
+ let descriptionElement = null;
1347
1990
  if (description) {
1348
- const descriptionElement = document.createElement("div");
1991
+ descriptionElement = document.createElement("div");
1349
1992
  descriptionElement.className = "toast-description";
1350
1993
  descriptionElement.textContent = description.substring(0, this.defaultOptions.maxLength * 2);
1351
1994
  if (confirmOptions.primaryColor) {
@@ -1363,6 +2006,7 @@ class ToastifyPro {
1363
2006
  // Cancel button
1364
2007
  const cancelBtn = document.createElement("button");
1365
2008
  cancelBtn.className = "toast-btn toast-btn-cancel";
2009
+ cancelBtn.setAttribute('type', 'button');
1366
2010
  cancelBtn.textContent = confirmOptions.cancelText;
1367
2011
  cancelBtn.onclick = () => {
1368
2012
  if (!isLoading) {
@@ -1384,18 +2028,25 @@ class ToastifyPro {
1384
2028
  // Confirm button
1385
2029
  const confirmBtn = document.createElement("button");
1386
2030
  confirmBtn.className = `toast-btn toast-btn-confirm`;
1387
-
1388
- // Create spinner element
1389
- const spinner = document.createElement("span");
1390
- spinner.className = "btn-spinner";
1391
- confirmBtn.appendChild(spinner);
1392
-
2031
+ confirmBtn.setAttribute('type', 'button');
2032
+
1393
2033
  // Create text wrapper
1394
2034
  const textWrapper = document.createElement("span");
1395
2035
  textWrapper.className = "btn-text";
1396
2036
  textWrapper.textContent = confirmOptions.confirmText;
1397
2037
  confirmBtn.appendChild(textWrapper);
1398
2038
 
2039
+ // Create spinner element with custom SVG
2040
+ const spinner = document.createElement("span");
2041
+ spinner.className = "btn-spinner";
2042
+ spinner.innerHTML = `
2043
+ <svg width="25" height="25" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
2044
+ <path d="M9.5 2.9375V5.5625M9.5 13.4375V16.0625M2.9375 9.5H5.5625M13.4375 9.5H16.0625" stroke="currentColor" stroke-width="1.875" stroke-linecap="round" />
2045
+ <path d="M4.86011 4.85961L6.71627 6.71577M12.2847 12.2842L14.1409 14.1404M4.86011 14.1404L6.71627 12.2842M12.2847 6.71577L14.1409 4.85961" stroke="currentColor" stroke-width="1.875" stroke-linecap="round" />
2046
+ </svg>
2047
+ `;
2048
+ confirmBtn.appendChild(spinner);
2049
+
1399
2050
  confirmBtn.onclick = () => {
1400
2051
  if (!isLoading) {
1401
2052
  handleConfirmation(true);
@@ -1446,6 +2097,53 @@ class ToastifyPro {
1446
2097
  setLoading(true);
1447
2098
  }
1448
2099
 
2100
+ // ARIA accessibility for confirmation dialog
2101
+ toast.setAttribute('role', 'alertdialog');
2102
+ toast.setAttribute('aria-modal', 'true');
2103
+ toast.setAttribute('aria-labelledby', 'toast-conf-title');
2104
+ if (description) {
2105
+ toast.setAttribute('aria-describedby', 'toast-conf-desc');
2106
+ }
2107
+ messageElement.id = 'toast-conf-title';
2108
+ if (description && descriptionElement) {
2109
+ descriptionElement.id = 'toast-conf-desc';
2110
+ }
2111
+
2112
+ // Store previously focused element for restoration
2113
+ const previouslyFocused = document.activeElement;
2114
+
2115
+ // Focus trap for confirmation dialog
2116
+ const focusableElements = [cancelBtn, confirmBtn, closeBtn].filter(Boolean);
2117
+ let currentFocusIndex = 0;
2118
+
2119
+ const handleTabKey = (e) => {
2120
+ if (e.key === 'Tab' && toastElement && toastElement.parentNode) {
2121
+ e.preventDefault();
2122
+ if (e.shiftKey) {
2123
+ currentFocusIndex = (currentFocusIndex - 1 + focusableElements.length) % focusableElements.length;
2124
+ } else {
2125
+ currentFocusIndex = (currentFocusIndex + 1) % focusableElements.length;
2126
+ }
2127
+ focusableElements[currentFocusIndex]?.focus();
2128
+ }
2129
+ };
2130
+
2131
+ document.addEventListener('keydown', handleTabKey);
2132
+
2133
+ // Store cleanup function
2134
+ const originalClose = closeConfirmation;
2135
+ const cleanupAndClose = () => {
2136
+ document.removeEventListener('keydown', handleTabKey);
2137
+ // Restore focus to previously focused element
2138
+ if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
2139
+ setTimeout(() => previouslyFocused.focus(), 100);
2140
+ }
2141
+ originalClose();
2142
+ };
2143
+
2144
+ // Update control object with enhanced close
2145
+ controlObject.close = cleanupAndClose;
2146
+
1449
2147
  // Entrance animation
1450
2148
  setTimeout(() => {
1451
2149
  toast.classList.add("show");
@@ -1453,6 +2151,11 @@ class ToastifyPro {
1453
2151
  if (icon) {
1454
2152
  icon.style.animation = 'iconBounce 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
1455
2153
  }
2154
+
2155
+ // Focus the confirm button after animation
2156
+ setTimeout(() => {
2157
+ confirmBtn.focus();
2158
+ }, 100);
1456
2159
  }, 10);
1457
2160
 
1458
2161
  // Return control object with toast element and control functions