connect: leaving fullscreen keeps the session, with a button back

The launcher tore the session down whenever the tab left fullscreen, so
holding Escape, F11 or a gesture from the OS ended it. Leaving fullscreen
now keeps the session at the tab's size and shows a Fullscreen button
that goes back; only Exit, or the session ending, closes it. Tested with
the browser's fullscreen events faked, since headless Chromium refuses
the real request.
This commit is contained in:
Dave Kempe
2026-10-17 13:52:06 +11:00
parent 279da6f17f
commit 7a70f623a7
3 changed files with 68 additions and 5 deletions
+6 -3
View File
@@ -1141,12 +1141,15 @@ html:not([data-role-level="4"]) .admin-only { display: none !important; }
border: 3px solid rgba(255,255,255,0.25); border-top-color: var(--accent, #5bc0be);
border-radius: 50%; animation: rg-btn-spin 0.8s linear infinite;
}
#rg-session-exit {
position: absolute; top: 10px; right: 14px; z-index: 5001;
#rg-session-exit { position: absolute; top: 10px; right: 14px; }
#rg-session-fs { position: absolute; top: 10px; right: 104px; }
#rg-session-fs[hidden] { display: none; }
.rg-session-btn {
z-index: 5001;
height: 32px; padding: 0 var(--s-3, 14px);
background: rgba(20,24,38,0.72); color: var(--text, #e0e0e0);
border: 1px solid var(--border, #333); border-radius: var(--radius, 2px);
font-family: monospace; font-size: var(--fz-sm, 16px); cursor: pointer;
opacity: 0.35; transition: opacity 0.15s;
}
#rg-session-exit:hover { opacity: 1; background: var(--surface, #16213e); border-color: var(--primary, #e94560); color: var(--text, #e0e0e0); }
.rg-session-btn:hover { opacity: 1; background: var(--surface, #16213e); border-color: var(--primary, #e94560); color: var(--text, #e0e0e0); }
+15 -2
View File
@@ -422,7 +422,8 @@
} else fsRefused();
var ov = document.createElement('div'); ov.id = 'rg-session-overlay';
ov.innerHTML = '<div id="rg-session-spinner"></div>' +
'<button id="rg-session-exit" type="button" title="Exit">× Exit</button>' +
'<button id="rg-session-fs" class="rg-session-btn" type="button" title="Back to fullscreen" hidden>⛶ Fullscreen</button>' +
'<button id="rg-session-exit" class="rg-session-btn" type="button" title="Leave the session">× Exit</button>' +
'<iframe id="rg-session-frame" allow="fullscreen; clipboard-read; clipboard-write" title="Session"></iframe>';
document.body.appendChild(ov);
if (btn) { btn.classList.add('loading'); btn.disabled = true; }
@@ -450,11 +451,23 @@
// the settle check covers both.
if (!started) armSettle();
}
else if (enteredFs) { teardown(); }
// Leaving fullscreen (Escape held, F11, the OS) keeps the session:
// it carries on at the tab's size and the button takes it back
// to fullscreen. Only Exit, or the session ending, closes it.
else if (enteredFs) { showFsButton(true); }
if (document.fullscreenElement) showFsButton(false);
}
function showFsButton(on) {
var b = document.getElementById('rg-session-fs'); if (b) b.hidden = !on;
if (!on) { var f = document.getElementById('rg-session-frame'); try { if (f) f.focus(); } catch (e) {} }
}
window.addEventListener('message', onMsg);
document.addEventListener('fullscreenchange', onFsChange);
document.getElementById('rg-session-exit').addEventListener('click', teardown);
document.getElementById('rg-session-fs').addEventListener('click', function () {
// A click is a fresh gesture, so the browser allows fullscreen again.
try { var p = req && req.call(el); if (p && p.catch) p.catch(function () {}); } catch (e) {}
});
// Create the session at the measured viewport size — the size the
// frame paints into — so there is nothing to resize afterwards.
function startConnect() {
@@ -55,3 +55,50 @@ test('a non-fullscreen entry connects the old way, with no frame', async ({ page
expect(await page.locator('#rg-session-overlay').count()).toBe(0);
if (popup) await popup.close();
});
// Leaving fullscreen (Escape held, F11, the OS) must not end the session: the
// frame stays at the tab's size and a button takes it back to fullscreen.
// Headless Chromium refuses real fullscreen, so the browser's side is faked:
// fullscreenElement is set and cleared and fullscreenchange fired, exactly
// what the launcher listens to.
test('leaving fullscreen keeps the session and offers to go back', async ({ page }) => {
await page.addInitScript(() => {
let fs = null;
Object.defineProperty(Document.prototype, 'fullscreenElement', { configurable: true, get: () => fs });
// Like a real browser, the change event fires after the call returns.
const change = (el) => new Promise((resolve) => setTimeout(() => {
fs = el;
document.dispatchEvent(new Event('fullscreenchange'));
resolve();
}, 50));
Element.prototype.requestFullscreen = function () { return change(document.documentElement); };
document.exitFullscreen = function () { return change(null); };
window.__leaveFullscreen = () => document.exitFullscreen();
});
await installApi(page);
await page.goto('/connections.html');
await settle(page);
await page.locator('.folder-row').filter({ hasText: /acme/ }).first().click();
const kiosk = page.locator('.entries-table tbody tr').filter({ hasText: 'kiosk' });
await kiosk.locator('button', { hasText: /^Connect/ }).first().click();
const overlay = page.locator('#rg-session-overlay');
const back = page.locator('#rg-session-fs');
await expect(overlay).toBeVisible();
await expect(back).toBeHidden();
// The user leaves fullscreen: the session is still there.
await page.evaluate(() => window.__leaveFullscreen());
await expect(overlay).toBeVisible();
await expect(page.locator('#rg-session-frame')).toHaveCount(1);
await expect(back).toBeVisible();
// The button goes back to fullscreen and hides itself.
await back.click();
await expect(back).toBeHidden();
await expect(overlay).toBeVisible();
// Exit still ends it.
await page.locator('#rg-session-exit').click();
await expect(overlay).toHaveCount(0);
});