mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-06 04:21:55 +00:00
# Conflicts: # astro.config.ts # package-lock.json # package.json # src/types/plausible.d.ts # src/utils/debounce.ts
159 lines
4.1 KiB
TypeScript
159 lines
4.1 KiB
TypeScript
const LOCAL_STORAGE_KEY = "tabs-selection";
|
|
|
|
type TabEntry = Map<
|
|
string,
|
|
{
|
|
tab: HTMLElement;
|
|
panel: HTMLElement;
|
|
}
|
|
>;
|
|
|
|
export const enableTabs = () => {
|
|
// Array of all TabName->Element mappings in tab sets
|
|
const tabEntries: TabEntry[] = [];
|
|
|
|
// If overflow-anchor cannot be applied, tabs should scroll into view when clicked
|
|
// to prevent confusing content jumps
|
|
const shouldScrollToTab = !(
|
|
CSS.supports && CSS.supports("overflow-anchor", "none")
|
|
);
|
|
|
|
// Handle arrow navigation between tabs in the tab list
|
|
function handleKeydown(this: HTMLElement, e: KeyboardEvent) {
|
|
if (e.keyCode === 39 || e.keyCode === 37) {
|
|
const tabs = this.children;
|
|
let tabfocus = Number(this.dataset.tabfocus || 0);
|
|
tabs[tabfocus].setAttribute("tabindex", "-1");
|
|
if (e.keyCode === 39) {
|
|
// Move right
|
|
// Increase tab index, wrap by # of tabs
|
|
tabfocus = (tabfocus + 1) % tabs.length;
|
|
} else if (e.keyCode === 37) {
|
|
// Move left
|
|
tabfocus--;
|
|
// If we're at the start, move to the end
|
|
if (tabfocus < 0) {
|
|
tabfocus = tabs.length - 1;
|
|
}
|
|
}
|
|
|
|
// Update tabfocus values
|
|
this.dataset.tabfocus = tabfocus + "";
|
|
// Focus + click selected tab
|
|
const tab = tabs[tabfocus] as HTMLElement;
|
|
tab.setAttribute("tabindex", "0");
|
|
tab.focus();
|
|
tab.click();
|
|
|
|
// Scroll onto screen in order to avoid jumping page locations
|
|
setTimeout(() => {
|
|
tab.scrollIntoView &&
|
|
tab.scrollIntoView({
|
|
behavior: "auto",
|
|
block: "center",
|
|
inline: "center",
|
|
});
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
function handleClick(e: Event) {
|
|
const target = e.target as HTMLElement;
|
|
const tabName = target.dataset.tabname;
|
|
changeTabs(tabName);
|
|
|
|
if (shouldScrollToTab) {
|
|
// Scroll onto screen in order to avoid jumping page locations
|
|
setTimeout(() => {
|
|
target.scrollIntoView &&
|
|
target.scrollIntoView({
|
|
behavior: "auto",
|
|
block: "center",
|
|
inline: "center",
|
|
});
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
// Iterate through all tabs to populate tabEntries & set listeners
|
|
document.querySelectorAll('[role="tablist"]').forEach((tabList) => {
|
|
const entry: TabEntry = new Map();
|
|
const parent = tabList.parentElement;
|
|
|
|
const tabs: NodeListOf<HTMLElement> =
|
|
tabList.querySelectorAll('[role="tab"]');
|
|
|
|
tabs.forEach((tab) => {
|
|
const panel = parent.querySelector<HTMLElement>(
|
|
`#${tab.getAttribute("aria-controls")}`,
|
|
);
|
|
entry.set(tab.dataset.tabname, {
|
|
tab,
|
|
panel,
|
|
});
|
|
|
|
// Add a click event handler to each tab
|
|
tab.addEventListener("click", handleClick);
|
|
});
|
|
|
|
// Enable arrow navigation between tabs in the tab list
|
|
tabList.addEventListener("keydown", handleKeydown);
|
|
|
|
tabEntries.push(entry);
|
|
});
|
|
|
|
function changeTabs(tabName: string) {
|
|
// find all tabs on the page that match the selected tabname
|
|
for (const tabEntry of tabEntries) {
|
|
const tab = tabEntry.get(tabName);
|
|
if (!tab) continue;
|
|
|
|
// Set all encountered tabs as selected
|
|
tab.tab.setAttribute("aria-selected", "true");
|
|
// Show the selected panel
|
|
tab.panel.removeAttribute("aria-hidden");
|
|
|
|
// Iterate through sibling tabs
|
|
for (const [otherKey, otherTab] of tabEntry) {
|
|
if (otherKey === tabName) continue;
|
|
|
|
// Set all sibling tabs as unselected
|
|
otherTab.tab.setAttribute("aria-selected", "false");
|
|
// Hide sibling tab panels
|
|
otherTab.panel.setAttribute("aria-hidden", `true`);
|
|
}
|
|
}
|
|
|
|
localStorage.setItem(LOCAL_STORAGE_KEY, tabName);
|
|
}
|
|
|
|
/* -------------------- */
|
|
|
|
// If the user has already visited a tab name, enable it on load
|
|
const currentTab = localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
if (currentTab) {
|
|
changeTabs(currentTab);
|
|
}
|
|
|
|
// If user has linked to a heading that's inside of a tab
|
|
const hash = window.location.hash;
|
|
if (!hash) return;
|
|
const heading = document.getElementById(hash.slice(1));
|
|
if (!heading) return;
|
|
|
|
for (const tabEntry of tabEntries)
|
|
for (const [, tab] of tabEntry) {
|
|
// If the tab is hidden and the heading is contained within the tab
|
|
if (
|
|
tab.panel.hasAttribute("aria-hidden") &&
|
|
tab.panel.contains(heading)
|
|
) {
|
|
tab.tab.click();
|
|
setTimeout(() => {
|
|
heading.scrollIntoView(true);
|
|
}, 0);
|
|
return;
|
|
}
|
|
}
|
|
};
|