mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 01:20:07 -05:00
Thanks to MarcusDger and xet7 . Fixes #5667
This commit is contained in:
1 parent
b26494766f
commit
ac368de06a
5 files changed
+326
-102
No files matched your search
@@ -39,6 +39,24 @@ them up next.
|
||||
[#3823](https://github.com/wekan/wekan/issues/3823), [#3138](https://github.com/wekan/wekan/issues/3138),
|
||||
[#2204](https://github.com/wekan/wekan/issues/2204) (restrict permanent delete to the Admin role).
|
||||
|
||||
# Upcoming WeKan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- **[Fix #5667: date-picker calendar stays fully visible when opened low on a scrolled page](https://github.com/wekan/wekan/issues/5667)**:
|
||||
Opening a date field (due/start/end date, or a date custom field) low on the screen showed the
|
||||
calendar popup extending past the visible area, and — because the pop-over is `position: absolute`
|
||||
(document coordinates) — scrolling to reach it moved the calendar along with the page, so the full
|
||||
calendar could never be seen (the workaround was to close it, drag the field to the center and
|
||||
reopen). `Popup._getOffset` computed the space above/below the opener and the clamped `top` from
|
||||
the opener's DOCUMENT offset mixed with the VIEWPORT height, ignoring the page scroll, so on a
|
||||
scrolled page the anchored popup landed outside the visible viewport. The geometry now runs in
|
||||
viewport coordinates (subtracting the page scroll) and clamps the popup fully within the visible
|
||||
viewport, then converts back to document coordinates for the absolute style; when the page is not
|
||||
scrolled the output is unchanged. Extracted the math into the pure, unit-tested
|
||||
`client/lib/popupOffset.js`, used by `client/lib/popup.js`. Covered by `tests/popupOffset.test.cjs`.
|
||||
Thanks to MarcusDger and xet7.
|
||||
|
||||
# v9.79 2026-07-06 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
+27
-101
@@ -4,6 +4,7 @@ import { Tracker } from 'meteor/tracker';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { computePopupOffset } from '/client/lib/popupOffset';
|
||||
|
||||
window.Popup = new (class {
|
||||
constructor() {
|
||||
@@ -222,108 +223,33 @@ window.Popup = new (class {
|
||||
return () => {
|
||||
Utils.windowResizeDep.depend();
|
||||
|
||||
if (Utils.isMiniScreen()) return { left: 0, top: 0 };
|
||||
// #5667: read the current page scroll and the opener's document offset,
|
||||
// then delegate the geometry to the pure, unit-tested computePopupOffset.
|
||||
// It lays the popup out in VIEWPORT coordinates — so a date-picker opened
|
||||
// low on a scrolled page stays fully visible instead of extending past the
|
||||
// visible area — and returns document coordinates for the absolute style.
|
||||
const hasOpener = !!($element && $element.length);
|
||||
const offset = hasOpener ? $element.offset() : null;
|
||||
const opener = hasOpener
|
||||
? { top: offset.top, left: offset.left, height: $element.outerHeight() }
|
||||
: null;
|
||||
|
||||
const viewportWidth = $(window).width();
|
||||
const viewportHeight = $(window).height();
|
||||
const viewportPadding = 10;
|
||||
// Calculate actual popup width based on CSS: min(380px, 55vw)
|
||||
const popupWidth = Math.min(380, viewportWidth * 0.55);
|
||||
|
||||
// Card details popup: dock it to the top of the viewport so it overlays
|
||||
// the global and board header bars (its z-index already sits above them)
|
||||
// instead of opening downward from the clicked minicard.
|
||||
if (popupName === 'cardDetailsPopup') {
|
||||
const cardWidth = Math.min(600, viewportWidth * 0.8);
|
||||
const centeredLeft = (viewportWidth - cardWidth) / 2;
|
||||
// top is forced to 0 in CSS (.pop-over[data-popup="cardDetailsPopup"])
|
||||
// so the card overlays the header bars; use the full viewport height.
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft),
|
||||
top: 0,
|
||||
maxHeight: viewportHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// If the opener element is missing (e.g., programmatic open), fallback to viewport origin
|
||||
if (!$element || $element.length === 0) {
|
||||
return {
|
||||
left: viewportPadding,
|
||||
top: viewportPadding,
|
||||
maxHeight: viewportHeight - viewportPadding * 2,
|
||||
};
|
||||
}
|
||||
|
||||
const offset = $element.offset();
|
||||
const openerTop = offset.top;
|
||||
const openerBottom = offset.top + $element.outerHeight();
|
||||
const clampedLeft = Math.max(
|
||||
viewportPadding,
|
||||
Math.min(offset.left, viewportWidth - popupWidth - viewportPadding),
|
||||
);
|
||||
|
||||
// Check if this is an admin panel edit popup
|
||||
const isAdminEditPopup = $element.hasClass('edit-user') ||
|
||||
$element.hasClass('edit-org') ||
|
||||
$element.hasClass('edit-team');
|
||||
|
||||
if (isAdminEditPopup) {
|
||||
// Center the popup horizontally and use full height
|
||||
const centeredLeft = (viewportWidth - popupWidth) / 2;
|
||||
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft), // Ensure popup doesn't go off screen
|
||||
top: viewportPadding, // Start from top with small margin
|
||||
maxHeight: viewportHeight - viewportPadding * 2, // Use full height minus small margins
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate available heights around the opener
|
||||
const spaceBelow = viewportHeight - openerBottom - viewportPadding;
|
||||
const spaceAbove = openerTop - viewportPadding;
|
||||
const preferBelow = spaceBelow >= spaceAbove;
|
||||
|
||||
// For language popup, don't use dynamic height to avoid overlapping board
|
||||
const isLanguagePopup = $element.hasClass('js-change-language');
|
||||
let availableHeight;
|
||||
|
||||
if (isLanguagePopup) {
|
||||
// For language popup, position content area below right vertical scrollbar
|
||||
const languageTop = openerBottom;
|
||||
const languageAvailable = Math.max(
|
||||
0,
|
||||
viewportHeight - languageTop - viewportPadding,
|
||||
);
|
||||
const calculatedHeight = Math.min(
|
||||
languageAvailable,
|
||||
viewportHeight * 0.5,
|
||||
); // Max 50% of viewport
|
||||
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top: languageTop,
|
||||
maxHeight: Math.max(calculatedHeight, 0),
|
||||
};
|
||||
} else {
|
||||
// For other popups, choose the side with more space and keep popup fully in viewport.
|
||||
if (preferBelow) {
|
||||
availableHeight = Math.max(0, Math.min(spaceBelow, viewportHeight * 0.8));
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top: openerBottom,
|
||||
maxHeight: availableHeight,
|
||||
};
|
||||
}
|
||||
|
||||
availableHeight = Math.max(0, Math.min(spaceAbove, viewportHeight * 0.8));
|
||||
const top = Math.max(viewportPadding, openerTop - availableHeight);
|
||||
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top,
|
||||
maxHeight: availableHeight,
|
||||
};
|
||||
}
|
||||
return computePopupOffset({
|
||||
viewportWidth: $(window).width(),
|
||||
viewportHeight: $(window).height(),
|
||||
scrollTop: $(window).scrollTop() || 0,
|
||||
scrollLeft: $(window).scrollLeft() || 0,
|
||||
opener,
|
||||
popupName,
|
||||
isMiniScreen: Utils.isMiniScreen(),
|
||||
isAdminEditPopup:
|
||||
hasOpener &&
|
||||
($element.hasClass('edit-user') ||
|
||||
$element.hasClass('edit-org') ||
|
||||
$element.hasClass('edit-team')),
|
||||
isLanguagePopup: hasOpener && $element.hasClass('js-change-language'),
|
||||
viewportPadding: 10,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
'use strict';
|
||||
|
||||
// Pure geometry for Popup._getOffset — extracted so the positioning math is
|
||||
// unit-testable without a DOM / Meteor (see tests/popupOffset.test.cjs).
|
||||
//
|
||||
// #5667 ("Problem when scrolling in a map with date fields"): a date-picker
|
||||
// calendar (due/start/end date, or a date custom field) opened low on the screen
|
||||
// appeared partly outside the visible area, and — because the pop-over is
|
||||
// `position: absolute` (document coordinates) — trying to scroll it into view
|
||||
// moved it along with the page, so the calendar could never be seen fully.
|
||||
//
|
||||
// Root cause: _getOffset computed the space above / below the opener (and the
|
||||
// clamped `top`) from the opener's DOCUMENT offset (`$el.offset()`) mixed with
|
||||
// the VIEWPORT height, ignoring the page scroll. On a scrolled page that placed
|
||||
// the anchored popup outside the visible viewport.
|
||||
//
|
||||
// Fix: do the space / clamp math in VIEWPORT coordinates (subtract
|
||||
// scrollTop/scrollLeft), then convert the chosen top/left back to DOCUMENT
|
||||
// coordinates (add the scroll back) for the absolute style. The popup box is then
|
||||
// always laid out fully within the visible viewport. When the page is not
|
||||
// scrolled (scrollTop === scrollLeft === 0) the output is identical to the
|
||||
// previous behaviour, so nothing regresses in the common case.
|
||||
|
||||
function computePopupOffset(params) {
|
||||
const {
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
scrollTop = 0,
|
||||
scrollLeft = 0,
|
||||
// Opener document offset { top, left, height }, or null for a programmatic
|
||||
// open with no reference element.
|
||||
opener = null,
|
||||
popupName,
|
||||
isMiniScreen = false,
|
||||
isAdminEditPopup = false,
|
||||
isLanguagePopup = false,
|
||||
viewportPadding = 10,
|
||||
} = params;
|
||||
|
||||
if (isMiniScreen) return { left: 0, top: 0 };
|
||||
|
||||
// Actual popup width from CSS: min(380px, 55vw).
|
||||
const popupWidth = Math.min(380, viewportWidth * 0.55);
|
||||
|
||||
// Card details popup: docked to the top of the viewport (CSS also forces
|
||||
// top:0) so it overlays the header bars instead of opening from the minicard.
|
||||
if (popupName === 'cardDetailsPopup') {
|
||||
const cardWidth = Math.min(600, viewportWidth * 0.8);
|
||||
const centeredLeft = (viewportWidth - cardWidth) / 2;
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft),
|
||||
top: scrollTop, // viewport top in document coords
|
||||
maxHeight: viewportHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// No opener element: fall back to the top-left of the VISIBLE viewport.
|
||||
if (!opener) {
|
||||
return {
|
||||
left: viewportPadding + scrollLeft,
|
||||
top: viewportPadding + scrollTop,
|
||||
maxHeight: viewportHeight - viewportPadding * 2,
|
||||
};
|
||||
}
|
||||
|
||||
// Opener position in viewport coordinates (this is the #5667 fix: the previous
|
||||
// code used the raw document offset here).
|
||||
const openerTopVp = opener.top - scrollTop;
|
||||
const openerBottomVp = openerTopVp + opener.height;
|
||||
const openerLeftVp = opener.left - scrollLeft;
|
||||
|
||||
const clampedLeftVp = Math.max(
|
||||
viewportPadding,
|
||||
Math.min(openerLeftVp, viewportWidth - popupWidth - viewportPadding),
|
||||
);
|
||||
const clampedLeft = clampedLeftVp + scrollLeft;
|
||||
|
||||
// Admin panel edit popups: horizontally centered, anchored to the viewport top.
|
||||
if (isAdminEditPopup) {
|
||||
const centeredLeft = (viewportWidth - popupWidth) / 2;
|
||||
return {
|
||||
left: Math.max(viewportPadding, centeredLeft),
|
||||
top: viewportPadding + scrollTop,
|
||||
maxHeight: viewportHeight - viewportPadding * 2,
|
||||
};
|
||||
}
|
||||
|
||||
const spaceBelow = viewportHeight - openerBottomVp - viewportPadding;
|
||||
const spaceAbove = openerTopVp - viewportPadding;
|
||||
const preferBelow = spaceBelow >= spaceAbove;
|
||||
|
||||
// Language popup: fixed-ish height below the opener, capped at 50% viewport.
|
||||
if (isLanguagePopup) {
|
||||
const languageTopVp = openerBottomVp;
|
||||
const languageAvailable = Math.max(
|
||||
0,
|
||||
viewportHeight - languageTopVp - viewportPadding,
|
||||
);
|
||||
const calculatedHeight = Math.min(languageAvailable, viewportHeight * 0.5);
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top: languageTopVp + scrollTop,
|
||||
maxHeight: Math.max(calculatedHeight, 0),
|
||||
};
|
||||
}
|
||||
|
||||
// Other popups: open on the side with more room and stay fully in the viewport.
|
||||
if (preferBelow) {
|
||||
const maxHeight = Math.max(0, Math.min(spaceBelow, viewportHeight * 0.8));
|
||||
return { left: clampedLeft, top: openerBottomVp + scrollTop, maxHeight };
|
||||
}
|
||||
|
||||
const maxHeight = Math.max(0, Math.min(spaceAbove, viewportHeight * 0.8));
|
||||
const topVp = Math.max(viewportPadding, openerTopVp - maxHeight);
|
||||
return { left: clampedLeft, top: topVp + scrollTop, maxHeight };
|
||||
}
|
||||
|
||||
module.exports = { computePopupOffset };
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "meteor test --once --driver-package meteortesting:mocha",
|
||||
"test:unit:node": "node tests/filenameSanitizer.test.cjs && node tests/cardBoardConsistency.test.cjs && node tests/cardUrl.test.cjs && node tests/defaultSwimlane.test.cjs && node tests/cardMoveModifier.test.cjs && node tests/authenticationMethod.test.cjs && node tests/linkedCardTarget.test.cjs && node tests/staleWhileRevalidate.test.cjs && node tests/restArrayParam.test.cjs && node tests/minicardLabelHeight.test.cjs && node tests/dateUtils.normalizeDigits.test.cjs && node tests/reconcileBoardTeamMembers.test.cjs && node tests/rulesBoardSelector.test.cjs && node tests/notificationCleanup.test.cjs && node tests/cardSearch.test.cjs && node tests/linkedCardMembers.test.cjs && node tests/verificationEmail.test.cjs && node tests/boardSortReorder.test.cjs && node tests/checklistItemTitles.test.cjs && node tests/swimlaneFilter.test.cjs && node tests/unit/resetPasswordEmail.test.js",
|
||||
"test:unit:node": "node tests/filenameSanitizer.test.cjs && node tests/cardBoardConsistency.test.cjs && node tests/cardUrl.test.cjs && node tests/defaultSwimlane.test.cjs && node tests/cardMoveModifier.test.cjs && node tests/authenticationMethod.test.cjs && node tests/linkedCardTarget.test.cjs && node tests/staleWhileRevalidate.test.cjs && node tests/restArrayParam.test.cjs && node tests/minicardLabelHeight.test.cjs && node tests/dateUtils.normalizeDigits.test.cjs && node tests/reconcileBoardTeamMembers.test.cjs && node tests/rulesBoardSelector.test.cjs && node tests/notificationCleanup.test.cjs && node tests/cardSearch.test.cjs && node tests/linkedCardMembers.test.cjs && node tests/verificationEmail.test.cjs && node tests/boardSortReorder.test.cjs && node tests/checklistItemTitles.test.cjs && node tests/swimlaneFilter.test.cjs && node tests/popupOffset.test.cjs && node tests/unit/resetPasswordEmail.test.js",
|
||||
"test:unit:all": "npm run test:unit:node && node tests/cards.stickers.test.js && node tests/stickers.test.js && node tests/stickers.packs.test.js && node tests/linkCardPopup.test.js && node tests/trelloCreator.import.test.js && node tests/trelloImport.maplink.test.js && node tests/trelloImport.zip.test.js && node tests/wekanCreator.import.test.js && node tests/wekanCreator.inconsistent.test.js && node tests/unit/oauth2-admin-groups.test.js",
|
||||
"test:watch": "meteor test --driver-package meteortesting:mocha",
|
||||
"test:e2e": "node tests/e2e/list-regressions.js",
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
'use strict';
|
||||
|
||||
// Plain-Node unit test (no DOM/Meteor) for the popup positioning geometry.
|
||||
// Run: node tests/popupOffset.test.cjs
|
||||
//
|
||||
// Regression guard for #5667 ("Problem when scrolling in a map with date
|
||||
// fields"): a date-picker opened low on a SCROLLED page used to be positioned
|
||||
// from the opener's DOCUMENT offset mixed with the VIEWPORT height, so it landed
|
||||
// partly outside the visible viewport (and, being position:absolute, scrolled
|
||||
// away when the user tried to reach it). computePopupOffset must lay the popup
|
||||
// out so it is fully within the visible viewport for any scroll position, while
|
||||
// staying byte-identical to the old behaviour when the page is not scrolled.
|
||||
|
||||
const assert = require('assert');
|
||||
const { computePopupOffset } = require('../client/lib/popupOffset');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
const VW = 1200;
|
||||
const VH = 800;
|
||||
const PAD = 10;
|
||||
|
||||
// The popup box in VIEWPORT coordinates (result.top is a DOCUMENT coordinate).
|
||||
function viewportBox(result, scrollTop) {
|
||||
const topVp = result.top - scrollTop;
|
||||
return { topVp, bottomVp: topVp + (result.maxHeight || 0) };
|
||||
}
|
||||
function assertFullyVisible(result, scrollTop, label) {
|
||||
const { topVp, bottomVp } = viewportBox(result, scrollTop);
|
||||
assert.ok(topVp >= 0, `${label}: top ${topVp} must be >= 0 (in viewport)`);
|
||||
assert.ok(
|
||||
bottomVp <= VH + 0.001,
|
||||
`${label}: bottom ${bottomVp} must be <= viewportHeight ${VH}`,
|
||||
);
|
||||
}
|
||||
|
||||
// --- POSITIVE: fully visible regardless of scroll / opener position ---------
|
||||
test('#5667: low opener on a scrolled page stays fully within the viewport', () => {
|
||||
const scrollTop = 1000;
|
||||
// Opener is near the BOTTOM of the viewport (viewport y ~= 720), so its
|
||||
// document top is 1000 + 720.
|
||||
const result = computePopupOffset({
|
||||
viewportWidth: VW,
|
||||
viewportHeight: VH,
|
||||
scrollTop,
|
||||
opener: { top: scrollTop + 720, left: 300, height: 24 },
|
||||
popupName: 'editCardDueDatePopup',
|
||||
});
|
||||
assertFullyVisible(result, scrollTop, 'low+scrolled');
|
||||
// A low opener has more room above, so it opens upward (bottom is at/above the opener).
|
||||
assert.ok(result.top - scrollTop < 720, 'should open above the low opener');
|
||||
});
|
||||
|
||||
test('#5667: opener near the top of a scrolled page does not overflow the top', () => {
|
||||
const scrollTop = 1000;
|
||||
const result = computePopupOffset({
|
||||
viewportWidth: VW,
|
||||
viewportHeight: VH,
|
||||
scrollTop,
|
||||
opener: { top: scrollTop + 40, left: 300, height: 24 },
|
||||
popupName: 'editCardDueDatePopup',
|
||||
});
|
||||
assertFullyVisible(result, scrollTop, 'top+scrolled');
|
||||
});
|
||||
|
||||
test('opener in the upper half opens below and stays visible', () => {
|
||||
const result = computePopupOffset({
|
||||
viewportWidth: VW,
|
||||
viewportHeight: VH,
|
||||
scrollTop: 0,
|
||||
opener: { top: 100, left: 300, height: 24 },
|
||||
popupName: 'editCardDueDatePopup',
|
||||
});
|
||||
assert.strictEqual(result.top, 124, 'opens just below the opener (100+24)');
|
||||
assertFullyVisible(result, 0, 'upper-half');
|
||||
});
|
||||
|
||||
test('scrollLeft keeps the popup within the viewport horizontally', () => {
|
||||
const result = computePopupOffset({
|
||||
viewportWidth: VW,
|
||||
viewportHeight: VH,
|
||||
scrollTop: 0,
|
||||
scrollLeft: 500,
|
||||
opener: { top: 100, left: 500 + 1195, height: 24 }, // far right in viewport
|
||||
popupName: 'editCardDueDatePopup',
|
||||
});
|
||||
const leftVp = result.left - 500;
|
||||
const popupWidth = Math.min(380, VW * 0.55);
|
||||
assert.ok(leftVp >= PAD, 'left within viewport');
|
||||
assert.ok(leftVp + popupWidth <= VW - PAD + 0.001, 'right edge within viewport');
|
||||
});
|
||||
|
||||
// --- Behaviour parity when NOT scrolled (no regression) ---------------------
|
||||
test('unscrolled output equals the pre-fix behaviour (below case)', () => {
|
||||
const r = computePopupOffset({
|
||||
viewportWidth: VW, viewportHeight: VH, scrollTop: 0,
|
||||
opener: { top: 200, left: 300, height: 24 }, popupName: 'x',
|
||||
});
|
||||
// Old code: top = openerBottom = 224; maxHeight = min(spaceBelow, 0.8*VH).
|
||||
const spaceBelow = VH - 224 - PAD;
|
||||
assert.strictEqual(r.top, 224);
|
||||
assert.strictEqual(r.maxHeight, Math.min(spaceBelow, VH * 0.8));
|
||||
});
|
||||
|
||||
test('special popups: cardDetails / no-opener / admin edit', () => {
|
||||
const cd = computePopupOffset({ viewportWidth: VW, viewportHeight: VH, scrollTop: 0, popupName: 'cardDetailsPopup' });
|
||||
assert.strictEqual(cd.top, 0);
|
||||
assert.strictEqual(cd.maxHeight, VH);
|
||||
|
||||
const none = computePopupOffset({ viewportWidth: VW, viewportHeight: VH, scrollTop: 0, opener: null, popupName: 'x' });
|
||||
assert.deepStrictEqual(none, { left: PAD, top: PAD, maxHeight: VH - PAD * 2 });
|
||||
|
||||
const mini = computePopupOffset({ viewportWidth: VW, viewportHeight: VH, isMiniScreen: true, popupName: 'x' });
|
||||
assert.deepStrictEqual(mini, { left: 0, top: 0 });
|
||||
});
|
||||
|
||||
// --- NEGATIVE / the actual bug: the OLD formula overflowed when scrolled -----
|
||||
test('#5667 regression: the OLD document-coord formula placed it OFF-SCREEN', () => {
|
||||
const scrollTop = 1000;
|
||||
const opener = { top: scrollTop + 40, left: 300, height: 24 }; // near viewport top
|
||||
|
||||
// Reproduce the pre-fix math (document offset mixed with viewport height).
|
||||
const openerTop = opener.top;
|
||||
const openerBottom = opener.top + opener.height;
|
||||
const spaceBelow = VH - openerBottom - PAD;
|
||||
const spaceAbove = openerTop - PAD;
|
||||
const preferBelow = spaceBelow >= spaceAbove;
|
||||
let oldTop, oldMax;
|
||||
if (preferBelow) {
|
||||
oldMax = Math.max(0, Math.min(spaceBelow, VH * 0.8));
|
||||
oldTop = openerBottom;
|
||||
} else {
|
||||
oldMax = Math.max(0, Math.min(spaceAbove, VH * 0.8));
|
||||
oldTop = Math.max(PAD, openerTop - oldMax);
|
||||
}
|
||||
const oldTopVp = oldTop - scrollTop;
|
||||
// The old popup's TOP edge is far above the visible viewport (negative) — the bug.
|
||||
assert.ok(oldTopVp < 0, `old top ${oldTopVp} should be off-screen (the bug)`);
|
||||
|
||||
// The fixed function keeps it on-screen for the exact same inputs.
|
||||
const fixed = computePopupOffset({
|
||||
viewportWidth: VW, viewportHeight: VH, scrollTop, opener, popupName: 'editCardDueDatePopup',
|
||||
});
|
||||
assertFullyVisible(fixed, scrollTop, 'fixed');
|
||||
});
|
||||
|
||||
test('maxHeight is never negative for an opener above the viewport', () => {
|
||||
const scrollTop = 1000;
|
||||
const result = computePopupOffset({
|
||||
viewportWidth: VW, viewportHeight: VH, scrollTop,
|
||||
opener: { top: scrollTop - 500, left: 300, height: 24 }, // scrolled past, above viewport
|
||||
popupName: 'editCardDueDatePopup',
|
||||
});
|
||||
assert.ok(result.maxHeight >= 0, 'maxHeight must never be negative');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
Reference in new issue
Block a user