From 999ef3ba7b75137bceed512c8aea8e43ea40c812 Mon Sep 17 00:00:00 2001 From: jjsa Date: Sat, 14 Dec 2024 13:19:39 +0100 Subject: [PATCH] changes for keyboard and scrennreader users --- KEYBOARD | 39 +++++++ static/galene.css | 138 +++++++++++++++++++----- static/galene.html | 94 ++++++++-------- static/galene.js | 27 +++-- static/key.js | 263 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 482 insertions(+), 79 deletions(-) create mode 100644 KEYBOARD create mode 100644 static/key.js diff --git a/KEYBOARD b/KEYBOARD new file mode 100644 index 0000000..d7832b8 --- /dev/null +++ b/KEYBOARD @@ -0,0 +1,39 @@ +# Use of the keyboard with Galene + +People wich are not able to use a mouse need to have other possibilities. + +Some user will use the keyboard in order to navigate to the galene Web +Interface and will use the Tab Key to select the wanted element and +other key as arrow up, down, space. + +User with visual impairement, may use a screenreader. + +For keyboard user we can use the following keys: + +- 'u' goto user list +- 'c' go to the chat input box +- 'r' raise, unraise hand +- 'm' mute, unmute + + +- Esc close contextual menu, close the setting, close the chat box, +close the user list + + +For the screenreader user, it is difficult to provide shortcuts, most +keys or keys combination are reserved from the OS the screenreader and +the browser. + +In order to offer a faster way for selecting the wanted element, we use +thw header navigation (h1, h2. h3). + +- The key 1 will got to the main Title of the page. +- The key 2 will allow to select one of the area user list, chat box or media area. +- The key 3 allow to navigate from message to message. +- The Tab and Esc key work as for the normal keyboard user + +Screenreader shall announce the error and warning textes issued by galene, +this is also implemented. + + + diff --git a/static/galene.css b/static/galene.css index 096a2fd..322e3ce 100644 --- a/static/galene.css +++ b/static/galene.css @@ -1,3 +1,15 @@ + +button { + background: none; + border: 0; +} +.screenreader { + width: 0px; + height: 0px; + overflow: hidden; + margin: 0; +} + .nav-fixed .topnav { z-index: 1039; } @@ -109,7 +121,7 @@ display: none; } -.sidenav .user-logout a { +.sidenav .user-logout button { font-size: 1em; padding: 7px 0 0; color: #e4157e; @@ -117,7 +129,7 @@ line-height: .7; } -.sidenav .user-logout a:hover { +.sidenav .user-logout button:hover { color: #ab0659; } @@ -269,12 +281,12 @@ } .full-width { - width: calc(100vw - 200px); + /*width: calc(100vw - 200px);*/ height: calc(var(--vh, 1vh) * 100 - 56px); } .full-width-active { - width: 100vw; +/* width: 100vw;*/ } .container { @@ -312,7 +324,6 @@ resize: none; overflow: hidden; padding: 5px; - outline: none; border: none; text-indent: 5px; box-shadow: none; @@ -440,6 +451,13 @@ textarea.form-reply { white-space: pre-wrap; } +.message button { + margin: 1em; + padding: .4em .8em; + border-radius: .7em; + background: #a1ceff; +} + .video-container { height: calc(var(--vh, 1vh) * 100 - 56px); position: relative; @@ -467,7 +485,7 @@ textarea.form-reply { } .collapse-video { - left: 30px; + left: calc(-100vw); right: inherit; } @@ -518,6 +536,7 @@ textarea.form-reply { background: linear-gradient(180deg, rgb(0 0 0 / 20%) 0%, rgb(0 0 0 / 50%) 0%, rgb(0 0 0 / 70%) 100%); } +.peer:focus-within > .video-controls, .peer:focus-within > .top-video-controls, .peer:hover > .video-controls, .peer:hover > .top-video-controls { opacity: 1; } @@ -529,6 +548,10 @@ textarea.form-reply { cursor: pointer; } +.peer button i { + color: #eaeaea; +} + .video-controls span:last-child, .top-video-controls span:last-child { margin-right: 0; } @@ -583,6 +606,7 @@ textarea.form-reply { transition: opacity .5s ease-out; } +.video-controls .volume:focus-within, .video-controls .volume:hover { --ov: 1; --dv: inline; @@ -663,7 +687,7 @@ textarea.form-reply { } .nav-cancel, .muted, .nav-cancel label, .muted label { - color: #d03e3e + color: #d03e3e; } .nav-cancel:hover, .muted:hover, .nav-cancel label:hover, .muted label:hover { @@ -675,7 +699,7 @@ textarea.form-reply { font-size: 25px; } -.nav-more { +#openside .nav-more { padding-top: 5px; margin-left: 0; } @@ -786,21 +810,25 @@ h1 { } #filterselect { + width: 8em; text-align-last: center; margin-right: 0.4em; } #sendselect { + width: 8em; text-align-last: center; margin-right: 0.4em; } #simulcastselect { + width: 8em; text-align-last: center; margin-right: 0.4em; } #requestselect { + width: 8em; text-align-last: center; } @@ -864,14 +892,6 @@ h1 { overflow-y: hidden; } -#input:focus { - outline: none; -} - -#inputbutton:focus { - outline: none; -} - #resizer { width: 4px; margin-left: -4px; @@ -955,7 +975,7 @@ h1 { overflow-y: hidden; } -.sidenav a { +.sidenav button { padding: 10px 20px; text-decoration: none; font-size: 30px; @@ -965,7 +985,11 @@ h1 { line-height: 1.0; } -.sidenav a:hover { +.sidenav button:hover { + color: #c2a4e0; +} + +.sidenav button:hover { color: #c2a4e0; } @@ -1111,9 +1135,22 @@ header .collapse:hover { } /* Shrinking the sidebar from 200px to 0px */ +#sidebarnav { + display: none; + width: 250px; +} + +#sidebarnav[open=true] { + display: block; +} + #left-sidebar.active { - min-width: 0; - max-width: 0; + display: none; +} + +#left-sidebar:not(.active) { + display: block; + width:200px; } #left-sidebar .sidebar-header strong { @@ -1148,17 +1185,23 @@ header .collapse:hover { cursor: pointer; overflow: hidden; white-space: pre; + display: block; + width: 100%; + text-align: left; } -#left-sidebar.active #users > div { +#left-sidebar.active #users > button { padding: 10px 5px !important; } -#users > div:hover { +#users > button:hover { background-color: #f2f2f2; } +#users > button:focus, #users > button:focus-visible { + outline-offset: -2px; +} -#users > div::before { +#users > button::before { content: "\f111"; font-family: 'Font Awesome 6 Free'; color: #20b91e; @@ -1166,11 +1209,11 @@ header .collapse:hover { font-weight: 900; } -#users > div.user-status-raisehand::before { +#users > button.user-status-raisehand::before { content: "\f256"; } -#users > div::after { +#users > button::after { font-family: 'Font Awesome 6 Free'; color: #808080; margin-left: 5px; @@ -1178,11 +1221,11 @@ header .collapse:hover { float: right; } -#users > div.user-status-microphone::after { +#users > button.user-status-microphone::after { content: "\f130"; } -#users > div.user-status-camera::after { +#users > button.user-status-camera::after { content: "\f030"; } @@ -1231,6 +1274,10 @@ header .collapse:hover { display: none; } + .video-on #chat { + display: none; + } + .video-container { position: fixed; height: calc(var(--vh, 1vh) * 100 - 56px); @@ -1261,7 +1308,7 @@ header .collapse:hover { flex: 100%; width: 100vw; /* chat is always visible here */ - display: block !important; + display: block /*!important*/; } .coln-right { @@ -1273,6 +1320,7 @@ header .collapse:hover { width: 100vw; } +/* #left-sidebar.active { min-width: 200px; max-width: 200px; @@ -1282,10 +1330,11 @@ header .collapse:hover { min-width: 0; max-width: 0; } - +*/ /* Reappearing the sidebar on toggle button click */ #left-sidebar { margin-left: 0; + z-index: 1; } #left-sidebar .sidebar-header strong { @@ -1368,6 +1417,12 @@ header .collapse:hover { margin-right: 10px; } +.contextualMenu button { + outline-offset: 3px; + display: inline-block; + width: calc(100% - 2em); +} + #invite-dialog { background-color: #eee; } @@ -1394,3 +1449,28 @@ header .collapse:hover { .toastify.info .toast-close { color: #000; } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* wrong preset for some browser! */ +video::-webkit-media-controls-enclosure { + display: none !important; +} + +video::-webkit-media-controls-panel { + display: none !important; +} + +video::-webkit-media-controls { + display: none !important; +} + diff --git a/static/galene.html b/static/galene.html index d76c09e..b35d76c 100644 --- a/static/galene.html +++ b/static/galene.html @@ -13,71 +13,75 @@ + -
+
+
-
-
+ @@ -260,32 +264,36 @@ +
diff --git a/static/galene.js b/static/galene.js index 7334019..f64d094 100644 --- a/static/galene.js +++ b/static/galene.js @@ -2039,9 +2039,13 @@ function setVolumeButton(muted, button, slider) { if(!muted) { button.classList.remove("fa-volume-mute"); button.classList.add("fa-volume-up"); + let von = document.querySelector('.sr-help .von').textContent; + button.setAttribute("aria-label", von); } else { button.classList.remove("fa-volume-up"); button.classList.add("fa-volume-mute"); + let voff = document.querySelector('.sr-help .voff').textContent; + button.setAttribute("aria-label",voff); } if(!(slider instanceof HTMLInputElement)) @@ -2416,7 +2420,8 @@ function userMenu(elt) { */ function addUser(id, userinfo) { let div = document.getElementById('users'); - let user = document.createElement('div'); + //let user = document.createElement('div'); + let user = document.createElement('button'); user.id = 'user-' + id; user.classList.add("user-p"); setUserStatus(id, user, userinfo); @@ -3126,7 +3131,8 @@ function addToChatbox(id, peerId, dest, nick, time, privileged, history, kind, m } if(doHeader) { - let header = document.createElement('p'); +// let header = document.createElement('p'); + let header = document.createElement('h3'); let user = document.createElement('span'); let u = dest && serverConnection.users[dest]; let name = (u && u.username); @@ -4131,11 +4137,16 @@ document.getElementById('disconnectbutton').onclick = function(e) { }; function openNav() { - document.getElementById("sidebarnav").style.width = "250px"; + document.getElementById("sidebarnav").setAttribute("open",'true'); + document.getElementById("sidebarnav").removeAttribute('aria-hidden'); + document.querySelector('#sidebarnav .closebtn').focus(); } function closeNav() { - document.getElementById("sidebarnav").style.width = "0"; + document.getElementById("sidebarnav").removeAttribute('open'); + document.getElementById("sidebarnav").setAttribute("aria-hidden",'true'); + document.querySelector('#openside').focus(); + } document.getElementById('sidebarCollapse').onclick = function(e) { @@ -4144,9 +4155,9 @@ document.getElementById('sidebarCollapse').onclick = function(e) { }; document.getElementById('openside').onclick = function(e) { - e.preventDefault(); - let sidewidth = document.getElementById("sidebarnav").style.width; - if (sidewidth !== "0px" && sidewidth !== "") { + //e.preventDefault(); + let open = document.getElementById("sidebarnav").getAttribute('open'); + if ( open ) { closeNav(); return; } else { @@ -4165,6 +4176,7 @@ document.getElementById('collapse-video').onclick = function(e) { setVisibility('collapse-video', false); setVisibility('show-video', true); hideVideo(true); + document.getElementById('mainrow').classList.remove('video-on'); }; document.getElementById('show-video').onclick = function(e) { @@ -4172,6 +4184,7 @@ document.getElementById('show-video').onclick = function(e) { setVisibility('video-container', true); setVisibility('collapse-video', true); setVisibility('show-video', false); + document.getElementById('mainrow').classList.add('video-on'); }; document.getElementById('close-chat').onclick = function(e) { diff --git a/static/key.js b/static/key.js new file mode 100644 index 0000000..173e1ee --- /dev/null +++ b/static/key.js @@ -0,0 +1,263 @@ +'use strict'; +let galeneKeys = { + from : null, + focusAgain : function() { + if ( galeneKeys.from ) { + galeneKeys.from.focus(); + //galeneKeys.from = null; + } + else { + document.body.focus(); + } + }, + leave : function (event) { + if ( event.target.classList.contains('first') ) { + if ( event.shiftKey ) { + let cm = document.querySelector('.contextualMenu'); + if ( cm ) { + cm.remove(); + setTimeout(galeneKeys.focusAgain, 20); + return; + } + } + } + if ( event.target.classList.contains('last') ) { + if ( ! event.shiftKey ) { + let cm = document.querySelector('.contextualMenu'); + if ( cm ) { + cm.remove(); + setTimeout(galeneKeys.focusAgain, 20); + return; + } + } + } + if ( event.key === 'Escape' ) + setTimeout(galeneKeys.focusAgain, 20); + }, + setTabindexContextMenu : function (target) { + let context = document.querySelectorAll('.contextualMenuItemTitle'); + if ( context && context.length) { + context.forEach( c => { + let btn = document.createElement('button'); + btn.classList.add('contextualMenuItemTitle'); + btn.classList.add('contextualJs'); + btn.textContent = c.textContent; + btn.addEventListener('click', galeneKeys.menuClick); + c.replaceWith(btn); + }); + context = document.querySelectorAll('.contextualMenuItemTitle'); + context[0].classList.add('first'); + let last = context.length - 1; + context[last].classList.add('last'); + galeneKeys.from = target; + context[0].focus(); + let pos = target.getBoundingClientRect(); + + let contextParent = document.querySelector('.contextualMenu'); + contextParent.style.top = pos.top+20+'px'; + // the following work only if the display with is enough we may have to correct this. + let x = 180; + let w = window.innerWidth; + if ( x + 200 > window.innerWidth ) { + x = w - 205; + } + contextParent.style.left = x+'px'; + + let menu = document.querySelector('ul.contextualMenu'); + } else { + // Enter pressed + galeneKeys.focusAgain(); + } + }, + menuClick : function(e) { + galeneKeys.focusAgain(); + }, + processKey : function (event) { + let target = event.target; + let key = event.key; + switch(key) { + case 'Tab': + if ( document.activeElement.classList.contains('contextualMenuItemTitle') ) { + galeneKeys.leave(event); + return; + } + if ( document.activeElement.id === 'sideBarCollapse') + return; + break; + case 'Escape': + let dialog = document.querySelector('dialog'); + if ( dialog ) { + let open = dialog.getAttribute('open'); + if ( open == '' ) { + dialog.close(); + galeneKeys.focusAgain(); + return; + } + } + let cm = document.querySelector('.contextualMenu'); + if ( cm ) { + cm.remove(); + galeneKeys.leave(event); + return; + } + // if the setting are open close them + let sidebar = document.querySelector('#sidebarnav[open]'); + if ( sidebar ) { + event.preventDefault(); + sidebar.removeAttribute('open'); + return; + } + // check if we are within the chat + let active = document.activeElement; + let chat = document.querySelector('#left:not(.invisible)'); + if ( chat ) { + event.preventDefault(); + chat.classList.add('invisible'); + let showChat = document.querySelector('#show-chat'); + showChat.classList.remove('invisible'); + return; + } + // finally close possibly the user list + let userList = document.querySelector('#left-sidebar:not(.active)'); + if ( userList ) { + event.preventDefault(); + document.querySelector('#left-sidebar').classList.add('active'); + return; + } + break; + case 'Enter': + case ' ': + if ( target.classList.contains('volume-mute') ) { + let state = + target.click(); + if ( translate && translate.translateList ) { + translate.setVolumeAria(); + } + return; + } + break; + + } + switch(target.nodeName) { + case 'INPUT': + let type = target.getAttribute('type'); + if ( type && type === 'submit' ) { + return; + } + case 'TEXTAREA': + case 'SELECT': + return; + break; + } + let usercont = null; + switch(key) { + case 'u': + event.preventDefault(); + usercont = document.querySelector('#left-sidebar'); + if ( !usercont.classList.contains('active')) { + /* focus first user */ + let user = document.querySelector('#users .user-p'); + if ( user ) + user.focus(); + } else { + usercont.classList.remove('active'); + } + break; + case 'r': + /* raise hand */ + let me = document.querySelector('#left-sidebar #users .user-p'); + if ( me ) { + if (me.classList.contains('user-status-raisehand') ) + me.classList.remove('user-status-raisehand'); + else + me.classList.add('user-status-raisehand'); + } + break; + case 'm': + let localMute = getSettings().localMute; + localMute = !localMute; + setLocalMute(localMute, true); + break; + case 'c': + /* Chat */ + event.preventDefault(); + let chat = document.querySelector('#left:not(.invisible)'); + if (!chat) { + setVisibility('left', true); + setVisibility('show-chat', false); + resizePeers(); + chat = document.querySelector('#left:not(.invisible) textarea'); + } + if (chat) + chat.focus(); + break; + } + }, + userClick : function (e) { + galeneKeys.from = e.target; + setTimeout(galeneKeys.setTabindexContextMenu,20, e.target); + }, + addClickListener : function (mutationList) { + for (const mutation of mutationList) { + if (mutation.type === "childList") { + if (mutation && mutation.addedNodes) { + let idx = mutation.addedNodes.length -1; + if (idx >= 0) { + mutation.addedNodes[idx].addEventListener('click', galeneKeys.userClick); + } + } + } + } + }, + focusInput : function() { + let input = document.querySelector('#input'); + input.focus(); + }, + collapseSidebar : function(e) { + let sidebar = document.querySelector('#left-sidebar.active'); + if ( !sidebar ) { + // focus the first user-p button done but no outline! + //let user = document.querySelector('.user-p'); + let userP = document.querySelector('.user-p'); + if (userP) + userP.focus(); + } + }, + setSr : function (sr, toast) { + sr.textContent = toast.textContent; + }, + resetSr : function(sr) { + sr.textContent = ''; + }, + setSrText : function() { + let toast = document.querySelector('.toastify'); + let sr = document.querySelector('#srSpeak'); + if ( toast ) { + setTimeout(galeneKeys.setSr, 50, sr, toast); + setTimeout(galeneKeys.resetSr, 4000, sr); + } + }, + init : function () { + // add mutation oberver + let toObserve = document.querySelector('#users'); + if ( toObserve ) { + const observer = new MutationObserver(galeneKeys.addClickListener); + observer.observe(toObserve, {childList:true, subtree:false}); + } + const popup = new MutationObserver(galeneKeys.setSrText); + popup.observe(document.body, {childList: true,subtree: false}); + + // add event listener to the show-chat button + let showChat = document.querySelector('#show-chat'); + if ( showChat ) { + showChat.addEventListener('click',galeneKeys.focusInput); + } + let left = document.querySelector('#sidebarCollapse'); + if ( left ) { + left.addEventListener('click', galeneKeys.collapseSidebar); + } + }, +}; +document.addEventListener('keydown', galeneKeys.processKey); +document.addEventListener('DOMContentLoaded', galeneKeys.init); +