mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
fix: sidebar breaking on tab
This commit is contained in:
+37
-3
@@ -462,12 +462,12 @@ body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) {
|
|||||||
#menu {
|
#menu {
|
||||||
width: 70px !important;
|
width: 70px !important;
|
||||||
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
overflow: hidden !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu > ul {
|
#menu > ul {
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
overflow-x: hidden !important;
|
overflow-x: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
@@ -503,7 +503,7 @@ body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) {
|
|||||||
flex: 0 0 auto !important;
|
flex: 0 0 auto !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
overflow: hidden !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu li > label > svg,
|
#menu li > label > svg,
|
||||||
@@ -511,6 +511,32 @@ body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) {
|
|||||||
margin: 0 auto !important;
|
margin: 0 auto !important;
|
||||||
flex-shrink: 0 !important;
|
flex-shrink: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu li:focus-visible,
|
||||||
|
#menu section:focus-visible {
|
||||||
|
width: 270px !important;
|
||||||
|
max-width: 270px !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu li:focus-visible > label,
|
||||||
|
#menu section:focus-visible > label {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu li:focus-visible > label > span,
|
||||||
|
#menu section:focus-visible > label > span {
|
||||||
|
display: inline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu li:focus-visible > label > svg,
|
||||||
|
#menu section:focus-visible > label > svg {
|
||||||
|
margin: 0 10px 0 4px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
[class*="notifications__items___"] {
|
[class*="notifications__items___"] {
|
||||||
-ms-overflow-style: none !important;
|
-ms-overflow-style: none !important;
|
||||||
@@ -930,6 +956,14 @@ html.transparencyEffects
|
|||||||
#menu li:hover {
|
#menu li:hover {
|
||||||
background: rgba(0, 0, 0, 0.15) !important;
|
background: rgba(0, 0, 0, 0.15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu li:focus-visible,
|
||||||
|
#menu section:focus-visible {
|
||||||
|
outline: 1px solid rgb(from currentColor r g b / 0.55);
|
||||||
|
outline-offset: -1px;
|
||||||
|
background: rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#main > .timetablepage > .container {
|
#main > .timetablepage > .container {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import { delay } from "@/seqta/utils/delay";
|
|||||||
let cachedUserInfo: any = null;
|
let cachedUserInfo: any = null;
|
||||||
|
|
||||||
let LightDarkModeSnakeEggButton = 0;
|
let LightDarkModeSnakeEggButton = 0;
|
||||||
|
let sidebarAccessibilityObserver: MutationObserver | null = null;
|
||||||
|
let sidebarTabOrderAnimationFrame: number | null = null;
|
||||||
|
let sidebarAccessibilityListenersAttached = false;
|
||||||
|
|
||||||
export async function getUserInfo() {
|
export async function getUserInfo() {
|
||||||
if (cachedUserInfo) return cachedUserInfo;
|
if (cachedUserInfo) return cachedUserInfo;
|
||||||
@@ -66,6 +69,7 @@ export async function AddBetterSEQTAElements() {
|
|||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
await addDarkLightToggle();
|
await addDarkLightToggle();
|
||||||
customizeMenuToggle();
|
customizeMenuToggle();
|
||||||
|
setupSidebarAccessibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
addExtensionSettings();
|
addExtensionSettings();
|
||||||
@@ -221,22 +225,27 @@ function setupEventListeners() {
|
|||||||
const homebutton = document.getElementById("homebutton");
|
const homebutton = document.getElementById("homebutton");
|
||||||
const newsbutton = document.getElementById("newsbutton");
|
const newsbutton = document.getElementById("newsbutton");
|
||||||
|
|
||||||
homebutton?.addEventListener("click", function () {
|
const activateMenuAction = (button: HTMLElement, action: () => void) => {
|
||||||
if (
|
if (
|
||||||
!homebutton.classList.contains("draggable") &&
|
button.classList.contains("draggable") ||
|
||||||
!homebutton.classList.contains("active")
|
button.classList.contains("active")
|
||||||
) {
|
) {
|
||||||
loadHomePage();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
|
||||||
|
homebutton?.addEventListener("click", function () {
|
||||||
|
activateMenuAction(homebutton, () => {
|
||||||
|
loadHomePage();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
newsbutton?.addEventListener("click", function () {
|
newsbutton?.addEventListener("click", function () {
|
||||||
if (
|
activateMenuAction(newsbutton, () => {
|
||||||
!newsbutton.classList.contains("draggable") &&
|
|
||||||
!newsbutton.classList.contains("active")
|
|
||||||
) {
|
|
||||||
SendNewsPage();
|
SendNewsPage();
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
menuCover?.addEventListener("click", function () {
|
menuCover?.addEventListener("click", function () {
|
||||||
@@ -327,3 +336,216 @@ function customizeMenuToggle() {
|
|||||||
menuToggle.appendChild(line);
|
menuToggle.appendChild(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupSidebarAccessibility() {
|
||||||
|
updateSidebarAccessibility();
|
||||||
|
|
||||||
|
const menu = document.getElementById("menu");
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
sidebarAccessibilityObserver?.disconnect();
|
||||||
|
sidebarAccessibilityObserver = new MutationObserver(() => {
|
||||||
|
scheduleSidebarAccessibilityUpdate();
|
||||||
|
});
|
||||||
|
sidebarAccessibilityObserver.observe(menu, {
|
||||||
|
subtree: true,
|
||||||
|
childList: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class", "style"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sidebarAccessibilityListenersAttached) {
|
||||||
|
document.addEventListener("keydown", handleSidebarKeyboardActivation);
|
||||||
|
sidebarAccessibilityListenersAttached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSidebarAccessibilityUpdate() {
|
||||||
|
if (sidebarTabOrderAnimationFrame !== null) {
|
||||||
|
cancelAnimationFrame(sidebarTabOrderAnimationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebarTabOrderAnimationFrame = requestAnimationFrame(() => {
|
||||||
|
sidebarTabOrderAnimationFrame = null;
|
||||||
|
updateSidebarAccessibility();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSidebarKeyboardActivation(event: KeyboardEvent) {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof HTMLElement)) return;
|
||||||
|
|
||||||
|
const menuItem = target.closest("#menu li, #menu section") as
|
||||||
|
| HTMLElement
|
||||||
|
| null;
|
||||||
|
if (!menuItem || target !== menuItem) return;
|
||||||
|
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
const menu = document.getElementById("menu");
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
const visibleList = getVisibleSidebarList(menu);
|
||||||
|
if (!visibleList) return;
|
||||||
|
|
||||||
|
const visibleEntries = getDirectSidebarEntries(visibleList);
|
||||||
|
if (visibleEntries.length === 0) return;
|
||||||
|
|
||||||
|
const boundaryEntry = event.shiftKey
|
||||||
|
? visibleEntries[0]
|
||||||
|
: visibleEntries[visibleEntries.length - 1];
|
||||||
|
|
||||||
|
if (boundaryEntry !== menuItem) return;
|
||||||
|
|
||||||
|
const parentEntry = getSidebarListParentEntry(visibleList);
|
||||||
|
if (!parentEntry) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
parentEntry.classList.remove("active");
|
||||||
|
scheduleSidebarAccessibilityUpdate();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
parentEntry.focus();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const childSubmenu = menuItem.querySelector(":scope > .sub > ul") as
|
||||||
|
| HTMLElement
|
||||||
|
| null;
|
||||||
|
|
||||||
|
menuItem.click();
|
||||||
|
scheduleSidebarAccessibilityUpdate();
|
||||||
|
|
||||||
|
if (childSubmenu) {
|
||||||
|
focusFirstSidebarSubmenuEntry(menuItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSidebarAccessibility() {
|
||||||
|
const menu = document.getElementById("menu");
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
const visibleEntries = new Set(getVisibleSidebarEntries(menu));
|
||||||
|
const menuEntries = menu.querySelectorAll("li.item, section.item, li, section");
|
||||||
|
|
||||||
|
for (const entry of menuEntries) {
|
||||||
|
if (!(entry instanceof HTMLElement)) continue;
|
||||||
|
|
||||||
|
const label = entry.querySelector(":scope > label") as HTMLLabelElement | null;
|
||||||
|
if (!label) continue;
|
||||||
|
|
||||||
|
const childSubmenu = entry.querySelector(":scope > .sub") as HTMLElement | null;
|
||||||
|
const isHidden =
|
||||||
|
entry.offsetParent === null ||
|
||||||
|
window.getComputedStyle(entry).display === "none" ||
|
||||||
|
window.getComputedStyle(label).display === "none" ||
|
||||||
|
!visibleEntries.has(entry);
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
entry.tabIndex = -1;
|
||||||
|
label.tabIndex = -1;
|
||||||
|
entry.setAttribute("aria-hidden", "true");
|
||||||
|
label.setAttribute("aria-hidden", "true");
|
||||||
|
if (childSubmenu) {
|
||||||
|
childSubmenu.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.tabIndex = 0;
|
||||||
|
label.tabIndex = -1;
|
||||||
|
entry.removeAttribute("aria-hidden");
|
||||||
|
label.removeAttribute("aria-hidden");
|
||||||
|
|
||||||
|
if (!entry.hasAttribute("role")) {
|
||||||
|
entry.setAttribute("role", "button");
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessibleLabel = label.textContent?.trim();
|
||||||
|
if (accessibleLabel) {
|
||||||
|
entry.setAttribute("aria-label", accessibleLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childSubmenu) {
|
||||||
|
const isExpanded = entry.classList.contains("active");
|
||||||
|
entry.setAttribute("aria-expanded", String(isExpanded));
|
||||||
|
childSubmenu.setAttribute("aria-hidden", String(!isExpanded));
|
||||||
|
} else {
|
||||||
|
entry.removeAttribute("aria-expanded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleSidebarEntries(menu = document.getElementById("menu")) {
|
||||||
|
if (!menu) return [] as HTMLElement[];
|
||||||
|
|
||||||
|
const visibleList = getVisibleSidebarList(menu);
|
||||||
|
if (!visibleList) return [] as HTMLElement[];
|
||||||
|
|
||||||
|
return getDirectSidebarEntries(visibleList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirectSidebarEntries(list: HTMLElement) {
|
||||||
|
return Array.from(list.querySelectorAll(":scope > li, :scope > section")).filter(
|
||||||
|
(entry): entry is HTMLElement => entry instanceof HTMLElement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleSidebarList(menu: HTMLElement) {
|
||||||
|
let currentList = menu.querySelector(":scope > ul") as HTMLElement | null;
|
||||||
|
|
||||||
|
while (currentList) {
|
||||||
|
const activeSubmenuParent = currentList.querySelector(
|
||||||
|
":scope > li.hasChildren.active, :scope > section.hasChildren.active",
|
||||||
|
) as HTMLElement | null;
|
||||||
|
|
||||||
|
if (!activeSubmenuParent) {
|
||||||
|
return currentList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextList = activeSubmenuParent.querySelector(
|
||||||
|
":scope > .sub > ul",
|
||||||
|
) as HTMLElement | null;
|
||||||
|
|
||||||
|
if (!nextList) {
|
||||||
|
return currentList;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentList = nextList;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSidebarListParentEntry(list: HTMLElement) {
|
||||||
|
return list.closest(".sub")?.parentElement instanceof HTMLElement
|
||||||
|
? (list.closest(".sub")!.parentElement as HTMLElement)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusFirstSidebarSubmenuEntry(parentEntry: HTMLElement) {
|
||||||
|
const menu = document.getElementById("menu");
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!parentEntry.classList.contains("active")) return;
|
||||||
|
|
||||||
|
const visibleList = getVisibleSidebarList(menu);
|
||||||
|
if (!visibleList || getSidebarListParentEntry(visibleList) !== parentEntry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstEntry = getDirectSidebarEntries(visibleList).find(
|
||||||
|
(entry) =>
|
||||||
|
entry.offsetParent !== null &&
|
||||||
|
window.getComputedStyle(entry).display !== "none",
|
||||||
|
);
|
||||||
|
|
||||||
|
firstEntry?.focus({ preventScroll: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user