Merge pull request #977 from PR0M3TH3AN/codex/fix-login-modal-positioning

Ensure add-profile flow elevates the login modal
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-27 17:49:11 -04:00
commit 9dfdeaf8e8
2 files changed
+358

No files matched your search

+228
View File
@@ -1132,6 +1132,11 @@ export class ProfileModalController {
this.boundFocusIn = null;
this.focusableElements = [];
this.focusTrapContainer = null;
this.focusTrapSuspended = false;
this.focusTrapSuspendCount = 0;
this.focusTrapAriaHiddenBeforeSuspend = null;
this.focusTrapPointerEventsBeforeSuspend = null;
this.focusTrapVisibilityBeforeSuspend = null;
this.profileSwitcherSelectionPubkey = null;
this.previouslyFocusedElement = null;
this.largeLayoutQuery = null;
@@ -2843,6 +2848,26 @@ export class ProfileModalController {
this.setAddAccountLoading(true);
try {
this.bringLoginModalToFront();
} catch (error) {
devLogger.warn(
"[ProfileModalController] Failed to elevate login modal before add profile authentication:",
error,
);
}
let suspendedFocusTrap = false;
try {
this.suspendFocusTrap();
suspendedFocusTrap = true;
} catch (error) {
devLogger.warn(
"[ProfileModalController] Failed to suspend profile modal focus trap before login:",
error,
);
}
try {
const loginResult = await requestLogin({
controller: this,
@@ -2884,6 +2909,17 @@ export class ProfileModalController {
this.showError(message);
}
} finally {
if (suspendedFocusTrap) {
try {
this.resumeFocusTrap();
} catch (error) {
devLogger.warn(
"[ProfileModalController] Failed to resume profile modal focus trap after login:",
error,
);
}
}
this.setAddAccountLoading(false);
}
}
@@ -8118,6 +8154,11 @@ export class ProfileModalController {
return;
}
if (this.focusTrapSuspended) {
this.focusTrapContainer = targetContainer;
return;
}
if (!this.boundKeydown) {
this.boundKeydown = (event) => {
if (event.key === "Escape") {
@@ -8198,6 +8239,179 @@ export class ProfileModalController {
document.addEventListener("focusin", this.boundFocusIn);
}
getModalRootElement() {
if (this.profileModalRoot instanceof HTMLElement) {
return this.profileModalRoot;
}
if (this.profileModal instanceof HTMLElement) {
return this.profileModal;
}
return null;
}
getModalPanelElement() {
if (this.profileModalPanel instanceof HTMLElement) {
return this.profileModalPanel;
}
return this.getModalRootElement();
}
suspendFocusTrap() {
this.focusTrapSuspendCount += 1;
this.focusTrapSuspended = true;
if (this.focusTrapSuspendCount > 1) {
return this.focusTrapSuspendCount;
}
if (
this.boundKeydown &&
this.focusTrapContainer instanceof HTMLElement
) {
this.focusTrapContainer.removeEventListener(
"keydown",
this.boundKeydown,
);
}
if (this.boundFocusIn) {
document.removeEventListener("focusin", this.boundFocusIn);
}
const modalRoot = this.getModalRootElement();
if (modalRoot) {
this.focusTrapAriaHiddenBeforeSuspend = modalRoot.getAttribute(
"aria-hidden",
);
this.focusTrapPointerEventsBeforeSuspend =
typeof modalRoot.style.pointerEvents === "string" &&
modalRoot.style.pointerEvents
? modalRoot.style.pointerEvents
: null;
this.focusTrapVisibilityBeforeSuspend =
typeof modalRoot.style.visibility === "string" &&
modalRoot.style.visibility
? modalRoot.style.visibility
: null;
modalRoot.dataset.nestedModalActive = "true";
modalRoot.setAttribute("aria-hidden", "true");
modalRoot.style.setProperty("pointer-events", "none");
modalRoot.style.setProperty("visibility", "hidden");
} else {
this.focusTrapAriaHiddenBeforeSuspend = null;
this.focusTrapPointerEventsBeforeSuspend = null;
this.focusTrapVisibilityBeforeSuspend = null;
}
const panel = this.getModalPanelElement();
if (panel) {
panel.setAttribute("inert", "");
}
return this.focusTrapSuspendCount;
}
resumeFocusTrap() {
if (this.focusTrapSuspendCount === 0) {
return 0;
}
this.focusTrapSuspendCount = Math.max(
0,
this.focusTrapSuspendCount - 1,
);
if (this.focusTrapSuspendCount > 0) {
return this.focusTrapSuspendCount;
}
this.focusTrapSuspended = false;
const modalRoot = this.getModalRootElement();
if (modalRoot) {
delete modalRoot.dataset.nestedModalActive;
if (!modalRoot.classList.contains("hidden")) {
if (this.focusTrapAriaHiddenBeforeSuspend === null) {
modalRoot.removeAttribute("aria-hidden");
} else {
modalRoot.setAttribute(
"aria-hidden",
this.focusTrapAriaHiddenBeforeSuspend,
);
}
}
if (
this.focusTrapPointerEventsBeforeSuspend === null ||
this.focusTrapPointerEventsBeforeSuspend === undefined ||
this.focusTrapPointerEventsBeforeSuspend === ""
) {
modalRoot.style.removeProperty("pointer-events");
} else {
modalRoot.style.setProperty(
"pointer-events",
this.focusTrapPointerEventsBeforeSuspend,
);
}
if (
this.focusTrapVisibilityBeforeSuspend === null ||
this.focusTrapVisibilityBeforeSuspend === undefined ||
this.focusTrapVisibilityBeforeSuspend === ""
) {
modalRoot.style.removeProperty("visibility");
} else {
modalRoot.style.setProperty(
"visibility",
this.focusTrapVisibilityBeforeSuspend,
);
}
}
const panel = this.getModalPanelElement();
if (panel) {
panel.removeAttribute("inert");
}
this.focusTrapAriaHiddenBeforeSuspend = null;
this.focusTrapPointerEventsBeforeSuspend = null;
this.focusTrapVisibilityBeforeSuspend = null;
this.updateFocusTrap();
return 0;
}
bringLoginModalToFront() {
if (typeof document === "undefined") {
return;
}
const loginModal = document.getElementById("loginModal");
if (!(loginModal instanceof HTMLElement)) {
return;
}
const container =
this.modalContainer instanceof HTMLElement
? this.modalContainer
: loginModal.parentElement;
if (!container) {
return;
}
if (loginModal.parentElement !== container) {
container.appendChild(loginModal);
return;
}
if (container.lastElementChild !== loginModal) {
container.appendChild(loginModal);
}
}
ensureModalOrder(modalRoot) {
if (!(modalRoot instanceof HTMLElement)) {
return;
@@ -8421,6 +8635,9 @@ export class ProfileModalController {
if (modalElement) {
modalElement.classList.add("hidden");
modalElement.setAttribute("aria-hidden", "true");
delete modalElement.dataset.nestedModalActive;
modalElement.style.removeProperty("pointer-events");
modalElement.style.removeProperty("visibility");
this.setGlobalModalState("profile", false);
this.setMobileView("menu", { skipFocusTrap: true });
@@ -8436,8 +8653,19 @@ export class ProfileModalController {
document.removeEventListener("focusin", this.boundFocusIn);
}
const panel = this.getModalPanelElement();
if (panel) {
panel.removeAttribute("inert");
}
this.focusTrapContainer = null;
this.focusTrapSuspendCount = 0;
this.focusTrapSuspended = false;
this.focusTrapAriaHiddenBeforeSuspend = null;
this.focusTrapPointerEventsBeforeSuspend = null;
this.focusTrapVisibilityBeforeSuspend = null;
if (
this.boundProfileHistoryVisibility &&
typeof document !== "undefined" &&
+130
View File
@@ -26,6 +26,20 @@ async function waitForAnimationFrame(window, cycles = 1) {
}
}
function createDeferred() {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return {
promise,
resolve,
reject,
};
}
function createMatchMediaList(query, initialMatches = false) {
let matches = Boolean(initialMatches);
const listeners = new Set();
@@ -384,6 +398,122 @@ for (const _ of [0]) {
},
);
test(
'Add profile request suspends focus trap and elevates login modal',
async (t) => {
const loginDeferred = createDeferred();
const loginModal = document.createElement('div');
loginModal.id = 'loginModal';
loginModal.className = 'bv-modal hidden';
container.appendChild(loginModal);
const controller = createController({
services: {
requestAddProfileLogin: async ({ triggerElement }) => {
assert.strictEqual(triggerElement, controller.addAccountButton);
assert.equal(controller.focusTrapSuspended, true);
return loginDeferred.promise;
},
},
callbacks: {
onAddAccount: async () => {},
},
});
let cleanupRan = false;
const cleanup = () => {
try {
loginModal.remove();
} catch {}
try {
controller.hide({ silent: true });
} catch {}
cleanupRan = true;
};
try {
await controller.load();
applyDesignSystemAttributes(document);
await controller.show('account');
await waitForAnimationFrame(window, 1);
const loginPromise = controller.handleAddAccountRequest();
await Promise.resolve();
const profileModalRoot =
document.getElementById('profileModal') || controller.profileModal;
assert.ok(profileModalRoot, 'profile modal root should exist');
assert.equal(
profileModalRoot?.dataset.nestedModalActive,
'true',
'profile modal should mark nested modal state',
);
assert.equal(
profileModalRoot?.getAttribute('aria-hidden'),
'true',
'profile modal should be hidden from assistive tech',
);
const panel =
profileModalRoot?.querySelector('.bv-modal__panel') || profileModalRoot;
assert.ok(panel?.hasAttribute('inert'));
assert.equal(
profileModalRoot?.style.visibility,
'hidden',
'profile modal should hide visually while login modal is active',
);
assert.equal(
profileModalRoot?.style.pointerEvents,
'none',
'profile modal should not intercept pointer events while login modal is active',
);
assert.strictEqual(
container.lastElementChild,
loginModal,
'login modal should be last in the container',
);
loginDeferred.resolve({ provider: 'test' });
await loginPromise;
await waitForAnimationFrame(window, 1);
assert.equal(controller.focusTrapSuspended, false);
assert.equal(
profileModalRoot?.dataset.nestedModalActive,
undefined,
'profile modal nested state should clear after login',
);
assert.equal(
profileModalRoot?.getAttribute('aria-hidden'),
'false',
'profile modal should restore aria-hidden',
);
assert.equal(panel?.hasAttribute('inert'), false);
assert.equal(
profileModalRoot?.style.visibility ?? '',
'',
'profile modal visibility should restore after login',
);
assert.equal(
profileModalRoot?.style.pointerEvents ?? '',
'',
'profile modal pointer events should restore after login',
);
} finally {
cleanup();
}
t.after(() => {
if (!cleanupRan) {
cleanup();
}
resetRuntimeFlags();
});
},
);
test(
'Profile modal navigation buttons toggle active state',
async (t) => {