mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #977 from PR0M3TH3AN/codex/fix-login-modal-positioning
Ensure add-profile flow elevates the login modal
This commit is contained in:
2 files changed
+358
No files matched your search
@@ -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" &&
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user