diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js index 6a9f1c7..108bc51 100644 --- a/webui-src/app/chat/chat_state.js +++ b/webui-src/app/chat/chat_state.js @@ -121,13 +121,25 @@ function isEmbeddedImageSrc(src) { // strand embedded browsers such as Android WebView without a usable Back entry. let chatImageViewer = null; let chatImageViewerPreviousOverflow = ''; +let chatImageViewerOpener = null; +let chatImageViewerKeyHandler = null; const CHAT_IMAGE_VIEWER_HISTORY_KEY = 'chatImageViewer'; function removeChatImageViewer() { if (!chatImageViewer) return; + if (chatImageViewerKeyHandler) { + document.removeEventListener('keydown', chatImageViewerKeyHandler, true); + chatImageViewerKeyHandler = null; + } chatImageViewer.remove(); chatImageViewer = null; document.body.style.overflow = chatImageViewerPreviousOverflow; + // Put the focus back where it was taken from, so closing the preview does + // not leave the caret on with the message list scrolled away. + if (chatImageViewerOpener && document.contains(chatImageViewerOpener)) { + chatImageViewerOpener.focus(); + } + chatImageViewerOpener = null; } function closeChatImageViewer() { @@ -168,12 +180,30 @@ function openChatImageViewer(src) { overlay.onclick = (event) => { if (event.target === overlay) closeChatImageViewer(); }; - overlay.onkeydown = (event) => { - if (event.key === 'Escape') closeChatImageViewer(); + + // The overlay says role=dialog and aria-modal=true, so it has to behave like + // one. Listening on the overlay only caught what bubbled through it: tapping + // the picture moves the focus to and Escape went dead from then on. + // Listening on the document, in the capture phase, means Escape closes the + // preview wherever the focus has drifted, and Tab cannot walk out of it into + // the page underneath -- the close button is the only thing to land on. + chatImageViewerKeyHandler = (event) => { + if (event.key === 'Escape') { + event.preventDefault(); + closeChatImageViewer(); + } else if (event.key === 'Tab') { + event.preventDefault(); + closeButton.focus(); + } }; + document.addEventListener('keydown', chatImageViewerKeyHandler, true); chatImageViewerPreviousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; + // Captured before the overlay steals the focus, and restored on close. + chatImageViewerOpener = document.activeElement instanceof HTMLElement + ? document.activeElement + : null; document.body.appendChild(overlay); chatImageViewer = overlay; history.pushState({ ...(history.state || {}), [CHAT_IMAGE_VIEWER_HISTORY_KEY]: true }, ''); diff --git a/webui-src/app/main.js b/webui-src/app/main.js index 4b87b26..14abeae 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -116,7 +116,7 @@ const navbar = () => { ? 'Connected to RetroShare Core' : 'Connection Lost', }), - m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v153'), + m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v154'), m('i.fas.fa-sync-alt.refresh-icon', { style: { cursor: 'pointer', fontSize: '0.8em' }, onclick: () => window.location.reload(true), @@ -239,7 +239,7 @@ const MobileStatus = () => { m('small', statusbar.formatBytes(state.totalOut)), ]), ]), - m('.mobile-status-sheet__version', 'WebUI v153'), + m('.mobile-status-sheet__version', 'WebUI v154'), ])), ]; },