toastify-pro 1.5.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.
@@ -17,8 +17,13 @@
17
17
  * - Confirmation overlay with blur effect for focus
18
18
  * - Center position support for enhanced focus
19
19
  * - Independent positioning for confirmations
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
20
25
  *
21
- * @version 1.5.0
26
+ * @version 1.6.0
22
27
  * @author ToastifyPro Team
23
28
  * @license MIT
24
29
  */
@@ -36,6 +41,10 @@ class ToastifyPro {
36
41
  * @param {number} options.maxLength - Maximum message length
37
42
  * @param {string} options.primaryColor - Primary color for custom() method
38
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')
39
48
  */
40
49
  constructor(options = {}) {
41
50
  // Validate options parameter
@@ -52,7 +61,14 @@ class ToastifyPro {
52
61
  maxLength: options.maxLength || 100,
53
62
  primaryColor: options.primaryColor || null, // Custom primary color for custom() method
54
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'
55
68
  };
69
+
70
+ // Track active toasts for queue management
71
+ this.activeToasts = [];
56
72
 
57
73
  // Validate position
58
74
  const validPositions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'center'];
@@ -85,6 +101,46 @@ class ToastifyPro {
85
101
 
86
102
  // Inject styles once
87
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
+ });
88
144
  }
89
145
 
90
146
  /**
@@ -479,6 +535,7 @@ class ToastifyPro {
479
535
  transition: all 0.2s ease;
480
536
  flex-shrink: 0;
481
537
  width: 32px;
538
+ border: none;
482
539
  height: 32px;
483
540
  display: flex;
484
541
  align-items: center;
@@ -710,6 +767,9 @@ class ToastifyPro {
710
767
  }
711
768
 
712
769
  .toast-btn-confirm {
770
+ display: flex;
771
+ align-items: center;
772
+ justify-content: center;
713
773
  color: white;
714
774
  font-weight: 700;
715
775
  border: 2px solid rgba(255, 255, 255, 0.4);
@@ -748,17 +808,20 @@ class ToastifyPro {
748
808
 
749
809
  .toast-btn-confirm .btn-spinner {
750
810
  display: none;
751
- width: 16px;
752
- height: 16px;
753
- border: 2px solid rgba(255, 255, 255, 0.3);
754
- border-top-color: white;
755
- border-radius: 50%;
756
- animation: spin 0.6s linear infinite;
757
- 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;
758
821
  }
759
822
 
760
823
  .toast-btn-confirm.loading .btn-spinner {
761
- display: inline-block;
824
+ display: inline-flex;
762
825
  }
763
826
 
764
827
  .toast-btn-confirm.loading .btn-text {
@@ -848,6 +911,117 @@ class ToastifyPro {
848
911
  .toastify-pro-overlay.show {
849
912
  opacity: 1;
850
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
+ }
851
1025
  `;
852
1026
  document.head.appendChild(style);
853
1027
  } catch (error) {
@@ -864,6 +1038,9 @@ class ToastifyPro {
864
1038
  * @param {number} opts.timeout - Override default timeout
865
1039
  * @param {boolean} opts.allowClose - Override close button setting
866
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')
867
1044
  */
868
1045
  show(message, type = "dark", opts = {}) {
869
1046
  // Input validation
@@ -893,10 +1070,30 @@ class ToastifyPro {
893
1070
  const options = { ...this.defaultOptions, ...opts };
894
1071
 
895
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
+
896
1084
  // Create toast element
897
1085
  const toast = document.createElement("div");
898
1086
  toast.className = `toastify-pro ${type}`;
899
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
+
900
1097
  // Set duration for progress bar animation
901
1098
  if (options.timeout > 0) {
902
1099
  toast.style.setProperty('--duration', `${options.timeout}ms`);
@@ -905,6 +1102,7 @@ class ToastifyPro {
905
1102
  // Create icon wrapper
906
1103
  const iconWrapper = document.createElement("div");
907
1104
  iconWrapper.className = "toast-icon";
1105
+ iconWrapper.setAttribute('aria-hidden', 'true');
908
1106
  iconWrapper.innerHTML = this.getIconSVG(type);
909
1107
  toast.appendChild(iconWrapper);
910
1108
 
@@ -926,20 +1124,50 @@ class ToastifyPro {
926
1124
  contentWrapper.appendChild(descriptionElement);
927
1125
  }
928
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
+
929
1142
  toast.appendChild(contentWrapper);
930
1143
 
931
1144
  // Add close button if enabled
932
1145
  if (options.allowClose) {
933
- const closeBtn = document.createElement("span");
1146
+ const closeBtn = document.createElement("button");
934
1147
  closeBtn.className = "close-btn";
935
1148
  closeBtn.innerHTML = "&times;";
1149
+ closeBtn.setAttribute('type', 'button');
936
1150
  closeBtn.setAttribute('aria-label', 'Close notification');
937
1151
  closeBtn.onclick = () => this.removeToast(toast);
938
1152
  toast.appendChild(closeBtn);
939
1153
  }
940
1154
 
941
- // Add toast to container
942
- 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);
943
1171
 
944
1172
  // Apple AirDrop-style entrance animation
945
1173
  setTimeout(() => {
@@ -951,16 +1179,90 @@ class ToastifyPro {
951
1179
  }
952
1180
  }, 10);
953
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
+
954
1211
  // Auto-remove after timeout
955
1212
  if (options.timeout > 0) {
956
- setTimeout(() => this.removeToast(toast), options.timeout);
1213
+ toastData.startTime = Date.now();
1214
+ toastData.timeout = setTimeout(() => this.removeToast(toast), options.timeout);
957
1215
  }
958
1216
 
959
- 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
+ };
960
1223
  } catch (error) {
961
1224
  console.error('ToastifyPro: Failed to create toast:', error);
962
1225
  }
963
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
+ }
964
1266
 
965
1267
  /**
966
1268
  * Removes a toast with position-aware car swipe animation
@@ -973,6 +1275,16 @@ class ToastifyPro {
973
1275
  }
974
1276
 
975
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
+
976
1288
  // Detect position to choose the right swipe direction
977
1289
  const container = toast.parentNode;
978
1290
  const position = container.className.split(' ')[1]; // get position class
@@ -1015,6 +1327,33 @@ class ToastifyPro {
1015
1327
  }
1016
1328
  }
1017
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
+ }
1018
1357
 
1019
1358
  /**
1020
1359
  * Shows a success toast notification
@@ -1189,6 +1528,14 @@ class ToastifyPro {
1189
1528
  const toast = document.createElement("div");
1190
1529
  toast.className = `toastify-pro custom${options.customTextLight ? ' light-text' : ''}`;
1191
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
+
1192
1539
  // Apply custom gradient
1193
1540
  if (options.customGradient) {
1194
1541
  toast.style.background = options.customGradient;
@@ -1201,6 +1548,7 @@ class ToastifyPro {
1201
1548
  // Create icon wrapper
1202
1549
  const iconWrapper = document.createElement("div");
1203
1550
  iconWrapper.className = "toast-icon";
1551
+ iconWrapper.setAttribute('aria-hidden', 'true');
1204
1552
  iconWrapper.innerHTML = this.getIconSVG('success'); // Use success icon for custom
1205
1553
  toast.appendChild(iconWrapper);
1206
1554
 
@@ -1223,15 +1571,53 @@ class ToastifyPro {
1223
1571
  toast.appendChild(contentWrapper);
1224
1572
 
1225
1573
  if (options.allowClose) {
1226
- const closeBtn = document.createElement("span");
1574
+ const closeBtn = document.createElement("button");
1227
1575
  closeBtn.className = "close-btn";
1228
1576
  closeBtn.innerHTML = "&times;";
1577
+ closeBtn.setAttribute('type', 'button');
1229
1578
  closeBtn.setAttribute('aria-label', 'Close notification');
1230
1579
  closeBtn.onclick = () => this.removeToast(toast);
1231
1580
  toast.appendChild(closeBtn);
1232
1581
  }
1233
1582
 
1234
- this.container.appendChild(toast);
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
+ }
1235
1621
 
1236
1622
  setTimeout(() => {
1237
1623
  toast.classList.add("show");
@@ -1242,10 +1628,15 @@ class ToastifyPro {
1242
1628
  }, 10);
1243
1629
 
1244
1630
  if (options.timeout > 0) {
1245
- setTimeout(() => this.removeToast(toast), options.timeout);
1631
+ toastData.startTime = Date.now();
1632
+ toastData.timeout = setTimeout(() => this.removeToast(toast), options.timeout);
1246
1633
  }
1247
1634
 
1248
- return toast;
1635
+ return {
1636
+ element: toast,
1637
+ dismiss: () => this.removeToast(toast),
1638
+ update: (newMessage, newOpts) => this.updateToast(toast, newMessage, newOpts)
1639
+ };
1249
1640
  } catch (error) {
1250
1641
  console.error('ToastifyPro: Failed to create custom toast:', error);
1251
1642
  }
@@ -1555,9 +1946,10 @@ class ToastifyPro {
1555
1946
  }
1556
1947
 
1557
1948
  // Create close button for confirmation
1558
- const closeBtn = document.createElement("span");
1949
+ const closeBtn = document.createElement("button");
1559
1950
  closeBtn.className = "conf-close-btn";
1560
1951
  closeBtn.innerHTML = "&times;";
1952
+ closeBtn.setAttribute('type', 'button');
1561
1953
  closeBtn.setAttribute('aria-label', 'Cancel confirmation');
1562
1954
  closeBtn.onclick = () => {
1563
1955
  if (!isLoading) {
@@ -1573,6 +1965,7 @@ class ToastifyPro {
1573
1965
  // Create icon wrapper
1574
1966
  const iconWrapper = document.createElement("div");
1575
1967
  iconWrapper.className = "toast-icon";
1968
+ iconWrapper.setAttribute('aria-hidden', 'true');
1576
1969
  iconWrapper.innerHTML = this.getIconSVG('info'); // Default to info icon
1577
1970
  if (confirmOptions.primaryColor) {
1578
1971
  iconWrapper.style.color = textColor;
@@ -1593,8 +1986,9 @@ class ToastifyPro {
1593
1986
  contentWrapper.appendChild(messageElement);
1594
1987
 
1595
1988
  // Optional description
1989
+ let descriptionElement = null;
1596
1990
  if (description) {
1597
- const descriptionElement = document.createElement("div");
1991
+ descriptionElement = document.createElement("div");
1598
1992
  descriptionElement.className = "toast-description";
1599
1993
  descriptionElement.textContent = description.substring(0, this.defaultOptions.maxLength * 2);
1600
1994
  if (confirmOptions.primaryColor) {
@@ -1612,6 +2006,7 @@ class ToastifyPro {
1612
2006
  // Cancel button
1613
2007
  const cancelBtn = document.createElement("button");
1614
2008
  cancelBtn.className = "toast-btn toast-btn-cancel";
2009
+ cancelBtn.setAttribute('type', 'button');
1615
2010
  cancelBtn.textContent = confirmOptions.cancelText;
1616
2011
  cancelBtn.onclick = () => {
1617
2012
  if (!isLoading) {
@@ -1633,18 +2028,25 @@ class ToastifyPro {
1633
2028
  // Confirm button
1634
2029
  const confirmBtn = document.createElement("button");
1635
2030
  confirmBtn.className = `toast-btn toast-btn-confirm`;
1636
-
1637
- // Create spinner element
1638
- const spinner = document.createElement("span");
1639
- spinner.className = "btn-spinner";
1640
- confirmBtn.appendChild(spinner);
1641
-
2031
+ confirmBtn.setAttribute('type', 'button');
2032
+
1642
2033
  // Create text wrapper
1643
2034
  const textWrapper = document.createElement("span");
1644
2035
  textWrapper.className = "btn-text";
1645
2036
  textWrapper.textContent = confirmOptions.confirmText;
1646
2037
  confirmBtn.appendChild(textWrapper);
1647
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
+
1648
2050
  confirmBtn.onclick = () => {
1649
2051
  if (!isLoading) {
1650
2052
  handleConfirmation(true);
@@ -1695,6 +2097,53 @@ class ToastifyPro {
1695
2097
  setLoading(true);
1696
2098
  }
1697
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
+
1698
2147
  // Entrance animation
1699
2148
  setTimeout(() => {
1700
2149
  toast.classList.add("show");
@@ -1702,6 +2151,11 @@ class ToastifyPro {
1702
2151
  if (icon) {
1703
2152
  icon.style.animation = 'iconBounce 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
1704
2153
  }
2154
+
2155
+ // Focus the confirm button after animation
2156
+ setTimeout(() => {
2157
+ confirmBtn.focus();
2158
+ }, 100);
1705
2159
  }, 10);
1706
2160
 
1707
2161
  // Return control object with toast element and control functions