Address sticky Header review feedback

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-04-26 15:20:12 +01:00
parent 6c4606a71c
commit 6b05c0a4de
4 changed files with 38 additions and 5 deletions
-1
View File
@@ -1743,7 +1743,6 @@ export interface HeaderOwnProps {
description?: string;
// (undocumented)
metadata?: HeaderMetadataItem[];
// (undocumented)
sticky?: boolean;
// (undocumented)
tabs?: HeaderNavTabItem[];
@@ -24,7 +24,7 @@ import { MemoryRouter } from 'react-router-dom';
import { BUIProvider } from '../../provider';
import { Button, ButtonIcon, MenuTrigger, Menu, MenuItem } from '../../';
import { RiMore2Line } from '@remixicon/react';
import { Container } from '../Container/Container';
import { Container } from '../Container';
const meta = preview.meta({
title: 'Backstage UI/Header',
@@ -415,8 +415,8 @@ export const NonSticky = meta.story({
<Container pb="3">
{Array.from({ length: 60 }, (_, i) => (
<p key={i} style={{ marginBottom: '16px' }}>
Scroll down to see the title bar stick to the top while the tags,
description, and metadata scroll away. Line {i + 1}.
Scroll down to see the entire header scroll away with the rest of
the page content. Line {i + 1}.
</p>
))}
</Container>
+31 -1
View File
@@ -43,6 +43,16 @@ const getScrollParent = (element: HTMLElement | null): Element | null => {
return null;
};
const isStickySentinelOutOfView = (
sentinel: HTMLElement,
root: Element | null,
) => {
const sentinelRect = sentinel.getBoundingClientRect();
const rootTop = root ? root.getBoundingClientRect().top : 0;
return sentinelRect.bottom <= rootTop;
};
/**
* Parses inline Markdown links in a string and returns an array of React nodes.
* URLs are sanitized via `@braintree/sanitize-url`; unsafe URLs are rendered as
@@ -103,11 +113,31 @@ export const Header = (props: HeaderProps) => {
return;
}
const root = getScrollParent(sentinel);
if (typeof IntersectionObserver === 'undefined') {
const updateStuckState = () => {
setIsStuck(isStickySentinelOutOfView(sentinel, root));
};
const scrollTarget = root ?? window;
updateStuckState();
scrollTarget.addEventListener('scroll', updateStuckState, {
passive: true,
});
window.addEventListener('resize', updateStuckState);
return () => {
scrollTarget.removeEventListener('scroll', updateStuckState);
window.removeEventListener('resize', updateStuckState);
};
}
const observer = new IntersectionObserver(
([entry]) => {
setIsStuck(!entry.isIntersecting);
},
{ root: getScrollParent(sentinel), threshold: 0 },
{ root, threshold: 0 },
);
observer.observe(sentinel);
@@ -116,6 +116,10 @@ export interface HeaderOwnProps {
tags?: HeaderTag[];
metadata?: HeaderMetadataItem[];
className?: string;
/**
* Makes the title-and-actions row stick to the top of its nearest scroll
* container while the rest of the header content scrolls away.
*/
sticky?: boolean;
}