accessibility

This commit is contained in:
Philipinho
2026-04-20 19:45:55 +01:00
parent fadeeaa59d
commit 6fdab5fe70
10 changed files with 152 additions and 63 deletions
@@ -28,4 +28,22 @@
}
}
.skipLink {
position: fixed;
left: 8px;
top: 8px;
padding: 8px 12px;
background: var(--mantine-color-blue-6);
color: #fff;
border-radius: 4px;
text-decoration: none;
z-index: 1000;
transform: translateY(-150%);
&:focus {
transform: translateY(0);
outline: 2px solid var(--mantine-color-blue-3);
}
}
@@ -1,6 +1,7 @@
import { AppShell, Container } from "@mantine/core";
import React, { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
import { useAtom } from "jotai";
import {
@@ -23,11 +24,12 @@ export default function GlobalAppShell({
}: {
children: React.ReactNode;
}) {
const { t } = useTranslation();
useTrialEndAction();
const [mobileOpened] = useAtom(mobileSidebarAtom);
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
const [desktopOpened] = useAtom(desktopSidebarAtom);
const [{ isAsideOpen }] = useAtom(asideStateAtom);
const [{ isAsideOpen, tab: asideTab }] = useAtom(asideStateAtom);
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
const [isResizing, setIsResizing] = useState(false);
const sidebarRef = useRef(null);
@@ -79,7 +81,11 @@ export default function GlobalAppShell({
const showGlobalSidebar = !isSpaceRoute && !isSettingsRoute && !isAiRoute;
return (
<AppShell
<>
<a href="#main-content" className={classes.skipLink}>
{t("Skip to main content")}
</a>
<AppShell
header={{ height: 45 }}
navbar={{
width: isSpaceRoute ? sidebarWidth : 300,
@@ -105,6 +111,15 @@ export default function GlobalAppShell({
className={classes.navbar}
withBorder={false}
ref={sidebarRef}
aria-label={
isSpaceRoute
? t("Space navigation")
: isSettingsRoute
? t("Settings navigation")
: isAiRoute
? t("AI navigation")
: t("Main navigation")
}
>
{isSpaceRoute && (
<div className={classes.resizeHandle} onMouseDown={startResizing} />
@@ -114,7 +129,7 @@ export default function GlobalAppShell({
{isAiRoute && <AiChatSidebar />}
{showGlobalSidebar && <GlobalSidebar />}
</AppShell.Navbar>
<AppShell.Main>
<AppShell.Main id="main-content">
{isSettingsRoute ? (
<Container size={900}>{children}</Container>
) : (
@@ -123,10 +138,24 @@ export default function GlobalAppShell({
</AppShell.Main>
{isPageRoute && (
<AppShell.Aside className={classes.aside} p="md" withBorder={false}>
<AppShell.Aside
className={classes.aside}
p="md"
withBorder={false}
aria-label={
asideTab === "comments"
? t("Comments")
: asideTab === "toc"
? t("Table of contents")
: asideTab === "chat"
? t("AI Chat")
: undefined
}
>
<Aside />
</AppShell.Aside>
)}
</AppShell>
</>
);
}
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { ScrollArea, Text, Divider, Modal } from "@mantine/core";
import { ScrollArea, Text, Divider, Modal, UnstyledButton } from "@mantine/core";
import {
IconHome,
IconClock,
@@ -119,17 +119,13 @@ export default function GlobalSidebar() {
</ScrollArea>
<div className={classes.bottomSection}>
<a
<UnstyledButton
className={classes.link}
onClick={(e) => {
e.preventDefault();
openInvite();
}}
href="#"
onClick={openInvite}
>
<IconUserPlus className={classes.linkIcon} stroke={2} />
<span>{t("Invite People")}</span>
</a>
</UnstyledButton>
<Link
className={classes.link}
data-active={active.startsWith("/settings") || undefined}
@@ -226,32 +226,6 @@ export default function SettingsSidebar() {
}
const isDisabled = isItemDisabled(item);
const linkElement = (
<Link
onMouseEnter={!isDisabled ? prefetchHandler : undefined}
className={classes.link}
data-active={active.startsWith(item.path) || undefined}
data-disabled={isDisabled || undefined}
key={item.label}
to={isDisabled ? "#" : item.path}
onClick={(e) => {
if (isDisabled) {
e.preventDefault();
return;
}
if (mobileSidebarOpened) {
toggleMobileSidebar();
}
}}
style={{
opacity: isDisabled ? 0.5 : 1,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</Link>
);
if (isDisabled) {
return (
@@ -261,12 +235,41 @@ export default function SettingsSidebar() {
position="right"
withArrow
>
{linkElement}
<span
className={classes.link}
data-disabled
role="link"
aria-disabled="true"
tabIndex={0}
style={{
opacity: 0.5,
cursor: "not-allowed",
}}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</span>
</Tooltip>
);
}
return linkElement;
return (
<Link
onMouseEnter={prefetchHandler}
className={classes.link}
data-active={active.startsWith(item.path) || undefined}
key={item.label}
to={item.path}
onClick={() => {
if (mobileSidebarOpened) {
toggleMobileSidebar();
}
}}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</Link>
);
})}
</div>
);
@@ -284,7 +287,7 @@ export default function SettingsSidebar() {
}}
variant="transparent"
c="gray"
aria-label="Back"
aria-label={t("Back")}
>
<IconArrowLeft stroke={2} />
</ActionIcon>