fix widget focus interactions

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:07:08 +02:00
1 parent e429d7540a
commit f5cced4080
12 files changed
+80 -5

No files matched your search

@@ -35,7 +35,7 @@
.surfaceHostAdvanced {
position: fixed;
inset: 0;
z-index: calc(var(--mantine-z-index-modal) - 1);
z-index: calc(var(--mantine-z-index-popover) - 1);
width: auto;
height: auto;
pointer-events: none;
@@ -201,7 +201,7 @@ export const BoardAdvancedFocusProvider = ({ children }: PropsWithChildren) => {
data-advanced-focus-overlay
backgroundOpacity={0.38}
blur={0}
zIndex="calc(var(--mantine-z-index-modal) - 2)"
zIndex="calc(var(--mantine-z-index-popover) - 2)"
className={`${classes.backdrop} ${active.phase === "closing" ? classes.backdropClosing : ""}`}
aria-hidden
/>
@@ -0,0 +1,30 @@
const canScrollVertically = (element: HTMLElement, delta: number) => {
if (element.scrollHeight <= element.clientHeight) return false;
const overflowY = getComputedStyle(element).overflowY;
if (overflowY !== "auto" && overflowY !== "scroll") return false;
return delta < 0 ? element.scrollTop > 0 : element.scrollTop + element.clientHeight < element.scrollHeight;
};
export const redirectShiftWheel = (root: HTMLElement, target: EventTarget | null, delta: number) => {
if (delta === 0) return false;
let current = target instanceof HTMLElement ? target : null;
while (current && root.contains(current)) {
if (canScrollVertically(current, delta)) {
current.scrollTop += delta;
return true;
}
if (current === root) break;
current = current.parentElement;
}
const viewport = [...root.querySelectorAll<HTMLElement>("[data-scrollbars='y'], [data-scrollbars='xy']")].find(
(element) => canScrollVertically(element, delta),
);
if (!viewport) return false;
viewport.scrollTop += delta;
return true;
};
+12
View File
@@ -115,6 +115,18 @@ describe("Board advanced interactions", () => {
await expect(page.getByRole("button", { name: /advanced view is pinned|keep advanced view open/i })).toHaveCount(
0,
);
await advancedSurface.evaluate((element) => {
const probe = document.createElement("div");
probe.dataset.shiftScrollProbe = "true";
probe.style.cssText = "height:80px;overflow-y:auto";
probe.innerHTML = '<div style="height:600px"></div>';
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 });
await page.keyboard.up("Shift");
expect(await scrollProbe.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
await page.keyboard.press("Escape");
await expect(advancedSurface).toBeHidden();
await expect(widget).toBeFocused();
@@ -1,7 +1,7 @@
export interface StreamSession {
sessionId: string;
sessionName: string;
user: {
user?: {
userId: string;
username: string;
profilePictureUrl: string | null;
+1
View File
@@ -3506,6 +3506,7 @@
"items": {
"currentlyPlaying": "Currently playing",
"user": "User",
"unknownUser": "Unknown user",
"name": "Name",
"id": "Id",
"status": "Status",
+1
View File
@@ -15,6 +15,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("app", {
icon: IconApps,
supportsAdvancedFocus: false,
queryKeys: [[["app", "byId"]], [["widget", "app", "ping"]]],
createOptions(settings) {
return optionsBuilder.from(
@@ -6,6 +6,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("beszelSystemGrid", {
icon: IconLayoutGrid,
supportsAdvancedFocus: false,
queryKey: [["widget", "beszel", "getSystems"]],
supportedIntegrations: ["beszel", "mock"],
integrationsRequired: true,
@@ -25,6 +25,7 @@ const sortDirectionOptions = [
export const { definition, componentLoader } = createWidgetDefinition("beszelSystemTable", {
icon: IconTable,
supportsAdvancedFocus: false,
queryKeys: [[["widget", "beszel", "getSystems"]], [["widget", "beszel", "getSystemStats"]]],
queryMatcher: (query, scope) =>
query.path.at(-1) === "getSystems"
@@ -1,4 +1,4 @@
import { Box, Group, HoverCard, Progress, Stack, Text, UnstyledButton } from "@mantine/core";
import { Box, getDefaultZIndex, Group, HoverCard, Progress, Stack, Text, UnstyledButton } from "@mantine/core";
import type { BeszelSystemRow } from "./types";
import { thresholdColor } from "./colors";
@@ -28,6 +28,7 @@ export const DiskUsage = ({ system, fontSize, progressSize, valueMiw, valueGap =
{formatPercent(system.disk)}
</Text>
<HoverCard
zIndex={getDefaultZIndex("popover") - 3}
position="right"
withArrow
shadow="md"
+28 -1
View File
@@ -1,6 +1,14 @@
import { describe, expect, test } from "vitest";
import { getWidgetQueryKeys, getWidgetRuntimeQueries, setWidgetRuntimeQueries } from "./definition";
import { definition as appDefinition } from "./app";
import { definition as beszelSystemGridDefinition } from "./beszel-system-grid";
import { definition as beszelSystemTableDefinition } from "./beszel-system-table";
import {
getWidgetQueryKeys,
getWidgetRuntimeQueries,
setWidgetRuntimeQueries,
supportsAdvancedFocus,
} from "./definition";
const togglePolling = () => undefined;
@@ -32,3 +40,22 @@ describe("getWidgetQueryKeys", () => {
expect(getWidgetRuntimeQueries(widgetStateRef)).toEqual(runtimeQueries);
});
});
describe("supportsAdvancedFocus", () => {
const definition = { supportsAdvancedFocus: undefined } as Parameters<typeof supportsAdvancedFocus>[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) => {
expect(supportsAdvancedFocus(widgetDefinition)).toBe(false);
},
);
});
+1
View File
@@ -79,6 +79,7 @@ export {
getWidgetQueryKeys,
getWidgetRuntimeQueries,
normalizeWidgetQuery,
supportsAdvancedFocus,
widgetQueryValueEquals,
} from "./definition";
export type { WidgetComponentProps };