fix(history-mode): lay the history iframe in the editor's flex slot (#7903)

In-pad history mode positioned the timeslider iframe (#history-frame-mount)
as an absolute, inset:0 overlay over #editorcontainerbox. That filled the
editor area but took the iframe out of flow, so any in-flow side panel in
#editorcontainerbox — e.g. ep_webrtc's video column (#rtcbox) — ended up
hidden beneath the overlay. In live mode the same panel sits beside the
editor, so the two modes disagreed (ether/ep_webrtc rendered nothing in the
timeslider).

Make the iframe occupy the same in-flow flex slot the live editor uses
(flex: 1 1 auto in the row-flex #editorcontainerbox) so side panels lay out
identically in both modes.

This surfaced a latent bug: `body.history-mode #editorcontainer { display:
none }` (specificity 0-1-1) was being outranked by the two-id layout rule
`#editorcontainerbox #editorcontainer { display: flex }` (0-2-0), so the live
editor was never actually removed from flow — the absolute overlay merely
painted over it. With the iframe now in normal flow that no longer hides the
editor, so the hide rule is given matching specificity
(body.history-mode #editorcontainerbox #editorcontainer) to win the cascade.

Added a padmode.spec.ts regression test that injects a left-pinned in-flow
side panel, enters history mode, and asserts the live editor is display:none,
the iframe is not absolutely positioned, and the iframe lays out beside the
panel filling the editor's remaining width (no overlap).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-06-05 15:49:45 +01:00 committed by GitHub
parent 189ea4a29b
commit e1476cf100
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 8 deletions

View file

@ -139,16 +139,22 @@ input {
.history-banner-date { opacity: 0.85; }
.history-banner #history-banner-return { margin-left: auto; }
/* The history iframe takes the place of the live ACE editor. We use the */
/* same positioning model (absolute, fill the editor area) so the page */
/* layout is identical between modes. */
/* The history iframe takes the place of the live ACE editor by occupying the
* same in-flow flex slot that #editorcontainer fills in live mode (flex: 1 1
* auto inside the row-flex #editorcontainerbox). This keeps the layout
* identical between modes crucially including any in-flow side panels in
* #editorcontainerbox, such as ep_webrtc's video column, which now sit beside
* the history iframe exactly as they sit beside the live editor.
*
* This used to be an absolute, inset:0 overlay: it filled the editor area but
* took the iframe out of flow, so an in-flow side panel ended up hidden
* beneath it (see ether/ep_webrtc rendering nothing in the timeslider). */
.history-frame-mount {
display: none;
position: absolute;
inset: 0;
flex: 1 1 auto;
min-width: 0; /* allow the iframe to shrink to make room for a side panel */
border: 0;
background: var(--bg-color, #f2f3f4);
z-index: 4;
}
.history-frame-mount[hidden] { display: none; }
.history-frame-mount > iframe {
@ -164,8 +170,14 @@ input {
/* editor, so we swap it for the history controls (slider + play/step */
/* buttons) that drive the iframe. The right-side menu (Settings / Share / */
/* Users / Chat / Home) stays fully interactive across modes. */
body.history-mode #editorcontainer { display: none; }
body.history-mode #editorcontainerbox { position: relative; }
/* The two-id `#editorcontainerbox #editorcontainer { display: flex }` layout
* rule (specificity 0-2-0) outranks a plain `body.history-mode #editorcontainer`
* (0-1-1), so match the same container path here to actually win the cascade
* and remove the live editor from flow. This was latent before: the old
* absolute, inset:0 iframe overlay painted over the still-in-flow editor, so
* nobody noticed it wasn't hidden. Now that the iframe sits in the normal
* flex flow, the editor must genuinely be gone or the two would split the row. */
body.history-mode #editorcontainerbox #editorcontainer { display: none; }
body.history-mode .history-frame-mount { display: block; }
body.history-mode #editbar .menu_left { display: none; }
body.history-mode #editbar .show-more-icon-btn { display: none; }

View file

@ -345,4 +345,55 @@ test.describe('in-pad history mode', () => {
await expect(page.locator('body.history-mode')).toHaveCount(0);
await expect(tbl.locator('.history-authors-row')).toHaveCount(0);
});
test('history iframe occupies the editor flex slot so side panels sit beside it', async ({page}) => {
await goToNewPad(page);
await clearPadContent(page);
await writeToPad(page, 'one');
await page.waitForTimeout(300);
await writeToPad(page, ' two');
await page.waitForTimeout(500);
// Simulate a plugin side panel mounted in #editorcontainerbox (e.g.
// ep_webrtc's #rtcbox video column): an in-flow flex item pinned left.
await page.evaluate(() => {
const box = document.getElementById('editorcontainerbox')!;
const panel = document.createElement('div');
panel.id = 'test-side-panel';
panel.style.cssText = 'display:flex;order:-1;flex:0 1 auto;width:120px';
panel.innerHTML = '<div style="width:120px;height:120px"></div>';
box.appendChild(panel);
});
const editorRight = await page.locator('#editorcontainer').evaluate(
(el) => Math.round(el.getBoundingClientRect().right));
await page.locator('.buttonicon-history').click();
await expect(page.locator('body.history-mode')).toBeVisible();
await page.locator('#history-frame-mount iframe').waitFor({state: 'attached'});
// 1) The live editor is genuinely removed from flow — not merely painted
// over. The two-id `#editorcontainerbox #editorcontainer { display:flex }`
// layout rule outranks a plain `body.history-mode #editorcontainer`, so
// the hide rule must match the same container path to win the cascade.
expect(await page.locator('#editorcontainer').evaluate(
(el) => getComputedStyle(el).display)).toBe('none');
// 2) The history iframe is in normal flow (not an absolute overlay)...
expect(await page.locator('#history-frame-mount').evaluate(
(el) => getComputedStyle(el).position)).not.toBe('absolute');
// 3) ...so it lays out beside the side panel instead of on top of it, and
// still fills the rest of the editor's old width.
const r = (sel: string) => page.locator(sel).evaluate((el) => {
const b = el.getBoundingClientRect();
return {left: b.left, right: b.right, top: b.top, bottom: b.bottom};
});
const panel = await r('#test-side-panel');
const frame = await r('#history-frame-mount');
const overlaps = !(frame.right <= panel.left || panel.right <= frame.left ||
frame.bottom <= panel.top || panel.bottom <= frame.top);
expect(overlaps).toBe(false);
expect(Math.round(frame.left)).toBeGreaterThanOrEqual(Math.round(panel.right) - 2);
expect(Math.round(frame.right)).toBeGreaterThanOrEqual(editorRight - 3);
});
});