From a2f5668c44e49074a0c07b4a9f27a042b617ced5 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 3 Aug 2026 12:21:31 +0200 Subject: [PATCH] fix focus scroll layering --- .../advanced-focus/advanced-focus.module.css | 2 +- .../board/advanced-focus/context.tsx | 2 +- .../components/board/items/item-content.tsx | 27 +++++++++++++------ e2e/board-advanced-interactions.spec.ts | 21 +++++++++++++-- .../media-server/media-server-types.ts | 2 +- .../widgets/src/beszel/_shared/disk-usage.tsx | 2 +- .../widgets/src/beszel/_shared/tooltip.tsx | 4 +-- packages/widgets/src/definition.spec.ts | 10 ------- 8 files changed, 44 insertions(+), 26 deletions(-) diff --git a/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css b/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css index c108d72c8..60cc31954 100644 --- a/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css +++ b/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css @@ -35,7 +35,7 @@ .surfaceHostAdvanced { position: fixed; inset: 0; - z-index: calc(var(--mantine-z-index-popover) - 1); + z-index: calc(var(--mantine-z-index-modal) - 1); width: auto; height: auto; pointer-events: none; diff --git a/apps/nextjs/src/components/board/advanced-focus/context.tsx b/apps/nextjs/src/components/board/advanced-focus/context.tsx index 4a5935efe..3f38bdc44 100644 --- a/apps/nextjs/src/components/board/advanced-focus/context.tsx +++ b/apps/nextjs/src/components/board/advanced-focus/context.tsx @@ -201,7 +201,7 @@ export const BoardAdvancedFocusProvider = ({ children }: PropsWithChildren) => { data-advanced-focus-overlay backgroundOpacity={0.38} blur={0} - zIndex="calc(var(--mantine-z-index-popover) - 2)" + zIndex="calc(var(--mantine-z-index-modal) - 2)" className={`${classes.backdrop} ${active.phase === "closing" ? classes.backdropClosing : ""}`} aria-hidden /> diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index 0eb52868d..32e71fe0a 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties, KeyboardEvent, MutableRefObject, WheelEvent } from "react"; +import type { CSSProperties, KeyboardEvent, MutableRefObject } from "react"; import { useEffect, useRef } from "react"; import { ActionIcon, Badge, Box, Card } from "@mantine/core"; import { useElementSize, useViewportSize } from "@mantine/hooks"; @@ -54,6 +54,7 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => { const [isEditMode] = useEditMode(); const widgetStateRef = useRef | null>(null); const closeButtonRef = useRef(null); + const surfaceRef = useRef(null); const { active, open, close, dismiss, hover, leave } = useAdvancedFocus(); const supportsAdvancedFocus = definitionSupportsAdvancedFocus( widgetImports[item.kind].definition as WidgetDefinition, @@ -90,12 +91,22 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => { event.preventDefault(); openAdvanced(true); }; - const handleWheel = (event: WheelEvent) => { - if (!isAdvanced || !event.shiftKey) return; - const delta = event.deltaY || event.deltaX; - if (!redirectShiftWheel(event.currentTarget, event.target, delta)) return; - event.preventDefault(); - }; + useEffect(() => { + const surface = surfaceRef.current; + if (!isAdvanced || !surface) return; + + const handleWheel = (event: WheelEvent) => { + if (!event.shiftKey) return; + const multiplier = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? surface.clientHeight : 1; + const delta = (event.deltaY || event.deltaX) * multiplier; + if (!redirectShiftWheel(surface, event.target, delta)) return; + event.preventDefault(); + event.stopPropagation(); + }; + + surface.addEventListener("wheel", handleWheel, { capture: true, passive: false }); + return () => surface.removeEventListener("wheel", handleWheel, { capture: true }); + }, [isAdvanced]); return ( <> @@ -140,11 +151,11 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => { )} { probe.dataset.shiftScrollProbe = "true"; probe.style.cssText = "height:80px;overflow-y:auto"; probe.innerHTML = '
'; + probe.addEventListener("wheel", (event) => event.stopPropagation()); element.append(probe); }); const scrollProbe = advancedSurface.locator("[data-shift-scroll-probe]"); await page.keyboard.down("Shift"); - await scrollProbe.dispatchEvent("wheel", { shiftKey: true, deltaX: 120, deltaY: 0 }); + const pixelEventCancelled = await scrollProbe.evaluate( + (element) => + !element.dispatchEvent( + new WheelEvent("wheel", { bubbles: true, cancelable: true, shiftKey: true, deltaX: 120 }), + ), + ); + expect(pixelEventCancelled).toBe(true); + await scrollProbe.evaluate((element) => { + element.scrollTop = 0; + }); + const lineEventCancelled = await scrollProbe.evaluate( + (element) => + !element.dispatchEvent( + new WheelEvent("wheel", { bubbles: true, cancelable: true, shiftKey: true, deltaX: 3, deltaMode: 1 }), + ), + ); await page.keyboard.up("Shift"); - expect(await scrollProbe.evaluate((element) => element.scrollTop)).toBeGreaterThan(0); + expect(lineEventCancelled).toBe(true); + expect(await scrollProbe.evaluate((element) => element.scrollTop)).toBeGreaterThanOrEqual(48); await page.keyboard.press("Escape"); await expect(advancedSurface).toBeHidden(); await expect(widget).toBeFocused(); diff --git a/packages/integrations/src/interfaces/media-server/media-server-types.ts b/packages/integrations/src/interfaces/media-server/media-server-types.ts index 42b6f0615..c9a287155 100644 --- a/packages/integrations/src/interfaces/media-server/media-server-types.ts +++ b/packages/integrations/src/interfaces/media-server/media-server-types.ts @@ -1,7 +1,7 @@ export interface StreamSession { sessionId: string; sessionName: string; - user?: { + user: { userId: string; username: string; profilePictureUrl: string | null; diff --git a/packages/widgets/src/beszel/_shared/disk-usage.tsx b/packages/widgets/src/beszel/_shared/disk-usage.tsx index 826d5c3c8..934981daf 100644 --- a/packages/widgets/src/beszel/_shared/disk-usage.tsx +++ b/packages/widgets/src/beszel/_shared/disk-usage.tsx @@ -28,7 +28,7 @@ export const DiskUsage = ({ system, fontSize, progressSize, valueMiw, valueGap = {formatPercent(system.disk)} { const mouseRef = useRef({ x: 0, y: 0 }); @@ -123,7 +123,7 @@ const PortalTooltipContent = ({ active, label, payload, formatter, showTotal }: position: "fixed", left: pos.x, top: pos.y, - zIndex: pos.isAdvanced ? getDefaultZIndex("popover") : TOOLTIP_Z_INDEX, + zIndex: pos.isAdvanced ? getDefaultZIndex("modal") : BACKGROUND_TOOLTIP_Z_INDEX, pointerEvents: "none", }; diff --git a/packages/widgets/src/definition.spec.ts b/packages/widgets/src/definition.spec.ts index 94089b161..314be143f 100644 --- a/packages/widgets/src/definition.spec.ts +++ b/packages/widgets/src/definition.spec.ts @@ -46,16 +46,6 @@ describe("getWidgetQueryKeys", () => { }); describe("supportsAdvancedFocus", () => { - const definition = { supportsAdvancedFocus: undefined } as Parameters[0]; - - test("defaults to enabled", () => { - expect(supportsAdvancedFocus(definition)).toBe(true); - }); - - test("allows widgets to opt out", () => { - expect(supportsAdvancedFocus({ ...definition, supportsAdvancedFocus: false })).toBe(false); - }); - test.each([appDefinition, beszelSystemGridDefinition, beszelSystemTableDefinition])( "keeps non-enhanced widgets compact", (widgetDefinition) => {