[tabs] Update tabs.

This commit is contained in:
2026-06-13 17:57:46 -04:00
parent 944b81e366
commit d6efdc59ff
7 changed files with 2777 additions and 543 deletions

View File

@@ -3194,9 +3194,9 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ init: function() { return /* binding */ init; } /* harmony export */ init: function() { return /* binding */ init; }
/* harmony export */ }); /* harmony export */ });
/* DS2 core (c) 2024 Alexander McIlwraith /* DS2 core (c) 2024-2026 Alexander McIlwraith
Released under Creative Commons Attribution-ShareAlike 4.0 International Released under Creative Commons Attribution-ShareAlike 4.0 International
*/ */
// create a pure JS mouse click event // create a pure JS mouse click event
var click = new MouseEvent('click', { var click = new MouseEvent('click', {
@@ -3204,70 +3204,67 @@ var click = new MouseEvent('click', {
bubbles: false, bubbles: false,
cancelable: true cancelable: true
}); });
// wait for an element to appear in the DOM
var waitForElement = function waitForElement(selector) { var waitForElement = function waitForElement(selector) {
return new Promise(function (resolve) { return new Promise(function (resolve) {
if (document.querySelector(selector)) { if (document.querySelector(selector)) {
return resolve(document.querySelector(selector)); return resolve(document.querySelector(selector));
} }
var observer = new MutationObserver(function (mutations) { var observer = new MutationObserver(function () {
if (document.querySelector(selector)) { if (document.querySelector(selector)) {
observer.disconnect(); observer.disconnect();
resolve(document.querySelector(selector)); resolve(document.querySelector(selector));
} }
}); });
// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
observer.observe(document.body, { observer.observe(document.body, {
childList: true, childList: true,
subtree: true subtree: true
}); });
}); });
}; };
// Tab logic
var tabNamespace = "#tab:";
var chooseTab = function chooseTab(tab) { var chooseTab = function chooseTab(tab) {
var siblings = Array.from(tab.parentNode.children); var siblings = Array.from(tab.closest('[role="tablist"]').querySelectorAll('[role="tab"]'));
siblings.forEach(function (sibling) { siblings.forEach(function (sibling) {
return sibling.classList.remove("selected"); return sibling.classList.remove("selected");
}); });
tab.classList.add("selected"); tab.classList.add("selected");
var tabPanels = Array.from(tab.parentNode.parentNode.children); var tabGroup = tab.closest("tabset, .tab-group");
var tabPanels = Array.from(tabGroup.querySelectorAll('[role="tabpanel"]'));
tabPanels.forEach(function (panel) { tabPanels.forEach(function (panel) {
return panel.classList.remove("open"); return panel.classList.remove("open");
}); });
var tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel"); var tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel");
document.getElementById(tabPanelId).classList.add("open"); document.getElementById(tabPanelId).classList.add("open");
}; };
var pushState = 0; var pushStateCount = 0;
var tabsetCount = 0; var tabsetCount = 0;
function init() { function init() {
var container = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; var container = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
var spacer = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var spacer = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
var args = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
container.querySelectorAll(".tab-group, tabset").forEach(function (tabGroup) { container.querySelectorAll(".tab-group, tabset").forEach(function (tabGroup) {
if (tabGroup.querySelector("[role=tablist]") === null) { if (tabGroup.querySelector('[role="tablist"]') === null) {
var _ul$querySelector;
if (tabGroup.getAttribute("id") == null) { if (tabGroup.getAttribute("id") == null) {
tabGroup.setAttribute("id", "tab-group-" + tabsetCount); tabGroup.setAttribute("id", "tab-group-".concat(tabsetCount++));
tabsetCount++;
} }
var tabgroup = tabGroup.getAttribute("id"); var tabgroup = tabGroup.getAttribute("id");
var tablist = ""; var tablist = "";
Array.from(tabGroup.children).forEach(function (child) { Array.from(tabGroup.children).forEach(function (child) {
// is details? var _child$querySelector;
var dtls = child.nodeName == "DETAILS" ? true : false; var isDetails = child.nodeName === "DETAILS";
var tabLabel = isDetails ? (_child$querySelector = child.querySelector("summary")) === null || _child$querySelector === void 0 ? void 0 : _child$querySelector.innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");
// get the tab text if (tabLabel !== null) {
var tab = dtls ? child.querySelector("summary").innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab"); var tabID = tabLabel.replace(/\W+/g, "-").toLowerCase();
var tabPanel;
// if the tab text is not blank if (isDetails) {
if (tab !== null) {
var tabID = tab.replace(/\W+/g, "-").toLowerCase();
// define the tab panel content
var tabPanel = null;
if (dtls) {
tabPanel = child; tabPanel = child;
tabPanel.setAttribute("open", ""); tabPanel.setAttribute("open", "");
} else { } else {
tabPanel = document.createElement('div'); tabPanel = document.createElement("div");
tabPanel.appendChild(child.cloneNode(true)); tabPanel.appendChild(child.cloneNode(true));
} }
tabPanel.id = "tab-panel-".concat(tabgroup, "-").concat(tabID); tabPanel.id = "tab-panel-".concat(tabgroup, "-").concat(tabID);
@@ -3276,75 +3273,166 @@ function init() {
tabPanel.setAttribute("tabindex", "0"); tabPanel.setAttribute("tabindex", "0");
tabPanel.setAttribute("aria-labelledby", "tab-".concat(tabgroup, "-").concat(tabID)); tabPanel.setAttribute("aria-labelledby", "tab-".concat(tabgroup, "-").concat(tabID));
child.parentNode.replaceChild(tabPanel, child); child.parentNode.replaceChild(tabPanel, child);
tablist += "<li tabindex=\"0\" role=\"tab\" id=\"tab-".concat(tabgroup, "-").concat(tabID, "\"><span>").concat(tab, "</span></li>"); tablist += "<li tabindex=\"0\" role=\"tab\" id=\"tab-".concat(tabgroup, "-").concat(tabID, "\">\n\t\t\t\t\t\t\t<span>").concat(tabLabel, "</span>\n\t\t\t\t\t\t</li>");
} else { } else {
child.classList.add("tab-hidden"); child.classList.add("tab-hidden");
} }
}); });
var ul = document.createElement('ul'); var ul = document.createElement("ul");
ul.setAttribute("role", "tablist"); ul.setAttribute("role", "tablist");
tabGroup.insertBefore(ul, tabGroup.firstChild); tabGroup.insertBefore(ul, tabGroup.firstChild);
ul.innerHTML = spacer != true ? "".concat(tablist) : "".concat(tablist, "<li role=\"separator\" class=\"separator\"></li>"); var wrapper = document.createElement("div");
wrapper.className = "tab-scroll-wrapper";
var scroller = document.createElement("div");
scroller.className = "tab-scroll";
var leftArrow = document.createElement("div");
leftArrow.className = "tab-scroll-arrow left";
leftArrow.setAttribute("aria-hidden", "true");
leftArrow.innerHTML = "\n\t\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" width=\"24\" height=\"24\">\n\t\t\t\t\t<path d=\"M400-80 0-480l400-400 71 71-329 329 329 329-71 71Z\"/>\n\t\t\t\t</svg>\n\t\t\t";
var rightArrow = document.createElement("div");
rightArrow.className = "tab-scroll-arrow right";
rightArrow.setAttribute("aria-hidden", "true");
rightArrow.innerHTML = "\n\t\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" width=\"24\" height=\"24\">\n\t\t\t\t\t<path d=\"m321-80-71-71 329-329-329-329 71-71 400 400L321-80Z\"/>\n\t\t\t\t</svg>\n\t\t\t";
ul.parentNode.insertBefore(wrapper, ul);
wrapper.appendChild(scroller);
wrapper.appendChild(leftArrow);
wrapper.appendChild(rightArrow);
scroller.appendChild(ul);
var updateScrollState = function updateScrollState() {
var isOverflowing = scroller.scrollWidth > scroller.clientWidth;
wrapper.classList.toggle("is-overflowing", isOverflowing);
var atStart = scroller.scrollLeft <= 1;
var atEnd = scroller.scrollLeft + scroller.clientWidth >= scroller.scrollWidth - 2;
leftArrow.classList.toggle("hidden", !isOverflowing || atStart);
rightArrow.classList.toggle("hidden", !isOverflowing || atEnd);
wrapper.classList.toggle("show-left", isOverflowing && !atStart);
wrapper.classList.toggle("show-right", isOverflowing && !atEnd);
};
var scrollAmount = 15; // smaller = smoother per frame
var scrollInterval = 16; // ~60fps
var scrollTimer = null;
var startScrolling = function startScrolling(direction) {
if (scrollTimer) return;
var speed = 0.1;
scrollTimer = setInterval(function () {
speed = Math.min(speed + 0.01, 5); // ramp up
scroller.scrollBy({
left: direction * scrollAmount * speed,
behavior: "auto"
});
}, scrollInterval);
};
var stopScrolling = function stopScrolling() {
clearInterval(scrollTimer);
scrollTimer = null;
};
leftArrow.addEventListener("mousedown", function () {
return startScrolling(-1);
});
leftArrow.addEventListener("mouseup", stopScrolling);
leftArrow.addEventListener("mouseleave", stopScrolling);
leftArrow.addEventListener("touchstart", function () {
return startScrolling(-1);
});
leftArrow.addEventListener("touchend", stopScrolling);
rightArrow.addEventListener("mousedown", function () {
return startScrolling(1);
});
rightArrow.addEventListener("mouseup", stopScrolling);
rightArrow.addEventListener("mouseleave", stopScrolling);
rightArrow.addEventListener("touchstart", function () {
return startScrolling(1);
});
rightArrow.addEventListener("touchend", stopScrolling);
var ARROW_ZONE = 40;
wrapper.addEventListener("contextmenu", function (e) {
var isOverflowing = wrapper.classList.contains("is-overflowing");
if (!isOverflowing) return;
var rect = wrapper.getBoundingClientRect();
var isLeftZone = e.clientX < rect.left + ARROW_ZONE;
var isRightZone = e.clientX > rect.right - ARROW_ZONE;
if (isLeftZone || isRightZone) {
e.preventDefault();
}
});
var scrollClickAmount = 150;
leftArrow.addEventListener("click", function () {
scroller.scrollBy({
left: -scrollClickAmount,
behavior: "smooth"
});
});
rightArrow.addEventListener("click", function () {
scroller.scrollBy({
left: scrollClickAmount,
behavior: "smooth"
});
});
scroller.addEventListener("scroll", updateScrollState);
window.addEventListener("resize", updateScrollState);
updateScrollState();
ul.innerHTML = spacer !== true ? tablist : "".concat(tablist, "<li role=\"separator\" class=\"separator\"></li>");
requestAnimationFrame(updateScrollState);
setTimeout(updateScrollState, 50);
ul.innerHTML = spacer !== true ? tablist : "".concat(tablist, "<li role=\"separator\" class=\"separator\"></li>");
// Tab ordering
if (tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order")) { if (tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order")) {
var order = (tabGroup.getAttribute("order") || tabGroup.getAttribute("data-order")).split(","); var order = (tabGroup.getAttribute("order") || tabGroup.getAttribute("data-order")).split(",");
var items = Array.from(ul.getElementsByTagName("li")); var items = Array.from(ul.getElementsByTagName("li"));
items.sort(function (a, b) { items.sort(function (a, b) {
console.log("here");
var aa = order.indexOf(a.textContent.trim()); var aa = order.indexOf(a.textContent.trim());
var bb = order.indexOf(b.textContent.trim()); var bb = order.indexOf(b.textContent.trim());
if (aa === -1) return 1;
// Check if both items exist in orderArray if (bb === -1) return -1;
if (aa === -1) return 1; // Move to the end if not found return aa - bb;
if (bb === -1) return -1; // Move to the end if not found
return aa - bb; // Sort based on the defined order
}); });
ul.innerHTML = ''; ul.innerHTML = "";
items.forEach(function (item) { items.forEach(function (item) {
return ul.appendChild(item); return ul.appendChild(item);
}); });
chooseTab(items[0]);
} }
// Tab event handlers
tabGroup.querySelectorAll('[role="tab"]').forEach(function (tab) { tabGroup.querySelectorAll('[role="tab"]').forEach(function (tab) {
tab.addEventListener("click", function (evt) { tab.addEventListener("click", function (evt) {
if (pushState == 0) { var tabId = tab.getAttribute("id");
window.history.pushState({ var hash = "".concat(tabNamespace).concat(tabId);
rand: Math.random(),
pushState: pushState,
tab: tab.parentNode.firstChild.getAttribute("id")
}, "", "#".concat(tab.parentNode.firstChild.getAttribute("id")));
pushState++;
}
chooseTab(evt.currentTarget); chooseTab(evt.currentTarget);
window.history.pushState({ window.history.pushState({
rand: Math.random(), tab: tabId
pushState: pushState, }, "", "".concat(location.pathname).concat(location.search).concat(hash));
tab: tab.getAttribute("id") pushStateCount++;
}, "", "#".concat(tab.getAttribute("id")));
pushState++;
}); });
tab.addEventListener("keypress", function (e) { tab.addEventListener("keypress", function (e) {
if (e.which === 32 || e.which === 13) {
e.preventDefault(); e.preventDefault();
if (e.which == 32 || e.which == 13) {
e.currentTarget.dispatchEvent(click); e.currentTarget.dispatchEvent(click);
} }
}); });
}); });
ul.querySelector("li").classList.add("selected"); (_ul$querySelector = ul.querySelector("li")) === null || _ul$querySelector === void 0 || _ul$querySelector.classList.add("selected");
} }
if (document.location.hash != "" && document.location.hash.substring(0, 5) == "#tab-") { });
waitForElement(document.location.hash).then(function (el) {
//el.scrollIntoView(); // Initial hash handling (tabs only)
if (location.hash.startsWith(tabNamespace)) {
var tabId = location.hash.replace(tabNamespace, "");
waitForElement("#".concat(tabId)).then(function (el) {
el.focus(); el.focus();
el.dispatchEvent(click); el.dispatchEvent(click);
}); });
} }
});
// History navigation (tabs only)
window.addEventListener("popstate", function (e) { window.addEventListener("popstate", function (e) {
e.preventDefault(); var _e$state;
if (e.state != null) { if (!location.hash.startsWith(tabNamespace)) return;
chooseTab(document.querySelector("#".concat(e.state.tab))); if ((_e$state = e.state) !== null && _e$state !== void 0 && _e$state.tab) {
} else { var tab = document.querySelector("#".concat(e.state.tab));
history.go(-1); if (tab) chooseTab(tab);
} }
}); });
} }

File diff suppressed because one or more lines are too long

View File

@@ -42,45 +42,70 @@
<pre class="language-css" tab="css">tabset, .tab-group { <pre class="language-css" tab="css">tabset, .tab-group {
margin: 2rem 0 1rem 0; margin: 2rem 0 1rem 0;
} }
tabset [role=tablist], .tab-group [role=tablist] { tabset [role=tablist] li.selected, .tab-group [role=tablist] li.selected {
background-color: var(--colour-green);
color: var(--colour-white);
}
tabset > ul,
tabset .tab-scroll > ul, .tab-group > ul,
.tab-group .tab-scroll > ul {
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
} }
tabset [role=tablist] li.separator, .tab-group [role=tablist] li.separator { tabset > ul li.separator,
tabset .tab-scroll > ul li.separator, .tab-group > ul li.separator,
.tab-group .tab-scroll > ul li.separator {
border-bottom: 1px solid #7f7f7f; border-bottom: 1px solid #7f7f7f;
display: inline-block;
margin: 0.45rem 0 0 0;
width: 100%;
}
tabset [role=tablist] li[role=tab], .tab-group [role=tablist] li[role=tab] {
background-color: #FFF;
border-left: 1px solid #7f7f7f; border-left: 1px solid #7f7f7f;
border-right: 1px solid #7f7f7f; display: inline-block;
border-radius: 0.5rem 0.5rem 0 0; -webkit-box-flex: 1;
border-top: 1px solid #7f7f7f; -ms-flex: 1 1 auto;
cursor: pointer; flex: 1 1 auto;
display: inline; margin: 0.45rem 0 0 0;
margin: 0; width: auto;
padding: 1rem 1.5rem 0.14rem 1.5rem;
z-index: 2;
}
tabset [role=tablist] li[role=tab]:last-of-type, .tab-group [role=tablist] li[role=tab]:last-of-type {
border-right: 1px solid #7f7f7f;
}
tabset [role=tablist] li[role=tab]:not(.selected), .tab-group [role=tablist] li[role=tab]:not(.selected) {
background-color: #f0f0f0;
border-bottom: 1px solid #7f7f7f;
}
tabset [role=tablist] li[role=tab] span, .tab-group [role=tablist] li[role=tab] span {
display: block;
margin: 0 0 0.5rem 0;
} }
tabset .tab-hidden, .tab-group .tab-hidden { tabset .tab-hidden, .tab-group .tab-hidden {
display: none; display: none;
} }
tabset [role=tab], .tab-group [role=tab] {
background-color: #FFF;
border-left: 1px solid #7f7f7f;
border-radius: 0.5rem 0.5rem 0 0;
border-top: 1px solid #7f7f7f;
cursor: pointer;
display: block;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
margin: 0;
max-width: 100vw;
overflow: hidden;
padding: 1rem 1.5rem 0.14rem 1.5rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
white-space: normal;
word-break: break-word;
}
tabset [role=tab]:last-of-type, .tab-group [role=tab]:last-of-type {
border-right: 1px solid #7f7f7f;
}
tabset [role=tab]:not(.selected), .tab-group [role=tab]:not(.selected) {
background-color: #f0f0f0;
border-bottom: 1px solid #7f7f7f;
}
tabset [role=tab] span, .tab-group [role=tab] span {
display: block;
margin: 0 0 0.5rem 0;
}
tabset [role=tabpanel], .tab-group [role=tabpanel] { tabset [role=tabpanel], .tab-group [role=tabpanel] {
background-color: #FFF; background-color: #FFF;
border: 1px solid #7f7f7f; border: 1px solid #7f7f7f;
@@ -90,6 +115,112 @@ tabset [role=tabpanel], .tab-group [role=tabpanel] {
} }
tabset [role=tabpanel]:not(.open), .tab-group [role=tabpanel]:not(.open) { tabset [role=tabpanel]:not(.open), .tab-group [role=tabpanel]:not(.open) {
display: none; display: none;
}
.tab-scroll {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tab-scroll::-webkit-scrollbar {
display: none;
}
.tab-scroll > ul {
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
min-width: 100%;
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
}
.tab-scroll-wrapper {
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tab-scroll-wrapper::before, .tab-scroll-wrapper::after {
content: "";
position: absolute;
top: 0;
width: 2.5rem;
height: 100%;
pointer-events: none;
z-index: 2;
opacity: 0;
-webkit-transition: opacity 0.2s ease;
transition: opacity 0.2s ease;
}
.tab-scroll-wrapper::before {
background: radial-gradient(circle at left center, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0.5) 75%, rgba(255, 255, 255, 0.25) 95%, transparent 100%);
border-top-right-radius: 2.5rem;
border-bottom-right-radius: 2.5rem;
left: 0;
}
.tab-scroll-wrapper::after {
right: 0;
background: radial-gradient(circle at right center, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0.5) 75%, rgba(255, 255, 255, 0.25) 95%, transparent 100%);
border-top-left-radius: 2.5rem;
border-bottom-left-radius: 2.5rem;
}
.tab-scroll-wrapper.is-overflowing::before, .tab-scroll-wrapper.is-overflowing::after {
opacity: 0;
-webkit-transition: opacity 0.15s ease-out;
transition: opacity 0.15s ease-out;
-webkit-transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
}
.tab-scroll-wrapper.is-overflowing.show-left::before {
opacity: 1;
}
.tab-scroll-wrapper.is-overflowing.show-right::after {
opacity: 1;
}
.tab-scroll-wrapper .tab-scroll-arrow {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
display: none;
height: 100%;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
position: absolute;
top: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 2.5rem;
z-index: 3;
}
.tab-scroll-wrapper .tab-scroll-arrow path {
fill: #7f7f7f;
}
.tab-scroll-wrapper .tab-scroll-arrow.left {
left: 0;
}
.tab-scroll-wrapper .tab-scroll-arrow.right {
right: 0;
}
.tab-scroll-wrapper .tab-scroll-arrow.hidden {
display: none !important;
pointer-events: none;
visibility: hidden;
}
.tab-scroll-wrapper.is-overflowing .tab-scroll-arrow {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}</pre> }</pre>
<div tab="scss"> <div tab="scss">
<h2>Example</h2> <h2>Example</h2>
@@ -98,47 +229,62 @@ tabset [role=tabpanel]:not(.open), .tab-group [role=tabpanel]:not(.open) {
// optional content block // optional content block
}; };
</pre> </pre>
<pre class="language-sass">// DS2 core (c) 2024 Alexander McIlwraith <pre class="language-sass">$tab-border: #7f7f7f !default;
// Licensed under CC BY-SA 4.0
$tab-border: #7f7f7f !default;
$tab-panel-background-color: #FFF !default;
$tab-panel-top-border: #7f7f7f !default;
$tab-panel-top-border-width: 1px !default;
$tab-selected: #FFF !default; $tab-selected: #FFF !default;
$tab-selected-text: #000 !default; $tab-selected-text: #000 !default;
$tab-notselected: #f0f0f0 !default; $tab-notselected: #f0f0f0 !default;
$tab-notselected-text: #000 !default; $tab-notselected-text: #000 !default;
@mixin tabs { @mixin tabs {
tabset, .tab-group { tabset, .tab-group {
margin: 2rem 0 1rem 0; margin: 2rem 0 1rem 0;
[role="tablist"] { [role="tablist"] li.selected {
background-color: var(--colour-green);
color: var(--colour-white);
}
> ul,
.tab-scroll > ul {
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;
user-select: none;
li.separator {
li {
&.separator {
border-bottom: 1px solid $tab-border; border-bottom: 1px solid $tab-border;
border-left: 1px solid $tab-border;
display: inline-block; display: inline-block;
flex: 1 1 auto;
margin: .45rem 0 0 0; margin: .45rem 0 0 0;
width: 100%; width: auto;
}
} }
&[role="tab"] { .tab-hidden {
display: none;
}
[role="tab"] {
background-color: $tab-selected; background-color: $tab-selected;
border-left: 1px solid $tab-border; border-left: 1px solid $tab-border;
border-right: 1px solid $tab-border;
border-radius: .5rem .5rem 0 0; border-radius: .5rem .5rem 0 0;
border-top: 1px solid $tab-border; border-top: 1px solid $tab-border;
cursor:pointer; cursor: pointer;
display: inline; display: block;
flex: 0 0 auto;
margin: 0; margin: 0;
max-width: 100vw;
overflow: hidden;
padding: 1rem 1.5rem .14rem 1.5rem; padding: 1rem 1.5rem .14rem 1.5rem;
z-index: 2; user-select: none;
white-space: normal;
word-break: break-word;
&:last-of-type { &:last-of-type {
border-right: 1px solid $tab-border; border-right: 1px solid $tab-border;
@@ -147,7 +293,6 @@ $tab-notselected-text: #000 !default;
&:not(.selected) { &:not(.selected) {
background-color: $tab-notselected; background-color: $tab-notselected;
border-bottom: 1px solid $tab-border; border-bottom: 1px solid $tab-border;
/// color
} }
span { span {
@@ -155,12 +300,6 @@ $tab-notselected-text: #000 !default;
margin: 0 0 .5rem 0; margin: 0 0 .5rem 0;
} }
} }
}
}
.tab-hidden {
display: none;
}
[role="tabpanel"] { [role="tabpanel"] {
background-color: $tab-selected; background-color: $tab-selected;
@@ -172,18 +311,148 @@ $tab-notselected-text: #000 !default;
&:not(.open) { &:not(.open) {
display: none; display: none;
} }
}
@content; @content;
} }
}
// scroller
.tab-scroll {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
user-select: none;
&::-webkit-scrollbar {
display: none;
}
> ul {
flex-wrap: nowrap;
min-width: 100%;
width: max-content;
}
}
// wrapper (now owns the fade)
.tab-scroll-wrapper {
position: relative;
user-select: none;
$radial: 2.5rem;
// edge fades (this is the fix)
&::before,
&::after {
content: "";
position: absolute;
top: 0;
width: 2.5rem;
height: 100%;
pointer-events: none;
z-index: 2;
opacity: 0; // hidden by default
transition: opacity 0.2s ease;
}
// left fade
&::before {
background: radial-gradient(
circle at left center,
rgba(255,255,255,1) 0%,
rgba(255,255,255,0.9) 50%,
rgba(255,255,255,0.5) 75%,
rgba(255,255,255,0.25) 95%,
transparent 100%
);
border-top-right-radius: $radial;
border-bottom-right-radius: $radial;
left: 0;
}
// right fade
&::after {
right: 0;
background: radial-gradient(
circle at right center,
rgba(255,255,255,1) 0%,
rgba(255,255,255,0.9) 50%,
rgba(255,255,255,0.5) 75%,
rgba(255,255,255,0.25) 95%,
transparent 100%
);
border-top-left-radius: $radial;
border-bottom-left-radius: $radial;
}
// show fade only when overflowing
&.is-overflowing {
&::before,
&::after {
opacity: 0;
transition: opacity 0.15s ease-out;
transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
}
&.show-left {
&::before {
opacity: 1;
}
}
&.show-right {
&::after {
opacity: 1;
}
}
}
// arrows
.tab-scroll-arrow {
align-items: center;
cursor: pointer;
display: none;
height: 100%;
justify-content: center;
position: absolute;
top: 0;
user-select: none;
width: 2.5rem;
z-index: 3;
path {
fill: $tab-border;
}
&.left {
left: 0;
}
&.right {
right: 0;
}
&.hidden {
display: none !important;
pointer-events: none;
visibility: hidden;
}
}
// only show arrows when overflow
&.is-overflowing {
.tab-scroll-arrow {
display: flex;
}
}
}
}</pre> }</pre>
</div> </div>
<div tab="js"> <div tab="js">
<h2>Example</h2> <h2>Example</h2>
<pre class="language-js">import * as tabs from "./js/core/tabs/_tabs.js"; <pre class="language-js">import * as tabs from "./js/core/tabs/_tabs.js";
tabs.init();</pre> tabs.init();</pre>
<pre class="language-js">/* DS2 core (c) 2024 Alexander McIlwraith <pre class="language-js">/* DS2 core (c) 2024-2026 Alexander McIlwraith
Released under Creative Commons Attribution-ShareAlike 4.0 International Released under Creative Commons Attribution-ShareAlike 4.0 International
*/ */
// create a pure JS mouse click event // create a pure JS mouse click event
const click = new MouseEvent('click', { const click = new MouseEvent('click', {
@@ -192,48 +461,55 @@ const click = new MouseEvent('click', {
cancelable: true cancelable: true
}); });
// wait for an element to appear in the DOM
const waitForElement = (selector) => { const waitForElement = (selector) => {
return new Promise(resolve => { return new Promise(resolve => {
if (document.querySelector(selector)) { if (document.querySelector(selector)) {
return resolve(document.querySelector(selector)); return resolve(document.querySelector(selector));
} }
const observer = new MutationObserver(() => {
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) { if (document.querySelector(selector)) {
observer.disconnect(); observer.disconnect();
resolve(document.querySelector(selector)); resolve(document.querySelector(selector));
} }
}); });
// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
observer.observe(document.body, { observer.observe(document.body, {
childList: true, childList: true,
subtree: true subtree: true
}); });
}); });
} };
// Tab logic
const tabNamespace = "#tab:";
const chooseTab = (tab) => { const chooseTab = (tab) => {
const siblings = Array.from(tab.parentNode.children); const siblings = Array.from(
tab.closest('[role="tablist"]').querySelectorAll('[role="tab"]')
);
siblings.forEach(sibling => sibling.classList.remove("selected")); siblings.forEach(sibling => sibling.classList.remove("selected"));
tab.classList.add("selected"); tab.classList.add("selected");
const tabPanels = Array.from(tab.parentNode.parentNode.children) const tabGroup = tab.closest("tabset, .tab-group");
tabPanels.forEach(panel => panel.classList.remove("open")); const tabPanels = Array.from(
tabGroup.querySelectorAll('[role="tabpanel"]')
);
tabPanels.forEach(panel => panel.classList.remove("open"));
const tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel"); const tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel");
document.getElementById(tabPanelId).classList.add("open"); document.getElementById(tabPanelId).classList.add("open");
} };
let pushState = 0; let pushStateCount = 0;
let tabsetCount = 0; let tabsetCount = 0;
export function init(container = document, spacer = true, args = {}) { export function init(container = document, spacer = true) {
container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => { container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => {
if (tabGroup.querySelector("[role=tablist]") === null) {
if (tabGroup.querySelector('[role="tablist"]') === null) {
if (tabGroup.getAttribute("id") == null) { if (tabGroup.getAttribute("id") == null) {
tabGroup.setAttribute("id", "tab-group-" + tabsetCount); tabGroup.setAttribute("id", `tab-group-${tabsetCount++}`);
tabsetCount++;
} }
const tabgroup = tabGroup.getAttribute("id"); const tabgroup = tabGroup.getAttribute("id");
@@ -241,24 +517,19 @@ export function init(container = document, spacer = true, args = {}) {
Array.from(tabGroup.children).forEach(child => { Array.from(tabGroup.children).forEach(child => {
// is details? const isDetails = child.nodeName === "DETAILS";
let dtls = child.nodeName == "DETAILS" ? true : false; const tabLabel = isDetails ? child.querySelector("summary")?.innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");
// get the tab text if (tabLabel !== null) {
let tab = dtls ? child.querySelector("summary").innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");
// if the tab text is not blank const tabID = tabLabel.replace(/\W+/g, "-").toLowerCase();
if (tab !== null) { let tabPanel;
const tabID = tab.replace(/\W+/g, "-").toLowerCase();
if (isDetails) {
// define the tab panel content
let tabPanel = null;
if (dtls) {
tabPanel = child; tabPanel = child;
tabPanel.setAttribute("open", ""); tabPanel.setAttribute("open", "");
} else { } else {
tabPanel = document.createElement('div'); tabPanel = document.createElement("div");
tabPanel.appendChild(child.cloneNode(true)); tabPanel.appendChild(child.cloneNode(true));
} }
@@ -266,80 +537,225 @@ export function init(container = document, spacer = true, args = {}) {
tabPanel.className = tablist === "" ? "open" : ""; tabPanel.className = tablist === "" ? "open" : "";
tabPanel.setAttribute("role", "tabpanel"); tabPanel.setAttribute("role", "tabpanel");
tabPanel.setAttribute("tabindex", "0"); tabPanel.setAttribute("tabindex", "0");
tabPanel.setAttribute("aria-labelledby", `tab-${tabgroup}-${tabID}`); tabPanel.setAttribute(
"aria-labelledby",
`tab-${tabgroup}-${tabID}`
);
child.parentNode.replaceChild(tabPanel, child); child.parentNode.replaceChild(tabPanel, child);
tablist += `<li tabindex="0" role="tab" id="tab-${tabgroup}-${tabID}"><span>${tab}</span></li>`;
tablist += `<li tabindex="0" role="tab" id="tab-${tabgroup}-${tabID}">
<span>${tabLabel}</span>
</li>`;
} else { } else {
child.classList.add("tab-hidden"); child.classList.add("tab-hidden");
} }
}); });
const ul = document.createElement('ul'); const ul = document.createElement("ul");
ul.setAttribute("role", "tablist"); ul.setAttribute("role", "tablist");
tabGroup.insertBefore(ul, tabGroup.firstChild); tabGroup.insertBefore(ul, tabGroup.firstChild);
ul.innerHTML = spacer != true ? `${tablist}` : `${tablist}<li role="separator" class="separator"></li>`;
if ( tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order") ) {
let order = (tabGroup.getAttribute("order") || tabGroup.getAttribute("data-order")).split(",");
const items = Array.from(ul.getElementsByTagName("li")); const wrapper = document.createElement("div");
items.sort((a, b) => { wrapper.className = "tab-scroll-wrapper";
console.log("here")
const aa = order.indexOf(a.textContent.trim());
const bb = order.indexOf(b.textContent.trim());
// Check if both items exist in orderArray const scroller = document.createElement("div");
if (aa === -1) return 1; // Move to the end if not found scroller.className = "tab-scroll";
if (bb === -1) return -1; // Move to the end if not found
return aa - bb; // Sort based on the defined order const leftArrow = document.createElement("div");
leftArrow.className = "tab-scroll-arrow left";
leftArrow.setAttribute("aria-hidden", "true");
leftArrow.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" width="24" height="24">
<path d="M400-80 0-480l400-400 71 71-329 329 329 329-71 71Z"/>
</svg>
`;
const rightArrow = document.createElement("div");
rightArrow.className = "tab-scroll-arrow right";
rightArrow.setAttribute("aria-hidden", "true");
rightArrow.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" width="24" height="24">
<path d="m321-80-71-71 329-329-329-329 71-71 400 400L321-80Z"/>
</svg>
`;
ul.parentNode.insertBefore(wrapper, ul);
wrapper.appendChild(scroller);
wrapper.appendChild(leftArrow);
wrapper.appendChild(rightArrow);
scroller.appendChild(ul);
const updateScrollState = () => {
const isOverflowing = scroller.scrollWidth > scroller.clientWidth;
wrapper.classList.toggle("is-overflowing", isOverflowing);
const atStart = scroller.scrollLeft <= 1;
const atEnd = scroller.scrollLeft + scroller.clientWidth >= scroller.scrollWidth - 2;
leftArrow.classList.toggle("hidden", !isOverflowing || atStart);
rightArrow.classList.toggle("hidden", !isOverflowing || atEnd);
wrapper.classList.toggle("show-left", isOverflowing && !atStart);
wrapper.classList.toggle("show-right", isOverflowing && !atEnd);
};
const scrollAmount = 15; // smaller = smoother per frame
const scrollInterval = 16; // ~60fps
let scrollTimer = null;
const startScrolling = (direction) => {
if (scrollTimer) return;
let speed = 0.1;
scrollTimer = setInterval(() => {
speed = Math.min(speed + 0.01, 5); // ramp up
scroller.scrollBy({
left: direction * scrollAmount * speed,
behavior: "auto"
});
}, scrollInterval);
};
const stopScrolling = () => {
clearInterval(scrollTimer);
scrollTimer = null;
};
leftArrow.addEventListener("mousedown", () => startScrolling(-1));
leftArrow.addEventListener("mouseup", stopScrolling);
leftArrow.addEventListener("mouseleave", stopScrolling);
leftArrow.addEventListener("touchstart", () => startScrolling(-1));
leftArrow.addEventListener("touchend", stopScrolling);
rightArrow.addEventListener("mousedown", () => startScrolling(1));
rightArrow.addEventListener("mouseup", stopScrolling);
rightArrow.addEventListener("mouseleave", stopScrolling);
rightArrow.addEventListener("touchstart", () => startScrolling(1));
rightArrow.addEventListener("touchend", stopScrolling);
const ARROW_ZONE = 40;
wrapper.addEventListener("contextmenu", (e) => {
const isOverflowing = wrapper.classList.contains("is-overflowing");
if (!isOverflowing) return;
const rect = wrapper.getBoundingClientRect();
const isLeftZone = e.clientX < rect.left + ARROW_ZONE;
const isRightZone = e.clientX > rect.right - ARROW_ZONE;
if (isLeftZone || isRightZone) {
e.preventDefault();
}
}); });
ul.innerHTML = '';
const scrollClickAmount = 150;
leftArrow.addEventListener("click", () => {
scroller.scrollBy({ left: -scrollClickAmount, behavior: "smooth" });
});
rightArrow.addEventListener("click", () => {
scroller.scrollBy({ left: scrollClickAmount, behavior: "smooth" });
});
scroller.addEventListener("scroll", updateScrollState);
window.addEventListener("resize", updateScrollState);
updateScrollState();
ul.innerHTML = spacer !== true ? tablist : `${tablist}<li role="separator" class="separator"></li>`;
requestAnimationFrame(updateScrollState);
setTimeout(updateScrollState, 50);
ul.innerHTML = spacer !== true ? tablist : `${tablist}<li role="separator" class="separator"></li>`;
// Tab ordering
if (tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order")) {
const order = (
tabGroup.getAttribute("order") ||
tabGroup.getAttribute("data-order")
).split(",");
const items = Array.from(ul.getElementsByTagName("li"));
items.sort((a, b) => {
const aa = order.indexOf(a.textContent.trim());
const bb = order.indexOf(b.textContent.trim());
if (aa === -1) return 1;
if (bb === -1) return -1;
return aa - bb;
});
ul.innerHTML = "";
items.forEach(item => ul.appendChild(item)); items.forEach(item => ul.appendChild(item));
chooseTab(items[0]);
} }
// Tab event handlers
tabGroup.querySelectorAll('[role="tab"]').forEach(tab => { tabGroup.querySelectorAll('[role="tab"]').forEach(tab => {
tab.addEventListener("click", (evt) => { tab.addEventListener("click", (evt) => {
if (pushState == 0) {
window.history.pushState({rand: Math.random(), pushState: pushState, tab: tab.parentNode.firstChild.getAttribute("id")}, "", `#${tab.parentNode.firstChild.getAttribute("id")}`); const tabId = tab.getAttribute("id");
pushState++; const hash = `${tabNamespace}${tabId}`;
}
chooseTab(evt.currentTarget); chooseTab(evt.currentTarget);
window.history.pushState({rand: Math.random(), pushState: pushState, tab: tab.getAttribute("id")}, "", `#${tab.getAttribute("id")}`);
pushState++; window.history.pushState(
{ tab: tabId },
"",
`${location.pathname}${location.search}${hash}`
);
pushStateCount++;
}); });
tab.addEventListener("keypress", (e) => { tab.addEventListener("keypress", (e) => {
if (e.which === 32 || e.which === 13) {
e.preventDefault(); e.preventDefault();
if( e.which == 32 || e.which == 13 ) {
e.currentTarget.dispatchEvent(click); e.currentTarget.dispatchEvent(click);
} }
})
}); });
ul.querySelector("li").classList.add("selected"); });
}
if (document.location.hash != "" && document.location.hash.substring(0,5) == "#tab-") { ul.querySelector("li")?.classList.add("selected");
waitForElement(document.location.hash).then((el) => { }
//el.scrollIntoView(); });
// Initial hash handling (tabs only)
if (location.hash.startsWith(tabNamespace)) {
const tabId = location.hash.replace(tabNamespace, "");
waitForElement(`#${tabId}`).then(el => {
el.focus(); el.focus();
el.dispatchEvent(click); el.dispatchEvent(click);
}); });
} }
});
window.addEventListener("popstate", function (e) { // History navigation (tabs only)
e.preventDefault(); window.addEventListener("popstate", (e) => {
if (e.state != null) { if (!location.hash.startsWith(tabNamespace)) return;
chooseTab(document.querySelector(`#${e.state.tab}`)); if (e.state?.tab) {
} else { const tab = document.querySelector(`#${e.state.tab}`);
history.go(-1); if (tab) chooseTab(tab);
} }
}); });
} }</pre>
</pre>
</div> </div>
</tabset> </tabset>
</body> </body>

View File

@@ -1,27 +0,0 @@
jQuery(document).ready(function($){
$(".tab-group").each(function(){
if ($(this).children('[role="tablist"]').length > 0) {
let tabgroup = $(this).attr("id"),
tablist = "";
$(this).children("*").each(function(){
let tab = $(this).attr("data-tab");
if (typeof tab !== 'undefined' && tab !== false) {
let tabID = tab.replace(/\W+/g,"-").toLowerCase();
$(this).wrap(`<div id="tab-panel-${tabgroup + "-" + tabID }" ${(tablist == "" ? "class='open'" : "")} role="tabpanel" tabindex="0" aria-labeledby="tab-${tabgroup + "-" + tabID }"></div>`);
tablist += `<li role="tab" ${(tablist == "" ? "class='selected'" : "")} id="tab-${tabgroup + "-" + tab.replace(/\W+/g,"-").toLowerCase()}"><span>${tab}</span></li>`;
} else {
$(this).addClass("tab-hidden");
}
})
$(this).prepend(`<ul role="tablist">${tablist}<li role="separator" class="separator"></li></ul>`);
$(this).children('[role="tab"]').on("click", function(){
$(this).parent().children('[role="tab"]').removeClass("selected");
$(this).addClass("selected");
$(this).parent().parent().children('[role="tabpanel"]').removeClass("open");
$("#" + $(this).attr("id").replace("tab", "tab-panel")).addClass("open");
})
}
})
})

View File

@@ -32,27 +32,26 @@ const waitForElement = (selector) => {
const tabNamespace = "#tab:"; const tabNamespace = "#tab:";
const chooseTab = (tab) => { const chooseTab = (tab) => {
const siblings = Array.from(tab.parentNode.children); const siblings = Array.from(
tab.closest('[role="tablist"]').querySelectorAll('[role="tab"]')
);
siblings.forEach(sibling => sibling.classList.remove("selected")); siblings.forEach(sibling => sibling.classList.remove("selected"));
tab.classList.add("selected"); tab.classList.add("selected");
const tabPanels = Array.from(tab.parentNode.parentNode.children); const tabGroup = tab.closest("tabset, .tab-group");
const tabPanels = Array.from(
tabGroup.querySelectorAll('[role="tabpanel"]')
);
tabPanels.forEach(panel => panel.classList.remove("open")); tabPanels.forEach(panel => panel.classList.remove("open"));
const tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel");
const tabPanelId = tab document.getElementById(tabPanelId).classList.add("open");
.getAttribute("id")
.replace("tab", "tab-panel");
document
.getElementById(tabPanelId)
.classList.add("open");
}; };
let pushStateCount = 0; let pushStateCount = 0;
let tabsetCount = 0; let tabsetCount = 0;
export function init(container = document, spacer = true) { export function init(container = document, spacer = true) {
container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => { container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => {
if (tabGroup.querySelector('[role="tablist"]') === null) { if (tabGroup.querySelector('[role="tablist"]') === null) {
@@ -67,10 +66,7 @@ export function init(container = document, spacer = true) {
Array.from(tabGroup.children).forEach(child => { Array.from(tabGroup.children).forEach(child => {
const isDetails = child.nodeName === "DETAILS"; const isDetails = child.nodeName === "DETAILS";
const tabLabel = const tabLabel = isDetails ? child.querySelector("summary")?.innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");
isDetails
? child.querySelector("summary")?.innerHTML
: child.getAttribute("tab") || child.getAttribute("data-tab");
if (tabLabel !== null) { if (tabLabel !== null) {
@@ -96,15 +92,9 @@ export function init(container = document, spacer = true) {
child.parentNode.replaceChild(tabPanel, child); child.parentNode.replaceChild(tabPanel, child);
tablist += ` tablist += `<li tabindex="0" role="tab" id="tab-${tabgroup}-${tabID}">
<li
tabindex="0"
role="tab"
id="tab-${tabgroup}-${tabID}"
>
<span>${tabLabel}</span> <span>${tabLabel}</span>
</li> </li>`;
`;
} else { } else {
child.classList.add("tab-hidden"); child.classList.add("tab-hidden");
} }
@@ -114,9 +104,133 @@ export function init(container = document, spacer = true) {
ul.setAttribute("role", "tablist"); ul.setAttribute("role", "tablist");
tabGroup.insertBefore(ul, tabGroup.firstChild); tabGroup.insertBefore(ul, tabGroup.firstChild);
ul.innerHTML = spacer !== true
? tablist const wrapper = document.createElement("div");
: `${tablist}<li role="separator" class="separator"></li>`; wrapper.className = "tab-scroll-wrapper";
const scroller = document.createElement("div");
scroller.className = "tab-scroll";
const leftArrow = document.createElement("div");
leftArrow.className = "tab-scroll-arrow left";
leftArrow.setAttribute("aria-hidden", "true");
leftArrow.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" width="24" height="24">
<path d="M400-80 0-480l400-400 71 71-329 329 329 329-71 71Z"/>
</svg>
`;
const rightArrow = document.createElement("div");
rightArrow.className = "tab-scroll-arrow right";
rightArrow.setAttribute("aria-hidden", "true");
rightArrow.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" width="24" height="24">
<path d="m321-80-71-71 329-329-329-329 71-71 400 400L321-80Z"/>
</svg>
`;
ul.parentNode.insertBefore(wrapper, ul);
wrapper.appendChild(scroller);
wrapper.appendChild(leftArrow);
wrapper.appendChild(rightArrow);
scroller.appendChild(ul);
const updateScrollState = () => {
const isOverflowing = scroller.scrollWidth > scroller.clientWidth;
wrapper.classList.toggle("is-overflowing", isOverflowing);
const atStart = scroller.scrollLeft <= 1;
const atEnd = scroller.scrollLeft + scroller.clientWidth >= scroller.scrollWidth - 2;
leftArrow.classList.toggle("hidden", !isOverflowing || atStart);
rightArrow.classList.toggle("hidden", !isOverflowing || atEnd);
wrapper.classList.toggle("show-left", isOverflowing && !atStart);
wrapper.classList.toggle("show-right", isOverflowing && !atEnd);
};
const scrollAmount = 15; // smaller = smoother per frame
const scrollInterval = 16; // ~60fps
let scrollTimer = null;
const startScrolling = (direction) => {
if (scrollTimer) return;
let speed = 0.1;
scrollTimer = setInterval(() => {
speed = Math.min(speed + 0.01, 5); // ramp up
scroller.scrollBy({
left: direction * scrollAmount * speed,
behavior: "auto"
});
}, scrollInterval);
};
const stopScrolling = () => {
clearInterval(scrollTimer);
scrollTimer = null;
};
leftArrow.addEventListener("mousedown", () => startScrolling(-1));
leftArrow.addEventListener("mouseup", stopScrolling);
leftArrow.addEventListener("mouseleave", stopScrolling);
leftArrow.addEventListener("touchstart", () => startScrolling(-1));
leftArrow.addEventListener("touchend", stopScrolling);
rightArrow.addEventListener("mousedown", () => startScrolling(1));
rightArrow.addEventListener("mouseup", stopScrolling);
rightArrow.addEventListener("mouseleave", stopScrolling);
rightArrow.addEventListener("touchstart", () => startScrolling(1));
rightArrow.addEventListener("touchend", stopScrolling);
const ARROW_ZONE = 40;
wrapper.addEventListener("contextmenu", (e) => {
const isOverflowing = wrapper.classList.contains("is-overflowing");
if (!isOverflowing) return;
const rect = wrapper.getBoundingClientRect();
const isLeftZone = e.clientX < rect.left + ARROW_ZONE;
const isRightZone = e.clientX > rect.right - ARROW_ZONE;
if (isLeftZone || isRightZone) {
e.preventDefault();
}
});
const scrollClickAmount = 150;
leftArrow.addEventListener("click", () => {
scroller.scrollBy({ left: -scrollClickAmount, behavior: "smooth" });
});
rightArrow.addEventListener("click", () => {
scroller.scrollBy({ left: scrollClickAmount, behavior: "smooth" });
});
scroller.addEventListener("scroll", updateScrollState);
window.addEventListener("resize", updateScrollState);
updateScrollState();
ul.innerHTML = spacer !== true ? tablist : `${tablist}<li role="separator" class="separator"></li>`;
requestAnimationFrame(updateScrollState);
setTimeout(updateScrollState, 50);
ul.innerHTML = spacer !== true ? tablist : `${tablist}<li role="separator" class="separator"></li>`;
// Tab ordering // Tab ordering
if (tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order")) { if (tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order")) {

View File

@@ -1,44 +1,59 @@
// DS2 core (c) 2024 Alexander McIlwraith
// Licensed under CC BY-SA 4.0
$tab-border: #7f7f7f !default; $tab-border: #7f7f7f !default;
$tab-panel-background-color: #FFF !default;
$tab-panel-top-border: #7f7f7f !default;
$tab-panel-top-border-width: 1px !default;
$tab-selected: #FFF !default; $tab-selected: #FFF !default;
$tab-selected-text: #000 !default; $tab-selected-text: #000 !default;
$tab-notselected: #f0f0f0 !default; $tab-notselected: #f0f0f0 !default;
$tab-notselected-text: #000 !default; $tab-notselected-text: #000 !default;
@mixin tabs { @mixin tabs {
tabset, .tab-group { tabset, .tab-group {
margin: 2rem 0 1rem 0; margin: 2rem 0 1rem 0;
[role="tablist"] { [role="tablist"] li.selected {
background-color: var(--colour-green);
color: var(--colour-white);
}
> ul,
.tab-scroll > ul {
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;
user-select: none;
li.separator {
li {
&.separator {
border-bottom: 1px solid $tab-border; border-bottom: 1px solid $tab-border;
border-left: 1px solid $tab-border;
display: inline-block; display: inline-block;
flex: 1 1 auto;
margin: .45rem 0 0 0; margin: .45rem 0 0 0;
width: 100%; width: auto;
}
} }
&[role="tab"] { .tab-hidden {
display: none;
}
[role="tab"] {
background-color: $tab-selected; background-color: $tab-selected;
border-left: 1px solid $tab-border; border-left: 1px solid $tab-border;
border-right: 1px solid $tab-border;
border-radius: .5rem .5rem 0 0; border-radius: .5rem .5rem 0 0;
border-top: 1px solid $tab-border; border-top: 1px solid $tab-border;
cursor:pointer; cursor: pointer;
display: inline; display: block;
flex: 0 0 auto;
margin: 0; margin: 0;
max-width: 100vw;
overflow: hidden;
padding: 1rem 1.5rem .14rem 1.5rem; padding: 1rem 1.5rem .14rem 1.5rem;
z-index: 2; user-select: none;
white-space: normal;
word-break: break-word;
&:last-of-type { &:last-of-type {
border-right: 1px solid $tab-border; border-right: 1px solid $tab-border;
@@ -47,7 +62,6 @@ $tab-notselected-text: #000 !default;
&:not(.selected) { &:not(.selected) {
background-color: $tab-notselected; background-color: $tab-notselected;
border-bottom: 1px solid $tab-border; border-bottom: 1px solid $tab-border;
/// color
} }
span { span {
@@ -55,12 +69,6 @@ $tab-notselected-text: #000 !default;
margin: 0 0 .5rem 0; margin: 0 0 .5rem 0;
} }
} }
}
}
.tab-hidden {
display: none;
}
[role="tabpanel"] { [role="tabpanel"] {
background-color: $tab-selected; background-color: $tab-selected;
@@ -72,7 +80,137 @@ $tab-notselected-text: #000 !default;
&:not(.open) { &:not(.open) {
display: none; display: none;
} }
}
@content; @content;
} }
}
// scroller
.tab-scroll {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
user-select: none;
&::-webkit-scrollbar {
display: none;
}
> ul {
flex-wrap: nowrap;
min-width: 100%;
width: max-content;
}
}
// wrapper (now owns the fade)
.tab-scroll-wrapper {
position: relative;
user-select: none;
$radial: 2.5rem;
// edge fades (this is the fix)
&::before,
&::after {
content: "";
position: absolute;
top: 0;
width: 2.5rem;
height: 100%;
pointer-events: none;
z-index: 2;
opacity: 0; // hidden by default
transition: opacity 0.2s ease;
}
// left fade
&::before {
background: radial-gradient(
circle at left center,
rgba(255,255,255,1) 0%,
rgba(255,255,255,0.9) 50%,
rgba(255,255,255,0.5) 75%,
rgba(255,255,255,0.25) 95%,
transparent 100%
);
border-top-right-radius: $radial;
border-bottom-right-radius: $radial;
left: 0;
}
// right fade
&::after {
right: 0;
background: radial-gradient(
circle at right center,
rgba(255,255,255,1) 0%,
rgba(255,255,255,0.9) 50%,
rgba(255,255,255,0.5) 75%,
rgba(255,255,255,0.25) 95%,
transparent 100%
);
border-top-left-radius: $radial;
border-bottom-left-radius: $radial;
}
// show fade only when overflowing
&.is-overflowing {
&::before,
&::after {
opacity: 0;
transition: opacity 0.15s ease-out;
transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
}
&.show-left {
&::before {
opacity: 1;
}
}
&.show-right {
&::after {
opacity: 1;
}
}
}
// arrows
.tab-scroll-arrow {
align-items: center;
cursor: pointer;
display: none;
height: 100%;
justify-content: center;
position: absolute;
top: 0;
user-select: none;
width: 2.5rem;
z-index: 3;
path {
fill: $tab-border;
}
&.left {
left: 0;
}
&.right {
right: 0;
}
&.hidden {
display: none !important;
pointer-events: none;
visibility: hidden;
}
}
// only show arrows when overflow
&.is-overflowing {
.tab-scroll-arrow {
display: flex;
}
}
}
} }

View File

@@ -1,45 +1,70 @@
tabset, .tab-group { tabset, .tab-group {
margin: 2rem 0 1rem 0; margin: 2rem 0 1rem 0;
} }
tabset [role=tablist], .tab-group [role=tablist] { tabset [role=tablist] li.selected, .tab-group [role=tablist] li.selected {
background-color: var(--colour-green);
color: var(--colour-white);
}
tabset > ul,
tabset .tab-scroll > ul, .tab-group > ul,
.tab-group .tab-scroll > ul {
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
} }
tabset [role=tablist] li.separator, .tab-group [role=tablist] li.separator { tabset > ul li.separator,
tabset .tab-scroll > ul li.separator, .tab-group > ul li.separator,
.tab-group .tab-scroll > ul li.separator {
border-bottom: 1px solid #7f7f7f; border-bottom: 1px solid #7f7f7f;
display: inline-block;
margin: 0.45rem 0 0 0;
width: 100%;
}
tabset [role=tablist] li[role=tab], .tab-group [role=tablist] li[role=tab] {
background-color: #FFF;
border-left: 1px solid #7f7f7f; border-left: 1px solid #7f7f7f;
border-right: 1px solid #7f7f7f; display: inline-block;
border-radius: 0.5rem 0.5rem 0 0; -webkit-box-flex: 1;
border-top: 1px solid #7f7f7f; -ms-flex: 1 1 auto;
cursor: pointer; flex: 1 1 auto;
display: inline; margin: 0.45rem 0 0 0;
margin: 0; width: auto;
padding: 1rem 1.5rem 0.14rem 1.5rem;
z-index: 2;
}
tabset [role=tablist] li[role=tab]:last-of-type, .tab-group [role=tablist] li[role=tab]:last-of-type {
border-right: 1px solid #7f7f7f;
}
tabset [role=tablist] li[role=tab]:not(.selected), .tab-group [role=tablist] li[role=tab]:not(.selected) {
background-color: #f0f0f0;
border-bottom: 1px solid #7f7f7f;
}
tabset [role=tablist] li[role=tab] span, .tab-group [role=tablist] li[role=tab] span {
display: block;
margin: 0 0 0.5rem 0;
} }
tabset .tab-hidden, .tab-group .tab-hidden { tabset .tab-hidden, .tab-group .tab-hidden {
display: none; display: none;
} }
tabset [role=tab], .tab-group [role=tab] {
background-color: #FFF;
border-left: 1px solid #7f7f7f;
border-radius: 0.5rem 0.5rem 0 0;
border-top: 1px solid #7f7f7f;
cursor: pointer;
display: block;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
margin: 0;
max-width: 100vw;
overflow: hidden;
padding: 1rem 1.5rem 0.14rem 1.5rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
white-space: normal;
word-break: break-word;
}
tabset [role=tab]:last-of-type, .tab-group [role=tab]:last-of-type {
border-right: 1px solid #7f7f7f;
}
tabset [role=tab]:not(.selected), .tab-group [role=tab]:not(.selected) {
background-color: #f0f0f0;
border-bottom: 1px solid #7f7f7f;
}
tabset [role=tab] span, .tab-group [role=tab] span {
display: block;
margin: 0 0 0.5rem 0;
}
tabset [role=tabpanel], .tab-group [role=tabpanel] { tabset [role=tabpanel], .tab-group [role=tabpanel] {
background-color: #FFF; background-color: #FFF;
border: 1px solid #7f7f7f; border: 1px solid #7f7f7f;
@@ -50,3 +75,109 @@ tabset [role=tabpanel], .tab-group [role=tabpanel] {
tabset [role=tabpanel]:not(.open), .tab-group [role=tabpanel]:not(.open) { tabset [role=tabpanel]:not(.open), .tab-group [role=tabpanel]:not(.open) {
display: none; display: none;
} }
.tab-scroll {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tab-scroll::-webkit-scrollbar {
display: none;
}
.tab-scroll > ul {
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
min-width: 100%;
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
}
.tab-scroll-wrapper {
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tab-scroll-wrapper::before, .tab-scroll-wrapper::after {
content: "";
position: absolute;
top: 0;
width: 2.5rem;
height: 100%;
pointer-events: none;
z-index: 2;
opacity: 0;
-webkit-transition: opacity 0.2s ease;
transition: opacity 0.2s ease;
}
.tab-scroll-wrapper::before {
background: radial-gradient(circle at left center, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0.5) 75%, rgba(255, 255, 255, 0.25) 95%, transparent 100%);
border-top-right-radius: 2.5rem;
border-bottom-right-radius: 2.5rem;
left: 0;
}
.tab-scroll-wrapper::after {
right: 0;
background: radial-gradient(circle at right center, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0.5) 75%, rgba(255, 255, 255, 0.25) 95%, transparent 100%);
border-top-left-radius: 2.5rem;
border-bottom-left-radius: 2.5rem;
}
.tab-scroll-wrapper.is-overflowing::before, .tab-scroll-wrapper.is-overflowing::after {
opacity: 0;
-webkit-transition: opacity 0.15s ease-out;
transition: opacity 0.15s ease-out;
-webkit-transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
}
.tab-scroll-wrapper.is-overflowing.show-left::before {
opacity: 1;
}
.tab-scroll-wrapper.is-overflowing.show-right::after {
opacity: 1;
}
.tab-scroll-wrapper .tab-scroll-arrow {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
display: none;
height: 100%;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
position: absolute;
top: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 2.5rem;
z-index: 3;
}
.tab-scroll-wrapper .tab-scroll-arrow path {
fill: #7f7f7f;
}
.tab-scroll-wrapper .tab-scroll-arrow.left {
left: 0;
}
.tab-scroll-wrapper .tab-scroll-arrow.right {
right: 0;
}
.tab-scroll-wrapper .tab-scroll-arrow.hidden {
display: none !important;
pointer-events: none;
visibility: hidden;
}
.tab-scroll-wrapper.is-overflowing .tab-scroll-arrow {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}