fix widget focus interactions
This commit is contained in:
21 files changed
+119
-28
No files matched your search
@@ -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",
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user