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