feat: contain mobile Device Setup focus (Closes #1134)
CI / lint (pull_request) Successful in 2m54s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 3m24s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-19 14:31:38 +00:00
1 parent f3c9e89920
commit 4b3e53f4d0
3 files changed
+142 -4

No files matched your search

+63 -2
View File
@@ -13,6 +13,26 @@
];
let trigger = options.launcher;
let ownsDetour = false;
let backgroundInert = null;
let pendingResume = true;
function focusableControls() {
return [...options.sheet.querySelectorAll(
'button:not([disabled]), select:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)].filter(control => !control.hidden);
}
function containBackground() {
if (backgroundInert) return;
backgroundInert = new Map((options.backgroundElements || []).map(element => [element, element.inert]));
backgroundInert.forEach((_wasInert, element) => { element.inert = true; });
}
function releaseBackground() {
if (!backgroundInert) return;
backgroundInert.forEach((wasInert, element) => { element.inert = wasInert; });
backgroundInert = null;
}
function readinessCounts(readiness) {
const available = steps.filter(([name]) => readiness[name].state !== 'unavailable').length;
@@ -57,17 +77,31 @@
ownsDetour = options.timerView?.beginDetour?.('device-setup')?.reason === 'device-setup';
}
await render();
containBackground();
options.sheet.hidden = false;
if (options.history && !options.history.state?.deviceSetup) {
options.history.pushState({...options.history.state, deviceSetup:true}, '');
}
options.closeButton.focus();
}
function close(resume = true) {
function finishClose(resume = true) {
options.sheet.hidden = true;
releaseBackground();
if (resume && ownsDetour) options.timerView?.finishDetour?.();
ownsDetour = false;
trigger?.focus?.();
}
function close(resume = true) {
if (options.history?.state?.deviceSetup) {
pendingResume = resume;
options.history.back();
return;
}
finishClose(resume);
}
async function start() {
options.launcher.addEventListener('click', open);
options.promptLauncher?.addEventListener('click', open);
@@ -83,7 +117,28 @@
if (event.target === options.sheet) close();
});
options.escapeTarget.addEventListener('keydown', event => {
if (event.key === 'Escape' && !options.sheet.hidden) close();
if (event.key === 'Escape' && !options.sheet.hidden) {
event.preventDefault?.();
close();
}
if (event.key !== 'Tab' || options.sheet.hidden) return;
const controls = focusableControls();
if (!controls.length) return;
const first = controls[0];
const last = controls[controls.length - 1];
if (event.shiftKey && event.target === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && event.target === last) {
event.preventDefault();
first.focus();
}
});
options.historyTarget?.addEventListener('popstate', event => {
if (!options.sheet.hidden && !event.state?.deviceSetup) {
finishClose(pendingResume);
pendingResume = true;
}
});
steps.forEach(([_name, button, _status, action]) => {
button.addEventListener('click', async () => {
@@ -106,6 +161,7 @@ if (typeof module === 'object' && module.exports) {
function mountMobileDeviceSetup(options) {
const qs = selector => options.document.querySelector(selector);
const window = options.window || options.document.defaultView;
return createMobileDeviceSetup({
launcher:qs('#open-device-setup'), closeButton:qs('#close-device-setup'),
sheet:qs('#device-setup-sheet'), installButton:qs('#device-setup-install'),
@@ -123,6 +179,11 @@ function mountMobileDeviceSetup(options) {
timerView:options.timerView,
isMobile:options.isMobile || (() => innerWidth <= 600),
escapeTarget:options.document,
history:window?.history,
historyTarget:window,
backgroundElements:[
qs('header'), qs('main'), qs('#mobile-task-dock'),
].filter(Boolean),
getReadiness:() => ({
install:options.installApp.state(),
offline:!options.offlineAvailable()