762 lines
20 KiB
HTML
762 lines
20 KiB
HTML
|
|
<html>
|
|
<head>
|
|
<title>Pattern</title>
|
|
<script>
|
|
let u = document.location.href.substring(0, document.location.href.search(/patterns/i));
|
|
let p = document.location.pathname.substring(document.location.pathname.search(/patterns/i));
|
|
p = p.replace(/\/$|\/index\.html/i, "").substring(9);
|
|
window.location = u + "?p=" + p;
|
|
|
|
</script>
|
|
</head>
|
|
<body data-prismjs-copy-timeout="1500">
|
|
<h2>What is it</h2>
|
|
<p>A tabs component that provides different sections of content that are displayed one at a time when the user selects that information. </p>
|
|
<h2>When to use it</h2>
|
|
<p>The tabbed user interface enables users to jump to their target section quickly. Tabs present like logically group information on the same page. Information should </p>
|
|
<ul>
|
|
<li>be logically chunked and ordered</li>
|
|
<li>be arallel in nature</li>
|
|
<li>show user's context</li>
|
|
<li>obvious where they begin and end </li>
|
|
</ul>
|
|
<p>Users should not need to see content of multiple tabs simultaneously and the user should be able to easily recognise where they are within the content. </p>
|
|
<h2>How to use it</h2>
|
|
<p>The structure of the tab set is defined in html. There are two forms supported. Adding a class of <code class="inline">.tab-group</code> to the container element will work in place of the <code class="inline">tabset</code> tag, and the tab panels can be defined using either <code class="inline">tab=""</code> or <code class="inline">data-tab=""</code>. Passing an optional element to the init function will initialise tabs within that element. Adding a <code class="inline">order=""</code> or <code class="inline">data-order=""</code> element to the tabset you can have the tabs sorted in a consistent order across tabsets.</p>
|
|
<h2>Example</h2>
|
|
<pre class="language-pug" tab="pug">tabset(order="tab2, tab1")
|
|
div(tab="tab1")
|
|
div(tab="tab2")
|
|
</pre>
|
|
<tabset id="tabs">
|
|
<pre class="language-html" tab="html">
|
|
<tabset id="uniqueID" order="tab title 2,tab title 1">
|
|
<div tab="[tab title 1]"></div>
|
|
<div tab="[tab title 2]"></div>
|
|
</tabset></pre>
|
|
<pre class="language-pug" tab="pug">tabset#uniqueID(order="tab title 2,tab title 1")
|
|
div(tab="[tab title 1]")
|
|
div(tab="[tab title 2]")
|
|
</pre>
|
|
<pre class="language-css" tab="css">tabset, .tab-group {
|
|
margin: 2rem 0 1rem 0;
|
|
}
|
|
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: -ms-flexbox;
|
|
display: flex;
|
|
margin: 0;
|
|
padding: 0;
|
|
-webkit-user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
user-select: none;
|
|
}
|
|
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-left: 1px solid #7f7f7f;
|
|
display: inline-block;
|
|
-webkit-box-flex: 1;
|
|
-ms-flex: 1 1 auto;
|
|
flex: 1 1 auto;
|
|
margin: 0.45rem 0 0 0;
|
|
width: auto;
|
|
}
|
|
tabset .tab-hidden, .tab-group .tab-hidden {
|
|
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] {
|
|
background-color: #FFF;
|
|
border: 1px solid #7f7f7f;
|
|
border-top: none;
|
|
padding: 1rem;
|
|
z-index: 1;
|
|
}
|
|
tabset [role=tabpanel]:not(.open), .tab-group [role=tabpanel]:not(.open) {
|
|
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>
|
|
<div tab="scss">
|
|
<h2>Example</h2>
|
|
<pre class="language-sass">@use "scss/core/tabs/_tabs";
|
|
@include tabs{
|
|
// optional content block
|
|
};
|
|
</pre>
|
|
<pre class="language-sass">$tab-border: #7f7f7f !default;
|
|
|
|
$tab-selected: #FFF !default;
|
|
$tab-selected-text: #000 !default;
|
|
|
|
$tab-notselected: #f0f0f0 !default;
|
|
$tab-notselected-text: #000 !default;
|
|
|
|
@mixin tabs {
|
|
|
|
tabset, .tab-group {
|
|
|
|
margin: 2rem 0 1rem 0;
|
|
|
|
[role="tablist"] li.selected {
|
|
background-color: var(--colour-green);
|
|
color: var(--colour-white);
|
|
|
|
}
|
|
|
|
> ul,
|
|
.tab-scroll > ul {
|
|
display: flex;
|
|
margin: 0;
|
|
padding: 0;
|
|
user-select: none;
|
|
|
|
li.separator {
|
|
border-bottom: 1px solid $tab-border;
|
|
border-left: 1px solid $tab-border;
|
|
display: inline-block;
|
|
flex: 1 1 auto;
|
|
margin: .45rem 0 0 0;
|
|
width: auto;
|
|
}
|
|
}
|
|
|
|
.tab-hidden {
|
|
display: none;
|
|
}
|
|
|
|
[role="tab"] {
|
|
background-color: $tab-selected;
|
|
border-left: 1px solid $tab-border;
|
|
border-radius: .5rem .5rem 0 0;
|
|
border-top: 1px solid $tab-border;
|
|
cursor: pointer;
|
|
display: block;
|
|
flex: 0 0 auto;
|
|
margin: 0;
|
|
max-width: 100vw;
|
|
overflow: hidden;
|
|
padding: 1rem 1.5rem .14rem 1.5rem;
|
|
user-select: none;
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
|
|
&:last-of-type {
|
|
border-right: 1px solid $tab-border;
|
|
}
|
|
|
|
&:not(.selected) {
|
|
background-color: $tab-notselected;
|
|
border-bottom: 1px solid $tab-border;
|
|
}
|
|
|
|
span {
|
|
display: block;
|
|
margin: 0 0 .5rem 0;
|
|
}
|
|
}
|
|
|
|
[role="tabpanel"] {
|
|
background-color: $tab-selected;
|
|
border: 1px solid $tab-border;
|
|
border-top: none;
|
|
padding: 1rem;
|
|
z-index: 1;
|
|
|
|
&:not(.open) {
|
|
display: none;
|
|
}
|
|
|
|
@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>
|
|
</div>
|
|
<div tab="js">
|
|
<h2>Example</h2>
|
|
<pre class="language-js">import * as tabs from "./js/core/tabs/_tabs.js";
|
|
tabs.init();</pre>
|
|
<pre class="language-js">/* DS2 core (c) 2024-2026 Alexander McIlwraith
|
|
Released under Creative Commons Attribution-ShareAlike 4.0 International
|
|
*/
|
|
|
|
// create a pure JS mouse click event
|
|
const click = new MouseEvent('click', {
|
|
view: window,
|
|
bubbles: false,
|
|
cancelable: true
|
|
});
|
|
|
|
// wait for an element to appear in the DOM
|
|
const waitForElement = (selector) => {
|
|
return new Promise(resolve => {
|
|
if (document.querySelector(selector)) {
|
|
return resolve(document.querySelector(selector));
|
|
}
|
|
const observer = new MutationObserver(() => {
|
|
if (document.querySelector(selector)) {
|
|
observer.disconnect();
|
|
resolve(document.querySelector(selector));
|
|
}
|
|
});
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
});
|
|
};
|
|
|
|
// Tab logic
|
|
const tabNamespace = "#tab:";
|
|
|
|
const chooseTab = (tab) => {
|
|
const siblings = Array.from(
|
|
tab.closest('[role="tablist"]').querySelectorAll('[role="tab"]')
|
|
);
|
|
siblings.forEach(sibling => sibling.classList.remove("selected"));
|
|
tab.classList.add("selected");
|
|
|
|
const tabGroup = tab.closest("tabset, .tab-group");
|
|
const tabPanels = Array.from(
|
|
tabGroup.querySelectorAll('[role="tabpanel"]')
|
|
);
|
|
|
|
tabPanels.forEach(panel => panel.classList.remove("open"));
|
|
const tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel");
|
|
document.getElementById(tabPanelId).classList.add("open");
|
|
};
|
|
|
|
let pushStateCount = 0;
|
|
let tabsetCount = 0;
|
|
|
|
export function init(container = document, spacer = true) {
|
|
container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => {
|
|
|
|
if (tabGroup.querySelector('[role="tablist"]') === null) {
|
|
|
|
if (tabGroup.getAttribute("id") == null) {
|
|
tabGroup.setAttribute("id", `tab-group-${tabsetCount++}`);
|
|
}
|
|
|
|
const tabgroup = tabGroup.getAttribute("id");
|
|
let tablist = "";
|
|
|
|
Array.from(tabGroup.children).forEach(child => {
|
|
|
|
const isDetails = child.nodeName === "DETAILS";
|
|
const tabLabel = isDetails ? child.querySelector("summary")?.innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");
|
|
|
|
if (tabLabel !== null) {
|
|
|
|
const tabID = tabLabel.replace(/\W+/g, "-").toLowerCase();
|
|
let tabPanel;
|
|
|
|
if (isDetails) {
|
|
tabPanel = child;
|
|
tabPanel.setAttribute("open", "");
|
|
} else {
|
|
tabPanel = document.createElement("div");
|
|
tabPanel.appendChild(child.cloneNode(true));
|
|
}
|
|
|
|
tabPanel.id = `tab-panel-${tabgroup}-${tabID}`;
|
|
tabPanel.className = tablist === "" ? "open" : "";
|
|
tabPanel.setAttribute("role", "tabpanel");
|
|
tabPanel.setAttribute("tabindex", "0");
|
|
tabPanel.setAttribute(
|
|
"aria-labelledby",
|
|
`tab-${tabgroup}-${tabID}`
|
|
);
|
|
|
|
child.parentNode.replaceChild(tabPanel, child);
|
|
|
|
tablist += `<li tabindex="0" role="tab" id="tab-${tabgroup}-${tabID}">
|
|
<span>${tabLabel}</span>
|
|
</li>`;
|
|
} else {
|
|
child.classList.add("tab-hidden");
|
|
}
|
|
});
|
|
|
|
const ul = document.createElement("ul");
|
|
ul.setAttribute("role", "tablist");
|
|
tabGroup.insertBefore(ul, tabGroup.firstChild);
|
|
|
|
|
|
const wrapper = document.createElement("div");
|
|
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
|
|
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));
|
|
chooseTab(items[0]);
|
|
}
|
|
|
|
// Tab event handlers
|
|
tabGroup.querySelectorAll('[role="tab"]').forEach(tab => {
|
|
|
|
tab.addEventListener("click", (evt) => {
|
|
|
|
const tabId = tab.getAttribute("id");
|
|
const hash = `${tabNamespace}${tabId}`;
|
|
|
|
chooseTab(evt.currentTarget);
|
|
|
|
window.history.pushState(
|
|
{ tab: tabId },
|
|
"",
|
|
`${location.pathname}${location.search}${hash}`
|
|
);
|
|
|
|
pushStateCount++;
|
|
});
|
|
|
|
tab.addEventListener("keypress", (e) => {
|
|
if (e.which === 32 || e.which === 13) {
|
|
e.preventDefault();
|
|
e.currentTarget.dispatchEvent(click);
|
|
}
|
|
});
|
|
});
|
|
|
|
ul.querySelector("li")?.classList.add("selected");
|
|
}
|
|
});
|
|
|
|
// Initial hash handling (tabs only)
|
|
if (location.hash.startsWith(tabNamespace)) {
|
|
const tabId = location.hash.replace(tabNamespace, "");
|
|
waitForElement(`#${tabId}`).then(el => {
|
|
el.focus();
|
|
el.dispatchEvent(click);
|
|
});
|
|
}
|
|
|
|
// History navigation (tabs only)
|
|
window.addEventListener("popstate", (e) => {
|
|
if (!location.hash.startsWith(tabNamespace)) return;
|
|
if (e.state?.tab) {
|
|
const tab = document.querySelector(`#${e.state.tab}`);
|
|
if (tab) chooseTab(tab);
|
|
}
|
|
});
|
|
}</pre>
|
|
</div>
|
|
</tabset>
|
|
</body>
|
|
</html> |