fix: refine advanced widget access
This commit is contained in:
1 parent
bd4bbcaca2
commit
464f30799a
7 files changed
+78
-45
No files matched your search
@@ -3,7 +3,7 @@
|
||||
import type { CSSProperties, PropsWithChildren } from "react";
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Overlay, Portal } from "@mantine/core";
|
||||
import { useIsomorphicEffect } from "@mantine/hooks";
|
||||
import { useIsomorphicEffect, useViewportSize } from "@mantine/hooks";
|
||||
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { FocusRect } from "./geometry";
|
||||
interface ActiveFocus {
|
||||
itemId: string;
|
||||
source: HTMLElement;
|
||||
restoreFocusTarget: HTMLElement;
|
||||
sourceRect: FocusRect;
|
||||
activation: "preview" | "manual";
|
||||
autofocusClose: boolean;
|
||||
@@ -24,10 +25,12 @@ interface ActiveFocus {
|
||||
interface OpenAdvancedFocusOptions {
|
||||
activation?: ActiveFocus["activation"];
|
||||
autofocusClose?: boolean;
|
||||
restoreFocusTarget?: HTMLElement;
|
||||
}
|
||||
|
||||
interface AdvancedFocusContextValue {
|
||||
active: ActiveFocus | null;
|
||||
viewportSize: { width: number; height: number };
|
||||
open: (itemId: string, source: HTMLElement, options?: OpenAdvancedFocusOptions) => void;
|
||||
close: (restoreFocus?: boolean) => void;
|
||||
dismiss: (itemId: string) => void;
|
||||
@@ -46,6 +49,7 @@ const isEditableTarget = (target: EventTarget | null) =>
|
||||
|
||||
export const BoardAdvancedFocusProvider = ({ children }: PropsWithChildren) => {
|
||||
const [isEditMode] = useEditMode();
|
||||
const viewportSize = useViewportSize();
|
||||
const [active, setActive] = useState<ActiveFocus | null>(null);
|
||||
const [isHoldPending, setIsHoldPending] = useState(false);
|
||||
const activeRef = useRef<ActiveFocus | null>(null);
|
||||
@@ -92,6 +96,7 @@ export const BoardAdvancedFocusProvider = ({ children }: PropsWithChildren) => {
|
||||
updateActive({
|
||||
itemId,
|
||||
source,
|
||||
restoreFocusTarget: options.restoreFocusTarget ?? source,
|
||||
sourceRect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
||||
activation: options.activation ?? "manual",
|
||||
autofocusClose: options.autofocusClose ?? false,
|
||||
@@ -113,7 +118,8 @@ export const BoardAdvancedFocusProvider = ({ children }: PropsWithChildren) => {
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
closeTimerRef.current = null;
|
||||
updateActive(null);
|
||||
if (restoreFocus && current.source.isConnected) requestAnimationFrame(() => current.source.focus());
|
||||
if (restoreFocus && current.restoreFocusTarget.isConnected)
|
||||
requestAnimationFrame(() => current.restoreFocusTarget.focus());
|
||||
}, closeDuration);
|
||||
},
|
||||
[cancelHoverTimer, cancelPreviewLeaveTimer, updateActive],
|
||||
@@ -210,7 +216,7 @@ export const BoardAdvancedFocusProvider = ({ children }: PropsWithChildren) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (underlyingSource.hasAttribute("aria-keyshortcuts")) {
|
||||
if (underlyingSource.hasAttribute("data-advanced-focus-enabled")) {
|
||||
hover(itemId, underlyingSource);
|
||||
return;
|
||||
}
|
||||
@@ -310,8 +316,8 @@ export const BoardAdvancedFocusProvider = ({ children }: PropsWithChildren) => {
|
||||
}, [active?.activation]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ active, open, close, dismiss, hover, leave }),
|
||||
[active, close, dismiss, hover, leave, open],
|
||||
() => ({ active, viewportSize, open, close, dismiss, hover, leave }),
|
||||
[active, close, dismiss, hover, leave, open, viewportSize],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -24,3 +24,18 @@
|
||||
pointer-events: none !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
.advancedFocusTrigger {
|
||||
position: absolute;
|
||||
top: var(--mantine-spacing-xs);
|
||||
left: 50%;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.advancedFocusTrigger:focus-visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ComponentType, CSSProperties, KeyboardEvent, MutableRefObject } from "react";
|
||||
import type { ComponentType, CSSProperties, MutableRefObject } from "react";
|
||||
import { memo, Suspense, use, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import dynamic from "next/dynamic";
|
||||
import type { CardProps } from "@mantine/core";
|
||||
import { Badge, Box, Card, Center, Loader, Portal } from "@mantine/core";
|
||||
import { useElementSize, useIsomorphicEffect, useViewportSize } from "@mantine/hooks";
|
||||
import { Badge, Box, Button, Card, Center, Loader, Portal } from "@mantine/core";
|
||||
import { useElementSize, useIsomorphicEffect } from "@mantine/hooks";
|
||||
import { QueryErrorResetBoundary, useQueryClient } from "@tanstack/react-query";
|
||||
import combineClasses from "clsx";
|
||||
import { NoIntegrationSelectedError } from "@homarr/widgets/errors/classes";
|
||||
@@ -153,15 +153,16 @@ const LoadedBoardItemContent = ({
|
||||
}: LoadedBoardItemContentProps) => {
|
||||
const { definition, Component } = use(loadWidgetResources(item.kind));
|
||||
const sourceRef = useRef<HTMLDivElement>(null);
|
||||
const advancedFocusTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const previewEntranceFrameRef = useRef<number | null>(null);
|
||||
const { width: viewportWidth, height: viewportHeight } = useViewportSize();
|
||||
const board = useRequiredBoard();
|
||||
const t = useI18n();
|
||||
const [isEditMode] = useEditMode();
|
||||
const [manualSurface, setManualSurface] = useState<HTMLDivElement | null>(null);
|
||||
const [surfacePortalTarget, setSurfacePortalTarget] = useState<HTMLDivElement | null>(null);
|
||||
const { active, open, close, dismiss, hover, leave } = useAdvancedFocus();
|
||||
const { active, viewportSize, open, close, dismiss, hover, leave } = useAdvancedFocus();
|
||||
const { width: viewportWidth, height: viewportHeight } = viewportSize;
|
||||
const supportsAdvancedFocus = definitionSupportsAdvancedFocus(definition);
|
||||
const widgetName = t(`widget.${item.kind}.name`);
|
||||
const advancedViewLabel = t("item.advancedFocus.label", { widget: widgetName });
|
||||
@@ -185,17 +186,13 @@ const LoadedBoardItemContent = ({
|
||||
|
||||
useEffect(() => () => dismiss(item.id), [dismiss, item.id]);
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
event.currentTarget !== event.target ||
|
||||
event.key !== "Enter" ||
|
||||
!event.shiftKey ||
|
||||
isEditMode ||
|
||||
!supportsAdvancedFocus
|
||||
)
|
||||
return;
|
||||
event.preventDefault();
|
||||
if (sourceRef.current) open(item.id, sourceRef.current, { activation: "manual", autofocusClose: true });
|
||||
const openAdvancedView = () => {
|
||||
if (sourceRef.current)
|
||||
open(item.id, sourceRef.current, {
|
||||
activation: "manual",
|
||||
autofocusClose: true,
|
||||
restoreFocusTarget: advancedFocusTriggerRef.current ?? sourceRef.current,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -353,16 +350,10 @@ const LoadedBoardItemContent = ({
|
||||
w="100%"
|
||||
h="100%"
|
||||
data-advanced-focus-source
|
||||
data-advanced-focus-enabled={supportsAdvancedFocus || undefined}
|
||||
data-item-id={item.id}
|
||||
tabIndex={!isEditMode && supportsAdvancedFocus ? 0 : undefined}
|
||||
role={!isEditMode && supportsAdvancedFocus ? "group" : undefined}
|
||||
aria-label={
|
||||
!isEditMode && supportsAdvancedFocus ? `${widgetName}: ${t("item.advancedFocus.open")}` : undefined
|
||||
}
|
||||
aria-expanded={supportsAdvancedFocus ? isAdvanced : undefined}
|
||||
aria-controls={isAdvanced ? advancedViewId : undefined}
|
||||
aria-keyshortcuts={supportsAdvancedFocus ? "Shift+Enter" : undefined}
|
||||
onKeyDown={supportsAdvancedFocus ? handleKeyDown : undefined}
|
||||
aria-label={!isEditMode && supportsAdvancedFocus ? widgetName : undefined}
|
||||
onPointerEnter={() => {
|
||||
if (supportsAdvancedFocus && sourceRef.current) hover(item.id, sourceRef.current);
|
||||
}}
|
||||
@@ -371,6 +362,21 @@ const LoadedBoardItemContent = ({
|
||||
}}
|
||||
className={combineClasses(isAdvanced && advancedFocusClasses.sourcePlaceholder)}
|
||||
>
|
||||
{!isEditMode && supportsAdvancedFocus && (
|
||||
<Button
|
||||
ref={advancedFocusTriggerRef}
|
||||
type="button"
|
||||
size="compact-xs"
|
||||
className={itemContentClasses.advancedFocusTrigger}
|
||||
aria-expanded={isAdvanced}
|
||||
aria-controls={isAdvanced ? advancedViewId : undefined}
|
||||
aria-keyshortcuts="Shift+Enter"
|
||||
data-advanced-focus-trigger
|
||||
onClick={openAdvancedView}
|
||||
>
|
||||
{t("item.advancedFocus.open")}
|
||||
</Button>
|
||||
)}
|
||||
<div ref={mountCompactHost} className={advancedFocusClasses.surfaceHost}>
|
||||
{!supportsAdvancedFocus && widgetCard}
|
||||
</div>
|
||||
|
||||
@@ -227,7 +227,12 @@ export const WidgetContextMenu = ({
|
||||
closeMenuOnClick
|
||||
leftSection={<IconMaximize size={16} />}
|
||||
onClick={() => {
|
||||
if (sourceRef.current) openAdvancedFocus(item.id, sourceRef.current);
|
||||
if (sourceRef.current)
|
||||
openAdvancedFocus(item.id, sourceRef.current, {
|
||||
restoreFocusTarget:
|
||||
sourceRef.current.querySelector<HTMLElement>("[data-advanced-focus-trigger]") ??
|
||||
sourceRef.current,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("item.advancedFocus.open")}
|
||||
|
||||
Reference in new issue
Block a user