Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/accessibility improvements #1121

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ab7625a
feat: add aria-label strings to locales for achievements page
cj-burning Oct 24, 2024
3b02a3c
chore: move aria-labels strings to their respective pages in locales
cj-burning Oct 24, 2024
c5d4db0
chore: change locale param names to match with args
cj-burning Oct 24, 2024
e9f6897
feat: add aria label to achievement summary
cj-burning Oct 24, 2024
012f872
chore: chore: remove unnecessary locale string and use locked status …
cj-burning Oct 24, 2024
5030325
feat: add semantic roles and aria labels to achievement list
cj-burning Oct 24, 2024
3ff15d2
chore: remove redundancy in list semantic markup
cj-burning Oct 24, 2024
79763b6
chore: remove redundancy in "View Profile" and "Game" button for scre…
cj-burning Oct 24, 2024
b754b1e
feat: add tooltip and screen reader support to back button in header
cj-burning Oct 24, 2024
a54983c
feat: add tabindex 0 to search bar icon for direct focus
cj-burning Oct 24, 2024
ba7e4c9
feat: add tooltip and screen reader support to clear search button
cj-burning Oct 24, 2024
e0ec79b
feat: add keyboard control for sidebar size adjustment
cj-burning Oct 24, 2024
455016c
lint: sidebar.tsx
cj-burning Oct 24, 2024
2a6346c
feat: add tooltip and screen reader support to sidebar resize button
cj-burning Oct 24, 2024
3adc866
feat: add keyboard shortcut to focus header search bar
cj-burning Oct 24, 2024
bf6ce2b
feat: add ability to blur search bar with Escape key
cj-burning Oct 24, 2024
c754710
feat: enable tab navigation through carousel previews, replacing arro…
cj-burning Oct 24, 2024
a25a960
feat: add aria labels to How Long to Beat and Achievements in game de…
cj-burning Oct 24, 2024
8f49196
lint: how-long-to-beat-section.tsx
cj-burning Oct 24, 2024
69d96cc
Merge branch 'main' into feature/accessibility-improvements
cj-burning Oct 29, 2024
a432306
Merge branch 'main' into feature/accessibility-improvements
cj-burning Oct 30, 2024
ba9232b
Merge branch 'main' into feature/accessibility-improvements
zamitto Nov 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"queued": "{{title}} (Queued)",
"game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in",
"friends": "Friends"
"friends": "Friends",
"aria_view_profile": "View profile",
"resize_sidebar": "Resize sidebar"
},
"header": {
"search": "Search games",
Expand All @@ -35,7 +37,9 @@
"search_results": "Search results",
"settings": "Settings",
"version_available_install": "Version {{version}} available. Click here to restart and install.",
"version_available_download": "Version {{version}} available. Click here to download."
"version_available_download": "Version {{version}} available. Click here to download.",
"back": "Back",
"clear_search": "Clear search"
},
"bottom_panel": {
"no_downloads_in_progress": "No downloads in progress",
Expand Down Expand Up @@ -132,6 +136,7 @@
"warning": "Warning:",
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util it's completed. If Hydra closes before completing, you will lose your progress.",
"achievements": "Achievements",
"achievement": "Achievement",
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Cloud save",
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
Expand Down Expand Up @@ -358,11 +363,13 @@
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",
"achievement_locked": "Achievement locked",
"user_achievements": "{{displayName}}'s Achievements",
"your_achievements": "Your Achievements",
"unlocked_at": "Unlocked at:",
"subscription_needed": "A Hydra Cloud subscription is required to see this content",
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games"
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games",
"aria_achievement_summary": "{{userDisplayName}} achievements for {{gameTitle}}, {{userAchievementCount}} unlocked of {{userTotalAchievementCount}}, {{percentage}} completed"
},
"tour": {
"subscription_tour_title": "Hydra Cloud Subscription",
Expand Down
13 changes: 10 additions & 3 deletions src/locales/pt-BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login",
"friends": "Amigos"
"friends": "Amigos",
"aria_view_profile": "Ver perfil",
"resize_sidebar": "Redimensionar barra lateral"
},
"header": {
"search": "Buscar jogos",
Expand All @@ -35,7 +37,9 @@
"settings": "Ajustes",
"home": "Início",
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download.",
"back": "Voltar",
"clear_search": "Limpar busca"
},
"bottom_panel": {
"no_downloads_in_progress": "Sem downloads em andamento",
Expand Down Expand Up @@ -128,6 +132,7 @@
"warning": "Aviso:",
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
"achievements": "Conquistas",
"achievement": "Conquista",
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"cloud_save": "Salvamento em nuvem",
"cloud_save_description": "Mantenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
Expand Down Expand Up @@ -356,11 +361,13 @@
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
"achievement_locked": "Conquista bloqueada",
"your_achievements": "Suas Conquistas",
"user_achievements": "Conquistas de {{displayName}}",
"unlocked_at": "Desbloqueado em:",
"subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo",
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos"
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
"aria_achievement_summary": "Conquistas de {{userDisplayName}} em {{gameTitle}}, {{userAchievementCount}} desbloqueadas de {{userTotalAchievementCount}}, {{percentage}} concluídas"
},
"tour": {
"subscription_tour_title": "Assinatura Hydra Cloud",
Expand Down
22 changes: 22 additions & 0 deletions src/renderer/src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
navigate(-1);
};

useEffect(() => {
window.onkeydown = (event: KeyboardEvent) => {
const { key, ctrlKey } = event;
if (!isFocused && ctrlKey && key === "k") {
focusInput();
}

if (isFocused && key === "Escape" && inputRef.current) {
inputRef.current.blur();
handleBlur();
}
};

return () => {
window.onkeydown = null;
};
}, [isFocused]);

return (
<>
<header
Expand All @@ -81,6 +99,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
})}
onClick={handleBackButtonClick}
disabled={location.key === "default"}
title={t("back")}
>
<ArrowLeftIcon />
</button>
Expand All @@ -100,6 +119,8 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
type="button"
className={styles.actionButton}
onClick={focusInput}
tabIndex={-1}
title={t("search")}
>
<SearchIcon />
</button>
Expand All @@ -121,6 +142,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
type="button"
onClick={onClear}
className={styles.actionButton}
title={t("clear_search")}
>
<XIcon />
</button>
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/components/sidebar/sidebar-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function SidebarProfile() {
type="button"
className={styles.profileButton}
onClick={handleProfileClick}
aria-label={t("aria_view_profile")}
>
<div className={styles.profileButtonContent}>
<Avatar
Expand Down
23 changes: 23 additions & 0 deletions src/renderer/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ export function Sidebar() {
sidebarRef.current?.clientWidth || SIDEBAR_INITIAL_WIDTH;
};

const handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (
event
) => {
const { key } = event;

if (key === "ArrowRight") {
setSidebarWidth((prevWidth) =>
prevWidth < SIDEBAR_INITIAL_WIDTH
? SIDEBAR_INITIAL_WIDTH
: SIDEBAR_MAX_WIDTH
);
} else if (key === "ArrowLeft") {
setSidebarWidth((prevWidth) =>
prevWidth > SIDEBAR_INITIAL_WIDTH
? SIDEBAR_INITIAL_WIDTH
: SIDEBAR_MIN_WIDTH
);
}
};

const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
setFilteredLibrary(
sortedLibrary.filter((game) =>
Expand Down Expand Up @@ -219,6 +239,7 @@ export function Sidebar() {
type="button"
className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)}
aria-label={game.title}
>
{game.iconUrl ? (
<img
Expand All @@ -245,6 +266,8 @@ export function Sidebar() {
type="button"
className={styles.handle}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
title={t("resize_sidebar")}
/>
</aside>
);
Expand Down
37 changes: 35 additions & 2 deletions src/renderer/src/pages/achievements/achievements-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { average } from "color.js";
import Color from "color";
import { Link } from "@renderer/components";
import { ComparedAchievementList } from "./compared-achievement-list";
import { TFunction } from "i18next/typescript/t";

interface UserInfo {
id: string;
Expand All @@ -39,10 +40,35 @@ interface AchievementSummaryProps {
isComparison?: boolean;
}

const ariaLabelSummary = (
t: TFunction,
gameTitle: string,
user: UserInfo
): string => {
return t("aria_achievement_summary", {
userDisplayName: user.displayName,
gameTitle: gameTitle,
userAchievementCount: user.unlockedAchievementCount,
userTotalAchievementCount: user.totalAchievementCount,
percentage: formatDownloadProgress(
user.unlockedAchievementCount / user.totalAchievementCount
),
});
};

const ariaLabelAchievement = (
t: TFunction,
achievement: UserAchievement
): string => {
return `${
achievement.unlocked ? t("achievement_unlocked") : t("achievement_locked")
}, ${achievement.displayName}, ${achievement.description}`;
};

function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
const { t } = useTranslation("achievement");
const { userDetails, hasActiveSubscription } = useUserDetails();
const { handleClickOpenCheckout } = useContext(gameDetailsContext);
const { handleClickOpenCheckout, gameTitle } = useContext(gameDetailsContext);

const getProfileImage = (
user: Pick<UserInfo, "profileImageUrl" | "displayName">
Expand Down Expand Up @@ -124,6 +150,8 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
alignItems: "center",
padding: `${SPACING_UNIT}px`,
}}
role="region"
aria-label={ariaLabelSummary(t, gameTitle, user)}
>
{getProfileImage(user)}
<div
Expand Down Expand Up @@ -178,7 +206,12 @@ function AchievementList({ achievements }: AchievementListProps) {
return (
<ul className={styles.list}>
{achievements.map((achievement, index) => (
<li key={index} className={styles.listItem} style={{ display: "flex" }}>
<li
key={index}
className={styles.listItem}
style={{ display: "flex" }}
aria-label={ariaLabelAchievement(t, achievement)}
>
<img
className={styles.listItemImage({
unlocked: achievement.unlocked,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function GallerySlider() {
direction: "left",
})}
aria-label={t("previous_screenshot")}
tabIndex={0}
tabIndex={-1}
>
<ChevronLeftIcon size={36} />
</button>
Expand All @@ -153,7 +153,7 @@ export function GallerySlider() {
direction: "right",
})}
aria-label={t("next_screenshot")}
tabIndex={0}
tabIndex={-1}
>
<ChevronRightIcon size={36} />
</button>
Expand All @@ -169,6 +169,7 @@ export function GallerySlider() {
})}
onClick={() => setMediaIndex(i)}
aria-label={t("open_screenshot", { number: i + 1 })}
onFocus={() => setMediaIndex(i)}
>
<img
src={media.thumbnail}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export function HowLongToBeatSection({
<li
key={category.title}
className={styles.howLongToBeatCategory}
aria-label={`${category.title}, ${getDuration(
category.duration
)}`}
>
<p
className={styles.howLongToBeatCategoryLabel}
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/src/pages/game-details/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ export function Sidebar() {
})}
className={styles.listItem}
title={achievement.description}
aria-label={`
${t("achievement")} ${index + 1},
${achievement.displayName}
`}
>
<img
className={styles.listItemImage({
Expand Down
Loading