fix widget focus interactions

This commit is contained in:
Thomas Camlong committed 2026-08-03 12:15:12 +02:00
1 parent 96e92485a5
commit 09e9cd4388
21 files changed
+119 -28

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;
};
@@ -1,4 +1,4 @@
import type { CSSProperties, KeyboardEvent, MutableRefObject } from "react";
import type { CSSProperties, KeyboardEvent, MutableRefObject, WheelEvent } from "react";
import { useEffect, useRef } from "react";
import { ActionIcon, Badge, Box, Card } from "@mantine/core";
import { useElementSize, useViewportSize } from "@mantine/hooks";
@@ -12,13 +12,20 @@ import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useSettings } from "@homarr/settings";
import { useI18n } from "@homarr/translation/client";
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
import type { WidgetDefinition } from "@homarr/widgets";
import {
loadWidgetDynamic,
reduceWidgetOptionsWithDefaultValues,
supportsAdvancedFocus as definitionSupportsAdvancedFocus,
widgetImports,
} from "@homarr/widgets";
import { WidgetError } from "@homarr/widgets/errors";
import type { SectionItem } from "~/app/[locale]/boards/_types";
import advancedFocusClasses from "../advanced-focus/advanced-focus.module.css";
import { useAdvancedFocus } from "../advanced-focus/context";
import { getAdvancedFocusClosePosition, getAdvancedFocusRect } from "../advanced-focus/geometry";
import { redirectShiftWheel } from "../advanced-focus/wheel";
import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions";
import itemContentClasses from "./item-content.module.css";
@@ -48,7 +55,9 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
const widgetStateRef = useRef<Record<string, unknown> | null>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const { active, open, close, dismiss, hover, leave } = useAdvancedFocus();
const supportsAdvancedFocus = item.kind !== "app";
const supportsAdvancedFocus = definitionSupportsAdvancedFocus(
widgetImports[item.kind].definition as WidgetDefinition,
);
const advancedViewId = `advanced-focus-${item.id}`;
const activeFocus = supportsAdvancedFocus && active?.itemId === item.id ? active : null;
const isAdvanced = activeFocus !== null;
@@ -81,6 +90,12 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
event.preventDefault();
openAdvanced(true);
};
const handleWheel = (event: WheelEvent<HTMLDivElement>) => {
if (!isAdvanced || !event.shiftKey) return;
const delta = event.deltaY || event.deltaX;
if (!redirectShiftWheel(event.currentTarget, event.target, delta)) return;
event.preventDefault();
};
return (
<>
@@ -128,6 +143,8 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
id={isAdvanced ? advancedViewId : undefined}
role={isAdvanced ? "region" : undefined}
aria-label={isAdvanced ? advancedViewLabel : undefined}
data-advanced-focus-surface={isAdvanced || undefined}
onWheel={handleWheel}
radius={board.itemRadius}
p={isAdvanced ? undefined : 0}
h={isAdvanced ? undefined : "100%"}
@@ -33,6 +33,7 @@ import {
getWidgetQueryKeys,
getWidgetRuntimeQueries,
reduceWidgetOptionsWithDefaultValues,
supportsAdvancedFocus,
widgetImports,
} from "@homarr/widgets";
import { WidgetEditModal } from "@homarr/widgets/modals";
@@ -70,6 +71,7 @@ export const WidgetContextMenu = ({ item, widgetStateRef, sourceRef, children }:
useItemActions();
const { data: integrationData, isPending } = clientApi.integration.all.useQuery();
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
const canOpenAdvancedFocus = supportsAdvancedFocus(currentDefinition as WidgetDefinition);
const { gridstack } = useSectionContext().refs;
const queryClient = useQueryClient();
const { open: openAdvancedFocus } = useAdvancedFocus();
@@ -250,7 +252,7 @@ export const WidgetContextMenu = ({ item, widgetStateRef, sourceRef, children }:
>
<Menu.ContextMenu>{children}</Menu.ContextMenu>
<Menu.Dropdown>
{item.kind !== "app" && (
{canOpenAdvancedFocus && (
<>
<Menu.Item
closeMenuOnClick