Merge pull request #33948 from backstage/flex-item-props

ui: add support for flex-item props to generic layout components
This commit is contained in:
MT Lewis
2026-04-20 09:54:14 +01:00
committed by GitHub
17 changed files with 313 additions and 13 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': minor
---
Add support for flex item props (`grow`, `shrink`, and `basis`) to `Box`, `Card`, `Grid`, and `Flex` itself.
**Affected components:** Box, Card, Grid, Flex
+24
View File
@@ -431,6 +431,9 @@ export const BoxDefinition: {
'py',
'position',
'display',
'grow',
'shrink',
'basis',
'width',
'minWidth',
'maxWidth',
@@ -452,6 +455,7 @@ export type BoxOwnProps = {
// @public (undocumented)
export interface BoxProps
extends SpaceProps,
FlexItemProps,
BoxOwnProps,
BoxUtilityProps,
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
@@ -642,6 +646,7 @@ export const Card: ForwardRefExoticComponent<
export type CardBaseProps = {
children?: ReactNode;
className?: string;
style?: CSSProperties;
};
// @public
@@ -702,7 +707,9 @@ export const CardDefinition: {
readonly target: {};
readonly rel: {};
readonly download: {};
readonly style: {};
};
readonly utilityProps: readonly ['grow', 'shrink', 'basis'];
};
// @public
@@ -786,10 +793,12 @@ export type CardOwnProps = Pick<
| 'target'
| 'rel'
| 'download'
| 'style'
>;
// @public
export type CardProps = CardBaseProps &
FlexItemProps &
Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> &
(CardButtonVariant | CardLinkVariant | CardStaticVariant);
@@ -1315,12 +1324,22 @@ export const FlexDefinition: {
'align',
'justify',
'direction',
'grow',
'shrink',
'basis',
];
};
// @public (undocumented)
export type FlexDirection = 'row' | 'column';
// @public
export interface FlexItemProps {
basis?: Responsive<CSSProperties['flexBasis']>;
grow?: Responsive<number | boolean>;
shrink?: Responsive<number | boolean>;
}
// @public (undocumented)
export type FlexOwnProps = {
children: ReactNode;
@@ -1332,6 +1351,7 @@ export type FlexOwnProps = {
// @public (undocumented)
export interface FlexProps
extends SpaceProps,
FlexItemProps,
FlexOwnProps,
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
// (undocumented)
@@ -1422,6 +1442,9 @@ export const GridDefinition: {
'pt',
'px',
'py',
'grow',
'shrink',
'basis',
];
};
@@ -1478,6 +1501,7 @@ export type GridOwnProps = {
// @public (undocumented)
export interface GridProps
extends SpaceProps,
FlexItemProps,
GridOwnProps,
Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
// (undocumented)
+1 -1
View File
@@ -36,7 +36,7 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
{
ref,
className: classes.root,
style: { ...ownProps.style, ...utilityStyle },
style: { ...utilityStyle, ...ownProps.style },
...dataAttributes,
...restProps,
},
@@ -52,6 +52,9 @@ export const BoxDefinition = defineComponent<BoxOwnProps>()({
'py',
'position',
'display',
'grow',
'shrink',
'basis',
'width',
'minWidth',
'maxWidth',
+7 -1
View File
@@ -15,7 +15,12 @@
*/
import type { ReactNode, CSSProperties } from 'react';
import type { Responsive, ProviderBg, SpaceProps } from '../../types';
import type {
Responsive,
ProviderBg,
SpaceProps,
FlexItemProps,
} from '../../types';
/** @public */
export type BoxOwnProps = {
@@ -43,6 +48,7 @@ export type BoxUtilityProps = {
/** @public */
export interface BoxProps
extends SpaceProps,
FlexItemProps,
BoxOwnProps,
BoxUtilityProps,
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
+2 -1
View File
@@ -41,7 +41,7 @@ const INTERACTIVE_ELEMENT_SELECTOR =
* @public
*/
export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
const { ownProps, restProps, dataAttributes } = useDefinition(
const { ownProps, restProps, dataAttributes, utilityStyle } = useDefinition(
CardDefinition,
props,
);
@@ -98,6 +98,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
{...dataAttributes}
{...restProps}
onClick={isInteractive ? handleClick : undefined}
style={{ ...utilityStyle, ...ownProps.style }}
>
{href && (
<Link
@@ -42,7 +42,9 @@ export const CardDefinition = defineComponent<CardOwnProps>()({
target: {},
rel: {},
download: {},
style: {},
},
utilityProps: ['grow', 'shrink', 'basis'],
});
/**
+9 -2
View File
@@ -14,11 +14,16 @@
* limitations under the License.
*/
import type { ReactNode } from 'react';
import type { ReactNode, CSSProperties } from 'react';
import type { ButtonProps as RAButtonProps } from 'react-aria-components';
import type { FlexItemProps } from '../../types';
/** @public */
export type CardBaseProps = { children?: ReactNode; className?: string };
export type CardBaseProps = {
children?: ReactNode;
className?: string;
style?: CSSProperties;
};
/** @public */
export type CardButtonVariant = {
@@ -63,6 +68,7 @@ export type CardStaticVariant = {
* @public
*/
export type CardProps = CardBaseProps &
FlexItemProps &
Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> &
(CardButtonVariant | CardLinkVariant | CardStaticVariant);
@@ -81,6 +87,7 @@ export type CardOwnProps = Pick<
| 'target'
| 'rel'
| 'download'
| 'style'
>;
/** @public */
@@ -16,7 +16,9 @@
import preview from '../../../../../.storybook/preview';
import { Flex } from './Flex';
import { Text } from '../Text';
import { Box } from '../Box';
import { Box, BoxProps } from '../Box';
import { Card, CardHeader, CardBody, CardFooter } from '../Card';
import { Grid } from '../Grid';
const meta = preview.meta({
title: 'Backstage UI/Flex',
@@ -41,10 +43,9 @@ const meta = preview.meta({
const DecorativeBox = ({
width = '48px',
height = '48px',
}: {
width?: string;
height?: string;
}) => {
style,
...props
}: Omit<BoxProps, 'children'>) => {
const diagonalStripePattern = (() => {
const svg = `
<svg width="6" height="6" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
@@ -58,9 +59,11 @@ const DecorativeBox = ({
return (
<Box
{...props}
width={width}
height={height}
style={{
...style,
background: '#eaf2fd',
borderRadius: '4px',
border: '1px solid #2563eb',
@@ -199,6 +202,122 @@ export const ResponsiveAlign = meta.story({
),
});
export const FlexItems = meta.story({
args: {
component: 'Box',
grow: 1,
shrink: 0,
basis: 'auto',
},
argTypes: {
component: {
control: { type: 'select' },
options: ['Box', 'Card', 'Grid', 'Flex'],
mapping: {
Box: props => <DecorativeBox height="100%" width="256px" {...props} />,
Card: props => (
<Card style={{ height: '100%', width: '256px' }} {...props}>
<CardHeader>
<Text>Header</Text>
</CardHeader>
<CardBody>
<Text>
This is the first paragraph of a long body text that
demonstrates how the Card component handles extensive content.
The card should adjust accordingly to display all the text
properly while maintaining its structure.
</Text>
<Text>
Here's a second paragraph that adds more content to our card
body. Having multiple paragraphs helps to visualize how spacing
works within the card component.
</Text>
<Text>
This third paragraph continues to add more text to ensure we
have a proper demonstration of a card with significant content.
This makes it easier to test scrolling behavior and overall
layout when content exceeds the initial view.
</Text>
</CardBody>
<CardFooter>
<Text>Footer</Text>
</CardFooter>
</Card>
),
Grid: props => (
<Grid.Root
{...props}
height="128px"
style={{ width: '256px' }}
columns="3"
>
<Grid.Item colSpan="1" rowSpan="2">
<DecorativeBox height="100%" width="100%" />
</Grid.Item>
<Grid.Item colSpan="2">
<DecorativeBox height="100%" width="100%" />
</Grid.Item>
<Grid.Item colSpan="2">
<DecorativeBox height="100%" width="100%" />
</Grid.Item>
</Grid.Root>
),
Flex: props => (
<Flex
{...props}
height="128px"
style={{ width: '256px' }}
justify="between"
>
<DecorativeBox height="100%" />
<DecorativeBox height="100%" />
<DecorativeBox height="100%" />
</Flex>
),
},
},
grow: {
control: 'radio',
options: [undefined, 0, 1, false, true],
},
shrink: {
control: 'radio',
options: [undefined, 0, 1, false, true],
},
basis: {
control: 'radio',
options: [undefined, '0%', '25%', '50%', '100%', 100, '250px', 'auto'],
},
},
render: ({ component: Component, ...args }) => {
return (
<Flex style={{ width: '100%', height: '256px' }}>
<div
style={{
width: '256px',
flex: '1 1 auto',
background:
'repeating-linear-gradient(-45deg, transparent 0px, transparent 5px, #e8e8e8 5px, #e8e8e8 10px)',
borderRadius: '12px',
}}
/>
<Component {...args} />
<div
style={{
width: '256px',
flex: '1 1 auto',
background:
'repeating-linear-gradient(-45deg, transparent 0px, transparent 5px, #e8e8e8 5px, #e8e8e8 10px)',
borderRadius: '12px',
}}
/>
</Flex>
);
},
});
export const ResponsiveGap = meta.story({
args: {
gap: { xs: '4', md: '8', lg: '12' },
@@ -53,5 +53,8 @@ export const FlexDefinition = defineComponent<FlexOwnProps>()({
'align',
'justify',
'direction',
'grow',
'shrink',
'basis',
],
});
+8 -1
View File
@@ -15,7 +15,13 @@
*/
import type { ReactNode, CSSProperties } from 'react';
import type { Responsive, Space, SpaceProps, ProviderBg } from '../../types';
import type {
Responsive,
Space,
SpaceProps,
ProviderBg,
FlexItemProps,
} from '../../types';
/** @public */
export type FlexOwnProps = {
@@ -28,6 +34,7 @@ export type FlexOwnProps = {
/** @public */
export interface FlexProps
extends SpaceProps,
FlexItemProps,
FlexOwnProps,
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
gap?: Responsive<Space>;
@@ -51,6 +51,9 @@ export const GridDefinition = defineComponent<GridOwnProps>()({
'pt',
'px',
'py',
'grow',
'shrink',
'basis',
],
});
+2
View File
@@ -21,6 +21,7 @@ import type {
Responsive,
Columns,
ProviderBg,
FlexItemProps,
} from '../../types';
/** @public */
@@ -34,6 +35,7 @@ export type GridOwnProps = {
/** @public */
export interface GridProps
extends SpaceProps,
FlexItemProps,
GridOwnProps,
Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
columns?: Responsive<Columns>;
+72
View File
@@ -67,6 +67,18 @@
flex-direction: column-reverse;
}
.bui-grow {
flex-grow: var(--grow);
}
.bui-shrink {
flex-shrink: var(--shrink);
}
.bui-basis {
flex-basis: var(--basis);
}
/* Breakpoint xs */
@media (min-width: 640px) {
.xs\:bui-align-start {
@@ -116,6 +128,18 @@
.xs\:bui-fd-column-reverse {
flex-direction: column-reverse;
}
.xs\:bui-grow {
flex-grow: var(--grow-xs);
}
.xs\:bui-shrink {
flex-shrink: var(--shrink-xs);
}
.xs\:bui-basis {
flex-basis: var(--basis-xs);
}
}
/* Breakpoint sm */
@@ -167,6 +191,18 @@
.sm\:bui-fd-column-reverse {
flex-direction: column-reverse;
}
.sm\:bui-grow {
flex-grow: var(--grow-sm);
}
.sm\:bui-shrink {
flex-shrink: var(--shrink-sm);
}
.sm\:bui-basis {
flex-basis: var(--basis-sm);
}
}
/* Breakpoint md */
@@ -218,6 +254,18 @@
.md\:bui-fd-column-reverse {
flex-direction: column-reverse;
}
.md\:bui-grow {
flex-grow: var(--grow-md);
}
.md\:bui-shrink {
flex-shrink: var(--shrink-md);
}
.md\:bui-basis {
flex-basis: var(--basis-md);
}
}
/* Breakpoint lg */
@@ -269,6 +317,18 @@
.lg\:bui-fd-column-reverse {
flex-direction: column-reverse;
}
.lg\:bui-grow {
flex-grow: var(--grow-lg);
}
.lg\:bui-shrink {
flex-shrink: var(--shrink-lg);
}
.lg\:bui-basis {
flex-basis: var(--basis-lg);
}
}
/* Breakpoint xl */
@@ -320,5 +380,17 @@
.xl\:bui-fd-column-reverse {
flex-direction: column-reverse;
}
.xl\:bui-grow {
flex-grow: var(--grow-xl);
}
.xl\:bui-shrink {
flex-shrink: var(--shrink-xl);
}
.xl\:bui-basis {
flex-basis: var(--basis-xl);
}
}
}
@@ -102,7 +102,7 @@ export function processUtilityProps<Keys extends string>(
const handleUtilityValue = (
key: string,
val: unknown,
inputVal: unknown,
prefix: string = '',
) => {
// Get utility class configuration for this key
@@ -113,6 +113,11 @@ export function processUtilityProps<Keys extends string>(
return;
}
const val =
'transform' in utilityConfig
? utilityConfig.transform(inputVal)
: inputVal;
// Check if value is in the list of valid values for this utility
const values = utilityConfig.values as readonly (string | number)[];
if (values.length > 0 && values.includes(val as string | number)) {
+16
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import type { CSSProperties } from 'react';
/** @public */
export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
@@ -120,6 +122,20 @@ export interface PaddingProps {
/** @public */
export interface SpaceProps extends MarginProps, PaddingProps {}
/**
* Flex item properties.
*
* @public
*/
export interface FlexItemProps {
/** Controls the flex-grow property. Values of `true` or `false` are converted to `1` or `0` respectively. */
grow?: Responsive<number | boolean>;
/** Controls the flex-shrink property. Values of `true` or `false` are converted to `1` or `0` respectively. */
shrink?: Responsive<number | boolean>;
/** Controls the flex-basis property. */
basis?: Responsive<CSSProperties['flexBasis']>;
}
/** @public */
export type TextVariants =
| 'title-large'
+24 -1
View File
@@ -196,7 +196,30 @@ export const utilityClassMap = {
class: 'bui-row-span',
values: columnsValues,
},
grow: {
class: 'bui-grow',
cssVar: '--grow',
values: [],
transform: input => (typeof input === 'boolean' ? Number(input) : input),
},
shrink: {
class: 'bui-shrink',
cssVar: '--shrink',
values: [],
transform: input => (typeof input === 'boolean' ? Number(input) : input),
},
basis: {
class: 'bui-basis',
cssVar: '--basis',
values: [],
transform: input => (typeof input === 'number' ? `${input}px` : input),
},
} as const satisfies Record<
string,
{ class: string; cssVar?: string; values: readonly (string | number)[] }
{
class: string;
cssVar?: string;
values: readonly (string | number)[];
transform?: (input: unknown) => unknown;
}
>;