fix(widgets): eliminate layout shift with fallback dimensions
Provide estimated width/height to widgets before useElementSize measures the DOM, preventing responsive breakpoint shifts during hydration. Replace the Loader spinner with a Skeleton for widget loading states.
This commit is contained in:
2 files changed
+10
-7
No files matched your search
@@ -27,10 +27,17 @@ const getOverflowFromKind = (kind: SectionItem["kind"]) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ASSUMED_VIEWPORT_WIDTH = 900;
|
||||||
|
const DEFAULT_COLUMN_COUNT = 12;
|
||||||
|
|
||||||
export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||||
const { ref, width, height } = useElementSize<HTMLDivElement>();
|
const { ref, width, height } = useElementSize<HTMLDivElement>();
|
||||||
const board = useRequiredBoard();
|
const board = useRequiredBoard();
|
||||||
|
|
||||||
|
const estimatedCellSize = ASSUMED_VIEWPORT_WIDTH / DEFAULT_COLUMN_COUNT;
|
||||||
|
const fallbackWidth = item.width * estimatedCellSize - 20;
|
||||||
|
const fallbackHeight = item.height * estimatedCellSize - 20;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card
|
<Card
|
||||||
@@ -53,7 +60,7 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
|||||||
}}
|
}}
|
||||||
p={0}
|
p={0}
|
||||||
>
|
>
|
||||||
<InnerContent item={item} width={width} height={height} />
|
<InnerContent item={item} width={width || fallbackWidth} height={height || fallbackHeight} />
|
||||||
</Card>
|
</Card>
|
||||||
{item.advancedOptions.title?.trim() && (
|
{item.advancedOptions.title?.trim() && (
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import type { Loader } from "next/dynamic";
|
import type { Loader } from "next/dynamic";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Center, Loader as UiLoader } from "@mantine/core";
|
import { Skeleton } from "@mantine/core";
|
||||||
|
|
||||||
import { objectEntries } from "@homarr/common";
|
import { objectEntries } from "@homarr/common";
|
||||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||||
@@ -107,11 +107,7 @@ export const loadWidgetDynamic = <TKind extends WidgetKind>(kind: TKind) => {
|
|||||||
const newlyLoadedComponent = dynamic<WidgetComponentProps<TKind>>(
|
const newlyLoadedComponent = dynamic<WidgetComponentProps<TKind>>(
|
||||||
widgetImports[kind].componentLoader as Loader<WidgetComponentProps<TKind>>,
|
widgetImports[kind].componentLoader as Loader<WidgetComponentProps<TKind>>,
|
||||||
{
|
{
|
||||||
loading: () => (
|
loading: () => <Skeleton w="100%" h="100%" radius="md" animate />,
|
||||||
<Center w="100%" h="100%">
|
|
||||||
<UiLoader />
|
|
||||||
</Center>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user