Merge pull request #32822 from backstage/bui-full-page

BUI - Add new FullPage component
This commit is contained in:
Charles de Dreuille
2026-02-12 22:47:55 +00:00
committed by GitHub
14 changed files with 326 additions and 16 deletions
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/ui': patch
---
Added a new `FullPage` component that fills the remaining viewport height below the `Header`.
```tsx
<Header title="My Plugin" tabs={tabs} />
<FullPage>
{/* content fills remaining height */}
</FullPage>
```
**Affected components:** FullPage
@@ -547,6 +547,7 @@ validator
validators
Valkey
varchar
viewport
vite
VMware
Vodafone
+2 -6
View File
@@ -5,7 +5,7 @@ import addonLinks from '@storybook/addon-links';
import { definePreview } from '@storybook/react-vite';
import { useEffect } from 'react';
import { TestApiProvider } from '@backstage/test-utils';
import { Content, AlertDisplay } from '@backstage/core-components';
import { AlertDisplay } from '@backstage/core-components';
import { apis } from './support/apis';
import { useGlobals } from 'storybook/preview-api';
import { UnifiedThemeProvider, themes } from '@backstage/theme';
@@ -57,8 +57,6 @@ export default definePreview({
},
parameters: {
layout: 'fullscreen',
backgrounds: {
disable: true,
},
@@ -146,9 +144,7 @@ export default definePreview({
{/* @ts-ignore */}
<TestApiProvider apis={apis}>
<AlertDisplay />
<Content>
<Story />
</Content>
<Story />
</TestApiProvider>
</UnifiedThemeProvider>
);
+16
View File
@@ -979,6 +979,21 @@ export interface FlexProps extends SpaceProps {
// @public (undocumented)
export type FlexWrap = 'wrap' | 'nowrap' | 'wrap-reverse';
// @public
export const FullPage: ForwardRefExoticComponent<
FullPageProps & RefAttributes<HTMLElement>
>;
// @public
export const FullPageDefinition: {
readonly classNames: {
readonly root: 'bui-FullPage';
};
};
// @public
export interface FullPageProps extends React.ComponentPropsWithoutRef<'main'> {}
// @public (undocumented)
export const Grid: {
Root: ForwardRefExoticComponent<GridProps & RefAttributes<HTMLDivElement>>;
@@ -1082,6 +1097,7 @@ export const Header: (props: HeaderProps) => JSX_2.Element;
// @public
export const HeaderDefinition: {
readonly classNames: {
readonly root: 'bui-Header';
readonly toolbar: 'bui-HeaderToolbar';
readonly toolbarWrapper: 'bui-HeaderToolbarWrapper';
readonly toolbarContent: 'bui-HeaderToolbarContent';
@@ -0,0 +1,24 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@layer tokens, base, components, utilities;
@layer components {
.bui-FullPage {
height: calc(100dvh - var(--bui-header-height, 0px));
overflow-y: auto;
}
}
@@ -0,0 +1,110 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import preview from '../../../../../.storybook/preview';
import type { StoryFn } from '@storybook/react-vite';
import { FullPage } from './FullPage';
import { Header } from '../Header';
import { Container } from '../Container';
import { Text } from '../Text';
import type { HeaderTab } from '../Header/types';
import { MemoryRouter } from 'react-router-dom';
const meta = preview.meta({
title: 'Backstage UI/FullPage',
component: FullPage,
parameters: {
layout: 'fullscreen',
},
});
const withRouter = (Story: StoryFn) => (
<MemoryRouter>
<Story />
</MemoryRouter>
);
const tabs: HeaderTab[] = [
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'checks', label: 'Checks', href: '/checks' },
{ id: 'tracks', label: 'Tracks', href: '/tracks' },
{ id: 'campaigns', label: 'Campaigns', href: '/campaigns' },
];
const paragraphs = Array.from({ length: 20 }, (_, i) => (
<Text as="p" key={i}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames
ac turpis egestas. Sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua.
</Text>
));
export const Default = meta.story({
decorators: [withRouter],
render: () => (
<>
<Header title="My Plugin" />
<FullPage style={{ backgroundColor: '#c3f0ff' }}>
<Container>
<Text as="p">
This content fills the remaining viewport height below the Header.
</Text>
</Container>
</FullPage>
</>
),
});
export const WithScrollableContent = meta.story({
decorators: [withRouter],
render: () => (
<>
<Header title="My Plugin" />
<FullPage>
<Container>
<Text as="h2" variant="title-medium">
Scrollable Content
</Text>
<Text as="p">
The content below scrolls independently while the Header stays
pinned at the top.
</Text>
{paragraphs}
</Container>
</FullPage>
</>
),
});
export const WithTabs = meta.story({
decorators: [withRouter],
render: () => (
<>
<Header title="My Plugin" tabs={tabs} />
<FullPage>
<Container>
<Text as="p">
The FullPage height adjusts automatically when the Header includes
tabs, thanks to the ResizeObserver measuring the Header's actual
height.
</Text>
{paragraphs}
</Container>
</FullPage>
</>
),
});
@@ -0,0 +1,45 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { forwardRef } from 'react';
import type { FullPageProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import { FullPageDefinition } from './definition';
import styles from './FullPage.module.css';
import clsx from 'clsx';
/**
* A component that fills the remaining viewport height below the Header.
*
* The FullPage component consumes the `--bui-header-height` CSS custom property
* set by the Header component to calculate its height as
* `calc(100dvh - var(--bui-header-height, 0px))`. Content inside the FullPage
* scrolls independently while the Header stays visible.
*
* @public
*/
export const FullPage = forwardRef<HTMLElement, FullPageProps>((props, ref) => {
const { classNames, cleanedProps } = useStyles(FullPageDefinition, props);
const { className, ...rest } = cleanedProps;
return (
<main
ref={ref}
className={clsx(classNames.root, styles[classNames.root], className)}
{...rest}
/>
);
});
@@ -0,0 +1,27 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ComponentDefinition } from '../../types';
/**
* Component definition for FullPage
* @public
*/
export const FullPageDefinition = {
classNames: {
root: 'bui-FullPage',
},
} as const satisfies ComponentDefinition;
@@ -0,0 +1,19 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { FullPage } from './FullPage';
export { FullPageDefinition } from './definition';
export type { FullPageProps } from './types';
@@ -0,0 +1,22 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Props for the FullPage component.
*
* @public
*/
export interface FullPageProps extends React.ComponentPropsWithoutRef<'main'> {}
@@ -17,9 +17,11 @@
@layer tokens, base, components, utilities;
@layer components {
.bui-HeaderToolbar {
margin-bottom: var(--bui-space-6);
.bui-Header {
display: block;
}
.bui-HeaderToolbar {
&::before {
content: '';
position: absolute;
@@ -30,10 +32,6 @@
background-color: var(--bui-bg-neutral-0);
z-index: 0;
}
&[data-has-tabs='true'] {
margin-bottom: 0;
}
}
.bui-HeaderToolbarWrapper {
@@ -90,7 +88,6 @@
}
.bui-HeaderTabsWrapper {
margin-bottom: var(--bui-space-4);
padding-inline: var(--bui-space-3);
border-bottom: 1px solid var(--bui-border);
background-color: var(--bui-bg-neutral-1);
+40 -3
View File
@@ -20,6 +20,8 @@ import { Tabs, TabList, Tab } from '../Tabs';
import { useStyles } from '../../hooks/useStyles';
import { HeaderDefinition } from './definition';
import { type NavigateOptions } from 'react-router-dom';
import { useRef } from 'react';
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
import styles from './Header.module.css';
import clsx from 'clsx';
@@ -47,9 +49,45 @@ export const Header = (props: HeaderProps) => {
} = cleanedProps;
const hasTabs = tabs && tabs.length > 0;
const headerRef = useRef<HTMLElement>(null);
useIsomorphicLayoutEffect(() => {
const el = headerRef.current;
if (!el) return undefined;
const updateHeight = () => {
const height = el.offsetHeight;
document.documentElement.style.setProperty(
'--bui-header-height',
`${height}px`,
);
};
// Set height once immediately
updateHeight();
// Observe for resize changes if ResizeObserver is available
// (not present in Jest/jsdom by default)
if (typeof ResizeObserver === 'undefined') {
return () => {
document.documentElement.style.removeProperty('--bui-header-height');
};
}
const observer = new ResizeObserver(updateHeight);
observer.observe(el);
return () => {
observer.disconnect();
document.documentElement.style.removeProperty('--bui-header-height');
};
}, []);
return (
<>
<header
ref={headerRef}
className={clsx(classNames.root, styles[classNames.root], className)}
>
<HeaderToolbar
icon={icon}
title={title}
@@ -62,7 +100,6 @@ export const Header = (props: HeaderProps) => {
className={clsx(
classNames.tabsWrapper,
styles[classNames.tabsWrapper],
className,
)}
>
<Tabs onSelectionChange={onTabSelectionChange}>
@@ -81,6 +118,6 @@ export const Header = (props: HeaderProps) => {
</Tabs>
</div>
)}
</>
</header>
);
};
@@ -22,6 +22,7 @@ import type { ComponentDefinition } from '../../types';
*/
export const HeaderDefinition = {
classNames: {
root: 'bui-Header',
toolbar: 'bui-HeaderToolbar',
toolbarWrapper: 'bui-HeaderToolbarWrapper',
toolbarContent: 'bui-HeaderToolbarContent',
+1
View File
@@ -25,6 +25,7 @@ export * from './components/Box';
export * from './components/Grid';
export * from './components/Flex';
export * from './components/Container';
export * from './components/FullPage';
// UI components
export * from './components/Accordion';