a11y: streamer-profile header — avatar wrap + live card keyboard-activatable

Two click-only divs in the new profile header (4.6.17+) had no
keyboard equivalent:

- .streamer-profile-avatar-wrap (clicking the avatar opens the
  channel on twitch.tv) — the only way to trigger that action
  besides the "Open on Twitch" button in the action column, so
  keyboard users were missing a primary affordance
- .streamer-profile-live-card (clicking anywhere on the live
  preview card starts a live recording) — the embedded Record-now
  button inside the card already covered keyboard activation, so
  this one is more about completeness than necessity

Both got:
- role="button" + tabindex="0"
- aria-label = the existing tooltip locale string (so AT reads
  the same text shown to sighted users on hover)
- An inline onkeydown that re-fires the same onclick handler on
  Enter / Space. The live-card additionally checks
  event.target === event.currentTarget so a focused inner button
  pressing Enter doesn't double-fire the wrapper handler

CSS adds focus-visible rings:
- Purple ring on the avatar wrap (matching the existing avatar's
  purple border)
- Red ring + glow on the live card (matching the existing card
  border colour)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-05-11 03:41:21 +02:00
parent 5e369fef35
commit 96113dc267
2 changed files with 13 additions and 2 deletions

View File

@ -121,7 +121,7 @@ function renderStreamerProfileCard(p: StreamerProfile): void {
// current preview frame + viewer count + title + game + record CTA.
const liveCard = p.isLive
? `
<div class="streamer-profile-live-card" onclick="triggerLiveRecordingFromProfile('${safeLogin}')" title="${escapeProfileHtml(UI_TEXT.profile.liveCardTooltip)}">
<div class="streamer-profile-live-card" role="button" tabindex="0" aria-label="${escapeProfileHtml(UI_TEXT.profile.liveCardTooltip)}" onclick="triggerLiveRecordingFromProfile('${safeLogin}')" onkeydown="if((event.key==='Enter'||event.key===' ')&&event.target===event.currentTarget){event.preventDefault();triggerLiveRecordingFromProfile('${safeLogin}');}" title="${escapeProfileHtml(UI_TEXT.profile.liveCardTooltip)}">
${p.currentStreamPreviewUrl
? `<img class="streamer-profile-live-thumb" src="${escapeProfileHtml(p.currentStreamPreviewUrl)}" alt="Live preview" onerror="onProfileLivePreviewError(this)">`
: `<div class="streamer-profile-live-thumb-fallback"></div>`}
@ -140,7 +140,7 @@ function renderStreamerProfileCard(p: StreamerProfile): void {
applyProfileHtml(el, `
${bannerStyle ? `<div class="streamer-profile-banner-bg" style="${bannerStyle}"></div>` : ''}
<div class="streamer-profile-row">
<div class="streamer-profile-avatar-wrap" onclick="openTwitchChannel('${safeUrl}')" title="${escapeProfileHtml(UI_TEXT.profile.openTwitchTooltip)}">
<div class="streamer-profile-avatar-wrap" role="button" tabindex="0" aria-label="${escapeProfileHtml(UI_TEXT.profile.openTwitchTooltip)}" onclick="openTwitchChannel('${safeUrl}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openTwitchChannel('${safeUrl}');}" title="${escapeProfileHtml(UI_TEXT.profile.openTwitchTooltip)}">
${avatarBlock}
</div>
<div class="streamer-profile-body">

View File

@ -3340,6 +3340,17 @@ input[type="number"]::-webkit-outer-spin-button {
transform: scale(1.04);
}
.streamer-profile-avatar-wrap:focus-visible {
outline: none;
border-radius: 50%;
box-shadow: 0 0 0 3px rgba(145, 70, 255, 0.55);
}
.streamer-profile-live-card:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(233, 25, 22, 0.55), 0 6px 22px rgba(233, 25, 22, 0.20);
}
.streamer-profile-avatar {
width: 88px;
height: 88px;