Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/layout-engine/painters/dom/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export const containerStyles: Partial<CSSStyleDeclaration> = {
padding: '0',
// gap is set dynamically by renderer based on pageGap option (default: 24px)
overflowY: 'auto',
// Contain child z-indices (SDT labels, hover states) so they cannot escape
// above sibling UI surfaces like the toolbar or ruler. (SD-2015)
isolation: 'isolate',
};

export const containerStylesHorizontal: Partial<CSSStyleDeclaration> = {
Expand All @@ -44,6 +47,7 @@ export const containerStylesHorizontal: Partial<CSSStyleDeclaration> = {
// gap is set dynamically by renderer based on pageGap option (default: 20px for horizontal)
overflowX: 'auto',
minHeight: '100%',
isolation: 'isolate',
};

export const spreadStyles: Partial<CSSStyleDeclaration> = {
Expand Down
3 changes: 3 additions & 0 deletions packages/super-editor/src/components/SuperEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,9 @@ onBeforeUnmount(() => {
justify-content: center;
width: 100%;
box-sizing: border-box;
position: relative;
z-index: var(--sd-ui-ruler-z-index, 10);
background: var(--sd-ui-ruler-bg, var(--sd-ui-bg, #ffffff));
}

.ruler {
Expand Down
4 changes: 3 additions & 1 deletion packages/super-editor/src/components/toolbar/Toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,12 @@ const handleToolbarMousedown = (e) => {
display: flex;
width: 100%;
justify-content: space-between;
background: var(--sd-ui-toolbar-bg, transparent);
background: var(--sd-ui-toolbar-bg, var(--sd-ui-bg, #ffffff));
padding: var(--sd-ui-toolbar-padding-y, 4px) var(--sd-ui-toolbar-padding-x, 16px);
box-sizing: border-box;
font-family: var(--sd-ui-font-family, Arial, Helvetica, sans-serif);
position: relative;
z-index: var(--sd-ui-toolbar-z-index, 10);
}

@media (max-width: 1280px) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ export class PresentationEditor extends EventEmitter {
// This prevents screen readers from encountering duplicate or non-semantic visual elements.
this.#viewportHost.setAttribute('aria-hidden', 'true');
this.#viewportHost.style.position = 'relative';
this.#viewportHost.style.isolation = 'isolate';
this.#viewportHost.style.width = '100%';
// Set min-height to at least one page so the viewport is clickable before layout renders
const pageHeight = this.#layoutOptions.pageSize?.h ?? DEFAULT_PAGE_SIZE.h;
Expand Down
5 changes: 4 additions & 1 deletion packages/superdoc/src/assets/styles/helpers/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,13 @@
--sd-ui-toolbar-padding-y: 4px;
--sd-ui-toolbar-item-gap: 2px;
--sd-ui-toolbar-item-padding: 5px;
--sd-ui-toolbar-bg: transparent;
--sd-ui-toolbar-bg: var(--sd-ui-bg);
--sd-ui-toolbar-button-text: var(--sd-ui-text);
--sd-ui-toolbar-button-hover-bg: var(--sd-ui-hover-bg);
--sd-ui-toolbar-button-active-bg: var(--sd-ui-active-bg);
--sd-ui-toolbar-z-index: 10;
--sd-ui-ruler-bg: var(--sd-ui-bg);
--sd-ui-ruler-z-index: 10;

/* UI: context menu — cascades from semantic tier */
--sd-ui-menu-bg: var(--sd-ui-bg);
Expand Down
88 changes: 88 additions & 0 deletions tests/behavior/tests/sdt/structured-content.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,91 @@ test.describe('viewing mode hides SDT affordances', () => {
await superdoc.snapshot('inline SDT viewing mode');
});
});

// ==========================================================================
// Stacking Context Regression (SD-2015)
// ==========================================================================

test.describe('SD-2015: SDT labels must not paint above the toolbar', () => {
test('layout container isolates SDT z-indices from the toolbar', async ({ superdoc }) => {
// Insert an SDT so the relevant DOM nodes exist.
await insertBlockSdt(superdoc.page, 'Stacking Test', 'SDT content');
await superdoc.waitForStable();

// Verify the stacking-context fix: the layout container must have
// isolation: isolate, which scopes all child z-indices (including the
// z-index: 9999999 hover boost) so they cannot escape above the toolbar.
const layoutIsolation = await superdoc.page.evaluate(() => {
const layout = document.querySelector('.superdoc-layout');
return layout ? getComputedStyle(layout).isolation : 'not-found';
});
expect(layoutIsolation).toBe('isolate');

// Verify the toolbar establishes its own stacking context above the
// document surface.
const toolbarStyles = await superdoc.page.evaluate(() => {
const toolbar = document.querySelector('.superdoc-toolbar');
if (!toolbar) return null;
const cs = getComputedStyle(toolbar);
return { position: cs.position, zIndex: cs.zIndex };
});
expect(toolbarStyles).not.toBeNull();
expect(toolbarStyles!.position).toBe('relative');
expect(Number(toolbarStyles!.zIndex)).toBeGreaterThan(0);

// Activate the hover boost on the SDT, then force the SDT element to
// overlay the toolbar via CSS. This creates the exact stacking conflict
// from the bug (z-index: 9999999 inside the document vs toolbar z-index)
// without depending on the harness scroll setup.
const hitTag = await superdoc.page.evaluate(
({ sdtSel, labelSel }) => {
const sdt = document.querySelector(sdtSel) as HTMLElement | null;
const label = document.querySelector(labelSel) as HTMLElement | null;
const probe = document.querySelector('.superdoc-toolbar [data-item="btn-undo"]') as HTMLElement | null;
if (!sdt || !probe) throw new Error('SDT or toolbar probe not found');

// Activate the hover boost (z-index: 9999999).
sdt.classList.remove('ProseMirror-selectednode');
sdt.classList.add('sdt-hover');
if (label) label.style.display = 'inline-flex';

// Confirm the boost is active.
const boost = getComputedStyle(sdt).zIndex;
if (Number(boost) < 9999999) return `z-index-not-active:${boost}`;
Comment thread
harbournick marked this conversation as resolved.

// Force the SDT to overlap the toolbar probe via fixed positioning.
const probeRect = probe.getBoundingClientRect();
const origPosition = sdt.style.position;
const origZIndex = sdt.style.zIndex;
const origTop = sdt.style.top;
const origLeft = sdt.style.left;
sdt.style.position = 'fixed';
sdt.style.top = `${probeRect.top}px`;
sdt.style.left = `${probeRect.left}px`;
sdt.style.zIndex = '9999999';

// Hit-test: the toolbar should still win because isolation: isolate
// on the layout container scopes the SDT's z-index.
const x = probeRect.left + probeRect.width / 2;
const y = probeRect.top + probeRect.height / 2;
const hit = document.elementFromPoint(x, y);

// Restore the SDT's original styles.
sdt.style.position = origPosition;
sdt.style.zIndex = origZIndex;
sdt.style.top = origTop;
sdt.style.left = origLeft;

if (!hit) return 'null';
if (hit.closest('.superdoc-toolbar')) return 'toolbar';
if (hit.closest(sdtSel)) return 'sdt';
return hit.tagName.toLowerCase();
},
{ sdtSel: BLOCK_SDT, labelSel: BLOCK_LABEL },
);

expect(hitTag).toBe('toolbar');

await superdoc.snapshot('SD-2015 toolbar above SDT');
});
});
Loading