Restore focus from non-button session action menus
This commit is contained in:
1 parent
bc85711af2
commit
d7c2cc2d5d
3 files changed
+66
-4
No files matched your search
+11
-3
@@ -3778,6 +3778,7 @@ let _sessionActionMenu = null;
|
||||
let _sessionActionAnchor = null;
|
||||
let _sessionActionSessionId = null;
|
||||
let _sessionActionMenuId = 0;
|
||||
let _sessionActionPreviousFocus = null;
|
||||
const _expandedChildSessionKeys = new Set();
|
||||
const _expandedLineageKeys = new Set();
|
||||
const _lineageReportCache = new Map();
|
||||
@@ -4176,8 +4177,15 @@ function _showBatchProjectPicker(){
|
||||
setTimeout(()=>document.addEventListener('click',close),0);
|
||||
}
|
||||
|
||||
function _focusSessionActionMenuRestoreTarget(target){
|
||||
if(!target||!target.isConnected||typeof target.focus!=='function') return false;
|
||||
try{target.focus({preventScroll:true});}catch(_){target.focus();}
|
||||
return document.activeElement===target;
|
||||
}
|
||||
|
||||
function closeSessionActionMenu({restoreFocus=false}={}){
|
||||
const focusTarget=restoreFocus?_sessionActionAnchor:null;
|
||||
const fallbackFocusTarget=restoreFocus?_sessionActionPreviousFocus:null;
|
||||
if(_sessionActionMenu){
|
||||
_sessionActionMenu.remove();
|
||||
_sessionActionMenu = null;
|
||||
@@ -4193,9 +4201,8 @@ function closeSessionActionMenu({restoreFocus=false}={}){
|
||||
_sessionActionAnchor = null;
|
||||
}
|
||||
_sessionActionSessionId = null;
|
||||
if(focusTarget&&focusTarget.isConnected&&typeof focusTarget.focus==='function'){
|
||||
try{focusTarget.focus({preventScroll:true});}catch(_){focusTarget.focus();}
|
||||
}
|
||||
_sessionActionPreviousFocus = null;
|
||||
if(!_focusSessionActionMenuRestoreTarget(focusTarget)) _focusSessionActionMenuRestoreTarget(fallbackFocusTarget);
|
||||
}
|
||||
|
||||
function _sessionActionMenuShouldIgnoreScrollTarget(target){
|
||||
@@ -4317,6 +4324,7 @@ async function _copySessionLink(session){
|
||||
}
|
||||
|
||||
function _mountSessionActionMenu(menu, session, anchorEl){
|
||||
_sessionActionPreviousFocus=document.activeElement;
|
||||
document.body.appendChild(menu);
|
||||
_sessionActionMenu = menu;
|
||||
_sessionActionAnchor = anchorEl;
|
||||
|
||||
@@ -41,9 +41,11 @@ def _fixture_script() -> str:
|
||||
"let _sessionActionMenu = null;",
|
||||
"let _sessionActionAnchor = null;",
|
||||
"let _sessionActionSessionId = null;",
|
||||
"let _sessionActionPreviousFocus = null;",
|
||||
"const esc = value => String(value);",
|
||||
"function _positionSessionActionMenu(){}",
|
||||
"function _playSessionActionMenuEntrance(){}",
|
||||
_function_source("_focusSessionActionMenuRestoreTarget"),
|
||||
_function_source("closeSessionActionMenu"),
|
||||
_function_source("_buildSessionAction"),
|
||||
_function_source("_mountSessionActionMenu"),
|
||||
@@ -89,6 +91,33 @@ def _fixture_script() -> str:
|
||||
row.remove();
|
||||
return result;
|
||||
};
|
||||
|
||||
window.__sessionActionMenuNonFocusableOpenerResult = () => {
|
||||
const priorFocus = document.createElement('button');
|
||||
priorFocus.textContent = 'Prior keyboard focus';
|
||||
const row = document.createElement('div');
|
||||
row.className = 'session-item';
|
||||
row.textContent = 'Non-focusable session row';
|
||||
document.body.append(priorFocus, row);
|
||||
priorFocus.focus();
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'session-action-menu';
|
||||
menu.id = 'sessionActionMenu-nonfocusable-opener-test';
|
||||
menu.setAttribute('role', 'menu');
|
||||
menu.setAttribute('aria-label', 'Conversation actions');
|
||||
menu.appendChild(_buildSessionAction('Rename conversation', '', '', () => {}));
|
||||
_mountSessionActionMenu(menu, {session_id: 'nonfocusable-opener-test'}, row);
|
||||
menu.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape', bubbles: true}));
|
||||
|
||||
const result = {
|
||||
menuRemovedOnEscape: !document.querySelector('.session-action-menu'),
|
||||
focusReturnedToPreviousControl: document.activeElement === priorFocus,
|
||||
};
|
||||
priorFocus.remove();
|
||||
row.remove();
|
||||
return result;
|
||||
};
|
||||
""",
|
||||
]
|
||||
)
|
||||
@@ -124,3 +153,26 @@ def test_session_action_menu_focus_lifecycle_in_browser():
|
||||
"expandedAfterEscape": "false",
|
||||
"controlsAfterEscape": None,
|
||||
}
|
||||
|
||||
|
||||
def test_session_action_menu_returns_to_prior_focus_for_nonfocusable_opener_in_browser():
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except Exception: # pragma: no cover - dependency missing path
|
||||
pytest.skip("playwright is unavailable; run the session action menu browser test")
|
||||
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(
|
||||
headless=True,
|
||||
args=["--no-sandbox", "--disable-dev-shm-usage"],
|
||||
)
|
||||
page = browser.new_page()
|
||||
page.set_content("<!doctype html><html><body></body></html>")
|
||||
page.add_script_tag(content=_fixture_script())
|
||||
result = page.evaluate("window.__sessionActionMenuNonFocusableOpenerResult()")
|
||||
browser.close()
|
||||
|
||||
assert result == {
|
||||
"menuRemovedOnEscape": True,
|
||||
"focusReturnedToPreviousControl": True,
|
||||
}
|
||||
@@ -55,7 +55,9 @@ def test_session_action_menu_exposes_popup_state_and_focus_contract():
|
||||
assert "_sessionActionAnchor.setAttribute('aria-expanded','false');" in close
|
||||
assert "_sessionActionAnchor.removeAttribute('aria-controls');" in close
|
||||
assert "restoreFocus" in close
|
||||
assert "focusTarget.focus({preventScroll:true});" in close
|
||||
assert "fallbackFocusTarget=restoreFocus?_sessionActionPreviousFocus:null;" in close
|
||||
assert "_focusSessionActionMenuRestoreTarget(focusTarget)" in close
|
||||
assert "_focusSessionActionMenuRestoreTarget(fallbackFocusTarget)" in close
|
||||
|
||||
escape_handler = _sessions_block("document.addEventListener('keydown',e=>{", "window.addEventListener('resize'")
|
||||
assert "closeSessionActionMenu({restoreFocus:true});" in escape_handler
|
||||
|
||||
Reference in new issue
Block a user