@phantompixeldev/retrocss 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +41 -0
- package/README.md +31 -9
- package/dist/retro.css +43 -23
- package/dist/retro.css.map +1 -1
- package/dist/retro.js +200 -44
- package/dist/retro.min.css +1 -1
- package/dist/retro.min.css.map +1 -1
- package/dist/retro.min.js +1 -1
- package/package.json +2 -1
package/dist/retro.js
CHANGED
|
@@ -494,7 +494,6 @@
|
|
|
494
494
|
root.querySelectorAll(".retro-file-upload").forEach((upload) => {
|
|
495
495
|
const input = upload.querySelector(".retro-file-input");
|
|
496
496
|
const display = upload.querySelector(".retro-file-filename, .retro-file-display");
|
|
497
|
-
const label = upload.querySelector(".retro-file-label");
|
|
498
497
|
const drop = upload.querySelector(".retro-file-drop");
|
|
499
498
|
if (!input || !display) return;
|
|
500
499
|
input.addEventListener("change", function() {
|
|
@@ -507,12 +506,6 @@
|
|
|
507
506
|
upload.classList.remove("has-files");
|
|
508
507
|
}
|
|
509
508
|
});
|
|
510
|
-
if (label) {
|
|
511
|
-
label.addEventListener("click", (e) => {
|
|
512
|
-
e.preventDefault();
|
|
513
|
-
input.click();
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
509
|
if (drop) {
|
|
517
510
|
drop.addEventListener("click", (e) => {
|
|
518
511
|
e.preventDefault();
|
|
@@ -603,7 +596,8 @@
|
|
|
603
596
|
newDotsContainer.className = "retro-carousel-dots";
|
|
604
597
|
this.root.appendChild(newDotsContainer);
|
|
605
598
|
for (let i = 0; i < this.slides.length; i++) {
|
|
606
|
-
const dot = document.createElement("
|
|
599
|
+
const dot = document.createElement("button");
|
|
600
|
+
dot.type = "button";
|
|
607
601
|
dot.className = "retro-carousel-dot";
|
|
608
602
|
dot.setAttribute("data-index", i);
|
|
609
603
|
newDotsContainer.appendChild(dot);
|
|
@@ -626,7 +620,36 @@
|
|
|
626
620
|
dot.addEventListener("click", () => {
|
|
627
621
|
this.goTo(i);
|
|
628
622
|
});
|
|
623
|
+
if (!dot.hasAttribute("aria-label")) {
|
|
624
|
+
dot.setAttribute("aria-label", `Go to slide ${i + 1} of ${this.slides.length}`);
|
|
625
|
+
}
|
|
626
|
+
if (dot.tagName !== "BUTTON") {
|
|
627
|
+
if (!dot.hasAttribute("role")) dot.setAttribute("role", "button");
|
|
628
|
+
if (!dot.hasAttribute("tabindex")) dot.tabIndex = 0;
|
|
629
|
+
dot.addEventListener("keydown", (e) => {
|
|
630
|
+
if (e.key === " " || e.key === "Enter") {
|
|
631
|
+
e.preventDefault();
|
|
632
|
+
dot.click();
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
this.root.addEventListener("keydown", (e) => {
|
|
638
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
639
|
+
if (e.target.closest("input, textarea, select, [contenteditable]")) return;
|
|
640
|
+
e.preventDefault();
|
|
641
|
+
this.goTo(this.current + (e.key === "ArrowLeft" ? -1 : 1));
|
|
629
642
|
});
|
|
643
|
+
if (this.track && !this.track.hasAttribute("aria-live")) {
|
|
644
|
+
this.track.setAttribute("aria-live", "polite");
|
|
645
|
+
this.track.setAttribute("aria-atomic", "false");
|
|
646
|
+
}
|
|
647
|
+
if (!this.root.hasAttribute("role")) {
|
|
648
|
+
this.root.setAttribute("role", "group");
|
|
649
|
+
if (!this.root.hasAttribute("aria-label")) {
|
|
650
|
+
this.root.setAttribute("aria-label", "Carousel");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
630
653
|
this.goTo(0);
|
|
631
654
|
let startX, moveX;
|
|
632
655
|
this.root.addEventListener("touchstart", (e) => {
|
|
@@ -660,6 +683,8 @@
|
|
|
660
683
|
}
|
|
661
684
|
this.dots.forEach((dot, i) => {
|
|
662
685
|
dot.classList.toggle("active", i === idx);
|
|
686
|
+
if (i === idx) dot.setAttribute("aria-current", "true");
|
|
687
|
+
else dot.removeAttribute("aria-current");
|
|
663
688
|
});
|
|
664
689
|
};
|
|
665
690
|
RetroCarousel.init = function() {
|
|
@@ -711,18 +736,37 @@
|
|
|
711
736
|
e.preventDefault();
|
|
712
737
|
this.activateTab(index);
|
|
713
738
|
}
|
|
739
|
+
let newIndex = null;
|
|
714
740
|
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
715
|
-
e.preventDefault();
|
|
716
741
|
const direction = e.key === "ArrowLeft" ? -1 : 1;
|
|
717
|
-
|
|
742
|
+
newIndex = index + direction;
|
|
718
743
|
if (newIndex < 0) newIndex = this.tabs.length - 1;
|
|
719
744
|
if (newIndex >= this.tabs.length) newIndex = 0;
|
|
745
|
+
} else if (e.key === "Home") {
|
|
746
|
+
newIndex = 0;
|
|
747
|
+
} else if (e.key === "End") {
|
|
748
|
+
newIndex = this.tabs.length - 1;
|
|
749
|
+
}
|
|
750
|
+
if (newIndex !== null) {
|
|
751
|
+
e.preventDefault();
|
|
752
|
+
this.setTabStop(newIndex);
|
|
720
753
|
this.tabs[newIndex].focus();
|
|
721
754
|
}
|
|
722
755
|
});
|
|
723
756
|
});
|
|
724
757
|
this.activateTab(this.options.defaultTab);
|
|
725
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Roving tabindex: exactly one tab is in the page tab order at a time.
|
|
761
|
+
* Every tab used to be tabindex=0, so Tab walked through all of them one by
|
|
762
|
+
* one instead of stepping over the tablist and into the panel -- twelve
|
|
763
|
+
* stops on the demo page before the content.
|
|
764
|
+
*/
|
|
765
|
+
setTabStop(index) {
|
|
766
|
+
this.tabs.forEach((tab, i) => {
|
|
767
|
+
tab.tabIndex = i === index ? 0 : -1;
|
|
768
|
+
});
|
|
769
|
+
}
|
|
726
770
|
activateTab(index) {
|
|
727
771
|
this.tabs.forEach((tab) => {
|
|
728
772
|
tab.classList.remove(this.options.activeClass);
|
|
@@ -730,6 +774,7 @@
|
|
|
730
774
|
});
|
|
731
775
|
this.tabs[index].classList.add(this.options.activeClass);
|
|
732
776
|
this.tabs[index].setAttribute("aria-selected", "true");
|
|
777
|
+
this.setTabStop(index);
|
|
733
778
|
var visibleContent = null;
|
|
734
779
|
for (var i = 0; i < this.contentElements.length; i++) {
|
|
735
780
|
if (this.contentElements[i].style.display === "block") {
|
|
@@ -1020,13 +1065,28 @@
|
|
|
1020
1065
|
});
|
|
1021
1066
|
|
|
1022
1067
|
// src/js/retro.js
|
|
1023
|
-
|
|
1068
|
+
var FOCUSABLE_SELECTOR = 'a[href],area[href],button:not([disabled]),input:not([disabled]):not([type="hidden"]),select:not([disabled]),textarea:not([disabled]),iframe,[contenteditable]:not([contenteditable="false"]),[tabindex]:not([tabindex="-1"])';
|
|
1069
|
+
var tooltipSeq = 0;
|
|
1070
|
+
function storedTheme() {
|
|
1071
|
+
try {
|
|
1072
|
+
return localStorage.getItem("retro-theme");
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function storeTheme(theme) {
|
|
1024
1078
|
try {
|
|
1025
|
-
|
|
1079
|
+
localStorage.setItem("retro-theme", theme);
|
|
1026
1080
|
} catch (e) {
|
|
1027
|
-
return "light";
|
|
1028
1081
|
}
|
|
1029
1082
|
}
|
|
1083
|
+
function systemTheme() {
|
|
1084
|
+
if (typeof window === "undefined" || !window.matchMedia) return null;
|
|
1085
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
1086
|
+
}
|
|
1087
|
+
function readStoredTheme() {
|
|
1088
|
+
return storedTheme() || systemTheme() || "light";
|
|
1089
|
+
}
|
|
1030
1090
|
var RetroCSS = {
|
|
1031
1091
|
modal: modal_default,
|
|
1032
1092
|
toast: toast_default,
|
|
@@ -1072,6 +1132,7 @@
|
|
|
1072
1132
|
this.initSearchBars();
|
|
1073
1133
|
this.initTagInputs();
|
|
1074
1134
|
this.initThemeToggle();
|
|
1135
|
+
this.initSystemThemeWatch();
|
|
1075
1136
|
this.applyTheme(this.theme);
|
|
1076
1137
|
events_default.emit("init", { timestamp: Date.now() });
|
|
1077
1138
|
this.initRatingStars();
|
|
@@ -1093,11 +1154,28 @@
|
|
|
1093
1154
|
if (existingTooltip) existingTooltip.remove();
|
|
1094
1155
|
trigger.style.position = "relative";
|
|
1095
1156
|
trigger.appendChild(tooltip);
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1157
|
+
tooltip.setAttribute("role", "tooltip");
|
|
1158
|
+
if (!tooltip.id) {
|
|
1159
|
+
tooltipSeq += 1;
|
|
1160
|
+
tooltip.id = `retro-tooltip-${tooltipSeq}`;
|
|
1161
|
+
}
|
|
1162
|
+
const describedBy = trigger.getAttribute("aria-describedby");
|
|
1163
|
+
if (!describedBy) {
|
|
1164
|
+
trigger.setAttribute("aria-describedby", tooltip.id);
|
|
1165
|
+
} else if (!describedBy.split(/\s+/).includes(tooltip.id)) {
|
|
1166
|
+
trigger.setAttribute("aria-describedby", `${describedBy} ${tooltip.id}`);
|
|
1167
|
+
}
|
|
1168
|
+
if (!trigger.matches(FOCUSABLE_SELECTOR) && !trigger.hasAttribute("tabindex")) {
|
|
1169
|
+
trigger.tabIndex = 0;
|
|
1170
|
+
}
|
|
1171
|
+
const show = () => tooltip.classList.add("show");
|
|
1172
|
+
const hide = () => tooltip.classList.remove("show");
|
|
1173
|
+
trigger.addEventListener("mouseenter", show);
|
|
1174
|
+
trigger.addEventListener("mouseleave", hide);
|
|
1175
|
+
trigger.addEventListener("focus", show);
|
|
1176
|
+
trigger.addEventListener("blur", hide);
|
|
1177
|
+
trigger.addEventListener("keydown", (e) => {
|
|
1178
|
+
if (e.key === "Escape") hide();
|
|
1101
1179
|
});
|
|
1102
1180
|
});
|
|
1103
1181
|
},
|
|
@@ -1168,6 +1246,19 @@
|
|
|
1168
1246
|
tags.push(tagText);
|
|
1169
1247
|
const removeBtn = tag.querySelector(".retro-tag-remove");
|
|
1170
1248
|
if (removeBtn) {
|
|
1249
|
+
if (removeBtn.tagName !== "BUTTON") {
|
|
1250
|
+
removeBtn.setAttribute("role", "button");
|
|
1251
|
+
if (!removeBtn.hasAttribute("tabindex")) removeBtn.tabIndex = 0;
|
|
1252
|
+
removeBtn.addEventListener("keydown", (e) => {
|
|
1253
|
+
if (e.key === " " || e.key === "Enter") {
|
|
1254
|
+
e.preventDefault();
|
|
1255
|
+
removeBtn.click();
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
if (!removeBtn.hasAttribute("aria-label")) {
|
|
1260
|
+
removeBtn.setAttribute("aria-label", `Remove ${tagText}`);
|
|
1261
|
+
}
|
|
1171
1262
|
removeBtn.addEventListener("click", () => {
|
|
1172
1263
|
tag.remove();
|
|
1173
1264
|
const index = tags.indexOf(tagText);
|
|
@@ -1186,9 +1277,11 @@
|
|
|
1186
1277
|
const tag = document.createElement("span");
|
|
1187
1278
|
tag.className = "retro-tag";
|
|
1188
1279
|
tag.textContent = tagText;
|
|
1189
|
-
const removeBtn = document.createElement("
|
|
1280
|
+
const removeBtn = document.createElement("button");
|
|
1281
|
+
removeBtn.type = "button";
|
|
1190
1282
|
removeBtn.className = "retro-tag-remove";
|
|
1191
1283
|
removeBtn.textContent = "\xD7";
|
|
1284
|
+
removeBtn.setAttribute("aria-label", `Remove ${tagText}`);
|
|
1192
1285
|
removeBtn.addEventListener("click", () => {
|
|
1193
1286
|
tag.remove();
|
|
1194
1287
|
const index = tags.indexOf(tagText);
|
|
@@ -1222,13 +1315,31 @@
|
|
|
1222
1315
|
toggle.addEventListener("click", () => {
|
|
1223
1316
|
this.theme = this.theme === "light" ? "dark" : "light";
|
|
1224
1317
|
this.applyTheme(this.theme);
|
|
1225
|
-
|
|
1318
|
+
storeTheme(this.theme);
|
|
1226
1319
|
toast_default.show(`Theme switched to ${this.theme} mode!`, {
|
|
1227
1320
|
type: this.theme === "dark" ? "primary" : "light"
|
|
1228
1321
|
});
|
|
1229
1322
|
});
|
|
1230
1323
|
});
|
|
1231
1324
|
},
|
|
1325
|
+
/**
|
|
1326
|
+
* Follow the OS while the visitor has expressed no preference of their own.
|
|
1327
|
+
* Once the toggle has been used, that choice is stored and this stops
|
|
1328
|
+
* applying -- flipping the system theme must not silently undo a deliberate
|
|
1329
|
+
* choice.
|
|
1330
|
+
*/
|
|
1331
|
+
initSystemThemeWatch() {
|
|
1332
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
1333
|
+
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1334
|
+
const onChange = (e) => {
|
|
1335
|
+
if (storedTheme()) return;
|
|
1336
|
+
this.theme = e.matches ? "dark" : "light";
|
|
1337
|
+
this.applyTheme(this.theme);
|
|
1338
|
+
events_default.emit("themechange", { theme: this.theme, source: "system" });
|
|
1339
|
+
};
|
|
1340
|
+
if (query.addEventListener) query.addEventListener("change", onChange);
|
|
1341
|
+
else if (query.addListener) query.addListener(onChange);
|
|
1342
|
+
},
|
|
1232
1343
|
// Apply theme to document
|
|
1233
1344
|
applyTheme(theme) {
|
|
1234
1345
|
if (theme === "dark") {
|
|
@@ -1237,36 +1348,81 @@
|
|
|
1237
1348
|
document.documentElement.removeAttribute("data-theme");
|
|
1238
1349
|
}
|
|
1239
1350
|
},
|
|
1240
|
-
|
|
1351
|
+
/**
|
|
1352
|
+
* Interactive rating stars, on the ARIA radiogroup pattern.
|
|
1353
|
+
*
|
|
1354
|
+
* These were five <span>s wired to mouseenter/mouseleave/click: an input a
|
|
1355
|
+
* keyboard user could not reach, let alone set, with no role and no state to
|
|
1356
|
+
* announce. The markup is unchanged -- the roles, tabindex and key handling
|
|
1357
|
+
* are added here, so existing pages get the fix without editing their HTML.
|
|
1358
|
+
*/
|
|
1241
1359
|
initRatingStars() {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1360
|
+
document.querySelectorAll(".retro-rating").forEach((rating) => {
|
|
1361
|
+
if (rating.dataset.retroRatingBound === "true") return;
|
|
1362
|
+
rating.dataset.retroRatingBound = "true";
|
|
1363
|
+
const stars = Array.from(rating.querySelectorAll(".retro-rating-star"));
|
|
1364
|
+
if (!stars.length) return;
|
|
1365
|
+
const max = stars.length;
|
|
1366
|
+
const readValue = () => parseInt(rating.getAttribute("data-rating"), 10) || 0;
|
|
1367
|
+
rating.setAttribute("role", "radiogroup");
|
|
1368
|
+
if (!rating.hasAttribute("aria-label") && !rating.hasAttribute("aria-labelledby")) {
|
|
1369
|
+
rating.setAttribute("aria-label", `Rating out of ${max}`);
|
|
1370
|
+
}
|
|
1371
|
+
const render = (value, previewOnly) => {
|
|
1248
1372
|
stars.forEach((star, i) => {
|
|
1249
|
-
|
|
1373
|
+
star.classList.toggle("active", previewOnly !== void 0 && i <= previewOnly);
|
|
1374
|
+
if (previewOnly === void 0) star.classList.remove("active");
|
|
1375
|
+
star.classList.toggle("selected", i < value);
|
|
1376
|
+
star.setAttribute("aria-checked", String(i === value - 1));
|
|
1377
|
+
star.tabIndex = i === (value > 0 ? value - 1 : 0) ? 0 : -1;
|
|
1250
1378
|
});
|
|
1251
|
-
}
|
|
1379
|
+
};
|
|
1380
|
+
const select = (index, focus) => {
|
|
1381
|
+
const value = index + 1;
|
|
1382
|
+
rating.setAttribute("data-rating", String(value));
|
|
1383
|
+
render(value);
|
|
1384
|
+
if (focus) stars[index].focus();
|
|
1385
|
+
rating.dispatchEvent(
|
|
1386
|
+
new CustomEvent("retro:rating", { detail: { rating: value, max }, bubbles: true })
|
|
1387
|
+
);
|
|
1388
|
+
};
|
|
1252
1389
|
stars.forEach((star, idx) => {
|
|
1253
|
-
star.
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
star.addEventListener("
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1390
|
+
star.setAttribute("role", "radio");
|
|
1391
|
+
star.setAttribute("aria-label", `${idx + 1} of ${max}`);
|
|
1392
|
+
star.addEventListener("mouseenter", () => render(readValue(), idx));
|
|
1393
|
+
star.addEventListener("mouseleave", () => render(readValue()));
|
|
1394
|
+
star.addEventListener("click", () => select(idx));
|
|
1395
|
+
star.addEventListener("keydown", (e) => {
|
|
1396
|
+
const current = readValue() - 1;
|
|
1397
|
+
const clamp = (n) => Math.max(0, Math.min(max - 1, n));
|
|
1398
|
+
let next;
|
|
1399
|
+
switch (e.key) {
|
|
1400
|
+
case "ArrowRight":
|
|
1401
|
+
case "ArrowDown":
|
|
1402
|
+
next = clamp(current + 1);
|
|
1403
|
+
break;
|
|
1404
|
+
case "ArrowLeft":
|
|
1405
|
+
case "ArrowUp":
|
|
1406
|
+
next = current < 0 ? 0 : clamp(current - 1);
|
|
1407
|
+
break;
|
|
1408
|
+
case "Home":
|
|
1409
|
+
next = 0;
|
|
1410
|
+
break;
|
|
1411
|
+
case "End":
|
|
1412
|
+
next = max - 1;
|
|
1413
|
+
break;
|
|
1414
|
+
case " ":
|
|
1415
|
+
case "Enter":
|
|
1416
|
+
next = idx;
|
|
1417
|
+
break;
|
|
1418
|
+
default:
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
e.preventDefault();
|
|
1422
|
+
select(next, true);
|
|
1268
1423
|
});
|
|
1269
1424
|
});
|
|
1425
|
+
render(readValue());
|
|
1270
1426
|
});
|
|
1271
1427
|
}
|
|
1272
1428
|
};
|