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

+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(
@@ -72,7 +72,7 @@ export default function AudioStatsWidget({
{currentlyPlaying.name}
</Text>
<Text size="xs" c="dimmed" truncate>
{currentlyPlaying.seasonName ?? currentlyPlaying.albumName ?? session.user.username}
{currentlyPlaying.seasonName ?? currentlyPlaying.albumName ?? session.user?.username ?? "—"}
</Text>
</Stack>
<Badge
@@ -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"
@@ -61,7 +61,7 @@ const TOOLTIP_Z_INDEX = getDefaultZIndex("modal");
const PortalTooltipContent = ({ active, label, payload, formatter, showTotal }: PortalTooltipProps) => {
const mouseRef = useRef({ x: 0, y: 0 });
const [pos, setPos] = useState({ x: 0, y: 0 });
const [pos, setPos] = useState({ x: 0, y: 0, isAdvanced: false });
const rafRef = useRef(0);
const tooltipRef = useRef<HTMLDivElement>(null);
@@ -72,9 +72,10 @@ const PortalTooltipContent = ({ active, label, payload, formatter, showTotal }:
const el = tooltipRef.current;
const mx = mouseRef.current.x;
const my = mouseRef.current.y;
const isAdvanced = Boolean(document.elementFromPoint(mx, my)?.closest("[data-advanced-focus-surface]"));
if (!el) {
setPos({ x: mx + MARGIN, y: my });
setPos({ x: mx + MARGIN, y: my, isAdvanced });
return;
}
@@ -89,7 +90,7 @@ const PortalTooltipContent = ({ active, label, payload, formatter, showTotal }:
if (y < MARGIN) y = MARGIN;
if (y + height > vh - MARGIN) y = vh - MARGIN - height;
setPos({ x, y });
setPos({ x, y, isAdvanced });
});
}, []);
@@ -122,7 +123,7 @@ const PortalTooltipContent = ({ active, label, payload, formatter, showTotal }:
position: "fixed",
left: pos.x,
top: pos.y,
zIndex: TOOLTIP_Z_INDEX,
zIndex: pos.isAdvanced ? getDefaultZIndex("popover") : TOOLTIP_Z_INDEX,
pointerEvents: "none",
};
+28 -1
View File
@@ -3,7 +3,15 @@ import { getQueryKey } from "@trpc/react-query";
import { clientApi } from "@homarr/api/client";
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;
@@ -36,3 +44,22 @@ describe("getWidgetQueryKeys", () => {
expect(getWidgetRuntimeQueries(widgetStateRef)).toEqual([{ path: ["widget", "calendar", "findAllEvents"], input }]);
});
});
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);
},
);
});
+3
View File
@@ -86,6 +86,7 @@ export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition ext
export interface WidgetDefinition {
icon: TablerIcon;
supportsAdvancedFocus?: boolean;
queryKey?: QueryKey;
queryKeys?: readonly QueryKey[];
queryMatcher?: WidgetQueryMatcher;
@@ -109,6 +110,8 @@ export interface WidgetDefinition {
contextActions?: (props: Omit<WidgetContextActionProps<WidgetKind>, "kind">) => WidgetContextMenuAction[];
}
export const supportsAdvancedFocus = (definition: WidgetDefinition) => definition.supportsAdvancedFocus !== false;
export const getWidgetQueryKeys = (definition: {
kind: string;
queryKey?: QueryKey;
+1
View File
@@ -78,6 +78,7 @@ export {
getWidgetQueryKeys,
getWidgetRuntimeQueries,
normalizeWidgetQuery,
supportsAdvancedFocus,
widgetQueryValueEquals,
} from "./definition";
export type { WidgetComponentProps };
@@ -81,10 +81,10 @@ export default function MediaServerWidget({
Cell: ({ row }) => (
<Group gap="xs" wrap="nowrap">
<Avatar size={28} src={row.original.user.profilePictureUrl} />
<Avatar size={28} src={row.original.user?.profilePictureUrl} />
<Stack gap={0} style={{ minWidth: 0 }}>
<Text size="xs" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{row.original.user.username}
{row.original.user?.username ?? t("items.unknownUser")}
</Text>
<Text
size="10px"
@@ -415,8 +415,8 @@ const ItemInfoModal = createModal<{ item: StreamSession }>(({ innerProps }) => {
itemKey={t("user")}
value={
<Group gap="sm" align="center">
<Avatar size="sm" src={innerProps.item.user.profilePictureUrl} />{" "}
<Text>{innerProps.item.user.username}</Text>
<Avatar size="sm" src={innerProps.item.user?.profilePictureUrl} />{" "}
<Text>{innerProps.item.user?.username ?? t("unknownUser")}</Text>
</Group>
}
/>