Skip to content
Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the data-view “View settings” popup layering issue where the bi-directional links “Show” button could appear above the menu. The root cause was that this popup was being mounted inside the local editor stacking context, so its z-index could still lose to sibling UI. This change only lifts the view-settings popup flow to document.body, which keeps the fix scoped to this menu family instead of changing popup behavior globally.

Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@ export class DataViewHeaderToolsViewOptions extends WidgetBase {
};

openMoreAction = (target: PopupTarget) => {
popViewOptions(target, this.dataViewLogic);
popViewOptions(
{
...target,
root: document.body,
},
this.dataViewLogic
);
Comment on lines +77 to +83

@coderabbitai coderabbitai Bot Apr 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect PopupTarget construction and popup target helper implementation.
rg -n -C3 "popupTargetFromElement|type PopupTarget" --type ts --type js

# 2) Confirm popup root is used as the modal container.
rg -n -C3 "createModal\\(target\\.root\\)|target\\.root" --type ts --type js blocksuite/affine/components/src/context-menu

# 3) Find other places overriding root to document.body (to assess consistency/risk).
rg -n -C2 "root:\\s*document\\.body" --type ts --type js

Repository: toeverything/AFFiNE

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find PopupTarget type definition
echo "=== PopupTarget Type Definition ==="
rg -n "type PopupTarget|interface PopupTarget" --type ts --type js -A 5

# Find popupTargetFromElement implementation
echo -e "\n=== popupTargetFromElement Implementation ==="
rg -n "export.*popupTargetFromElement|function popupTargetFromElement" --type ts --type js -A 10 | head -60

# Look for where root: document.body pattern appears
echo -e "\n=== root: document.body Overrides ==="
rg -n "root:\s*document\.body" --type ts --type js

# Check the specific file context around lines 77-83
echo -e "\n=== File Context around lines 70-85 ==="
sed -n '70,85p' blocksuite/affine/data-view/src/widget-presets/tools/presets/view-options/view-options.ts

Repository: toeverything/AFFiNE

Length of output: 14173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the complete popupTargetFromElement implementation
echo "=== Complete popupTargetFromElement Implementation ==="
sed -n '407,430p' blocksuite/affine/components/src/context-menu/menu-renderer.ts

# Check what popViewOptions does with the root parameter
echo -e "\n=== popViewOptions Implementation/Usage ==="
rg -n "popViewOptions|export.*popViewOptions" --type ts --type js -A 15 | head -80

# Check if there are any comments or tests explaining the root behavior
echo -e "\n=== Context around PopupTarget root usage ==="
rg -n "\.root" blocksuite/affine/components/src/context-menu/menu-renderer.ts -B 2 -A 2 | head -40

Repository: toeverything/AFFiNE

Length of output: 5555


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find getDefaultModalRoot implementation
echo "=== getDefaultModalRoot Implementation ==="
rg -n "getDefaultModalRoot|function getDefaultModalRoot|export.*getDefaultModalRoot" --type ts --type js -A 20 | head -60

# Check if there are shadow DOM or scoped root contexts in use
echo -e "\n=== Shadow DOM or scoped root patterns ==="
rg -n "shadowRoot|getRootNode" blocksuite/affine/components/src/context-menu --type ts --type js | head -20

Repository: toeverything/AFFiNE

Length of output: 4138


Override modal container carefully to preserve scoped root logic.

Line 80 hardcodes the popup container to the global document.body, which bypasses the scoped root selection from getDefaultModalRoot (which already checks for editor-host and .data-view-popup-container contexts). This can cause placement/layering regressions in scoped or embedded document contexts. If forcing the body is required for the z-index fix, derive it from the computed root instead.

Proposed change
     popViewOptions(
       {
         ...target,
-        root: document.body,
+        root: target.root.ownerDocument?.body ?? target.root,
       },
       this.dataViewLogic
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blocksuite/affine/data-view/src/widget-presets/tools/presets/view-options/view-options.ts`
around lines 77 - 83, The code is forcing the modal container to document.body
in the popViewOptions call which bypasses the scoped root selection from
getDefaultModalRoot; change the root passed into popViewOptions to be derived
from the computed default modal root instead of hardcoding document.body (e.g.,
compute const root = getDefaultModalRoot(target.root ?? document.body) or call
getDefaultModalRoot() and fall back to document.body only if that returns null),
then pass { ...target, root } and keep this.dataViewLogic; this preserves
scoped/embedded contexts while still allowing a fallback for z-index fixes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current behavior is intentionally narrow: only the data-view “View settings” popup flow overrides root to document.body. We do that because the default scoped root resolves to the nearest editor-host, and editor-host creates an isolated stacking context, which is the reason this menu can render underneath sibling UI like the bi-directional links “Show” button. Re-deriving the root from getDefaultModalRoot(...) here would usually put the popup back into editor-host and reintroduce the original layering bug.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibex088, understood — thanks for the detailed explanation. The isolated stacking context created by editor-host is exactly the kind of implicit constraint that's hard to infer from the code alone. Fixing the root to document.body specifically in this popup flow to escape that stacking context is the right targeted approach, and re-deriving via getDefaultModalRoot would indeed risk re-scoping to editor-host and reverting the fix.

I've noted this for future reviews.

( ˘▽˘)っ♨


✏️ Learnings added
Learnt from: ibex088
Repo: toeverything/AFFiNE PR: 14710
File: blocksuite/affine/data-view/src/widget-presets/tools/presets/view-options/view-options.ts:77-83
Timestamp: 2026-04-03T09:24:14.105Z
Learning: In `blocksuite/affine/data-view/src/widget-presets/tools/presets/view-options/view-options.ts`, the `openMoreAction` method in `DataViewHeaderToolsViewOptions` intentionally overrides `root` to `document.body` when calling `popViewOptions`. This is by design: the default scoped root resolves to the nearest `editor-host` element, which creates an isolated stacking context. That stacking context causes the "View settings" popup to render beneath sibling UI (e.g., the bi-directional links "Show/Hide" button). Hardcoding `document.body` as the root escapes that stacking context. Do NOT suggest replacing this with `getDefaultModalRoot()` or a derived root, as that would typically re-scope back to `editor-host` and reintroduce the original layering bug (GitHub issue `#14723`).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

};

override render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const affineEdgelessDocViewport = style({

export const docContainer = style({
display: 'block',
position: 'relative',
zIndex: 1,
selectors: ['generating', 'finished', 'error'].reduce<
NonNullable<StyleRule['selectors']>
>((rules, state) => {
Expand Down
4 changes: 1 addition & 3 deletions packages/frontend/core/src/desktop/dialogs/setting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
import { GlobalContextService } from '@affine/core/modules/global-context';
import { createIsland, type Island } from '@affine/core/utils/island';
import { ServerDeploymentType } from '@affine/graphql';
import { Trans, useTranslation } from '@affine/i18n';
import { Trans } from '@affine/i18n';
import { ContactWithUsIcon } from '@blocksuite/icons/rc';
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
import { debounce } from 'lodash-es';
Expand Down Expand Up @@ -72,12 +72,10 @@ const SettingModalInner = ({
scrollAnchor: initialScrollAnchor,
});
const globalContextService = useService(GlobalContextService);
const { i18n } = useTranslation('translation');

const currentServerId = useLiveData(
globalContextService.globalContext.serverId.$
);
const currentLanguageKey = i18n.resolvedLanguage ?? i18n.language;
const serversService = useService(ServersService);
const defaultServerService = useService(DefaultServerService);
const currentServer =
Expand Down