mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(ui): align the right rail to where the left rail settles
The right rail took its top from the left stack's bounding rect while the left stack's `top` was still transitioning (150 ms): the left layout sets its target and calls onAligned() synchronously, the right pass runs a frame later mid-transition, and nothing re-ran it when the transition ended. The rail then sat ~32 px off and jumped on the next unrelated relayout - under the pointer in CI's panel gates, which is why a header press missed on every run. settledLeftRailTop() reads the end value of a running `top` transition (falling back to dataset.safeTopPct, then the rect). A settled stack is read from its rect as before, which keeps Cyber's CSS top override (and cockpit's transition: none) exactly as they were. The Cyber test's pinned pattern follows the new helper. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f17bf7116d
commit
9dde7b4a62
3 files changed
+85
-2
No files matched your search
@@ -712,7 +712,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
rightRail,
|
rightRail,
|
||||||
/const leftStackTop = leftStack\?\.getBoundingClientRect\(\)\.top;[\s\S]*?const alignedTop = Number\.isFinite\(leftStackTop\)[\s\S]*?baseTop: alignedTop,/,
|
/const leftStackTop = settledLeftRailTop\([\s\S]*?const alignedTop = Number\.isFinite\(leftStackTop\)[\s\S]*?baseTop: alignedTop,/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
cyberStyles,
|
cyberStyles,
|
||||||
|
|||||||
@@ -357,6 +357,56 @@ test('right rail aligns to the current left rail and excludes hidden obstacles',
|
|||||||
assert.equal(f.stack.dataset.safeTop, '200.0');
|
assert.equal(f.stack.dataset.safeTop, '200.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The left pass commits its top and requests this pass in the same frame, but
|
||||||
|
// `top` then animates for 150 ms and nothing re-runs the right pass when it
|
||||||
|
// lands. CI docked the right rail at 234 px (26vh) while the left settled at
|
||||||
|
// 266.4 px (29.6vh). Mid-flight the right rail must read the destination.
|
||||||
|
function animatingLeftRail({ keyframes = true } = {}) {
|
||||||
|
const f = fixture('right');
|
||||||
|
const left = f.options.leftStack;
|
||||||
|
left.rect.top = 250;
|
||||||
|
left.computed.top = '250px';
|
||||||
|
left.dataset.safeTopPct = '29.60';
|
||||||
|
left.getAnimations = () => [
|
||||||
|
{ transitionProperty: 'bottom', effect: null },
|
||||||
|
{
|
||||||
|
transitionProperty: 'top',
|
||||||
|
effect: {
|
||||||
|
getKeyframes: () =>
|
||||||
|
keyframes ? [{ top: '234px' }, { top: '266.4px' }] : [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('right rail aligns to where a mid-transition left rail settles', () => {
|
||||||
|
const f = animatingLeftRail();
|
||||||
|
f.run();
|
||||||
|
assert.equal(f.stack.dataset.safeTop, '266.4');
|
||||||
|
assert.equal(
|
||||||
|
f.stack.style.getPropertyValue('--right-stack-safe-top'),
|
||||||
|
'266.4px',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('right rail falls back to the committed left target when the transition end is unreadable', () => {
|
||||||
|
const f = animatingLeftRail({ keyframes: false });
|
||||||
|
f.run();
|
||||||
|
assert.equal(f.stack.dataset.safeTop, '266.4');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('right rail follows a settled left rail whose CSS overrides the committed target', () => {
|
||||||
|
// Cyber pins the left rail with `top: ... !important`, so the committed
|
||||||
|
// percentage is not where it renders and no `top` transition runs.
|
||||||
|
const f = fixture('right', { hud: { visible: true, variant: 'cyber' } });
|
||||||
|
f.options.leftStack.rect.top = 220;
|
||||||
|
f.options.leftStack.dataset.safeTopPct = '26.00';
|
||||||
|
f.options.leftStack.getAnimations = () => [];
|
||||||
|
f.run();
|
||||||
|
assert.equal(f.stack.dataset.safeTop, '220.0');
|
||||||
|
});
|
||||||
|
|
||||||
test('natural height includes visible content, margins and wrapper chrome, excluding hidden rows', () => {
|
test('natural height includes visible content, margins and wrapper chrome, excluding hidden rows', () => {
|
||||||
const panel = element('panel');
|
const panel = element('panel');
|
||||||
const inner = element('inner', { top: 100 });
|
const inner = element('inner', { top: 100 });
|
||||||
|
|||||||
@@ -11,6 +11,35 @@ import { displayPanelScroller } from './displayPanelScroll.js';
|
|||||||
|
|
||||||
const pendingCollapseRetries = new WeakSet();
|
const pendingCollapseRetries = new WeakSet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read where the left rail's top edge is going to settle, not where it is.
|
||||||
|
* The left pass commits `--left-stack-safe-top` and requests this pass in the
|
||||||
|
* same frame, but `top` then animates; nothing re-runs this pass when the
|
||||||
|
* animation ends, because ResizeObserver ignores moves. Mid-flight, take the
|
||||||
|
* running transition's end value (which also covers Cyber's fixed rail top),
|
||||||
|
* falling back to the left pass's committed target.
|
||||||
|
* @param {HTMLElement} leftStack Rail supplying the shared top baseline.
|
||||||
|
* @param {number} viewportHeight Current viewport height in px.
|
||||||
|
* @param {Function} getComputedStyle DOM style reader.
|
||||||
|
* @returns {number} Settled top in px, or NaN when there is no left rail.
|
||||||
|
*/
|
||||||
|
function settledLeftRailTop(leftStack, viewportHeight, getComputedStyle) {
|
||||||
|
const top = leftStack?.getBoundingClientRect().top;
|
||||||
|
if (!Number.isFinite(top)) return NaN;
|
||||||
|
const transition = leftStack
|
||||||
|
.getAnimations?.()
|
||||||
|
.find((animation) => animation.transitionProperty === 'top');
|
||||||
|
if (!transition) return top;
|
||||||
|
const keyframes = transition.effect?.getKeyframes?.() || [];
|
||||||
|
const targetTop = parseFloat(keyframes.at(-1)?.top);
|
||||||
|
const currentTop = parseFloat(getComputedStyle(leftStack).top);
|
||||||
|
// The rect and the computed `top` move together; keep any offset between them.
|
||||||
|
if (Number.isFinite(targetTop) && Number.isFinite(currentTop))
|
||||||
|
return top + targetTop - currentTop;
|
||||||
|
const targetPct = parseFloat(leftStack.dataset?.safeTopPct);
|
||||||
|
return Number.isFinite(targetPct) ? (targetPct * viewportHeight) / 100 : top;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measure and place the right panel rail for one synchronous layout pass.
|
* Measure and place the right panel rail for one synchronous layout pass.
|
||||||
* The caller owns scheduling, obstacle selection, disclosure preferences and
|
* The caller owns scheduling, obstacle selection, disclosure preferences and
|
||||||
@@ -113,7 +142,11 @@ export function layoutRightPanelRail({
|
|||||||
const viewportHeight = Math.max(1, windowRef.innerHeight);
|
const viewportHeight = Math.max(1, windowRef.innerHeight);
|
||||||
const safeGap = Math.max(8, viewportHeight * 0.012);
|
const safeGap = Math.max(8, viewportHeight * 0.012);
|
||||||
const stackRect = stack.getBoundingClientRect();
|
const stackRect = stack.getBoundingClientRect();
|
||||||
const leftStackTop = leftStack?.getBoundingClientRect().top;
|
const leftStackTop = settledLeftRailTop(
|
||||||
|
leftStack,
|
||||||
|
viewportHeight,
|
||||||
|
getComputedStyle,
|
||||||
|
);
|
||||||
const alignedTop = Number.isFinite(leftStackTop)
|
const alignedTop = Number.isFinite(leftStackTop)
|
||||||
? leftStackTop
|
? leftStackTop
|
||||||
: viewportHeight * 0.26;
|
: viewportHeight * 0.26;
|
||||||
|
|||||||
Reference in new issue
Block a user