Add accessibility improvements for screen readers and keyboard navigation
This commit addresses critical accessibility issues that prevent blind users and keyboard-only users from using Galene effectively. Changes: - Convert navigation buttons from divs to proper button elements with ARIA labels * Mute button now has aria-pressed state and updates label (Mute/Unmute) * Share screen button has proper aria-label * More options button has aria-label and aria-haspopup * Collapse sidebar button converted to proper button element * Close chat button converted to proper button element - Add visible focus indicators for keyboard navigation * Restore focus outlines for all interactive elements * Add consistent 2px blue outline for buttons, inputs, selects, and links * Remove outline: none that was blocking keyboard users - Add aria-live region for chat messages * New messages announced to screen readers in real-time * History messages not announced to avoid spam on page load * Includes .sr-only CSS class for screen-reader-only content - Add aria-labels to video stream elements * Video containers labeled with username * Video elements labeled to identify stream owner * Helps screen reader users identify which video belongs to whom - Add raised hand notifications for screen readers * Announces when users raise their hand * User list items show status (hand raised, camera on, microphone on) * Does not announce when you raise your own hand * Updates aria-label on user elements with full status These changes make Galene significantly more accessible for users who rely on screen readers or keyboard navigation.
This commit is contained in:
+28
-3
@@ -1,3 +1,27 @@
|
||||
/* Accessibility: Visible focus indicators for keyboard navigation */
|
||||
button:focus,
|
||||
[role="button"]:focus,
|
||||
select:focus,
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
a:focus {
|
||||
outline: 2px solid #0066cc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Accessibility: Screen reader only content */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.nav-fixed .topnav {
|
||||
z-index: 1039;
|
||||
}
|
||||
@@ -312,7 +336,6 @@
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
padding: 5px;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-indent: 5px;
|
||||
box-shadow: none;
|
||||
@@ -865,11 +888,13 @@ h1 {
|
||||
}
|
||||
|
||||
#input:focus {
|
||||
outline: none;
|
||||
outline: 2px solid #0066cc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
#inputbutton:focus {
|
||||
outline: none;
|
||||
outline: 2px solid #0066cc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
#resizer {
|
||||
|
||||
+13
-12
@@ -29,9 +29,9 @@
|
||||
<header>
|
||||
<nav class="topnav navbar navbar-expand navbar-light fixed-top">
|
||||
<div id="header">
|
||||
<div class="collapse" title="Collapse left panel" id="sidebarCollapse">
|
||||
<button class="collapse" aria-label="Collapse left panel" id="sidebarCollapse">
|
||||
<i class="fas fa-align-left" aria-hidden="true"></i>
|
||||
</div>
|
||||
</button>
|
||||
<h1 id="title" class="header-title">Galène</h1>
|
||||
</div>
|
||||
|
||||
@@ -47,21 +47,21 @@
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<div id="mutebutton" class="nav-link nav-button">
|
||||
<button id="mutebutton" class="nav-link nav-button" aria-pressed="false" aria-label="Mute microphone">
|
||||
<span><i class="fas fa-microphone-slash" aria-hidden="true"></i></span>
|
||||
<label>Mute</label>
|
||||
</div>
|
||||
<span class="nav-text">Mute</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<div id="sharebutton" class="invisible nav-link nav-button">
|
||||
<button id="sharebutton" class="invisible nav-link nav-button" aria-label="Share screen">
|
||||
<span><i class="fas fa-share-square" aria-hidden="true"></i></span>
|
||||
<label>Share Screen</label>
|
||||
</div>
|
||||
<span class="nav-text">Share Screen</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<div class="nav-button nav-link nav-more" id="openside">
|
||||
<button class="nav-button nav-link nav-more" id="openside" aria-label="More options" aria-haspopup="true">
|
||||
<span><i class="fas fa-ellipsis-v" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -70,10 +70,11 @@
|
||||
<div class="coln-left" id="left">
|
||||
<div id="chat">
|
||||
<div id="chatbox">
|
||||
<div class="close-chat" id="close-chat" title="Hide chat">
|
||||
<button class="close-chat" id="close-chat" aria-label="Hide chat">
|
||||
<span class="close-icon"></span>
|
||||
</div>
|
||||
</button>
|
||||
<div id="box"></div>
|
||||
<div id="chat-announcements" aria-live="polite" aria-atomic="true" class="sr-only"></div>
|
||||
<div class="reply">
|
||||
<form id="inputform">
|
||||
<textarea id="input" class="form-reply"></textarea>
|
||||
|
||||
+47
-3
@@ -626,10 +626,14 @@ function setLocalMute(mute, reflect) {
|
||||
icon.classList.add('fa-microphone-slash');
|
||||
icon.classList.remove('fa-microphone');
|
||||
button.classList.add('muted');
|
||||
button.setAttribute('aria-pressed', 'true');
|
||||
button.setAttribute('aria-label', 'Unmute microphone');
|
||||
} else {
|
||||
icon.classList.remove('fa-microphone-slash');
|
||||
icon.classList.add('fa-microphone');
|
||||
button.classList.remove('muted');
|
||||
button.setAttribute('aria-pressed', 'false');
|
||||
button.setAttribute('aria-label', 'Mute microphone');
|
||||
}
|
||||
if(reflect)
|
||||
updateSettings({localMute: mute});
|
||||
@@ -2060,6 +2064,10 @@ async function setMedia(c, mirror, video) {
|
||||
peersdiv.appendChild(div);
|
||||
}
|
||||
|
||||
// Add aria-label to identify video stream owner
|
||||
let username = c.username || 'Participant';
|
||||
div.setAttribute('aria-label', `Video stream from ${username}`);
|
||||
|
||||
showHideMedia(c, div)
|
||||
|
||||
let media = /** @type {HTMLVideoElement} */
|
||||
@@ -2077,6 +2085,9 @@ async function setMedia(c, mirror, video) {
|
||||
media.autoplay = true;
|
||||
media.playsInline = true;
|
||||
media.id = 'media-' + c.localId;
|
||||
// Add aria-label for screen readers
|
||||
let username = c.username || 'Participant';
|
||||
media.setAttribute('aria-label', `Video from ${username}`);
|
||||
div.appendChild(media);
|
||||
addCustomControls(media, div, c, !!video);
|
||||
}
|
||||
@@ -2653,11 +2664,24 @@ function changeUser(id, userinfo) {
|
||||
* @param {user} userinfo
|
||||
*/
|
||||
function setUserStatus(id, elt, userinfo) {
|
||||
elt.textContent = userinfo.username ? userinfo.username : '(anon)';
|
||||
if(userinfo.data.raisehand)
|
||||
let username = userinfo.username ? userinfo.username : '(anon)';
|
||||
elt.textContent = username;
|
||||
|
||||
let wasRaised = elt.classList.contains('user-status-raisehand');
|
||||
if(userinfo.data.raisehand) {
|
||||
elt.classList.add('user-status-raisehand');
|
||||
else
|
||||
elt.setAttribute('aria-label', `${username} (hand raised)`);
|
||||
// Announce when hand is newly raised (not on initial load)
|
||||
if(!wasRaised && id !== (serverConnection && serverConnection.id)) {
|
||||
let announcement = document.getElementById('chat-announcements');
|
||||
if(announcement) {
|
||||
announcement.textContent = `${username} raised their hand`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
elt.classList.remove('user-status-raisehand');
|
||||
elt.setAttribute('aria-label', username);
|
||||
}
|
||||
|
||||
let microphone=false, camera = false;
|
||||
for(let label in userinfo.streams) {
|
||||
@@ -2668,6 +2692,17 @@ function setUserStatus(id, elt, userinfo) {
|
||||
camera = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Build descriptive aria-label with media status
|
||||
let statusParts = [username];
|
||||
if(userinfo.data.raisehand)
|
||||
statusParts.push('hand raised');
|
||||
if(camera)
|
||||
statusParts.push('camera on');
|
||||
if(microphone)
|
||||
statusParts.push('microphone on');
|
||||
elt.setAttribute('aria-label', statusParts.join(', '));
|
||||
|
||||
if(camera) {
|
||||
elt.classList.remove('user-status-microphone');
|
||||
elt.classList.add('user-status-camera');
|
||||
@@ -3384,6 +3419,15 @@ function addToChatbox(id, peerId, dest, nick, time, privileged, history, kind, m
|
||||
box.scrollTop = box.scrollHeight - box.clientHeight;
|
||||
}
|
||||
|
||||
// Announce new messages to screen readers (but not history)
|
||||
if(!history) {
|
||||
let announcement = document.getElementById('chat-announcements');
|
||||
if(announcement) {
|
||||
let messageText = typeof message === 'string' ? message : body.textContent;
|
||||
announcement.textContent = nick ? `${nick}: ${messageText}` : messageText;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user