Merge pull request #32822 from backstage/bui-full-page
BUI - Add new FullPage component
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user