Compare commits

...

22 Commits

Author SHA1 Message Date
Zamitto
ba9232b821 Merge branch 'main' into feature/accessibility-improvements 2024-11-01 09:19:02 -03:00
cj do gta sander pegando fogo aaa aaa aaa
a432306b1d Merge branch 'main' into feature/accessibility-improvements 2024-10-30 01:16:04 -03:00
cj do gta sander pegando fogo aaa aaa aaa
69d96cc290 Merge branch 'main' into feature/accessibility-improvements 2024-10-29 18:27:49 -03:00
cj-do-gta-sander
8f4919615f lint: how-long-to-beat-section.tsx 2024-10-24 14:53:36 -03:00
cj-do-gta-sander
a25a960235 feat: add aria labels to How Long to Beat and Achievements in game details sidebar 2024-10-24 14:52:54 -03:00
cj-do-gta-sander
c754710171 feat: enable tab navigation through carousel previews, replacing arrow buttons 2024-10-24 14:27:54 -03:00
cj-do-gta-sander
bf6ce2b465 feat: add ability to blur search bar with Escape key 2024-10-24 12:24:20 -03:00
cj-do-gta-sander
3adc8662dc feat: add keyboard shortcut to focus header search bar 2024-10-24 12:11:49 -03:00
cj-do-gta-sander
2a6346cb69 feat: add tooltip and screen reader support to sidebar resize button 2024-10-24 11:50:01 -03:00
cj-do-gta-sander
455016c1a7 lint: sidebar.tsx 2024-10-24 11:46:37 -03:00
cj-do-gta-sander
e0ec79b105 feat: add keyboard control for sidebar size adjustment 2024-10-24 11:45:41 -03:00
cj-do-gta-sander
ba7e4c979d feat: add tooltip and screen reader support to clear search button 2024-10-24 11:06:03 -03:00
cj-do-gta-sander
a54983c339 feat: add tabindex 0 to search bar icon for direct focus 2024-10-24 11:02:48 -03:00
cj-do-gta-sander
b754b1e052 feat: add tooltip and screen reader support to back button in header 2024-10-24 10:58:00 -03:00
cj-do-gta-sander
79763b6072 chore: remove redundancy in "View Profile" and "Game" button for screen readers 2024-10-24 10:49:01 -03:00
cj-do-gta-sander
3ff15d2d61 chore: remove redundancy in list semantic markup 2024-10-24 10:04:34 -03:00
cj-do-gta-sander
50303251a2 feat: add semantic roles and aria labels to achievement list 2024-10-24 09:57:11 -03:00
cj-do-gta-sander
012f872f60 chore: chore: remove unnecessary locale string and use locked status strings 2024-10-24 09:55:22 -03:00
cj-do-gta-sander
e9f68977fe feat: add aria label to achievement summary 2024-10-24 09:39:15 -03:00
cj-do-gta-sander
c5d4db0a1e chore: change locale param names to match with args 2024-10-24 09:37:51 -03:00
cj-do-gta-sander
3b02a3c43f chore: move aria-labels strings to their respective pages in locales 2024-10-24 09:27:39 -03:00
cj-do-gta-sander
ab7625a314 feat: add aria-label strings to locales for achievements page 2024-10-24 09:03:48 -03:00
9 changed files with 111 additions and 10 deletions

View File

@@ -25,7 +25,9 @@
"queued": "{{title}} (Queued)", "queued": "{{title}} (Queued)",
"game_has_no_executable": "Game has no executable selected", "game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in", "sign_in": "Sign in",
"friends": "Friends" "friends": "Friends",
"aria_view_profile": "View profile",
"resize_sidebar": "Resize sidebar"
}, },
"header": { "header": {
"search": "Search games", "search": "Search games",
@@ -35,7 +37,9 @@
"search_results": "Search results", "search_results": "Search results",
"settings": "Settings", "settings": "Settings",
"version_available_install": "Version {{version}} available. Click here to restart and install.", "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": { "bottom_panel": {
"no_downloads_in_progress": "No downloads in progress", "no_downloads_in_progress": "No downloads in progress",
@@ -132,6 +136,7 @@
"warning": "Warning:", "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.", "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", "achievements": "Achievements",
"achievement": "Achievement",
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}", "achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Cloud save", "cloud_save": "Cloud save",
"cloud_save_description": "Save your progress in the cloud and continue playing on any device", "cloud_save_description": "Save your progress in the cloud and continue playing on any device",
@@ -358,11 +363,13 @@
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Achievement unlocked", "achievement_unlocked": "Achievement unlocked",
"achievement_locked": "Achievement locked",
"user_achievements": "{{displayName}}'s Achievements", "user_achievements": "{{displayName}}'s Achievements",
"your_achievements": "Your Achievements", "your_achievements": "Your Achievements",
"unlocked_at": "Unlocked at:", "unlocked_at": "Unlocked at:",
"subscription_needed": "A Hydra Cloud subscription is required to see this content", "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": { "tour": {
"subscription_tour_title": "Hydra Cloud Subscription", "subscription_tour_title": "Hydra Cloud Subscription",

View File

@@ -25,7 +25,9 @@
"queued": "{{title}} (Na fila)", "queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado", "game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login", "sign_in": "Login",
"friends": "Amigos" "friends": "Amigos",
"aria_view_profile": "Ver perfil",
"resize_sidebar": "Redimensionar barra lateral"
}, },
"header": { "header": {
"search": "Buscar jogos", "search": "Buscar jogos",
@@ -35,7 +37,9 @@
"settings": "Ajustes", "settings": "Ajustes",
"home": "Início", "home": "Início",
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.", "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": { "bottom_panel": {
"no_downloads_in_progress": "Sem downloads em andamento", "no_downloads_in_progress": "Sem downloads em andamento",
@@ -128,6 +132,7 @@
"warning": "Aviso:", "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.", "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", "achievements": "Conquistas",
"achievement": "Conquista",
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})", "achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"cloud_save": "Salvamento em nuvem", "cloud_save": "Salvamento em nuvem",
"cloud_save_description": "Mantenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo", "cloud_save_description": "Mantenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
@@ -356,11 +361,13 @@
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Conquista desbloqueada", "achievement_unlocked": "Conquista desbloqueada",
"achievement_locked": "Conquista bloqueada",
"your_achievements": "Suas Conquistas", "your_achievements": "Suas Conquistas",
"user_achievements": "Conquistas de {{displayName}}", "user_achievements": "Conquistas de {{displayName}}",
"unlocked_at": "Desbloqueado em:", "unlocked_at": "Desbloqueado em:",
"subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo", "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": { "tour": {
"subscription_tour_title": "Assinatura Hydra Cloud", "subscription_tour_title": "Assinatura Hydra Cloud",

View File

@@ -65,6 +65,24 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
navigate(-1); 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 ( return (
<> <>
<header <header
@@ -81,6 +99,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
})} })}
onClick={handleBackButtonClick} onClick={handleBackButtonClick}
disabled={location.key === "default"} disabled={location.key === "default"}
title={t("back")}
> >
<ArrowLeftIcon /> <ArrowLeftIcon />
</button> </button>
@@ -100,6 +119,8 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
type="button" type="button"
className={styles.actionButton} className={styles.actionButton}
onClick={focusInput} onClick={focusInput}
tabIndex={-1}
title={t("search")}
> >
<SearchIcon /> <SearchIcon />
</button> </button>
@@ -121,6 +142,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
type="button" type="button"
onClick={onClear} onClick={onClear}
className={styles.actionButton} className={styles.actionButton}
title={t("clear_search")}
> >
<XIcon /> <XIcon />
</button> </button>

View File

@@ -89,6 +89,7 @@ export function SidebarProfile() {
type="button" type="button"
className={styles.profileButton} className={styles.profileButton}
onClick={handleProfileClick} onClick={handleProfileClick}
aria-label={t("aria_view_profile")}
> >
<div className={styles.profileButtonContent}> <div className={styles.profileButtonContent}>
<Avatar <Avatar

View File

@@ -68,6 +68,26 @@ export function Sidebar() {
sidebarRef.current?.clientWidth || SIDEBAR_INITIAL_WIDTH; 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) => { const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
setFilteredLibrary( setFilteredLibrary(
sortedLibrary.filter((game) => sortedLibrary.filter((game) =>
@@ -219,6 +239,7 @@ export function Sidebar() {
type="button" type="button"
className={styles.menuItemButton} className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)} onClick={(event) => handleSidebarGameClick(event, game)}
aria-label={game.title}
> >
{game.iconUrl ? ( {game.iconUrl ? (
<img <img
@@ -245,6 +266,8 @@ export function Sidebar() {
type="button" type="button"
className={styles.handle} className={styles.handle}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
title={t("resize_sidebar")}
/> />
</aside> </aside>
); );

View File

@@ -16,6 +16,7 @@ import { average } from "color.js";
import Color from "color"; import Color from "color";
import { Link } from "@renderer/components"; import { Link } from "@renderer/components";
import { ComparedAchievementList } from "./compared-achievement-list"; import { ComparedAchievementList } from "./compared-achievement-list";
import { TFunction } from "i18next/typescript/t";
interface UserInfo { interface UserInfo {
id: string; id: string;
@@ -39,10 +40,35 @@ interface AchievementSummaryProps {
isComparison?: boolean; 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) { function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
const { t } = useTranslation("achievement"); const { t } = useTranslation("achievement");
const { userDetails, hasActiveSubscription } = useUserDetails(); const { userDetails, hasActiveSubscription } = useUserDetails();
const { handleClickOpenCheckout } = useContext(gameDetailsContext); const { handleClickOpenCheckout, gameTitle } = useContext(gameDetailsContext);
const getProfileImage = ( const getProfileImage = (
user: Pick<UserInfo, "profileImageUrl" | "displayName"> user: Pick<UserInfo, "profileImageUrl" | "displayName">
@@ -124,6 +150,8 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
alignItems: "center", alignItems: "center",
padding: `${SPACING_UNIT}px`, padding: `${SPACING_UNIT}px`,
}} }}
role="region"
aria-label={ariaLabelSummary(t, gameTitle, user)}
> >
{getProfileImage(user)} {getProfileImage(user)}
<div <div
@@ -178,7 +206,12 @@ function AchievementList({ achievements }: AchievementListProps) {
return ( return (
<ul className={styles.list}> <ul className={styles.list}>
{achievements.map((achievement, index) => ( {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 <img
className={styles.listItemImage({ className={styles.listItemImage({
unlocked: achievement.unlocked, unlocked: achievement.unlocked,

View File

@@ -140,7 +140,7 @@ export function GallerySlider() {
direction: "left", direction: "left",
})} })}
aria-label={t("previous_screenshot")} aria-label={t("previous_screenshot")}
tabIndex={0} tabIndex={-1}
> >
<ChevronLeftIcon size={36} /> <ChevronLeftIcon size={36} />
</button> </button>
@@ -153,7 +153,7 @@ export function GallerySlider() {
direction: "right", direction: "right",
})} })}
aria-label={t("next_screenshot")} aria-label={t("next_screenshot")}
tabIndex={0} tabIndex={-1}
> >
<ChevronRightIcon size={36} /> <ChevronRightIcon size={36} />
</button> </button>
@@ -169,6 +169,7 @@ export function GallerySlider() {
})} })}
onClick={() => setMediaIndex(i)} onClick={() => setMediaIndex(i)}
aria-label={t("open_screenshot", { number: i + 1 })} aria-label={t("open_screenshot", { number: i + 1 })}
onFocus={() => setMediaIndex(i)}
> >
<img <img
src={media.thumbnail} src={media.thumbnail}

View File

@@ -38,6 +38,9 @@ export function HowLongToBeatSection({
<li <li
key={category.title} key={category.title}
className={styles.howLongToBeatCategory} className={styles.howLongToBeatCategory}
aria-label={`${category.title}, ${getDuration(
category.duration
)}`}
> >
<p <p
className={styles.howLongToBeatCategoryLabel} className={styles.howLongToBeatCategoryLabel}

View File

@@ -194,6 +194,10 @@ export function Sidebar() {
})} })}
className={styles.listItem} className={styles.listItem}
title={achievement.description} title={achievement.description}
aria-label={`
${t("achievement")} ${index + 1},
${achievement.displayName}
`}
> >
<img <img
className={styles.listItemImage({ className={styles.listItemImage({