mirror of
https://github.com/mmis1000/SofaNav.git
synced 2026-10-17 13:01:08 +02:00
B sends Escape to the focused element, wherever it is (E31)
User's debug report from x.com: X's full-screen mask was detected, but
it's a sibling of the menu, and Escape was sent from the mask, so X's
handler on the menu never saw it. Escape now always goes to the focused
element like a real key (X focuses the menu when it opens). Test page:
mask beside the menu, menu off the overlay sample points, B pressed
with SofaNav on the account button; fails on 29e2788.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29e2788cfa
commit
8bc104f2d0
5 files changed
+51
-25
No files matched your search
Vendored
+10
-9
@@ -1,8 +1,8 @@
|
||||
// ==UserScript==
|
||||
// @name SofaNav
|
||||
// @namespace https://sofanav.mmis1000.me
|
||||
// @version 0.3.0.202610121845
|
||||
// @description Navigate web pages with a controller: D-pad / left stick move focus, A click, B back, X enter, Y graph view. (build 971c074a2345)
|
||||
// @version 0.3.0.202610122320
|
||||
// @description Navigate web pages with a controller: D-pad / left stick move focus, A click, B back, X enter, Y graph view. (build fff84d1e8ea6)
|
||||
// @homepageURL https://mmis1000.me/SofaNav/
|
||||
// @downloadURL https://mmis1000.me/SofaNav/sofanav.user.js
|
||||
// @updateURL https://mmis1000.me/SofaNav/sofanav.user.js
|
||||
@@ -16,7 +16,7 @@
|
||||
// @inject-into content
|
||||
// ==/UserScript==
|
||||
// Generated from extension/content.js by build-userscript.js. Edit that file, not this one.
|
||||
// Build 971c074a2345 = first 12 hex digits of sha256(extension/content.js).
|
||||
// Build fff84d1e8ea6 = first 12 hex digits of sha256(extension/content.js).
|
||||
// LB/RB (switch tabs) needs the extension; in a userscript they do nothing.
|
||||
// @sandbox DOM / @inject-into content run SofaNav in an isolated world, where the page can't
|
||||
// reach it. Only then does it control cross-origin frames (over GM storage, see content.js
|
||||
@@ -34,7 +34,7 @@
|
||||
// parent and carries out commands; the top frame does all navigation.
|
||||
const IS_FRAME = window.top !== window;
|
||||
// Replaced with the content hash by build-userscript.js; 'dev' when loaded unbuilt (extension, test pages).
|
||||
const BUILD = '971c074a2345';
|
||||
const BUILD = 'fff84d1e8ea6';
|
||||
const USERSCRIPT = true; // the userscript build sets this
|
||||
if (!IS_FRAME) console.info('[sofanav] build', BUILD);
|
||||
|
||||
@@ -210,13 +210,14 @@
|
||||
}
|
||||
|
||||
// Something is open over the page (aria-modal dialog, floating menu, covering layer): one
|
||||
// Escape attempt per B press. It goes where a real key would: the focused element (handlers
|
||||
// sit on it or above it; events only bubble up, so sending from a wrapper around it, such as
|
||||
// a scroll box, misses them). If nothing inside has focus: SofaNav's element, or the first
|
||||
// focusable thing in it. Resolves true if it closed.
|
||||
// Escape attempt per B press. It goes where a real key would: the focused element, wherever
|
||||
// it is (handlers sit on it or above it; events only bubble up). `box` may not contain it:
|
||||
// X's mask is a sibling of its menu, and X focuses the menu. Only with nothing focused:
|
||||
// SofaNav's element (or the first focusable thing in it) if it's inside, else the box.
|
||||
// Resolves true if the box closed.
|
||||
async function dismissOpen(box, trigger) {
|
||||
const ae = document.activeElement, cur = nodeOf(current);
|
||||
let target = ae && ae !== document.body && box.contains(ae) ? ae : null;
|
||||
let target = ae && ae !== document.body && ae !== document.documentElement ? ae : null;
|
||||
if (!target && cur instanceof Element && box.contains(cur)) target = cur.querySelector(FOCUSABLE_SEL) || cur;
|
||||
sendEscape(target || box);
|
||||
backNote(`Escape sent from ${brief(target || box)}`);
|
||||
|
||||
@@ -457,6 +457,19 @@ usual (leave scope / go back), since the hint is weaker than markup or a mask.
|
||||
Light popups are now closed before leaving a scope inside them. The test page's
|
||||
X-style menu copies X's real structure; fails on 883b834 and acd22c7.
|
||||
|
||||
*Still failing on X (user's debug report with the new `lastBack`/`open`
|
||||
fields, build 971c074a2345):* on the user's (larger) screen X's transparent
|
||||
full-screen mask *was* detected (`overlay`), but it's a sibling of the menu,
|
||||
not around it; SofaNav's element was the account button and X's focus was on
|
||||
the first menu item, so Escape went out from the mask, outside the menu, and
|
||||
X's handler never saw it. On a smaller screen the menu covered one of the
|
||||
overlay check's sample points, so only the floating-box path ran, which is
|
||||
why my checks passed. *Fix:* Escape always goes to the focused element, like a
|
||||
real key, whatever box was detected; only with nothing focused does it use
|
||||
SofaNav's element or the box. The test page now adds a mask beside the menu,
|
||||
keeps the menu off the sample points, and presses B with SofaNav on the
|
||||
account button (the report's state); fails on 29e2788.
|
||||
|
||||
**E32. Mouse mode: an escape hatch for what focus navigation can't reach**
|
||||
(requested by the user after E31). Even with E31, some UI can't be handled by
|
||||
moving focus: popups that only close on an outside click, controls that only
|
||||
|
||||
@@ -186,13 +186,14 @@
|
||||
}
|
||||
|
||||
// Something is open over the page (aria-modal dialog, floating menu, covering layer): one
|
||||
// Escape attempt per B press. It goes where a real key would: the focused element (handlers
|
||||
// sit on it or above it; events only bubble up, so sending from a wrapper around it, such as
|
||||
// a scroll box, misses them). If nothing inside has focus: SofaNav's element, or the first
|
||||
// focusable thing in it. Resolves true if it closed.
|
||||
// Escape attempt per B press. It goes where a real key would: the focused element, wherever
|
||||
// it is (handlers sit on it or above it; events only bubble up). `box` may not contain it:
|
||||
// X's mask is a sibling of its menu, and X focuses the menu. Only with nothing focused:
|
||||
// SofaNav's element (or the first focusable thing in it) if it's inside, else the box.
|
||||
// Resolves true if the box closed.
|
||||
async function dismissOpen(box, trigger) {
|
||||
const ae = document.activeElement, cur = nodeOf(current);
|
||||
let target = ae && ae !== document.body && box.contains(ae) ? ae : null;
|
||||
let target = ae && ae !== document.body && ae !== document.documentElement ? ae : null;
|
||||
if (!target && cur instanceof Element && box.contains(cur)) target = cur.querySelector(FOCUSABLE_SEL) || cur;
|
||||
sendEscape(target || box);
|
||||
backNote(`Escape sent from ${brief(target || box)}`);
|
||||
|
||||
+10
-3
@@ -77,8 +77,15 @@ const checks = {
|
||||
}, [btn, sel]);
|
||||
await press('ArrowUp');
|
||||
await page.evaluate(() => { location.hash = 'before-menu'; });
|
||||
// As on X: from the menu's scroll box, X then ↓ lands on the role="group" (as in the user's
|
||||
// debug report); one B must close the menu.
|
||||
// As in the user's report from x.com: SofaNav still on the account button, X's focus on the
|
||||
// first menu item, a mask beside the menu. One B must close it.
|
||||
await page.evaluate(() => { const b = document.getElementById('menuBtn'); b.scrollIntoView({ block: 'center' }); __sofanav.focus(b); b.click(); });
|
||||
await wait(300);
|
||||
await press('Backspace', 700);
|
||||
const fromButton = await page.evaluate(() => ({ open: !document.getElementById('xMenu').hidden, hash: location.hash }));
|
||||
if (fromButton.hash !== '#before-menu') return { ok: false, detail: `B on the account button went back a page (menu open=${fromButton.open})` };
|
||||
if (fromButton.open) return { ok: false, detail: 'X-style menu still open after one B on the account button' };
|
||||
// From the menu's scroll box, X then ↓ lands on the role="group" (the earlier report).
|
||||
await open('menuBtn', '#xScroll');
|
||||
await wait(300);
|
||||
await press('KeyX');
|
||||
@@ -116,7 +123,7 @@ const checks = {
|
||||
await wait(300);
|
||||
await press('KeyF');
|
||||
r.on = await page.evaluate(() => !!__sofanav.mouse());
|
||||
const blank = await page.evaluate(() => { for (let y = 60; y < innerHeight; y += 20) for (let x = innerWidth - 20; x > innerWidth / 2; x -= 40) { const e = document.elementFromPoint(x, y); if (e === document.body || e === document.documentElement) return [x, y]; } return null; });
|
||||
const blank = await page.evaluate(() => { for (let y = 60; y < innerHeight; y += 20) for (let x = innerWidth - 20; x > innerWidth / 2; x -= 40) { const e = document.elementFromPoint(x, y); if (e === document.body || e === document.documentElement || e.id === 'xMask') return [x, y]; } return null; });
|
||||
await mouseTo(...blank);
|
||||
await press('Enter');
|
||||
r.menuClosed = await page.evaluate(() => document.getElementById('xMenu').hidden);
|
||||
|
||||
+12
-8
@@ -281,11 +281,12 @@
|
||||
<button type="button" id="stuckBtn">Overlay that ignores Escape</button>
|
||||
<button type="button" id="popBtn" popovertarget="pop">Popover menu</button>
|
||||
</div>
|
||||
<!-- Like X's real account menu (checked on x.com): a small position:fixed box, no mask, no
|
||||
menu/dialog markup, the button isn't marked expanded; a role="group" holds a scroll box
|
||||
(a SofaNav container) of links, and Escape is handled on the group, so it must be sent
|
||||
from inside it. -->
|
||||
<div id="xMenu" hidden style="position:fixed;left:40px;bottom:80px;width:300px;z-index:50;background:#2a2a2a;border:1px solid #555;border-radius:8px">
|
||||
<!-- Like X's real account menu (checked on x.com): a small position:fixed box, no menu/dialog
|
||||
markup, the button isn't marked expanded; a role="group" holds a scroll box (a SofaNav
|
||||
container) of links, and Escape is handled on the group, so it must come from inside it.
|
||||
A transparent full-screen mask sits *beside* the menu (not around it) and closes it on click. -->
|
||||
<div id="xMask" hidden style="position:fixed;inset:0;z-index:49"></div>
|
||||
<div id="xMenu" hidden style="position:fixed;left:16px;bottom:80px;width:150px;z-index:50;background:#2a2a2a;border:1px solid #555;border-radius:8px">
|
||||
<div role="group" tabindex="0" id="xGroup">
|
||||
<div id="xScroll" style="max-height:90px;overflow-y:auto;padding:8px">
|
||||
<div><a href="#add" role="menuitem">Add an existing account</a></div>
|
||||
@@ -332,10 +333,13 @@
|
||||
});
|
||||
document.getElementById('openDialog').onclick = () => document.getElementById('dlg').showModal();
|
||||
const xMenu = document.getElementById('xMenu'), menuLayer = document.getElementById('menuLayer');
|
||||
document.getElementById('menuBtn').onclick = () => { xMenu.hidden = false; document.querySelector('#xScroll a').focus(); };
|
||||
document.getElementById('xGroup').addEventListener('keydown', (e) => { if (e.key === 'Escape') xMenu.hidden = true; });
|
||||
const xMask = document.getElementById('xMask');
|
||||
const setX = (open) => { xMenu.hidden = xMask.hidden = !open; };
|
||||
document.getElementById('menuBtn').onclick = () => { setX(true); document.querySelector('#xScroll a').focus(); };
|
||||
document.getElementById('xGroup').addEventListener('keydown', (e) => { if (e.key === 'Escape') setX(false); });
|
||||
xMask.addEventListener('click', () => setX(false));
|
||||
// Like X: a press outside the menu closes it.
|
||||
document.addEventListener('pointerdown', (e) => { if (!xMenu.hidden && !xMenu.contains(e.target) && e.target.id !== 'menuBtn') xMenu.hidden = true; });
|
||||
document.addEventListener('pointerdown', (e) => { if (!xMenu.hidden && !xMenu.contains(e.target) && e.target.id !== 'menuBtn') setX(false); });
|
||||
const hoverCard = document.getElementById('hoverCard'), hoverTools = document.getElementById('hoverTools');
|
||||
hoverCard.addEventListener('mouseenter', () => { hoverTools.style.display = 'block'; });
|
||||
hoverCard.addEventListener('mouseleave', () => { hoverTools.style.display = 'none'; });
|
||||
|
||||
Reference in new issue
Block a user