From 0936d7f9ff2630e9713bd4f41f5c65a7611aece0 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 17 Jul 2025 12:13:33 +0200 Subject: [PATCH] Use anchors on home view for better accesibility Convert clickable elements for filtering repos to anchors so that they can be focused and activated with the keyboard. --- public/index.css | 21 +++++++++++ src/components/HomeSidebar.svelte | 58 +++++++++++++------------------ src/lib/router.ts | 40 ++++++++++++++++----- src/views/home/router.ts | 28 +++++++++++++++ 4 files changed, 106 insertions(+), 41 deletions(-) diff --git a/public/index.css b/public/index.css index f34d144d8..56b4b9f14 100644 --- a/public/index.css +++ b/public/index.css @@ -22,6 +22,27 @@ body { background-color: var(--color-background-default); } +a, +a:link, +a:visited, +a:hover, +a:active, +a:focus { + color: inherit; + text-decoration: none; + background: transparent; + cursor: pointer; +} + +a { + display: inline; + font-weight: inherit; + font-style: inherit; + font-size: inherit; + line-height: inherit; + font-family: inherit; +} + ::selection { background: var(--color-fill-yellow); color: var(--color-foreground-black); diff --git a/src/components/HomeSidebar.svelte b/src/components/HomeSidebar.svelte index a4bdd2275..f20f8b11d 100644 --- a/src/components/HomeSidebar.svelte +++ b/src/components/HomeSidebar.svelte @@ -27,7 +27,6 @@ align-items: center; gap: 0.5rem; background-color: var(--color-background-float); - clip-path: var(--1px-corner-fill); display: flex; font-size: var(--font-size-small); justify-content: space-between; @@ -54,65 +53,58 @@ styleFlexDirection="column" styleGap="2px" styleBackgroundColor="var(--color-background-float)"> - - -
router.push({ resource: "home", activeTab: "all" })}> + href={router.routeToPath({ + resource: "home", + activeTab: "all", + })}>
Repositories
{repoCount.total}
-
- - -
+ - router.push({ - resource: "home", - activeTab: "delegate", - })}> + href={router.routeToPath({ + resource: "home", + activeTab: "delegate", + })}>
Delegate
{repoCount.delegate}
-
- - -
+ - router.push({ - resource: "home", - activeTab: "contributor", - })}> + href={router.routeToPath({ + resource: "home", + activeTab: "contributor", + })}>
Contributor
{repoCount.contributor}
-
- - -
+ - router.push({ - resource: "home", - activeTab: "private", - })}> + href={router.routeToPath({ + resource: "home", + activeTab: "private", + })}>
Private
{repoCount.private}
-
+ diff --git a/src/lib/router.ts b/src/lib/router.ts index 8b350f328..c5c9bf2df 100644 --- a/src/lib/router.ts +++ b/src/lib/router.ts @@ -1,4 +1,9 @@ +import { + homeRouteToPath as homeRouteToPath, + homeUrlToRoute, +} from "@app/views/home/router"; import { repoRouteToPath, repoUrlToRoute } from "@app/views/repo/router"; +import { on } from "svelte/events"; import { get, writable } from "svelte/store"; import * as mutexExecutor from "@app/lib/mutexExecutor"; @@ -50,7 +55,27 @@ async function navigateToUrl( } } -window.addEventListener("popstate", () => loadFromLocation()); +const offPopstate = on(window, "popstate", () => loadFromLocation()); + +const offNavigateAnchor = on(document, "click", e => { + const [anchor] = e + .composedPath() + .flatMap(target => (target instanceof HTMLAnchorElement ? [target] : [])); + if (anchor && anchor.getAttribute("href")?.startsWith("/")) { + e.preventDefault(); + void navigateToUrl( + "push", + new URL(anchor.getAttribute("href") ?? "", window.location.href), + ); + } +}); + +if (import.meta.hot) { + import.meta.hot.dispose(() => { + offPopstate(); + offNavigateAnchor(); + }); +} const loadExecutor = mutexExecutor.create(); @@ -100,13 +125,12 @@ function urlToRoute(url: URL): Route | null { const segments = url.pathname.substring(1).split("/"); const resource = segments.shift(); + const homeRoute = homeUrlToRoute(url); + if (homeRoute) { + return homeRoute; + } + switch (resource) { - case "": { - return { - resource: "home", - activeTab: "all", - }; - } case "repos": { return repoUrlToRoute(segments, url.searchParams); } @@ -118,7 +142,7 @@ function urlToRoute(url: URL): Route | null { export function routeToPath(route: Route): string { if (route.resource === "home") { - return "/"; + return homeRouteToPath(route); } else if ( route.resource === "repo.home" || route.resource === "repo.createIssue" || diff --git a/src/views/home/router.ts b/src/views/home/router.ts index aaf426fd6..43bb1a137 100644 --- a/src/views/home/router.ts +++ b/src/views/home/router.ts @@ -2,10 +2,19 @@ import type { Config } from "@bindings/config/Config"; import type { RepoCount } from "@bindings/repo/RepoCount"; import type { RepoInfo } from "@bindings/repo/RepoInfo"; +import z from "zod"; + import { invoke } from "@app/lib/invoke"; export type HomeReposTab = "all" | "delegate" | "private" | "contributor"; +const homeReposTabSchema = z.union([ + z.literal("all"), + z.literal("delegate"), + z.literal("private"), + z.literal("contributor"), +]); + export interface HomeRoute { resource: "home"; activeTab: HomeReposTab; @@ -56,3 +65,22 @@ export async function loadHome(route: HomeRoute): Promise { }, }; } + +export function homeUrlToRoute(url: URL): HomeRoute | undefined { + if (url.pathname === "/") { + return { + resource: "home", + activeTab: homeReposTabSchema + .catch(() => "all" as const) + .parse(url.searchParams.get("tab")), + }; + } +} + +export function homeRouteToPath(route: HomeRoute): string { + const searchParams = new URLSearchParams(); + if (route.activeTab !== "all") { + searchParams.append("tab", route.activeTab); + } + return `/?${searchParams.toString()}`; +}