diff --git a/public/_locales/de/messages.json b/public/_locales/de/messages.json
index cc08ce9..26e4f8d 100644
--- a/public/_locales/de/messages.json
+++ b/public/_locales/de/messages.json
@@ -67,5 +67,36 @@
},
"settingsLinks": {
"message": "Links"
+ },
+ "enabledCountLabel": {
+ "message": "$1 von $2 Trackern aktiviert",
+ "placeholders": {
+ "enabled": {
+ "content": "$1",
+ "example": "3"
+ },
+ "total": {
+ "content": "$2",
+ "example": "7"
+ }
+ }
+ },
+ "allTrackersAlreadyEnabled": {
+ "message": "Alle Tracker sind bereits aktiviert"
+ },
+ "allTrackersAlreadyDisabled": {
+ "message": "Alle Tracker sind bereits deaktiviert"
+ },
+ "preferencesSaveError": {
+ "message": "Änderungen konnten nicht gespeichert werden. Versuchen Sie es erneut."
+ },
+ "externalLinkLabel": {
+ "message": "$1 (wird in neuem Tab geöffnet)",
+ "placeholders": {
+ "label": {
+ "content": "$1",
+ "example": "Open Source"
+ }
+ }
}
}
diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json
index 801e8d2..85c1546 100644
--- a/public/_locales/en/messages.json
+++ b/public/_locales/en/messages.json
@@ -67,5 +67,36 @@
},
"settingsLinks": {
"message": "Links"
+ },
+ "enabledCountLabel": {
+ "message": "$1 of $2 trackers enabled",
+ "placeholders": {
+ "enabled": {
+ "content": "$1",
+ "example": "3"
+ },
+ "total": {
+ "content": "$2",
+ "example": "7"
+ }
+ }
+ },
+ "allTrackersAlreadyEnabled": {
+ "message": "All trackers are already enabled"
+ },
+ "allTrackersAlreadyDisabled": {
+ "message": "All trackers are already disabled"
+ },
+ "preferencesSaveError": {
+ "message": "Couldn't save changes. Try again."
+ },
+ "externalLinkLabel": {
+ "message": "$1 (opens in new tab)",
+ "placeholders": {
+ "label": {
+ "content": "$1",
+ "example": "Open source"
+ }
+ }
}
}
diff --git a/public/_locales/es/messages.json b/public/_locales/es/messages.json
index 1ce3230..1d17d45 100644
--- a/public/_locales/es/messages.json
+++ b/public/_locales/es/messages.json
@@ -67,5 +67,36 @@
},
"settingsLinks": {
"message": "Enlaces"
+ },
+ "enabledCountLabel": {
+ "message": "$1 de $2 rastreadores activados",
+ "placeholders": {
+ "enabled": {
+ "content": "$1",
+ "example": "3"
+ },
+ "total": {
+ "content": "$2",
+ "example": "7"
+ }
+ }
+ },
+ "allTrackersAlreadyEnabled": {
+ "message": "Todos los rastreadores ya están activados"
+ },
+ "allTrackersAlreadyDisabled": {
+ "message": "Todos los rastreadores ya están desactivados"
+ },
+ "preferencesSaveError": {
+ "message": "No se pudieron guardar los cambios. Inténtalo de nuevo."
+ },
+ "externalLinkLabel": {
+ "message": "$1 (se abre en una pestaña nueva)",
+ "placeholders": {
+ "label": {
+ "content": "$1",
+ "example": "Código abierto"
+ }
+ }
}
}
diff --git a/public/_locales/fr/messages.json b/public/_locales/fr/messages.json
index a8b0419..6d952d0 100644
--- a/public/_locales/fr/messages.json
+++ b/public/_locales/fr/messages.json
@@ -67,5 +67,36 @@
},
"settingsLinks": {
"message": "Liens"
+ },
+ "enabledCountLabel": {
+ "message": "$1 tracker(s) activé(s) sur $2",
+ "placeholders": {
+ "enabled": {
+ "content": "$1",
+ "example": "3"
+ },
+ "total": {
+ "content": "$2",
+ "example": "7"
+ }
+ }
+ },
+ "allTrackersAlreadyEnabled": {
+ "message": "Tous les trackers sont déjà activés"
+ },
+ "allTrackersAlreadyDisabled": {
+ "message": "Tous les trackers sont déjà désactivés"
+ },
+ "preferencesSaveError": {
+ "message": "Impossible d'enregistrer les modifications. Réessayez."
+ },
+ "externalLinkLabel": {
+ "message": "$1 (s'ouvre dans un nouvel onglet)",
+ "placeholders": {
+ "label": {
+ "content": "$1",
+ "example": "Open source"
+ }
+ }
}
}
diff --git a/public/_locales/pt_BR/messages.json b/public/_locales/pt_BR/messages.json
index 52ae103..ee1ffa9 100644
--- a/public/_locales/pt_BR/messages.json
+++ b/public/_locales/pt_BR/messages.json
@@ -67,5 +67,36 @@
},
"settingsLinks": {
"message": "Links"
+ },
+ "enabledCountLabel": {
+ "message": "$1 de $2 rastreadores ativados",
+ "placeholders": {
+ "enabled": {
+ "content": "$1",
+ "example": "3"
+ },
+ "total": {
+ "content": "$2",
+ "example": "7"
+ }
+ }
+ },
+ "allTrackersAlreadyEnabled": {
+ "message": "Todos os rastreadores já estão ativados"
+ },
+ "allTrackersAlreadyDisabled": {
+ "message": "Todos os rastreadores já estão desativados"
+ },
+ "preferencesSaveError": {
+ "message": "Não foi possível salvar as alterações. Tente novamente."
+ },
+ "externalLinkLabel": {
+ "message": "$1 (abre em uma nova aba)",
+ "placeholders": {
+ "label": {
+ "content": "$1",
+ "example": "Código aberto"
+ }
+ }
}
}
diff --git a/public/_locales/ru/messages.json b/public/_locales/ru/messages.json
index 7a1ef76..851e4dc 100644
--- a/public/_locales/ru/messages.json
+++ b/public/_locales/ru/messages.json
@@ -67,5 +67,36 @@
},
"settingsLinks": {
"message": "Ссылки"
+ },
+ "enabledCountLabel": {
+ "message": "Включено трекеров: $1 из $2",
+ "placeholders": {
+ "enabled": {
+ "content": "$1",
+ "example": "3"
+ },
+ "total": {
+ "content": "$2",
+ "example": "7"
+ }
+ }
+ },
+ "allTrackersAlreadyEnabled": {
+ "message": "Все трекеры уже включены"
+ },
+ "allTrackersAlreadyDisabled": {
+ "message": "Все трекеры уже выключены"
+ },
+ "preferencesSaveError": {
+ "message": "Не удалось сохранить изменения. Повторите попытку."
+ },
+ "externalLinkLabel": {
+ "message": "$1 (откроется в новой вкладке)",
+ "placeholders": {
+ "label": {
+ "content": "$1",
+ "example": "Открытый код"
+ }
+ }
}
}
diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json
index 5b6504c..379ab5c 100644
--- a/public/_locales/zh_CN/messages.json
+++ b/public/_locales/zh_CN/messages.json
@@ -67,5 +67,36 @@
},
"settingsLinks": {
"message": "链接"
+ },
+ "enabledCountLabel": {
+ "message": "已启用 $1 / $2 个追踪器",
+ "placeholders": {
+ "enabled": {
+ "content": "$1",
+ "example": "3"
+ },
+ "total": {
+ "content": "$2",
+ "example": "7"
+ }
+ }
+ },
+ "allTrackersAlreadyEnabled": {
+ "message": "所有追踪器都已启用"
+ },
+ "allTrackersAlreadyDisabled": {
+ "message": "所有追踪器都已禁用"
+ },
+ "preferencesSaveError": {
+ "message": "无法保存更改。请重试。"
+ },
+ "externalLinkLabel": {
+ "message": "$1(在新标签页中打开)",
+ "placeholders": {
+ "label": {
+ "content": "$1",
+ "example": "开源"
+ }
+ }
}
}
diff --git a/src/entrypoints/steam.content/style.css b/src/entrypoints/steam.content/style.css
index be7b56b..a7254d1 100644
--- a/src/entrypoints/steam.content/style.css
+++ b/src/entrypoints/steam.content/style.css
@@ -15,10 +15,25 @@
}
.trackeroo-empty .count_link_label {
+ display: block;
font-size: 12px;
color: #56707f;
}
+.trackeroo-empty-help {
+ display: block;
+ margin-top: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font:
+ 11px / 14px "Motiva Sans",
+ Arial,
+ Helvetica,
+ sans-serif;
+ color: #8f98a0;
+ white-space: nowrap;
+}
+
.trackeroo-trigger:hover .count_link_label,
.trackeroo-root.is-open .count_link_label {
color: #fff;
@@ -75,6 +90,7 @@
cursor: default;
}
+.trackeroo-trigger:focus-visible,
.trackeroo-menu-item:focus-visible {
outline: 1px solid #66c0f4;
outline-offset: -1px;
diff --git a/src/popup/app.tsx b/src/popup/app.tsx
index 8fa31e3..d2a4d3b 100644
--- a/src/popup/app.tsx
+++ b/src/popup/app.tsx
@@ -7,13 +7,41 @@ import type { PopupTab } from "./components/popup-tabs";
import { SettingsTab } from "./components/settings-tab";
import { TrackersTab } from "./components/trackers-tab";
+const skeletonRows = ["first", "second", "third", "fourth"] as const;
+
+function PopupLoadingSkeleton() {
+ return (
+
+
+
+
+ {skeletonRows.map((row) => (
+
+ ))}
+
+
+ );
+}
+
export function PopupApp() {
const { ready, locale, setLocale } = useLocale();
- const { preferences, toggle, setAll } = useTrackerPreferences();
+ const { error, preferences, toggle, setAll } = useTrackerPreferences();
const [activeTab, setActiveTab] = useState("trackers");
if (!ready) {
- return null;
+ return ;
}
const enabledCount = TRACKERS.filter(
@@ -29,6 +57,7 @@ export function PopupApp() {
>
{activeTab === "trackers" ? (
-
{t("extName")}
-
- {enabledCount}/{totalCount}
+
+
+ {t("extName")}
+
+
+ {t("extDescription")}
+
+
+
+ {enabledCountLabel}
+
+ {enabledCount}/{totalCount}
+
-
+
diff --git a/src/popup/components/popup-tabs.tsx b/src/popup/components/popup-tabs.tsx
index 7f778a3..1976140 100644
--- a/src/popup/components/popup-tabs.tsx
+++ b/src/popup/components/popup-tabs.tsx
@@ -1,3 +1,4 @@
+import type { KeyboardEvent } from "react";
import { t } from "@/i18n/runtime";
export type PopupTab = "trackers" | "settings";
@@ -9,26 +10,78 @@ interface PopupTabsProps {
const tabs: PopupTab[] = ["trackers", "settings"];
+export function getPopupTabId(tab: PopupTab): string {
+ return `popup-tab-${tab}`;
+}
+
+export function getPopupTabPanelId(tab: PopupTab): string {
+ return `popup-tab-panel-${tab}`;
+}
+
function tabLabel(tab: PopupTab): string {
return tab === "trackers" ? t("tabTrackers") : t("tabSettings");
}
export function PopupTabs({ active, onChange }: PopupTabsProps) {
+ const selectTab = (tab: PopupTab) => {
+ onChange(tab);
+ requestAnimationFrame(() =>
+ document.getElementById(getPopupTabId(tab))?.focus()
+ );
+ };
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ const activeIndex = tabs.indexOf(active);
+ const selectByIndex = (index: number) => {
+ selectTab(tabs[(index + tabs.length) % tabs.length] ?? tabs[0]);
+ };
+
+ switch (event.key) {
+ case "ArrowLeft":
+ case "ArrowUp":
+ event.preventDefault();
+ selectByIndex(activeIndex - 1);
+ break;
+ case "ArrowRight":
+ case "ArrowDown":
+ event.preventDefault();
+ selectByIndex(activeIndex + 1);
+ break;
+ case "Home":
+ event.preventDefault();
+ selectTab(tabs[0]);
+ break;
+ case "End":
+ event.preventDefault();
+ selectTab(tabs.at(-1) ?? tabs[0]);
+ break;
+ default:
+ break;
+ }
+ };
+
return (
-
+
{tabs.map((tab) => {
const selected = active === tab;
return (
);
diff --git a/src/tracker-menu/elements.test.ts b/src/tracker-menu/elements.test.ts
index f2cf89a..d1913e7 100644
--- a/src/tracker-menu/elements.test.ts
+++ b/src/tracker-menu/elements.test.ts
@@ -60,11 +60,17 @@ describe("createDropdownTrigger", () => {
const trigger = createDropdownTrigger(2);
expect(trigger.getAttribute("role")).toBe("button");
+ expect(trigger.getAttribute("aria-label")).toBe("trackersWithCount:2");
expect(trigger.getAttribute("aria-haspopup")).toBe("true");
expect(trigger.getAttribute("aria-expanded")).toBe("false");
expect(trigger.querySelector(".count_link_label")?.textContent).toBe(
"trackersWithCount:2"
);
+ expect(
+ trigger
+ .querySelector(".profile_count_link_total")
+ ?.getAttribute("aria-hidden")
+ ).toBe("true");
});
it("omits the count when no trackers are enabled", () => {
@@ -98,5 +104,8 @@ describe("createEmptyState", () => {
expect(empty.querySelector(".count_link_label")?.textContent).toBe(
"noTrackersEnabled"
);
+ expect(empty.querySelector(".trackeroo-empty-help")?.textContent).toBe(
+ "emptyStateTitle"
+ );
});
});
diff --git a/src/tracker-menu/elements.ts b/src/tracker-menu/elements.ts
index d952f64..bb7a7bc 100644
--- a/src/tracker-menu/elements.ts
+++ b/src/tracker-menu/elements.ts
@@ -46,6 +46,9 @@ export function createDropdownMenu(
export function createDropdownTrigger(count: number): HTMLDivElement {
const trigger = document.createElement("div");
trigger.className = "profile_count_link ellipsis trackeroo-trigger";
+ const label =
+ count > 0 ? t("trackersWithCount", String(count)) : t("trackers");
+ trigger.setAttribute("aria-label", label);
trigger.setAttribute("role", "button");
trigger.setAttribute("tabindex", "0");
trigger.setAttribute("aria-expanded", "false");
@@ -56,10 +59,9 @@ export function createDropdownTrigger(count: number): HTMLDivElement {
link.className = "trackeroo-trigger-link";
link.tabIndex = -1;
- const label =
- count > 0 ? t("trackersWithCount", String(count)) : t("trackers");
const caret = document.createElement("span");
caret.className = "profile_count_link_total";
+ caret.setAttribute("aria-hidden", "true");
caret.textContent = "▾";
link.append(createCountLabel(label), "\u00a0", caret);
@@ -87,6 +89,9 @@ export function createEmptyState(): HTMLDivElement {
const el = document.createElement("div");
el.className = "profile_count_link ellipsis trackeroo-empty";
el.title = t("emptyStateTitle");
- el.append(createCountLabel(t("noTrackersEnabled")));
+ const help = document.createElement("span");
+ help.className = "trackeroo-empty-help";
+ help.textContent = t("emptyStateTitle");
+ el.append(createCountLabel(t("noTrackersEnabled")), help);
return el;
}
diff --git a/src/tracker-menu/mount.test.ts b/src/tracker-menu/mount.test.ts
index da73ca7..cab3179 100644
--- a/src/tracker-menu/mount.test.ts
+++ b/src/tracker-menu/mount.test.ts
@@ -33,10 +33,13 @@ const ctx = {
},
} as unknown as ContentScriptContext;
-function mount(enabledTrackers: Tracker[]): HTMLElement {
+function mount(
+ enabledTrackers: Tracker[],
+ pageUrl: string = PROFILE_URL
+): HTMLElement {
const container = document.createElement("div");
document.body.append(container);
- mountTrackerUi(ctx, container, enabledTrackers, PROFILE_URL);
+ mountTrackerUi(ctx, container, enabledTrackers, pageUrl);
return container;
}
@@ -62,6 +65,13 @@ describe("mountTrackerUi", () => {
expect(link?.href).toBe("https://xsteamcommunity.com/id/gaben");
});
+ it("shows the empty state when a single enabled tracker cannot link", () => {
+ const container = mount([trackers[0]], "https://example.com");
+
+ expect(container.className).toContain("trackeroo-root--empty");
+ expect(container.querySelector(".trackeroo-empty")).not.toBeNull();
+ });
+
it("renders a dropdown for multiple enabled trackers", () => {
const container = mount(trackers);
diff --git a/src/tracker-menu/mount.ts b/src/tracker-menu/mount.ts
index ba09f71..42e59d6 100644
--- a/src/tracker-menu/mount.ts
+++ b/src/tracker-menu/mount.ts
@@ -20,6 +20,8 @@ export function mountTrackerUi(
if (enabledTrackers.length === 1) {
const link = createDirectLink(enabledTrackers[0], pageUrl);
if (!link) {
+ container.className = "trackeroo-root trackeroo-root--empty";
+ container.append(createEmptyState());
return;
}
diff --git a/src/trackers/use-tracker-preferences.ts b/src/trackers/use-tracker-preferences.ts
index f49c33e..595e726 100644
--- a/src/trackers/use-tracker-preferences.ts
+++ b/src/trackers/use-tracker-preferences.ts
@@ -11,6 +11,7 @@ export function useTrackerPreferences() {
const [preferences, setPreferences] = useState
(
getDefaultPreferences
);
+ const [error, setError] = useState(false);
useEffect(() => {
getTrackerPreferences().then(setPreferences);
@@ -18,14 +19,17 @@ export function useTrackerPreferences() {
const toggle = (id: TrackerId) => {
const enabled = !preferences[id];
+ setError(false);
setPreferences((current) => ({ ...current, [id]: enabled }));
setTrackerPreference(id, enabled).catch(() => {
setPreferences((current) => ({ ...current, [id]: !enabled }));
+ setError(true);
});
};
const setAll = (enabled: boolean) => {
const previous = preferences;
+ setError(false);
setPreferences(
Object.fromEntries(
Object.keys(preferences).map((id) => [id, enabled])
@@ -33,8 +37,9 @@ export function useTrackerPreferences() {
);
setAllTrackerPreferences(enabled).catch(() => {
setPreferences(previous);
+ setError(true);
});
};
- return { preferences, toggle, setAll };
+ return { error, preferences, toggle, setAll };
}