Fix PluginHeader ResizeObserver loop
Defer header height updates to avoid ResizeObserver loop warnings in FullPage layouts and normalize header actions before rendering. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Fixed `PluginHeader` to avoid triggering `ResizeObserver loop completed with undelivered notifications` warnings when used in layouts that react to the header height, such as pages that use `FullPage`.
|
||||
|
||||
**Affected components:** PluginHeader
|
||||
@@ -19,7 +19,7 @@ import { Tabs, TabList, Tab } from '../Tabs';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { PluginHeaderDefinition } from './definition';
|
||||
import { type NavigateOptions } from 'react-router-dom';
|
||||
import { useRef } from 'react';
|
||||
import { Children, useMemo, useRef } from 'react';
|
||||
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
|
||||
import { Box } from '../Box';
|
||||
import { Link } from 'react-aria-components';
|
||||
@@ -55,35 +55,70 @@ export const PluginHeader = (props: PluginHeaderProps) => {
|
||||
const toolbarWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarContentRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarControlsRef = useRef<HTMLDivElement>(null);
|
||||
const animationFrameRef = useRef<number | undefined>(undefined);
|
||||
const lastAppliedHeightRef = useRef<number | undefined>(undefined);
|
||||
|
||||
const actionChildren = useMemo(() => {
|
||||
return Children.toArray(customActions);
|
||||
}, [customActions]);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const el = headerRef.current;
|
||||
if (!el) return undefined;
|
||||
if (!el) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updateHeight = () => {
|
||||
const height = el.offsetHeight;
|
||||
const cancelScheduledUpdate = () => {
|
||||
if (animationFrameRef.current === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
animationFrameRef.current = undefined;
|
||||
};
|
||||
|
||||
const applyHeight = (height: number) => {
|
||||
if (lastAppliedHeightRef.current === height) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastAppliedHeightRef.current = height;
|
||||
document.documentElement.style.setProperty(
|
||||
'--bui-header-height',
|
||||
`${height}px`,
|
||||
);
|
||||
};
|
||||
|
||||
// Set height once immediately
|
||||
updateHeight();
|
||||
const scheduleHeightUpdate = () => {
|
||||
cancelScheduledUpdate();
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
animationFrameRef.current = undefined;
|
||||
applyHeight(el.offsetHeight);
|
||||
});
|
||||
};
|
||||
|
||||
// Set height once immediately so the initial layout is correct.
|
||||
applyHeight(el.offsetHeight);
|
||||
|
||||
// Observe for resize changes if ResizeObserver is available
|
||||
// (not present in Jest/jsdom by default)
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
return () => {
|
||||
cancelScheduledUpdate();
|
||||
lastAppliedHeightRef.current = undefined;
|
||||
document.documentElement.style.removeProperty('--bui-header-height');
|
||||
};
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(updateHeight);
|
||||
const observer = new ResizeObserver(() => {
|
||||
scheduleHeightUpdate();
|
||||
});
|
||||
observer.observe(el);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
cancelScheduledUpdate();
|
||||
lastAppliedHeightRef.current = undefined;
|
||||
document.documentElement.style.removeProperty('--bui-header-height');
|
||||
};
|
||||
}, []);
|
||||
@@ -111,7 +146,7 @@ export const PluginHeader = (props: PluginHeaderProps) => {
|
||||
</Text>
|
||||
</div>
|
||||
<div className={classes.toolbarControls} ref={toolbarControlsRef}>
|
||||
{customActions}
|
||||
{actionChildren}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user